Files
AppleHillsProduction/Assets/Scripts/Minigames/DivingForPictures/PlayerCollisionBehavior.cs
tschesky 46755fecb3 Populate minigame with obstacles and monster spawns (#5)
- Simulated "fake" physics and collisions
- Object pooling for tiles, obstacles and monster spawns
- Base monster scoring with proximity triggers and depth multiplier

Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #5
2025-09-21 07:32:56 +00:00

372 lines
13 KiB
C#

using UnityEngine;
using System;
using System.Collections;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Base class for handling player collisions with world obstacles.
/// Uses trigger-based collision detection with shared immunity state across all collision behaviors.
/// </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;
[Tooltip("Reference to the PlayerController component (auto-assigned if empty)")]
[SerializeField] protected PlayerController playerController;
// Static shared immunity state across all collision behaviors
private static bool _isGloballyImmune;
private static Coroutine _globalImmunityCoroutine;
private static MonoBehaviour _coroutineRunner;
private static Collider2D _sharedPlayerCollider;
// Events for immunity and damage state changes
public static event Action OnImmunityStarted;
public static event Action OnImmunityEnded;
public static event Action OnDamageTaken;
// Instance tracking for shared state management
private static readonly System.Collections.Generic.HashSet<PlayerCollisionBehavior> _allInstances =
new System.Collections.Generic.HashSet<PlayerCollisionBehavior>();
/// <summary>
/// Public static method to trigger immunity start event from external classes
/// </summary>
public static void TriggerImmunityStarted()
{
OnImmunityStarted?.Invoke();
}
/// <summary>
/// Public static method to trigger immunity end event from external classes
/// </summary>
public static void TriggerImmunityEnded()
{
OnImmunityEnded?.Invoke();
}
/// <summary>
/// Public static method to trigger damage taken event from external classes
/// </summary>
public static void TriggerDamageTaken()
{
OnDamageTaken?.Invoke();
}
protected bool wasInputBlocked;
protected virtual void Awake()
{
// Auto-assign if not set in inspector
if (playerCharacter == null)
playerCharacter = gameObject;
if (playerController == null)
playerController = GetComponent<PlayerController>();
// Set up shared collider reference (only once)
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!");
}
}
// Set up coroutine runner (use first instance)
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)
{
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;
}
// Stop any existing immunity coroutine
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();
}
/// <summary>
/// Coroutine that handles the immunity timer
/// </summary>
private IEnumerator ImmunityCoroutine()
{
Debug.Log($"[PlayerCollisionBehavior] Starting immunity coroutine for {damageImmunityDuration} seconds");
yield return new WaitForSeconds(damageImmunityDuration);
Debug.Log($"[PlayerCollisionBehavior] Immunity period ended");
// End immunity
_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)
{
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();
}
}
}
}