372 lines
13 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
}
|