Revamp the settings system (#7)

- A Settings Provider system to utilize addressables for loading settings at runtime
- An editor UI for easy modifications of the settings objects
- A split out developer settings functionality to keep gameplay and nitty-gritty details separately
- Most settings migrated out of game objects and into the new system
- An additional Editor utility for fetching the settings at editor runtime, for gizmos, visualization etc

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com>
Reviewed-on: #7
This commit is contained in:
2025-09-24 13:33:43 +00:00
parent 4b206b9b2e
commit 63cb3f1a8c
77 changed files with 2795 additions and 978 deletions

View File

@@ -0,0 +1,69 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Collision behavior that handles damage from mobile obstacles.
/// Uses trigger-based collision detection with shared immunity state.
/// </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)
{
// Check if the obstacle is on the ObstacleLayer
if (obstacle.gameObject.layer != _devSettings.ObstacleLayer)
{
// If not on the obstacle layer, don't process the collision
Debug.Log($"[ObstacleCollision] Ignored collision with object on layer {obstacle.gameObject.layer} (expected {_devSettings.ObstacleLayer})");
return;
}
// Mark the obstacle as having dealt damage to prevent multiple hits
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
obstacleComponent.MarkDamageDealt();
}
Debug.Log($"[ObstacleCollision] Player hit by obstacle {obstacle.gameObject.name}");
}
/// <summary>
/// Handler for immunity started event - replaces OnImmunityStart method
/// </summary>
private void HandleImmunityStarted()
{
Debug.Log($"[ObstacleCollision] Damage immunity started for {_gameSettings.DamageImmunityDuration} seconds");
// Don't block input for obstacle damage - let player keep moving
// The shared immunity system will handle the collision prevention
}
/// <summary>
/// Handler for immunity ended event - replaces OnImmunityEnd method
/// </summary>
private void HandleImmunityEnded()
{
Debug.Log($"[ObstacleCollision] Damage immunity ended");
// No special handling needed - shared immunity system handles collider re-enabling
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c9c18dbd013d42ae8c221e6205e4d49c
timeCreated: 1758116850

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,8 @@
using UnityEngine;
using System;
using System.Collections;
using AppleHills.Core.Settings;
using AppleHills.Utilities;
namespace Minigames.DivingForPictures
{
@@ -10,17 +12,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,11 +19,16 @@ 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;
private static MonoBehaviour _coroutineRunner;
private static Collider2D _sharedPlayerCollider;
private static bool wasInputBlocked = false; // Track if input was blocked
// Events for immunity and damage state changes
public static event Action OnImmunityStarted;
@@ -67,214 +63,175 @@ 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;
// Use our extension method to check if the collider's layer is in the obstacle layer mask
if (_devSettings.PlayerObstacleLayerMask.Contains(other.gameObject))
{
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
BlockPlayerInput();
wasInputBlocked = true;
}
// 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.DamageImmunityDuration);
// 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
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)
// Restore player input if it was blocked
if (_devSettings.BlockInputDuringImmunity)
{
RestorePlayerInput();
}
}
// Trigger event for all listeners
OnImmunityEnded?.Invoke();
}
/// <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>
/// Restores player input after immunity
/// </summary>
@@ -291,22 +248,9 @@ namespace Minigames.DivingForPictures
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
/// Updates the player controller's target position to the current position to prevent snapping
/// </summary>
protected virtual void UpdateControllerTarget()
{
@@ -322,50 +266,5 @@ namespace Minigames.DivingForPictures
}
}
}
/// <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,4 +1,5 @@
using UnityEngine;
using AppleHills.Core.Settings;
namespace Minigames.DivingForPictures
{
@@ -8,11 +9,8 @@ namespace Minigames.DivingForPictures
/// </summary>
public class PlayerController : MonoBehaviour, ITouchInputConsumer
{
[Header("Tap Movement")]
[Tooltip("Maximum distance the player can move from a single tap")]
[SerializeField] private float tapMaxDistance = 0.5f;
[Tooltip("How quickly the tap impulse fades (higher = faster stop)")]
[SerializeField] private float tapDecelerationRate = 5.0f;
// Settings reference
private IDivingMinigameSettings _settings;
private float _targetFingerX;
private bool _isTouchActive;
@@ -25,6 +23,13 @@ namespace Minigames.DivingForPictures
void Awake()
{
_originY = transform.position.y;
// Get settings from GameManager
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
if (_settings == null)
{
Debug.LogError("[PlayerController] Failed to load diving minigame settings!");
}
}
void Start()
@@ -42,7 +47,7 @@ namespace Minigames.DivingForPictures
public void OnTap(Vector2 worldPosition)
{
// Debug.Log($"[EndlessDescenderController] OnTap at {worldPosition}");
float targetX = Mathf.Clamp(worldPosition.x, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax);
float targetX = Mathf.Clamp(worldPosition.x, _settings.ClampXMin, _settings.ClampXMax);
// Calculate tap direction (+1 for right, -1 for left)
_tapDirection = Mathf.Sign(targetX - transform.position.x);
@@ -63,7 +68,7 @@ namespace Minigames.DivingForPictures
public void OnHoldStart(Vector2 worldPosition)
{
// Debug.Log($"[EndlessDescenderController] OnHoldStart at {worldPosition}");
_targetFingerX = Mathf.Clamp(worldPosition.x, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax);
_targetFingerX = Mathf.Clamp(worldPosition.x, _settings.ClampXMin, _settings.ClampXMax);
_isTouchActive = true;
}
@@ -73,7 +78,7 @@ namespace Minigames.DivingForPictures
public void OnHoldMove(Vector2 worldPosition)
{
// Debug.Log($"[EndlessDescenderController] OnHoldMove at {worldPosition}");
_targetFingerX = Mathf.Clamp(worldPosition.x, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax);
_targetFingerX = Mathf.Clamp(worldPosition.x, _settings.ClampXMin, _settings.ClampXMax);
}
/// <summary>
@@ -91,9 +96,9 @@ namespace Minigames.DivingForPictures
if (_isTouchActive)
{
float currentX = transform.position.x;
float lerpSpeed = GameManager.Instance.EndlessDescenderLerpSpeed;
float maxOffset = GameManager.Instance.EndlessDescenderMaxOffset;
float exponent = GameManager.Instance.EndlessDescenderSpeedExponent;
float lerpSpeed = _settings.LerpSpeed;
float maxOffset = _settings.MaxOffset;
float exponent = _settings.SpeedExponent;
float targetX = _targetFingerX;
float offset = targetX - currentX;
offset = Mathf.Clamp(offset, -maxOffset, maxOffset);
@@ -103,7 +108,7 @@ namespace Minigames.DivingForPictures
// Prevent overshooting
moveStep = Mathf.Clamp(moveStep, -absOffset, absOffset);
float newX = currentX + moveStep;
newX = Mathf.Clamp(newX, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax);
newX = Mathf.Clamp(newX, _settings.ClampXMin, _settings.ClampXMax);
UpdatePosition(newX);
}
@@ -111,21 +116,21 @@ namespace Minigames.DivingForPictures
else if (_tapImpulseStrength > 0)
{
float currentX = transform.position.x;
float maxOffset = GameManager.Instance.EndlessDescenderMaxOffset;
float lerpSpeed = GameManager.Instance.EndlessDescenderLerpSpeed;
float maxOffset = _settings.MaxOffset;
float lerpSpeed = _settings.LerpSpeed;
// Calculate move distance based on impulse strength
float moveDistance = maxOffset * _tapImpulseStrength * Time.deltaTime * lerpSpeed;
// Limit total movement from single tap
moveDistance = Mathf.Min(moveDistance, tapMaxDistance * _tapImpulseStrength);
moveDistance = Mathf.Min(moveDistance, _settings.TapMaxDistance * _tapImpulseStrength);
// Apply movement in tap direction
float newX = currentX + (moveDistance * _tapDirection);
newX = Mathf.Clamp(newX, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax);
newX = Mathf.Clamp(newX, _settings.ClampXMin, _settings.ClampXMax);
// Reduce impulse strength over time
_tapImpulseStrength -= Time.deltaTime * tapDecelerationRate;
_tapImpulseStrength -= Time.deltaTime * _settings.TapDecelerationRate;
if (_tapImpulseStrength < 0.01f)
{
_tapImpulseStrength = 0f;

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using System.Collections;
using AppleHills.Core.Settings;
namespace Minigames.DivingForPictures
{
@@ -9,35 +10,21 @@ 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)
// Check if the obstacle is on the TrenchTileLayer
if (obstacle.gameObject.layer != _devSettings.TrenchTileLayer)
{
// If not on the trench tile layer, don't process the collision
Debug.Log($"[TileBumpCollision] Ignored collision with object on layer {obstacle.gameObject.layer} (expected {_devSettings.TrenchTileLayer})");
return;
}
// Use bump mode from developer settings
switch (_devSettings.BumpMode)
{
case BumpMode.Impulse:
StartImpulseBump();
@@ -48,7 +35,7 @@ namespace Minigames.DivingForPictures
break;
}
Debug.Log($"[TileBumpCollision] Collision handled with {bumpMode} mode");
Debug.Log($"[TileBumpCollision] Collision handled with {_devSettings.BumpMode} mode");
}
/// <summary>
@@ -64,7 +51,7 @@ namespace Minigames.DivingForPictures
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 bumpDistance = _gameSettings.BumpForce * 0.2f; // Scale factor for distance
float targetX = currentX + (directionToCenter * bumpDistance);
// Clamp to center if we would overshoot
@@ -77,7 +64,7 @@ namespace Minigames.DivingForPictures
StartBump(currentX, targetX, bumpDuration);
Debug.Log($"[TileBumpCollision] Starting impulse bump from X={currentX} to X={targetX} (force={bumpForce})");
Debug.Log($"[TileBumpCollision] Starting impulse bump from X={currentX} to X={targetX} (force={_gameSettings.BumpForce})");
}
/// <summary>
@@ -91,11 +78,11 @@ namespace Minigames.DivingForPictures
float distanceToCenter = Mathf.Abs(currentX);
float targetX = 0f; // Always move to center
float bumpDuration = distanceToCenter / smoothMoveSpeed; // Duration based on distance and speed
float bumpDuration = distanceToCenter / _gameSettings.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)");
Debug.Log($"[TileBumpCollision] Starting smooth move to center from X={currentX} (speed={_gameSettings.SmoothMoveSpeed}, duration={bumpDuration:F2}s)");
}
/// <summary>
@@ -112,14 +99,6 @@ namespace Minigames.DivingForPictures
_isBumping = true;
// Block player input if enabled (use bump-specific blocking)
if (blockInputDuringBump && playerController != null && playerController.enabled)
{
playerController.enabled = false;
_bumpInputBlocked = true;
Debug.Log("[TileBumpCollision] Player input blocked during bump");
}
// Start bump coroutine
_bumpCoroutine = StartCoroutine(BumpCoroutine(startX, targetX, duration));
}
@@ -137,7 +116,7 @@ namespace Minigames.DivingForPictures
// Calculate progress and apply curve
float progress = elapsedTime / duration;
float curveValue = bumpCurve.Evaluate(progress);
float curveValue = _devSettings.BumpCurve.Evaluate(progress);
// Interpolate position
float currentX = Mathf.Lerp(startX, targetX, curveValue);
@@ -146,7 +125,8 @@ namespace Minigames.DivingForPictures
if (playerCharacter != null)
{
Vector3 currentPos = playerCharacter.transform.position;
playerCharacter.transform.position = new Vector3(currentX, currentPos.y, currentPos.z);
currentPos.x = Mathf.Clamp(currentX, _gameSettings.ClampXMin, _gameSettings.ClampXMax);
playerCharacter.transform.position = currentPos;
}
yield return null;
@@ -156,139 +136,15 @@ namespace Minigames.DivingForPictures
if (playerCharacter != null)
{
Vector3 currentPos = playerCharacter.transform.position;
playerCharacter.transform.position = new Vector3(targetX, currentPos.y, currentPos.z);
float clampedTargetX = Mathf.Clamp(targetX, _gameSettings.ClampXMin, _gameSettings.ClampXMax);
playerCharacter.transform.position = new Vector3(clampedTargetX, currentPos.y, currentPos.z);
}
// Bump finished
_isBumping = false;
_bumpCoroutine = null;
if (_bumpInputBlocked)
{
RestoreBumpInput();
}
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();
// 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");
}
}
}
}

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;
}
}