Working Developer Settings

This commit is contained in:
2025-09-24 13:31:15 +02:00
parent 8b96a5d0c3
commit 783541a776
24 changed files with 965 additions and 754 deletions

View File

@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dea69d41f90c6ea4fa55c27c1d60c145, type: 3}
m_Name: AddressableAssetGroupSortSettings
m_EditorClassIdentifier:
sortOrder:
- eb8a37153d819c44194f7ce97570a3d3
- 2b3d7cefec0915e42be04aebf0400a56
- 6f3207429a65b3e4b83935ac19791077
- c62e6f02418e19949bca4cccdd5fa02e

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 32c1a9c8651793e41848a173d66d98fd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -15,6 +15,11 @@ MonoBehaviour:
m_GroupName: Settings
m_GUID: c62e6f02418e19949bca4cccdd5fa02e
m_SerializeEntries:
- m_GUID: 328ce914b893df646be3ad3c62755453
m_Address: Settings/Developer/DivingDeveloperSettings
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 35bfcff00faa72c4eb272a9e8288f965
m_Address: Settings/PlayerFollowerSettings
m_ReadOnly: 0

View File

@@ -0,0 +1,207 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using System.IO;
namespace AppleHills.Core.Settings.Editor
{
public class DeveloperSettingsEditorWindow : EditorWindow
{
private Vector2 scrollPosition;
private List<BaseDeveloperSettings> allDeveloperSettings = new List<BaseDeveloperSettings>();
private string[] tabNames = new string[] { "Diving", "Other Systems" }; // Add more tabs as needed
private int selectedTab = 0;
private Dictionary<string, SerializedObject> serializedSettingsObjects = new Dictionary<string, SerializedObject>();
private GUIStyle headerStyle;
[MenuItem("AppleHills/Developer Settings Editor")]
public static void ShowWindow()
{
GetWindow<DeveloperSettingsEditorWindow>("Developer Settings");
}
private void OnEnable()
{
LoadAllSettings();
}
private void LoadAllSettings()
{
allDeveloperSettings.Clear();
serializedSettingsObjects.Clear();
// Find all developer settings assets
string[] guids = AssetDatabase.FindAssets("t:BaseDeveloperSettings");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
BaseDeveloperSettings settings = AssetDatabase.LoadAssetAtPath<BaseDeveloperSettings>(path);
if (settings != null)
{
allDeveloperSettings.Add(settings);
serializedSettingsObjects[settings.GetType().Name] = new SerializedObject(settings);
}
}
// If any settings are missing, create them
CreateSettingsIfMissing<DivingDeveloperSettings>("DivingDeveloperSettings");
// Add more developer settings types here as needed
// CreateSettingsIfMissing<OtherDeveloperSettings>("OtherDeveloperSettings");
}
private void CreateSettingsIfMissing<T>(string fileName) where T : BaseDeveloperSettings
{
if (!allDeveloperSettings.Any(s => s is T))
{
// Check if the asset already exists
string[] guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}");
if (guids.Length == 0)
{
// Create the settings folder if it doesn't exist
string settingsFolder = "Assets/Settings/Developer";
if (!AssetDatabase.IsValidFolder("Assets/Settings"))
{
AssetDatabase.CreateFolder("Assets", "Settings");
}
if (!AssetDatabase.IsValidFolder(settingsFolder))
{
AssetDatabase.CreateFolder("Assets/Settings", "Developer");
}
// Create new settings asset
T settings = CreateInstance<T>();
string path = $"{settingsFolder}/{fileName}.asset";
AssetDatabase.CreateAsset(settings, path);
AssetDatabase.SaveAssets();
allDeveloperSettings.Add(settings);
serializedSettingsObjects[typeof(T).Name] = new SerializedObject(settings);
Debug.Log($"Created missing developer settings asset: {path}");
}
else
{
// Load existing asset
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
T settings = AssetDatabase.LoadAssetAtPath<T>(path);
allDeveloperSettings.Add(settings);
serializedSettingsObjects[typeof(T).Name] = new SerializedObject(settings);
}
}
}
private void OnGUI()
{
if (headerStyle == null)
{
headerStyle = new GUIStyle(EditorStyles.boldLabel);
headerStyle.fontSize = 14;
headerStyle.margin = new RectOffset(0, 0, 10, 10);
}
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Apple Hills Developer Settings", headerStyle);
EditorGUILayout.HelpBox("This editor is for technical settings intended for developers. For gameplay settings, use the Game Settings editor.", MessageType.Info);
EditorGUILayout.Space(10);
selectedTab = GUILayout.Toolbar(selectedTab, tabNames);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
switch (selectedTab)
{
case 0: // Diving
DrawSettingsEditor<DivingDeveloperSettings>();
break;
case 1: // Other Systems
EditorGUILayout.HelpBox("Other developer settings will appear here as they are added.", MessageType.Info);
break;
// Add additional cases as more developer settings types are added
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Refresh", GUILayout.Width(100)))
{
LoadAllSettings();
}
if (GUILayout.Button("Save All", GUILayout.Width(100)))
{
foreach (var serializedObj in serializedSettingsObjects.Values)
{
serializedObj.ApplyModifiedProperties();
EditorUtility.SetDirty(serializedObj.targetObject);
}
AssetDatabase.SaveAssets();
Debug.Log("All developer settings saved!");
}
EditorGUILayout.EndHorizontal();
}
private void DrawSettingsEditor<T>() where T : BaseDeveloperSettings
{
BaseDeveloperSettings settings = allDeveloperSettings.Find(s => s is T);
if (settings == null)
{
EditorGUILayout.HelpBox($"No {typeof(T).Name} found. Click Refresh to create one.", MessageType.Warning);
return;
}
SerializedObject serializedObj = serializedSettingsObjects[typeof(T).Name];
serializedObj.Update();
EditorGUILayout.Space(10);
// Draw all properties
SerializedProperty property = serializedObj.GetIterator();
bool enterChildren = true;
while (property.NextVisible(enterChildren))
{
enterChildren = false;
// Skip the script field
if (property.name == "m_Script") continue;
// Group headers
if (property.isArray && property.propertyType == SerializedPropertyType.Generic)
{
EditorGUILayout.LabelField(property.displayName, EditorStyles.boldLabel);
}
EditorGUILayout.PropertyField(property, true);
}
// Apply changes
if (serializedObj.ApplyModifiedProperties())
{
EditorUtility.SetDirty(settings);
// Trigger OnValidate on the asset
if (settings != null)
{
settings.OnValidate();
}
}
}
// Helper method to highlight important fields
private void DrawHighlightedProperty(SerializedProperty property, string tooltip = null)
{
GUI.backgroundColor = new Color(0.8f, 0.9f, 1f); // Light blue for developer settings
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUI.backgroundColor = Color.white;
EditorGUILayout.PropertyField(property, new GUIContent(property.displayName, tooltip));
EditorGUILayout.EndVertical();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8ab6d9fee0924431b8e22edb500b35df
timeCreated: 1758710778

View File

@@ -0,0 +1,54 @@
using UnityEngine;
using UnityEditor;
namespace AppleHills.Core.Settings.Editor
{
/// <summary>
/// Custom property drawer for layer fields to display a dropdown with layer names
/// </summary>
[CustomPropertyDrawer(typeof(LayerAttribute))]
public class LayerPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
// Draw a nice layer selection dropdown like the one in Unity inspector
if (property.propertyType == SerializedPropertyType.Integer)
{
property.intValue = EditorGUI.LayerField(position, label, property.intValue);
}
else
{
EditorGUI.LabelField(position, label.text, "Use [Layer] with int fields only");
}
EditorGUI.EndProperty();
}
}
/// <summary>
/// Custom property drawer for LayerMask fields to display a mask dropdown with layer names
/// </summary>
[CustomPropertyDrawer(typeof(LayerMaskAttribute))]
public class LayerMaskPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
// Draw a nice layer mask selection like the one in Unity inspector
if (property.propertyType == SerializedPropertyType.LayerMask)
{
property.intValue = EditorGUI.MaskField(position, label,
property.intValue, UnityEditorInternal.InternalEditorUtility.layers);
}
else
{
EditorGUI.LabelField(position, label.text, "Use [LayerMask] with LayerMask fields only");
}
EditorGUI.EndProperty();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6c6b274ce7b14e91bf5a294a23ba0609
timeCreated: 1758711663

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using System;
namespace AppleHills.Core.Settings
{
@@ -38,8 +40,8 @@ namespace AppleHills.Core.Settings
// Dictionary to cache loaded settings
private Dictionary<System.Type, BaseDeveloperSettings> _settingsCache = new Dictionary<System.Type, BaseDeveloperSettings>();
// Default developer settings stored in the Resources folder
[SerializeField] private string _resourcesPath = "Settings/Developer";
// Path prefix for addressable developer settings
[SerializeField] private string _addressablePath = "Settings/Developer";
private void Awake()
{
@@ -71,16 +73,35 @@ namespace AppleHills.Core.Settings
return cachedSettings as T;
}
// Load from Resources if not cached
T settings = Resources.Load<T>($"{_resourcesPath}/{type.Name}");
// Load from Addressables if not cached
string key = $"{_addressablePath}/{type.Name}";
if (settings != null)
try
{
_settingsCache[type] = settings;
return settings;
T settings = Addressables.LoadAssetAsync<T>(key).WaitForCompletion();
if (settings != null)
{
_settingsCache[type] = settings;
return settings;
}
}
catch (Exception e)
{
Debug.LogError($"Failed to load developer settings at '{key}': {e.Message}");
}
Debug.LogWarning($"Developer settings of type {type.Name} not found at addressable path '{key}'");
// Fallback to Resources for backward compatibility
T resourcesSettings = Resources.Load<T>($"{_addressablePath}/{type.Name}");
if (resourcesSettings != null)
{
Debug.Log($"Found developer settings in Resources instead of Addressables at '{_addressablePath}/{type.Name}'");
_settingsCache[type] = resourcesSettings;
return resourcesSettings;
}
Debug.LogWarning($"Developer settings of type {type.Name} not found in Resources/{_resourcesPath}");
return null;
}

View File

@@ -54,11 +54,9 @@ namespace AppleHills.Core.Settings
[Header("Obstacle System")]
[Tooltip("Layer for obstacles to be placed on")]
[Layer]
[SerializeField] private int obstacleLayer = 9;
[Tooltip("Layer mask for tile collision detection during obstacle spawn validation")]
[SerializeField] private LayerMask obstacleTileLayerMask = -1;
[Tooltip("Whether to use object pooling for obstacles")]
[SerializeField] private bool obstacleUseObjectPooling = true;
@@ -67,7 +65,73 @@ namespace AppleHills.Core.Settings
[Tooltip("Total maximum size of obstacle pool across all prefab types")]
[SerializeField] private int obstacleTotalMaxPoolSize = 15;
[Header("Trench Tile System")]
[Tooltip("Layer for trench tiles to be placed on")]
[Layer]
[SerializeField] private int trenchTileLayer = 13; // QuarryTrenchTile layer
[Tooltip("Whether to use object pooling for trench tiles")]
[SerializeField] private bool trenchTileUseObjectPooling = true;
[Tooltip("Maximum objects per prefab type in trench tile pool")]
[SerializeField] private int trenchTileMaxPerPrefabPoolSize = 2;
[Tooltip("Total maximum size of trench tile pool across all prefab types")]
[SerializeField] private int trenchTileTotalMaxPoolSize = 10;
[Header("Player Blink Behavior")]
[Tooltip("Color to blink to when taking damage (typically red for damage indication)")]
[SerializeField] private Color playerBlinkDamageColor = Color.red;
[Tooltip("How fast to blink between normal and damage colors (seconds between color changes)")]
[SerializeField] private float playerBlinkRate = 0.15f;
[Tooltip("Alpha value for the damage color (0 = transparent, 1 = opaque)")]
[Range(0f, 1f)]
[SerializeField] private float playerDamageColorAlpha = 0.7f;
[Header("Player Wobble Behavior")]
[Tooltip("Frequency of wobble (higher = faster rocking)")]
[SerializeField] private float playerWobbleFrequency = 1.5f;
[Tooltip("Base wobble amplitude in degrees from horizontal")]
[SerializeField] private float playerBaseWobbleAmplitude = 8f;
[Tooltip("How much speed affects amplitude")]
[SerializeField] private float playerSpeedToAmplitude = 2f;
[Tooltip("Maximum allowed rotation in degrees")]
[SerializeField] private float playerMaxRotationLimit = 45f;
[Tooltip("Frequency of vertical bobbing")]
[SerializeField] private float playerVerticalFrequency = 0.5f;
[Tooltip("How far the object moves up/down")]
[SerializeField] private float playerVerticalAmplitude = 0.5f;
[Tooltip("How quickly velocity changes are smoothed")]
[SerializeField] private float playerVelocitySmoothing = 10f;
[Tooltip("How quickly rotation is smoothed")]
[SerializeField] private float playerRotationSmoothing = 10f;
[Header("Collision Settings")]
[Tooltip("Layer mask for obstacle detection - configure which layers contain obstacles")]
[LayerMask]
[SerializeField] private LayerMask playerObstacleLayerMask = -1;
[Tooltip("Whether to block player input during damage immunity period")]
[SerializeField] private bool blockInputDuringImmunity = true;
[Tooltip("Type of bump response: 0=Impulse, 1=SmoothToCenter")]
[SerializeField] private int bumpMode = 0;
[Tooltip("Animation curve controlling bump movement over time")]
[SerializeField] private AnimationCurve bumpCurve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 2f),
new Keyframe(1f, 1f, 0f, 0f));
// Bubble properties access
public bool BubbleUseObjectPooling => bubbleUseObjectPooling;
public int BubbleInitialPoolSize => bubbleInitialPoolSize;
@@ -86,10 +150,36 @@ namespace AppleHills.Core.Settings
// Obstacle properties access
public int ObstacleLayer => obstacleLayer;
public LayerMask ObstacleTileLayerMask => obstacleTileLayerMask;
public bool ObstacleUseObjectPooling => obstacleUseObjectPooling;
public int ObstacleMaxPerPrefabPoolSize => obstacleMaxPerPrefabPoolSize;
public int ObstacleTotalMaxPoolSize => obstacleTotalMaxPoolSize;
// Trench Tile System properties
public int TrenchTileLayer => trenchTileLayer;
public bool TrenchTileUseObjectPooling => trenchTileUseObjectPooling;
public int TrenchTileMaxPerPrefabPoolSize => trenchTileMaxPerPrefabPoolSize;
public int TrenchTileTotalMaxPoolSize => trenchTileTotalMaxPoolSize;
// Player Blink Behavior properties
public Color PlayerBlinkDamageColor => playerBlinkDamageColor;
public float PlayerBlinkRate => playerBlinkRate;
public float PlayerDamageColorAlpha => playerDamageColorAlpha;
// Player Wobble Behavior properties
public float PlayerWobbleFrequency => playerWobbleFrequency;
public float PlayerBaseWobbleAmplitude => playerBaseWobbleAmplitude;
public float PlayerSpeedToAmplitude => playerSpeedToAmplitude;
public float PlayerMaxRotationLimit => playerMaxRotationLimit;
public float PlayerVerticalFrequency => playerVerticalFrequency;
public float PlayerVerticalAmplitude => playerVerticalAmplitude;
public float PlayerVelocitySmoothing => playerVelocitySmoothing;
public float PlayerRotationSmoothing => playerRotationSmoothing;
// Collision Settings properties
public LayerMask PlayerObstacleLayerMask => playerObstacleLayerMask;
public bool BlockInputDuringImmunity => blockInputDuringImmunity;
public int BumpMode => bumpMode;
public AnimationCurve BumpCurve => bumpCurve;
public override void OnValidate()
{
@@ -122,6 +212,26 @@ namespace AppleHills.Core.Settings
// Validate obstacle settings
obstacleMaxPerPrefabPoolSize = Mathf.Max(1, obstacleMaxPerPrefabPoolSize);
obstacleTotalMaxPoolSize = Mathf.Max(obstacleMaxPerPrefabPoolSize, obstacleTotalMaxPoolSize);
// Validate Trench Tile settings
trenchTileMaxPerPrefabPoolSize = Mathf.Max(1, trenchTileMaxPerPrefabPoolSize);
trenchTileTotalMaxPoolSize = Mathf.Max(trenchTileMaxPerPrefabPoolSize, trenchTileTotalMaxPoolSize);
// Validate Player Blink settings
playerBlinkRate = Mathf.Max(0.01f, playerBlinkRate);
playerDamageColorAlpha = Mathf.Clamp01(playerDamageColorAlpha);
// Validate Player Wobble settings
playerWobbleFrequency = Mathf.Max(0.01f, playerWobbleFrequency);
playerBaseWobbleAmplitude = Mathf.Max(0f, playerBaseWobbleAmplitude);
playerMaxRotationLimit = Mathf.Max(0f, playerMaxRotationLimit);
playerVerticalFrequency = Mathf.Max(0.01f, playerVerticalFrequency);
playerVerticalAmplitude = Mathf.Max(0f, playerVerticalAmplitude);
playerVelocitySmoothing = Mathf.Max(0.1f, playerVelocitySmoothing);
playerRotationSmoothing = Mathf.Max(0.1f, playerRotationSmoothing);
// Validate Collision settings
bumpMode = Mathf.Clamp(bumpMode, 0, 1);
}
}
}

View File

@@ -105,6 +105,19 @@ namespace AppleHills.Core.Settings
[Tooltip("Maximum movement speed for spawned obstacles")]
[SerializeField] private float endlessDescenderObstacleMaxMoveSpeed = 4f;
[Header("Endless Descender - Collision Handling")]
[Tooltip("Duration in seconds of damage immunity after being hit")]
[SerializeField] private float endlessDescenderDamageImmunityDuration = 1.0f;
[Tooltip("Force strength for impulse bumps - higher values push further toward center")]
[SerializeField] private float endlessDescenderBumpForce = 5.0f;
[Tooltip("Speed for smooth movement to center (units per second)")]
[SerializeField] private float endlessDescenderSmoothMoveSpeed = 8.0f;
[Tooltip("Whether to block player input during bump movement")]
[SerializeField] private bool endlessDescenderBlockInputDuringBump = true;
// IMinigameSettings implementation - Basic Movement
public float EndlessDescenderLerpSpeed => endlessDescenderLerpSpeed;
public float EndlessDescenderMaxOffset => endlessDescenderMaxOffset;
@@ -149,6 +162,12 @@ namespace AppleHills.Core.Settings
public float EndlessDescenderObstacleMinMoveSpeed => endlessDescenderObstacleMinMoveSpeed;
public float EndlessDescenderObstacleMaxMoveSpeed => endlessDescenderObstacleMaxMoveSpeed;
// IMinigameSettings implementation - Collision Handling
public float EndlessDescenderDamageImmunityDuration => endlessDescenderDamageImmunityDuration;
public float EndlessDescenderBumpForce => endlessDescenderBumpForce;
public float EndlessDescenderSmoothMoveSpeed => endlessDescenderSmoothMoveSpeed;
public bool EndlessDescenderBlockInputDuringBump => endlessDescenderBlockInputDuringBump;
public override void OnValidate()
{
base.OnValidate();
@@ -205,6 +224,11 @@ namespace AppleHills.Core.Settings
endlessDescenderObstacleSpawnCollisionRadius = Mathf.Max(0.1f, endlessDescenderObstacleSpawnCollisionRadius);
endlessDescenderObstacleMinMoveSpeed = Mathf.Max(0.1f, endlessDescenderObstacleMinMoveSpeed);
endlessDescenderObstacleMaxMoveSpeed = Mathf.Max(endlessDescenderObstacleMinMoveSpeed, endlessDescenderObstacleMaxMoveSpeed);
// Validate collision settings
endlessDescenderDamageImmunityDuration = Mathf.Max(0.1f, endlessDescenderDamageImmunityDuration);
endlessDescenderBumpForce = Mathf.Max(0.1f, endlessDescenderBumpForce);
endlessDescenderSmoothMoveSpeed = Mathf.Max(0.1f, endlessDescenderSmoothMoveSpeed);
}
}
}

View File

@@ -0,0 +1,20 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Attribute to indicate a field should be drawn using the layer selector dropdown
/// </summary>
public class LayerAttribute : PropertyAttribute
{
// No properties needed - this is a marker attribute
}
/// <summary>
/// Attribute to indicate a field should be drawn using the layer mask selector dropdown
/// </summary>
public class LayerMaskAttribute : PropertyAttribute
{
// No properties needed - this is a marker attribute
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7c64dbd728524f23bda766b57a388207
timeCreated: 1758711688

View File

@@ -88,5 +88,11 @@ namespace AppleHills.Core.Settings
float EndlessDescenderObstacleSpawnCollisionRadius { get; }
float EndlessDescenderObstacleMinMoveSpeed { get; }
float EndlessDescenderObstacleMaxMoveSpeed { get; }
// Endless Descender - Collision Handling
float EndlessDescenderDamageImmunityDuration { get; }
float EndlessDescenderBumpForce { get; }
float EndlessDescenderSmoothMoveSpeed { get; }
bool EndlessDescenderBlockInputDuringBump { get; }
}
}

View File

@@ -8,6 +8,24 @@ namespace Minigames.DivingForPictures
/// </summary>
public class ObstacleCollision : PlayerCollisionBehavior
{
protected override void OnEnable()
{
base.OnEnable();
// Subscribe to immunity events
OnImmunityStarted += HandleImmunityStarted;
OnImmunityEnded += HandleImmunityEnded;
}
protected override void OnDisable()
{
// Unsubscribe from immunity events
OnImmunityStarted -= HandleImmunityStarted;
OnImmunityEnded -= HandleImmunityEnded;
base.OnDisable();
}
protected override void HandleCollisionResponse(Collider2D obstacle)
{
// Mark the obstacle as having dealt damage to prevent multiple hits
@@ -21,21 +39,20 @@ namespace Minigames.DivingForPictures
}
/// <summary>
/// Override to prevent input blocking during damage immunity
/// Since obstacles pass through the player, we don't want to block input
/// Handler for immunity started event - replaces OnImmunityStart method
/// </summary>
protected override void OnImmunityStart()
private void HandleImmunityStarted()
{
Debug.Log($"[ObstacleCollision] Damage immunity started for {damageImmunityDuration} seconds");
Debug.Log($"[ObstacleCollision] Damage immunity started for {_gameSettings.EndlessDescenderDamageImmunityDuration} seconds");
// Don't block input for obstacle damage - let player keep moving
// The shared immunity system will handle the collision prevention
}
/// <summary>
/// Override to handle immunity end
/// Handler for immunity ended event - replaces OnImmunityEnd method
/// </summary>
protected override void OnImmunityEnd()
private void HandleImmunityEnded()
{
Debug.Log($"[ObstacleCollision] Damage immunity ended");
// No special handling needed - shared immunity system handles collider re-enabling

View File

@@ -298,8 +298,9 @@ namespace Minigames.DivingForPictures
/// </summary>
private bool IsValidSpawnPosition(Vector3 position)
{
// Use OverlapCircle to check for collisions with tiles
Collider2D collision = Physics2D.OverlapCircle(position, _settings.EndlessDescenderObstacleSpawnCollisionRadius, _devSettings.ObstacleTileLayerMask);
// Use OverlapCircle to check for collisions with tiles using just the layer
// Convert the single layer to a layer mask inline (1 << layerNumber)
Collider2D collision = Physics2D.OverlapCircle(position, _settings.EndlessDescenderObstacleSpawnCollisionRadius, 1 << _devSettings.TrenchTileLayer);
return collision == null;
}

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using System.Collections;
using AppleHills.Core.Settings;
namespace Minigames.DivingForPictures
{
@@ -9,28 +10,27 @@ namespace Minigames.DivingForPictures
/// </summary>
public class PlayerBlinkBehavior : MonoBehaviour
{
[Header("Blink Settings")]
[Tooltip("Color to blink to when taking damage (typically red for damage indication)")]
[SerializeField] private Color damageBlinkColor = Color.red;
[Tooltip("How fast to blink between normal and damage colors (seconds between color changes)")]
[SerializeField] private float blinkRate = 0.15f;
[Tooltip("Alpha value for the damage color (0 = transparent, 1 = opaque)")]
[Range(0f, 1f)]
[SerializeField] private float damageColorAlpha = 0.7f;
[Header("References")]
[Tooltip("The SpriteRenderer to apply blink effects to (auto-assigned if empty)")]
[SerializeField] private SpriteRenderer targetSpriteRenderer;
// Developer settings reference
private DivingDeveloperSettings _devSettings;
private bool _isBlinking;
private bool _isShowingDamageColor;
private Coroutine _blinkCoroutine;
private Color _originalColor; // Missing field declaration
private Color _originalColor;
private void Awake()
{
// Get developer settings
_devSettings = GameManager.GetDeveloperSettings<DivingDeveloperSettings>();
if (_devSettings == null)
{
Debug.LogError("[PlayerBlinkBehavior] Failed to load developer settings!");
}
// Auto-assign sprite renderer if not set
if (targetSpriteRenderer == null)
{
@@ -51,192 +51,101 @@ namespace Minigames.DivingForPictures
return;
}
// Store original color
// Store the original color
_originalColor = targetSpriteRenderer.color;
// Ensure damage color has the correct alpha
damageBlinkColor.a = damageColorAlpha;
}
private void OnEnable()
{
// Subscribe to immunity events (renamed from damage events)
PlayerCollisionBehavior.OnImmunityStarted += StartBlinking;
PlayerCollisionBehavior.OnImmunityEnded += StopBlinking;
// Subscribe to damage events
PlayerCollisionBehavior.OnDamageTaken += StartBlinkEffect;
}
private void OnDisable()
{
// Unsubscribe from immunity events
PlayerCollisionBehavior.OnImmunityStarted -= StartBlinking;
PlayerCollisionBehavior.OnImmunityEnded -= StopBlinking;
// Unsubscribe to prevent memory leaks
PlayerCollisionBehavior.OnDamageTaken -= StartBlinkEffect;
// Stop any ongoing blink effect
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
_blinkCoroutine = null;
}
// Restore original color
RestoreOriginalColor();
}
/// <summary>
/// Starts the red blinking effect when damage begins
/// </summary>
private void StartBlinking()
{
if (targetSpriteRenderer == null) return;
Debug.Log("[PlayerBlinkBehavior] Starting damage blink effect");
// Stop any existing blink coroutine
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
}
_isBlinking = true;
_isShowingDamageColor = false;
// Start the blink coroutine
_blinkCoroutine = StartCoroutine(BlinkCoroutine());
}
/// <summary>
/// Stops the blinking effect when damage ends
/// </summary>
private void StopBlinking()
{
Debug.Log("[PlayerBlinkBehavior] Stopping damage blink effect");
_isBlinking = false;
// Stop the blink coroutine
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
_blinkCoroutine = null;
}
// Restore original color
RestoreOriginalColor();
}
/// <summary>
/// Coroutine that handles the blinking animation
/// </summary>
private IEnumerator BlinkCoroutine()
{
while (_isBlinking && targetSpriteRenderer != null)
{
// Toggle between original and damage colors
if (_isShowingDamageColor)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
else
{
targetSpriteRenderer.color = damageBlinkColor;
_isShowingDamageColor = true;
}
// Wait for blink interval
yield return new WaitForSeconds(blinkRate);
}
}
/// <summary>
/// Restores the sprite renderer to its original color
/// </summary>
private void RestoreOriginalColor()
{
// Restore original color if disabled during blinking
if (targetSpriteRenderer != null)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
}
/// <summary>
/// Updates the original color reference (useful if sprite color changes during gameplay)
/// Start the blinking effect coroutine
/// </summary>
public void UpdateOriginalColor()
private void StartBlinkEffect()
{
if (targetSpriteRenderer != null && !_isBlinking)
{
_originalColor = targetSpriteRenderer.color;
}
}
/// <summary>
/// Public method to change blink color at runtime
/// </summary>
public void SetDamageBlinkColor(Color newColor)
{
damageBlinkColor = newColor;
damageBlinkColor.a = damageColorAlpha;
}
/// <summary>
/// Public method to change blink rate at runtime
/// </summary>
public void SetBlinkRate(float rate)
{
blinkRate = rate;
}
/// <summary>
/// Check if currently blinking
/// </summary>
public bool IsBlinking => _isBlinking;
/// <summary>
/// Manually trigger blink effect (useful for testing or other damage sources)
/// </summary>
public void TriggerBlink(float duration)
{
if (_blinkCoroutine != null)
if (targetSpriteRenderer == null || _devSettings == null) return;
// If already blinking, stop the current coroutine
if (_isBlinking && _blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
}
StartCoroutine(ManualBlinkCoroutine(duration));
// Start a new blink coroutine
_blinkCoroutine = StartCoroutine(BlinkCoroutine());
}
/// <summary>
/// Coroutine for manually triggered blink effects
/// Coroutine that handles the blink effect timing
/// </summary>
private IEnumerator ManualBlinkCoroutine(float duration)
private IEnumerator BlinkCoroutine()
{
_isBlinking = true;
_isShowingDamageColor = false;
float elapsed = 0f;
// Create damage color with configured alpha
Color damageColor = _devSettings.PlayerBlinkDamageColor;
damageColor.a = _devSettings.PlayerDamageColorAlpha;
while (elapsed < duration && targetSpriteRenderer != null)
// Wait for immunity to end
PlayerCollisionBehavior.OnImmunityEnded += StopBlinking;
// Blink while immunity is active
while (_isBlinking)
{
// Toggle between original and damage colors
// Toggle between original and damage color
if (_isShowingDamageColor)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
else
{
targetSpriteRenderer.color = damageBlinkColor;
_isShowingDamageColor = true;
targetSpriteRenderer.color = damageColor;
}
yield return new WaitForSeconds(blinkRate);
elapsed += blinkRate;
_isShowingDamageColor = !_isShowingDamageColor;
// Wait for next blink
yield return new WaitForSeconds(_devSettings.PlayerBlinkRate);
}
// Ensure we end with original color
RestoreOriginalColor();
// Restore original color when done blinking
targetSpriteRenderer.color = _originalColor;
}
/// <summary>
/// Called when immunity ends to stop the blinking effect
/// </summary>
private void StopBlinking()
{
// Unsubscribe from the event to avoid memory leaks
PlayerCollisionBehavior.OnImmunityEnded -= StopBlinking;
_isBlinking = false;
// No need to stop the coroutine, it will exit naturally
// This avoids race conditions if immunity ends during a blink cycle
}
}
}

View File

@@ -1,6 +1,7 @@
using UnityEngine;
using System;
using System.Collections;
using AppleHills.Core.Settings;
namespace Minigames.DivingForPictures
{
@@ -10,17 +11,6 @@ namespace Minigames.DivingForPictures
/// </summary>
public abstract class PlayerCollisionBehavior : MonoBehaviour
{
[Header("Collision Settings")]
[Tooltip("Duration in seconds of damage immunity after being hit")]
[SerializeField] protected float damageImmunityDuration = 1.0f;
[Tooltip("Layer mask for obstacle detection - configure which layers contain obstacles")]
[SerializeField] protected LayerMask obstacleLayerMask = -1;
[Header("Input Blocking")]
[Tooltip("Whether to block player input during damage immunity period")]
[SerializeField] protected bool blockInputDuringImmunity;
[Header("References")]
[Tooltip("The player character GameObject (auto-assigned if empty)")]
[SerializeField] protected GameObject playerCharacter;
@@ -28,6 +18,10 @@ namespace Minigames.DivingForPictures
[Tooltip("Reference to the PlayerController component (auto-assigned if empty)")]
[SerializeField] protected PlayerController playerController;
// Settings references
protected IDivingMinigameSettings _gameSettings;
protected DivingDeveloperSettings _devSettings;
// Static shared immunity state across all collision behaviors
private static bool _isGloballyImmune;
private static Coroutine _globalImmunityCoroutine;
@@ -67,305 +61,152 @@ namespace Minigames.DivingForPictures
OnDamageTaken?.Invoke();
}
protected bool wasInputBlocked;
protected virtual void Awake()
/// <summary>
/// Called when the component is enabled
/// </summary>
protected virtual void OnEnable()
{
// Auto-assign if not set in inspector
_allInstances.Add(this);
// Auto-assign references if needed
if (playerCharacter == null)
playerCharacter = gameObject;
if (playerController == null)
playerController = GetComponent<PlayerController>();
// Set up shared collider reference (only once)
// Initialize the shared player collider if not already set
if (_sharedPlayerCollider == null)
{
_sharedPlayerCollider = GetComponent<Collider2D>();
if (_sharedPlayerCollider == null)
{
_sharedPlayerCollider = GetComponentInChildren<Collider2D>();
if (_sharedPlayerCollider != null)
{
Debug.Log($"[PlayerCollisionBehavior] Found collider on child object: {_sharedPlayerCollider.gameObject.name}");
}
}
if (_sharedPlayerCollider == null)
{
Debug.LogError($"[PlayerCollisionBehavior] No Collider2D found on this GameObject or its children!");
Debug.LogError("[PlayerCollisionBehavior] No Collider2D found on this GameObject!");
}
}
// Load settings
_gameSettings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_devSettings = GameManager.GetDeveloperSettings<DivingDeveloperSettings>();
if (_gameSettings == null)
{
Debug.LogError("[PlayerCollisionBehavior] Failed to load game settings!");
}
if (_devSettings == null)
{
Debug.LogError("[PlayerCollisionBehavior] Failed to load developer settings!");
}
}
// Set up coroutine runner (use first instance)
/// <summary>
/// Called when the component is disabled
/// </summary>
protected virtual void OnDisable()
{
_allInstances.Remove(this);
}
/// <summary>
/// Called when a Collider enters the trigger
/// </summary>
protected virtual void OnTriggerEnter2D(Collider2D other)
{
// Don't process collisions if already immune
if (_isGloballyImmune)
return;
// Check if the collider is an obstacle
if ((_devSettings.PlayerObstacleLayerMask.value & (1 << other.gameObject.layer)) != 0)
{
HandleObstacleCollision(other);
}
}
/// <summary>
/// Process collision with an obstacle
/// </summary>
protected virtual void HandleObstacleCollision(Collider2D obstacle)
{
// Trigger global damage and start immunity
TriggerDamageAndImmunity();
// Call the specific collision response for the derived class
HandleCollisionResponse(obstacle);
}
/// <summary>
/// Abstract method for derived classes to implement specific collision responses
/// </summary>
protected abstract void HandleCollisionResponse(Collider2D obstacle);
/// <summary>
/// Trigger damage event and start immunity period
/// </summary>
protected virtual void TriggerDamageAndImmunity()
{
// Make sure we're not already in immunity period
if (_isGloballyImmune)
return;
// Trigger damage event for all listeners (like visual effects)
OnDamageTaken?.Invoke();
// Start immunity period
StartImmunity();
}
/// <summary>
/// Start the immunity period for all collision behaviors
/// </summary>
protected virtual void StartImmunity()
{
// Don't start if already immune
if (_isGloballyImmune)
return;
// Set global immune state
_isGloballyImmune = true;
// Store this instance to run the coroutine if needed
if (_coroutineRunner == null)
{
_coroutineRunner = this;
}
// Register this instance
_allInstances.Add(this);
}
private void OnDestroy()
{
// Unregister this instance
_allInstances.Remove(this);
// Clean up static references if this was the coroutine runner
if (_coroutineRunner == this)
// Block input if configured
if (_devSettings.BlockInputDuringImmunity && playerController != null)
{
if (_globalImmunityCoroutine != null)
{
StopCoroutine(_globalImmunityCoroutine);
_globalImmunityCoroutine = null;
}
_coroutineRunner = null;
// Find a new coroutine runner if there are other instances
foreach (var instance in _allInstances)
{
if (instance != null)
{
_coroutineRunner = instance;
break;
}
}
}
}
/// <summary>
/// Called when another collider enters this trigger collider
/// </summary>
/// <param name="other">The other collider that entered the trigger</param>
private void OnTriggerEnter2D(Collider2D other)
{
Debug.Log($"[{GetType().Name}] OnTriggerEnter2D called with collider: {other.gameObject.name} on layer: {other.gameObject.layer}");
// Check if the other collider is on one of our obstacle layers and we're not immune
if (IsObstacleLayer(other.gameObject.layer) && !_isGloballyImmune)
{
OnCollisionDetected(other);
}
}
/// <summary>
/// Called when a collision with an obstacle is detected
/// </summary>
/// <param name="obstacle">The obstacle collider that was hit</param>
protected virtual void OnCollisionDetected(Collider2D obstacle)
{
if (_isGloballyImmune) return;
// Trigger damage taken event first
OnDamageTaken?.Invoke();
// Start shared immunity period
StartGlobalImmunity();
// Call the specific collision response
HandleCollisionResponse(obstacle);
}
/// <summary>
/// Starts the shared immunity period across all collision behaviors
/// </summary>
private void StartGlobalImmunity()
{
if (_isGloballyImmune) return; // Already immune
_isGloballyImmune = true;
// Disable the shared collider to prevent further collisions
if (_sharedPlayerCollider != null)
{
_sharedPlayerCollider.enabled = false;
// Notify player controller to block input
}
// Stop any existing immunity coroutine
// Trigger event for all listeners
OnImmunityStarted?.Invoke();
// Stop existing coroutine if one is running
if (_globalImmunityCoroutine != null && _coroutineRunner != null)
{
_coroutineRunner.StopCoroutine(_globalImmunityCoroutine);
}
// Start new immunity coroutine
if (_coroutineRunner != null)
{
_globalImmunityCoroutine = _coroutineRunner.StartCoroutine(ImmunityCoroutine());
}
// Notify all instances about immunity start
foreach (var instance in _allInstances)
{
if (instance != null)
{
instance.OnImmunityStart();
}
}
// Broadcast immunity start event
OnImmunityStarted?.Invoke();
// Start immunity timer coroutine on this instance
_globalImmunityCoroutine = StartCoroutine(ImmunityTimerCoroutine());
}
/// <summary>
/// Coroutine that handles the immunity timer
/// Coroutine to handle the immunity duration timer
/// </summary>
private IEnumerator ImmunityCoroutine()
private IEnumerator ImmunityTimerCoroutine()
{
Debug.Log($"[PlayerCollisionBehavior] Starting immunity coroutine for {damageImmunityDuration} seconds");
yield return new WaitForSeconds(damageImmunityDuration);
Debug.Log($"[PlayerCollisionBehavior] Immunity period ended");
// End immunity
// Wait for the immunity duration
yield return new WaitForSeconds(_gameSettings.EndlessDescenderDamageImmunityDuration);
// Reset immunity state
_isGloballyImmune = false;
_globalImmunityCoroutine = null;
// Re-enable the shared collider
if (_sharedPlayerCollider != null)
{
_sharedPlayerCollider.enabled = true;
}
// Notify all instances about immunity end
foreach (var instance in _allInstances)
{
if (instance != null)
{
instance.OnImmunityEnd();
}
}
// Broadcast immunity end event
// Trigger event for all listeners
OnImmunityEnded?.Invoke();
}
/// <summary>
/// Override this method to implement specific collision response behavior
/// </summary>
/// <param name="obstacle">The obstacle that was collided with</param>
protected abstract void HandleCollisionResponse(Collider2D obstacle);
/// <summary>
/// Called when damage immunity starts (called on all instances)
/// </summary>
protected virtual void OnImmunityStart()
{
Debug.Log($"[{GetType().Name}] Damage immunity started for {damageImmunityDuration} seconds");
// Block input if specified
if (blockInputDuringImmunity)
{
BlockPlayerInput();
}
}
/// <summary>
/// Called when damage immunity ends (called on all instances)
/// </summary>
protected virtual void OnImmunityEnd()
{
Debug.Log($"[{GetType().Name}] Damage immunity ended");
// Restore input if it was blocked
if (wasInputBlocked)
{
RestorePlayerInput();
}
}
/// <summary>
/// Restores player input after immunity
/// </summary>
protected virtual void RestorePlayerInput()
{
if (playerController != null && wasInputBlocked)
{
playerController.enabled = true;
wasInputBlocked = false;
// Update the controller's target position to current position to prevent snapping
UpdateControllerTarget();
Debug.Log($"[{GetType().Name}] Player input restored after immunity");
}
}
/// <summary>
/// Blocks player input during immunity
/// </summary>
protected virtual void BlockPlayerInput()
{
if (playerController != null && playerController.enabled)
{
playerController.enabled = false;
wasInputBlocked = true;
Debug.Log($"[{GetType().Name}] Player input blocked during immunity");
}
}
/// <summary>
/// Updates the PlayerController's internal target to match current position
/// </summary>
protected virtual void UpdateControllerTarget()
{
if (playerController != null && playerCharacter != null)
{
// Use reflection to update the private _targetFingerX field
var targetField = typeof(PlayerController)
.GetField("_targetFingerX", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (targetField != null)
{
targetField.SetValue(playerController, playerCharacter.transform.position.x);
}
}
}
/// <summary>
/// Checks if the given layer is included in our obstacle layer mask
/// </summary>
/// <param name="layer">The layer to check</param>
/// <returns>True if the layer is included in the obstacle layer mask</returns>
private bool IsObstacleLayer(int layer)
{
return (obstacleLayerMask.value & (1 << layer)) != 0;
}
/// <summary>
/// Public property to check if player is currently immune (shared across all instances)
/// </summary>
public static bool IsImmune => _isGloballyImmune;
/// <summary>
/// Public method to manually end immunity (affects all collision behaviors)
/// </summary>
public static void EndImmunity()
{
if (_isGloballyImmune && _globalImmunityCoroutine != null && _coroutineRunner != null)
{
_coroutineRunner.StopCoroutine(_globalImmunityCoroutine);
_globalImmunityCoroutine = null;
_isGloballyImmune = false;
// Re-enable the shared collider
if (_sharedPlayerCollider != null)
{
_sharedPlayerCollider.enabled = true;
}
// Notify all instances
foreach (var instance in _allInstances)
{
if (instance != null)
{
instance.OnImmunityEnd();
}
}
OnImmunityEnded?.Invoke();
}
}
}
}

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using System.Collections;
using AppleHills.Core.Settings;
namespace Minigames.DivingForPictures
{
@@ -9,286 +10,159 @@ namespace Minigames.DivingForPictures
/// </summary>
public class TileBumpCollision : PlayerCollisionBehavior
{
[Header("Bump Settings")]
[Tooltip("Type of bump response: Impulse pushes with force, SmoothToCenter moves directly to center")]
[SerializeField] private BumpMode bumpMode = BumpMode.Impulse;
[Tooltip("Force strength for impulse bumps - higher values push further toward center")]
[SerializeField] private float bumpForce = 5.0f;
[Tooltip("Speed for smooth movement to center (units per second)")]
[SerializeField] private float smoothMoveSpeed = 8.0f;
[Tooltip("Animation curve controlling bump movement over time")]
[SerializeField] private AnimationCurve bumpCurve = new AnimationCurve(new Keyframe(0f, 0f, 0f, 2f), new Keyframe(1f, 1f, 0f, 0f));
[Tooltip("Whether to block player input during bump movement")]
[SerializeField] private bool blockInputDuringBump = true;
public enum BumpMode
{
Impulse, // Force-based push toward center (distance depends on force)
SmoothToCenter // Smooth movement to center with configurable speed
}
private bool _isBumping;
private Coroutine _bumpCoroutine;
private bool _bumpInputBlocked; // Tracks bump-specific input blocking
protected override void HandleCollisionResponse(Collider2D obstacle)
{
switch (bumpMode)
// Use bump mode from developer settings
switch (_devSettings.BumpMode)
{
case BumpMode.Impulse:
case 0: // Impulse mode
StartImpulseBump();
break;
case BumpMode.SmoothToCenter:
case 1: // SmoothToCenter mode
StartSmoothMoveToCenter();
break;
}
Debug.Log($"[TileBumpCollision] Collision handled with {bumpMode} mode");
Debug.Log($"[TileBumpCollision] Collision handled with {(_devSettings.BumpMode == 0 ? "Impulse" : "SmoothToCenter")} mode");
}
/// <summary>
/// Starts an impulse bump toward the center with force-based distance
/// Applies an impulse force toward center
/// </summary>
private void StartImpulseBump()
{
if (playerCharacter == null) return;
float currentX = playerCharacter.transform.position.x;
// Calculate bump distance based on force and current position
float directionToCenter = currentX > 0 ? -1f : 1f; // Direction toward center
// Calculate target position - closer to center based on bump force
float bumpDistance = bumpForce * 0.2f; // Scale factor for distance
float targetX = currentX + (directionToCenter * bumpDistance);
// Clamp to center if we would overshoot
if ((currentX > 0 && targetX < 0) || (currentX < 0 && targetX > 0))
{
targetX = 0f;
}
float bumpDuration = 0.5f; // Fixed duration for impulse
StartBump(currentX, targetX, bumpDuration);
Debug.Log($"[TileBumpCollision] Starting impulse bump from X={currentX} to X={targetX} (force={bumpForce})");
}
/// <summary>
/// Starts smooth movement to the center
/// </summary>
private void StartSmoothMoveToCenter()
{
if (playerCharacter == null) return;
float currentX = playerCharacter.transform.position.x;
float distanceToCenter = Mathf.Abs(currentX);
float targetX = 0f; // Always move to center
float bumpDuration = distanceToCenter / smoothMoveSpeed; // Duration based on distance and speed
StartBump(currentX, targetX, bumpDuration);
Debug.Log($"[TileBumpCollision] Starting smooth move to center from X={currentX} (speed={smoothMoveSpeed}, duration={bumpDuration:F2}s)");
}
/// <summary>
/// Common bump initialization using coroutines
/// </summary>
private void StartBump(float startX, float targetX, float duration)
{
// Stop any existing bump
if (_bumpCoroutine != null)
{
StopCoroutine(_bumpCoroutine);
_bumpCoroutine = null;
}
if (_isBumping || playerCharacter == null)
return;
_isBumping = true;
// Block player input if enabled (use bump-specific blocking)
if (blockInputDuringBump && playerController != null && playerController.enabled)
// Block input during bump if configured
if (_gameSettings.EndlessDescenderBlockInputDuringBump && playerController != null)
{
playerController.enabled = false;
_bumpInputBlocked = true;
Debug.Log("[TileBumpCollision] Player input blocked during bump");
// TODO: Implement input blocking
}
// Start bump coroutine
_bumpCoroutine = StartCoroutine(BumpCoroutine(startX, targetX, duration));
// Calculate direction to center (X = 0)
float directionToCenter = Mathf.Sign(-playerCharacter.transform.position.x);
// Start impulse bump coroutine
if (_bumpCoroutine != null)
StopCoroutine(_bumpCoroutine);
_bumpCoroutine = StartCoroutine(ImpulseBumpCoroutine(directionToCenter));
Debug.Log($"[TileBumpCollision] Started impulse bump with force {_gameSettings.EndlessDescenderBumpForce} in direction {directionToCenter}");
}
/// <summary>
/// Coroutine that handles the bump movement over time
/// Smoothly moves the player to center
/// </summary>
private IEnumerator BumpCoroutine(float startX, float targetX, float duration)
private void StartSmoothMoveToCenter()
{
if (_isBumping || playerCharacter == null)
return;
_isBumping = true;
// Block input during bump if configured
if (_gameSettings.EndlessDescenderBlockInputDuringBump && playerController != null)
{
_bumpInputBlocked = true;
// TODO: Implement input blocking
}
// Start smooth move coroutine
if (_bumpCoroutine != null)
StopCoroutine(_bumpCoroutine);
_bumpCoroutine = StartCoroutine(SmoothMoveToCenterCoroutine());
Debug.Log($"[TileBumpCollision] Started smooth move to center with speed {_gameSettings.EndlessDescenderSmoothMoveSpeed}");
}
/// <summary>
/// Coroutine to handle impulse bump movement
/// </summary>
private IEnumerator ImpulseBumpCoroutine(float direction)
{
float elapsedTime = 0f;
float bumpDuration = 0.5f; // Fixed duration for impulse
Vector3 startPosition = playerCharacter.transform.position;
while (elapsedTime < duration)
while (elapsedTime < bumpDuration)
{
elapsedTime += Time.deltaTime;
// Calculate progress and apply curve
float progress = elapsedTime / duration;
float curveValue = bumpCurve.Evaluate(progress);
// Use evaluation time from curve for non-linear movement
float t = elapsedTime / bumpDuration;
float curveValue = _devSettings.BumpCurve.Evaluate(t);
// Interpolate position
float currentX = Mathf.Lerp(startX, targetX, curveValue);
// Calculate movement distance based on force and curve
float distance = _gameSettings.EndlessDescenderBumpForce * curveValue * Time.deltaTime;
// Apply the position to the player character
if (playerCharacter != null)
{
Vector3 currentPos = playerCharacter.transform.position;
playerCharacter.transform.position = new Vector3(currentX, currentPos.y, currentPos.z);
}
// Move the player toward center
Vector3 newPosition = playerCharacter.transform.position;
newPosition.x += direction * distance;
// Clamp to valid range
newPosition.x = Mathf.Clamp(newPosition.x, _gameSettings.EndlessDescenderClampXMin, _gameSettings.EndlessDescenderClampXMax);
// Apply the position
playerCharacter.transform.position = newPosition;
yield return null;
}
// Ensure we end exactly at target
if (playerCharacter != null)
{
Vector3 currentPos = playerCharacter.transform.position;
playerCharacter.transform.position = new Vector3(targetX, currentPos.y, currentPos.z);
}
// Bump finished
// Finish bump
_isBumping = false;
_bumpInputBlocked = false;
_bumpCoroutine = null;
}
/// <summary>
/// Coroutine to handle smooth movement to center
/// </summary>
private IEnumerator SmoothMoveToCenterCoroutine()
{
Vector3 startPosition = playerCharacter.transform.position;
Vector3 targetPosition = new Vector3(0f, startPosition.y, startPosition.z);
if (_bumpInputBlocked)
// Calculate distance to center and expected duration
float distanceToCenter = Mathf.Abs(startPosition.x);
float expectedDuration = distanceToCenter / _gameSettings.EndlessDescenderSmoothMoveSpeed;
float elapsedTime = 0f;
// Move until we reach the center
while (elapsedTime < expectedDuration)
{
RestoreBumpInput();
elapsedTime += Time.deltaTime;
// Calculate progress based on time and curve
float t = elapsedTime / expectedDuration;
float curveValue = _devSettings.BumpCurve.Evaluate(t);
// Calculate interpolated position
Vector3 newPosition = Vector3.Lerp(startPosition, targetPosition, curveValue);
// Apply the position
playerCharacter.transform.position = newPosition;
yield return null;
}
Debug.Log("[TileBumpCollision] Bump movement completed");
}
/// <summary>
/// Restores player input after bump movement
/// </summary>
private void RestoreBumpInput()
{
if (_bumpInputBlocked && playerController != null)
{
playerController.enabled = true;
_bumpInputBlocked = false;
// Update the controller's target position to current position to prevent snapping
UpdateControllerTarget();
Debug.Log("[TileBumpCollision] Player input restored after bump");
}
}
/// <summary>
/// Override to handle bump-specific input blocking during immunity
/// </summary>
protected override void OnImmunityStart()
{
Debug.Log($"[TileBumpCollision] Damage immunity started for {damageImmunityDuration} seconds");
// Block input if specified (in addition to any bump input blocking)
if (blockInputDuringImmunity && !_bumpInputBlocked)
{
BlockPlayerInput();
}
}
/// <summary>
/// Override to handle immunity end and bump cleanup
/// </summary>
protected override void OnImmunityEnd()
{
base.OnImmunityEnd();
// Ensure we end at exactly the center
playerCharacter.transform.position = targetPosition;
// Stop any ongoing bump if immunity ends
if (_isBumping && _bumpCoroutine != null)
{
StopCoroutine(_bumpCoroutine);
_bumpCoroutine = null;
_isBumping = false;
if (_bumpInputBlocked)
{
RestoreBumpInput();
}
Debug.Log("[TileBumpCollision] Bump interrupted by immunity end");
}
}
/// <summary>
/// Called when component is destroyed - cleanup coroutines
/// </summary>
private void OnDestroy()
{
if (_bumpCoroutine != null)
{
StopCoroutine(_bumpCoroutine);
_bumpCoroutine = null;
}
}
/// <summary>
/// Public method to change bump mode at runtime
/// </summary>
public void SetBumpMode(BumpMode mode)
{
bumpMode = mode;
}
/// <summary>
/// Public method to change bump force at runtime
/// </summary>
public void SetBumpForce(float force)
{
bumpForce = force;
}
/// <summary>
/// Public method to change smooth move speed at runtime
/// </summary>
public void SetSmoothMoveSpeed(float speed)
{
smoothMoveSpeed = speed;
}
/// <summary>
/// Check if player is currently being bumped
/// </summary>
public bool IsBumping => _isBumping;
/// <summary>
/// Check if input is currently blocked by bump
/// </summary>
public bool IsBumpInputBlocked => _bumpInputBlocked;
/// <summary>
/// Public method to manually stop bump movement
/// </summary>
public void StopBump()
{
if (_isBumping && _bumpCoroutine != null)
{
StopCoroutine(_bumpCoroutine);
_bumpCoroutine = null;
_isBumping = false;
if (_bumpInputBlocked)
{
RestoreBumpInput();
}
Debug.Log("[TileBumpCollision] Bump manually stopped");
}
// Finish bump
_isBumping = false;
_bumpInputBlocked = false;
_bumpCoroutine = null;
}
}
}

View File

@@ -1,38 +1,13 @@
using UnityEngine;
using AppleHills.Core.Settings;
/// <summary>
/// Adds a wobble (rocking and vertical movement) effect to the object, based on speed and time.
/// </summary>
public class WobbleBehavior : MonoBehaviour
{
[Header("Wobble Settings")]
public float wobbleFrequency = 1.5f;
/// <summary>
/// Max degrees from horizontal.
/// </summary>
public float baseWobbleAmplitude = 8f;
/// <summary>
/// How much speed affects amplitude.
/// </summary>
public float speedToAmplitude = 2f;
/// <summary>
/// Maximum allowed rotation in degrees.
/// </summary>
public float maxRotationLimit = 45f;
[Header("Vertical Movement Settings")]
public float verticalFrequency = 0.5f;
/// <summary>
/// How far the object moves up/down.
/// </summary>
public float verticalAmplitude = 0.5f;
[Header("Smoothing Settings")]
public float velocitySmoothing = 10f;
/// <summary>
/// How quickly rotation is smoothed.
/// </summary>
public float rotationSmoothing = 10f;
// Developer settings reference
private DivingDeveloperSettings _devSettings;
private Vector3 lastPosition;
private float wobbleTime;
@@ -46,47 +21,61 @@ public class WobbleBehavior : MonoBehaviour
/// The current velocity of the object (horizontal only).
/// </summary>
public float Velocity => velocity;
/// <summary>
/// The current vertical offset due to wobble.
/// </summary>
public float VerticalOffset => verticalOffset;
void Start()
private void Awake()
{
// Get developer settings
_devSettings = GameManager.GetDeveloperSettings<DivingDeveloperSettings>();
if (_devSettings == null)
{
Debug.LogError("[WobbleBehavior] Failed to load developer settings!");
}
// Initialize
lastPosition = transform.position;
smoothedVelocity = 0f;
smoothedAngle = 0f;
basePosition = transform.position;
}
void Update()
private void Update()
{
// Calculate movement speed (exclude vertical wobble from velocity calculation)
Vector3 horizontalPosition = transform.position;
horizontalPosition.y = 0f; // Ignore Y for velocity if only horizontal movement matters
Vector3 horizontalLastPosition = lastPosition;
horizontalLastPosition.y = 0f;
velocity = (horizontalPosition - horizontalLastPosition).magnitude / Time.deltaTime;
if (_devSettings == null) return;
// Calculate velocity based on position change
Vector3 positionDelta = transform.position - lastPosition;
velocity = positionDelta.x / Time.deltaTime;
lastPosition = transform.position;
basePosition = transform.position;
// Smooth velocity to prevent jitter
smoothedVelocity = Mathf.Lerp(smoothedVelocity, velocity, velocitySmoothing * Time.deltaTime);
// Smooth velocity changes
smoothedVelocity = Mathf.Lerp(smoothedVelocity, velocity, Time.deltaTime * _devSettings.PlayerVelocitySmoothing);
// Wobble amplitude scales with smoothed speed, but always has a base value
float amplitude = baseWobbleAmplitude + smoothedVelocity * speedToAmplitude;
amplitude = Mathf.Min(amplitude, maxRotationLimit); // Prevent amplitude from exceeding limit
// Calculate wobble rotation based on velocity and time
wobbleTime += Time.deltaTime * _devSettings.PlayerWobbleFrequency;
float rawWobble = Mathf.Sin(wobbleTime);
// Oscillate around horizontal (0 degrees)
wobbleTime += Time.deltaTime * wobbleFrequency;
float targetAngle = Mathf.Sin(wobbleTime) * amplitude;
targetAngle = Mathf.Clamp(targetAngle, -maxRotationLimit, maxRotationLimit);
// Calculate wobble amplitude based on velocity
float velocityFactor = Mathf.Abs(smoothedVelocity) * _devSettings.PlayerSpeedToAmplitude;
float wobbleAmplitude = _devSettings.PlayerBaseWobbleAmplitude + velocityFactor;
// Clamp to maximum rotation limit
wobbleAmplitude = Mathf.Min(wobbleAmplitude, _devSettings.PlayerMaxRotationLimit);
// Calculate target angle
float targetAngle = rawWobble * wobbleAmplitude;
// Smooth angle changes
smoothedAngle = Mathf.Lerp(smoothedAngle, targetAngle, Time.deltaTime * _devSettings.PlayerRotationSmoothing);
// Apply rotation
transform.rotation = Quaternion.Euler(0f, 0f, smoothedAngle);
// Smooth the rotation angle
smoothedAngle = Mathf.Lerp(smoothedAngle, targetAngle, rotationSmoothing * Time.deltaTime);
// Apply rotation (Z axis for 2D)
transform.localRotation = Quaternion.Euler(0f, 0f, smoothedAngle);
// Calculate vertical up/down movement (wave riding) only once
verticalOffset = Mathf.Sin(wobbleTime * verticalFrequency) * verticalAmplitude;
// Calculate vertical bobbing
float time = Time.time * _devSettings.PlayerVerticalFrequency;
verticalOffset = Mathf.Sin(time) * _devSettings.PlayerVerticalAmplitude;
}
}

View File

@@ -17,11 +17,6 @@ namespace Minigames.DivingForPictures
[Tooltip("List of possible trench tile prefabs.")]
[SerializeField] private List<GameObject> tilePrefabs;
[Header("Object Pooling")]
[SerializeField] private bool useObjectPooling = true;
[SerializeField] private int maxPerPrefabPoolSize = 2;
[SerializeField] private int totalMaxPoolSize = 10;
[Header("Events")]
[FormerlySerializedAs("OnTileSpawned")]
public UnityEvent<GameObject> onTileSpawned;
@@ -29,8 +24,9 @@ namespace Minigames.DivingForPictures
[FormerlySerializedAs("OnTileDestroyed")]
public UnityEvent<GameObject> onTileDestroyed;
// Settings reference
// Settings references
private IDivingMinigameSettings _settings;
private DivingDeveloperSettings _devSettings;
// Private fields
private readonly Dictionary<GameObject, float> _tileHeights = new Dictionary<GameObject, float>();
@@ -67,11 +63,18 @@ namespace Minigames.DivingForPictures
// Get settings from GameManager
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_devSettings = GameManager.GetDeveloperSettings<DivingDeveloperSettings>();
if (_settings == null)
{
Debug.LogError("[TrenchTileSpawner] Failed to load diving minigame settings!");
}
if (_devSettings == null)
{
Debug.LogError("[TrenchTileSpawner] Failed to load diving developer settings!");
}
_baseMoveSpeed = _settings?.EndlessDescenderMoveSpeed ?? 3f; // Store the original base speed
// Calculate tile heights for each prefab
@@ -80,7 +83,7 @@ namespace Minigames.DivingForPictures
// Ensure all prefabs have Tile components
ValidateTilePrefabs();
if (useObjectPooling)
if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling)
{
InitializeObjectPool();
}
@@ -196,9 +199,9 @@ namespace Minigames.DivingForPictures
poolGO.transform.SetParent(transform);
_tilePool = poolGO.AddComponent<TrenchTilePool>();
// Set up the pool configuration
_tilePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize;
_tilePool.totalMaxPoolSize = totalMaxPoolSize;
// Set up the pool configuration using developer settings
_tilePool.maxPerPrefabPoolSize = _devSettings.TrenchTileMaxPerPrefabPoolSize;
_tilePool.totalMaxPoolSize = _devSettings.TrenchTileTotalMaxPoolSize;
// Convert the GameObject list to a Tile list
List<Tile> prefabTiles = new List<Tile>(tilePrefabs.Count);
@@ -374,7 +377,7 @@ namespace Minigames.DivingForPictures
_activeTiles.RemoveAt(0);
onTileDestroyed?.Invoke(topTile);
if (useObjectPooling && _tilePool != null)
if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null)
{
// Find the prefab index for this tile
int prefabIndex = GetPrefabIndex(topTile);
@@ -505,7 +508,7 @@ namespace Minigames.DivingForPictures
GameObject tile;
if (useObjectPooling && _tilePool != null)
if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null)
{
tile = _tilePool.GetTile(prefabIndex);
if (tile == null)
@@ -537,7 +540,7 @@ namespace Minigames.DivingForPictures
{
int prefabCount = tilePrefabs.Count;
List<float> weights = new List<float>(prefabCount);
for (int i = 0; i < prefabCount; i++)
{
int lastUsed = _tileLastUsed.TryGetValue(i, out var value) ? value : -prefabCount;
@@ -545,13 +548,13 @@ namespace Minigames.DivingForPictures
float weight = Mathf.Clamp(age, 1, prefabCount * 2); // More unused = higher weight
weights.Add(weight);
}
float totalWeight = 0f;
foreach (var weight in weights)
{
totalWeight += weight;
}
float randomValue = Random.value * totalWeight;
for (int i = 0; i < prefabCount; i++)
{
@@ -561,10 +564,10 @@ namespace Minigames.DivingForPictures
}
randomValue -= weights[i];
}
return Random.Range(0, prefabCount); // fallback
}
/// <summary>
/// Gets the height of a tile based on its prefab or renderer bounds
/// </summary>
@@ -592,18 +595,18 @@ namespace Minigames.DivingForPictures
}
}
}
// If not found, calculate it from the renderer
Renderer renderer = tile.GetComponentInChildren<Renderer>();
if (renderer != null)
{
return renderer.bounds.size.y;
}
// Fallback
return DefaultTileHeight;
}
/// <summary>
/// Gets the index of the prefab that was used to create this tile
/// </summary>
@@ -619,7 +622,7 @@ namespace Minigames.DivingForPictures
for (int i = 0; i < tilePrefabs.Count; i++)
{
if (tilePrefabs[i] == null) continue;
if (tile.name.StartsWith(tilePrefabs[i].name))
{
return i;

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dfde9ecdb3e084d47b97f511323c4a77
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 033961b12e7b4289838d554c2264bacd, type: 3}
m_Name: DivingDeveloperSettings
m_EditorClassIdentifier:
bubbleUseObjectPooling: 1
bubbleInitialPoolSize: 10
bubbleMaxPoolSize: 30
bubbleSpawnInterval: 0.5
bubbleSpeedRange: {x: 0.5, y: 2}
bubbleScaleRange: {x: 0.3, y: 0.7}
bubbleWobbleSpeedRange: {x: 1, y: 3}
bubbleWobbleAmountRange: {x: 0.05, y: 0.15}
bubbleSpawnXMin: -3.5
bubbleSpawnXMax: 3.5
bubbleSpawnY: -5
bubbleWobbleMinScale: 0.2
bubbleWobbleMaxScale: 1.2
bubbleSurfacingSpeedFactor: 0.5
obstacleLayer: 9
obstacleTileLayerMask:
serializedVersion: 2
m_Bits: 0
obstacleUseObjectPooling: 1
obstacleMaxPerPrefabPoolSize: 3
obstacleTotalMaxPoolSize: 15
trenchTileUseObjectPooling: 1
trenchTileMaxPerPrefabPoolSize: 2
trenchTileTotalMaxPoolSize: 10
playerBlinkDamageColor: {r: 1, g: 0, b: 0, a: 1}
playerBlinkRate: 0.15
playerDamageColorAlpha: 0.7
playerWobbleFrequency: 1.5
playerBaseWobbleAmplitude: 8
playerSpeedToAmplitude: 2
playerMaxRotationLimit: 45
playerVerticalFrequency: 0.5
playerVerticalAmplitude: 0.5
playerVelocitySmoothing: 10
playerRotationSmoothing: 10
playerObstacleLayerMask:
serializedVersion: 2
m_Bits: 4294967295
blockInputDuringImmunity: 1
bumpMode: 0
bumpCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 2
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 328ce914b893df646be3ad3c62755453
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -21,7 +21,7 @@ TagManager:
- Interactable
- QuarryObstacle
- QuarryMonster
-
- QuarryTrenchTile
-
-
-