From 44c1e48b8237c32628d85283f14c0619e2f81518 Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Thu, 18 Sep 2025 12:42:05 +0200 Subject: [PATCH] Working collisions on event-based triggers --- .../DivingForPictures/FloatingObstacle.prefab | 154 +++++++++ .../FloatingObstacle.prefab.meta | 7 + .../DivingForPictures/FloatingObstacle.cs | 65 +--- ...lisionBehavior.cs => ObstacleCollision.cs} | 33 +- ...vior.cs.meta => ObstacleCollision.cs.meta} | 0 .../DivingForPictures/ObstacleSpawner.cs | 15 +- .../PlayerCollisionBehavior.cs | 297 ++++++++++++------ .../DivingForPictures/PlayerController.cs | 8 +- .../PlayerPhaseCollisionBehavior.cs | 180 ----------- .../PlayerPhaseCollisionBehavior.cs.meta | 3 - ...lisionBehavior.cs => TileBumpCollision.cs} | 196 ++++++++---- ...vior.cs.meta => TileBumpCollision.cs.meta} | 0 12 files changed, 530 insertions(+), 428 deletions(-) create mode 100644 Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab create mode 100644 Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab.meta rename Assets/Scripts/Minigames/DivingForPictures/{PlayerDamageCollisionBehavior.cs => ObstacleCollision.cs} (70%) rename Assets/Scripts/Minigames/DivingForPictures/{PlayerDamageCollisionBehavior.cs.meta => ObstacleCollision.cs.meta} (100%) delete mode 100644 Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs delete mode 100644 Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs.meta rename Assets/Scripts/Minigames/DivingForPictures/{PlayerBumpCollisionBehavior.cs => TileBumpCollision.cs} (53%) rename Assets/Scripts/Minigames/DivingForPictures/{PlayerBumpCollisionBehavior.cs.meta => TileBumpCollision.cs.meta} (100%) diff --git a/Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab b/Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab new file mode 100644 index 00000000..fb73fd83 --- /dev/null +++ b/Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab @@ -0,0 +1,154 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &4743746373562280435 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7889589254526244618} + - component: {fileID: 2565520060777160406} + - component: {fileID: 586678365535466516} + - component: {fileID: 3635110434097976059} + m_Layer: 11 + m_Name: FloatingObstacle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &7889589254526244618 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4743746373562280435} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -0.62358, y: 4.23222, z: 0} + m_LocalScale: {x: 5, y: 5, z: 5} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &2565520060777160406 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4743746373562280435} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 32718083aef44be2a4318681fcdf5b2e, type: 3} + m_Name: + m_EditorClassIdentifier: + prefabIndex: 0 + damage: 1 + moveSpeed: 2 + enableMovement: 1 + spawner: {fileID: 0} +--- !u!212 &586678365535466516 +SpriteRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4743746373562280435} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} + m_Color: {r: 0.9150943, g: 0, b: 0, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 0.16, y: 0.16} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_MaskInteraction: 0 + m_SpriteSortPoint: 0 +--- !u!61 &3635110434097976059 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4743746373562280435} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_SpriteTilingProperty: + border: {x: 0.049999997, y: 0.049999997, z: 0.049999997, w: 0.049999997} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 0.16, y: 0.16} + newSize: {x: 0.16, y: 0.16} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 0.16, y: 0.16} + m_EdgeRadius: 0 diff --git a/Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab.meta b/Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab.meta new file mode 100644 index 00000000..7e6b992d --- /dev/null +++ b/Assets/Prefabs/Minigames/DivingForPictures/FloatingObstacle.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 315a624eb99600444a51bb1d37c51742 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs b/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs index 67bb347a..9c205a3b 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs @@ -4,8 +4,8 @@ using Pooling; namespace Minigames.DivingForPictures { /// - /// Complete floating obstacle component that handles data, movement, collision detection, and pooling. - /// Obstacles move upward toward the surface and detect collisions with the player. + /// Complete floating obstacle component that handles data, movement, and pooling. + /// Obstacles move upward toward the surface. Collision detection is now handled by the player. /// public class FloatingObstacle : MonoBehaviour, IPoolable { @@ -23,13 +23,6 @@ namespace Minigames.DivingForPictures [Tooltip("Whether this obstacle moves (can be disabled for static obstacles)")] [SerializeField] private bool enableMovement = true; - [Header("Collision Detection")] - [Tooltip("Layer mask for player detection - should match Player layer")] - [SerializeField] private LayerMask playerLayerMask = 1 << 7; // Player layer - - [Tooltip("How often to check for collisions (in seconds)")] - [SerializeField] private float collisionCheckInterval = 0.1f; - [Header("References")] [Tooltip("Reference to the spawner that created this obstacle")] [SerializeField] private ObstacleSpawner spawner; @@ -57,7 +50,6 @@ namespace Minigames.DivingForPictures // Private fields private Collider2D _collider; - private float _collisionCheckTimer; private bool _hasDealtDamage; private Camera _mainCamera; private float _screenTop; @@ -86,7 +78,6 @@ namespace Minigames.DivingForPictures HandleMovement(); } - HandleCollisionDetection(); CheckIfOffScreen(); } @@ -99,57 +90,17 @@ namespace Minigames.DivingForPictures } /// - /// Checks for collisions with the player at regular intervals + /// Marks this obstacle as having dealt damage (called by PlayerDamageCollisionBehavior) /// - private void HandleCollisionDetection() + public void MarkDamageDealt() { - if (_hasDealtDamage || _collider == null) return; - - _collisionCheckTimer -= Time.deltaTime; - if (_collisionCheckTimer <= 0f) + if (!_hasDealtDamage) { - _collisionCheckTimer = collisionCheckInterval; - CheckForPlayerCollision(); + _hasDealtDamage = true; + Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} dealt {damage} damage to player"); } } - /// - /// Checks if this obstacle is colliding with the player - /// - private void CheckForPlayerCollision() - { - Collider2D[] overlapping = new Collider2D[5]; - ContactFilter2D filter = new ContactFilter2D(); - filter.SetLayerMask(playerLayerMask); - filter.useTriggers = true; - - int count = _collider.Overlap(filter, overlapping); - - if (count > 0 && !_hasDealtDamage) - { - // Found collision with player - OnPlayerCollision(overlapping[0]); - } - } - - /// - /// Called when this obstacle collides with the player - /// - /// The player's collider - private void OnPlayerCollision(Collider2D playerCollider) - { - _hasDealtDamage = true; - - // Trigger damage through events (following the existing pattern) - Debug.Log($"[FloatingObstacle] Obstacle dealt {damage} damage to player"); - - // Broadcast damage event using the static method - PlayerCollisionBehavior.TriggerDamageStart(); - - // Continue moving upward - don't destroy or stop the obstacle - Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} hit player and continues moving"); - } - /// /// Checks if the obstacle has moved off-screen and should be despawned /// @@ -202,7 +153,6 @@ namespace Minigames.DivingForPictures public void OnSpawn() { _hasDealtDamage = false; - _collisionCheckTimer = 0f; _screenTop = 0f; // Reset cached screen bounds // Ensure the obstacle is active and visible @@ -217,7 +167,6 @@ namespace Minigames.DivingForPictures public void OnDespawn() { _hasDealtDamage = false; - _collisionCheckTimer = 0f; Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned"); } diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerDamageCollisionBehavior.cs b/Assets/Scripts/Minigames/DivingForPictures/ObstacleCollision.cs similarity index 70% rename from Assets/Scripts/Minigames/DivingForPictures/PlayerDamageCollisionBehavior.cs rename to Assets/Scripts/Minigames/DivingForPictures/ObstacleCollision.cs index 0d22e39f..bb418bb0 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/PlayerDamageCollisionBehavior.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/ObstacleCollision.cs @@ -4,10 +4,9 @@ namespace Minigames.DivingForPictures { /// /// Collision behavior that handles damage from mobile obstacles. - /// Unlike bump collisions, this only deals damage without physical response. - /// Detects collisions between Player layer (7) and QuarryObstacle layer (11). + /// Uses trigger-based collision detection with shared immunity state. /// - public class PlayerDamageCollisionBehavior : PlayerCollisionBehavior + public class ObstacleCollision : PlayerCollisionBehavior { [Header("Damage Settings")] [Tooltip("Base damage amount dealt by obstacles")] @@ -16,14 +15,6 @@ namespace Minigames.DivingForPictures [Tooltip("Whether to use the obstacle's individual damage value or the base damage")] [SerializeField] private bool useObstacleDamageValue = true; - protected override void Awake() - { - base.Awake(); - - // Override the obstacle layer mask to target QuarryObstacle layer (11) - obstacleLayerMask = 1 << 11; // QuarryObstacle layer - } - protected override void HandleCollisionResponse(Collider2D obstacle) { float damageAmount = baseDamage; @@ -35,13 +26,16 @@ namespace Minigames.DivingForPictures if (obstacleComponent != null) { damageAmount = obstacleComponent.Damage; + + // Mark the obstacle as having dealt damage to prevent multiple hits + obstacleComponent.MarkDamageDealt(); } } // Apply damage (this could be extended to integrate with a health system) ApplyDamage(damageAmount); - Debug.Log($"[PlayerDamageCollisionBehavior] Player took {damageAmount} damage from obstacle {obstacle.gameObject.name}"); + Debug.Log($"[ObstacleCollision] Player took {damageAmount} damage from obstacle {obstacle.gameObject.name}"); } /// @@ -53,7 +47,7 @@ namespace Minigames.DivingForPictures { // For now, just log the damage // In a full implementation, this would reduce player health, trigger UI updates, etc. - Debug.Log($"[PlayerDamageCollisionBehavior] Applied {damage} damage to player"); + Debug.Log($"[ObstacleCollision] Applied {damage} damage to player"); // TODO: Integrate with health system when available // Example: playerHealth.TakeDamage(damage); @@ -65,22 +59,19 @@ namespace Minigames.DivingForPictures /// protected override void OnImmunityStart() { - Debug.Log($"[PlayerDamageCollisionBehavior] Damage immunity started for {damageImmunityDuration} seconds"); + Debug.Log($"[ObstacleCollision] Damage immunity started for {damageImmunityDuration} seconds"); // Don't block input for obstacle damage - let player keep moving - // Only broadcast the damage event - TriggerDamageStart(); + // The shared immunity system will handle the collision prevention } /// - /// Override to handle immunity end without input restoration + /// Override to handle immunity end /// protected override void OnImmunityEnd() { - Debug.Log($"[PlayerDamageCollisionBehavior] Damage immunity ended"); - - // Broadcast damage end event - TriggerDamageEnd(); + Debug.Log($"[ObstacleCollision] Damage immunity ended"); + // No special handling needed - shared immunity system handles collider re-enabling } /// diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerDamageCollisionBehavior.cs.meta b/Assets/Scripts/Minigames/DivingForPictures/ObstacleCollision.cs.meta similarity index 100% rename from Assets/Scripts/Minigames/DivingForPictures/PlayerDamageCollisionBehavior.cs.meta rename to Assets/Scripts/Minigames/DivingForPictures/ObstacleCollision.cs.meta diff --git a/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs b/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs index 717940ff..60fc06c6 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs @@ -60,8 +60,11 @@ namespace Minigames.DivingForPictures [SerializeField] private int totalMaxPoolSize = 15; [Header("Layer Settings")] - [Tooltip("Layer mask for tile collision detection - should match WorldObstacle layer")] - [SerializeField] private LayerMask tileLayerMask = 1 << 6; // WorldObstacle layer + [Tooltip("Layer mask for tile collision detection during spawn position validation")] + [SerializeField] private LayerMask tileLayerMask = -1; // Let user configure which layers to avoid + + [Tooltip("Target layer for spawned obstacles - obstacles will be placed on this layer")] + [SerializeField] private int obstacleLayer = 11; // Default to layer 11, but configurable [Header("Events")] [Tooltip("Called when an obstacle is spawned")] @@ -117,11 +120,11 @@ namespace Minigames.DivingForPictures obstaclePrefabs[i].AddComponent(); } - // Ensure the prefab is on the correct layer - if (obstaclePrefabs[i].layer != 11) // QuarryObstacle layer + // Ensure the prefab is on the correct layer (using configurable obstacleLayer) + if (obstaclePrefabs[i].layer != obstacleLayer) { - Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on QuarryObstacle layer (11). Setting layer automatically."); - SetLayerRecursively(obstaclePrefabs[i], 11); + Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on the configured obstacle layer ({obstacleLayer}). Setting layer automatically."); + SetLayerRecursively(obstaclePrefabs[i], obstacleLayer); } } } diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerCollisionBehavior.cs b/Assets/Scripts/Minigames/DivingForPictures/PlayerCollisionBehavior.cs index 4916a7e5..050ed67c 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/PlayerCollisionBehavior.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/PlayerCollisionBehavior.cs @@ -1,36 +1,47 @@ using UnityEngine; using System; +using System.Collections; namespace Minigames.DivingForPictures { /// /// Base class for handling player collisions with world obstacles. - /// Detects collisions between Player layer (7) and WorldObstacle layer (6). + /// Uses trigger-based collision detection with shared immunity state across all collision behaviors. /// 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 - should match WorldObstacle layer")] - [SerializeField] protected LayerMask obstacleLayerMask = 1 << 6; // WorldObstacle layer - + + [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 damage state changes public static event Action OnDamageStart; public static event Action OnDamageEnd; - + + // Instance tracking for shared state management + private static readonly System.Collections.Generic.HashSet _allInstances = + new System.Collections.Generic.HashSet(); + /// /// Public static method to trigger damage start event from external classes /// @@ -38,7 +49,7 @@ namespace Minigames.DivingForPictures { OnDamageStart?.Invoke(); } - + /// /// Public static method to trigger damage end event from external classes /// @@ -46,137 +57,212 @@ namespace Minigames.DivingForPictures { OnDamageEnd?.Invoke(); } - - protected bool isImmune; - protected float immunityTimer; - protected Collider2D playerCollider; + protected bool wasInputBlocked; - + protected virtual void Awake() { // Auto-assign if not set in inspector if (playerCharacter == null) playerCharacter = gameObject; - + if (playerController == null) playerController = GetComponent(); - - // Look for collider on this GameObject first, then in children - playerCollider = GetComponent(); - if (playerCollider == null) + + // Set up shared collider reference (only once) + if (_sharedPlayerCollider == null) { - playerCollider = GetComponentInChildren(); - if (playerCollider != null) + _sharedPlayerCollider = GetComponent(); + if (_sharedPlayerCollider == null) { - Debug.Log($"[{GetType().Name}] Found collider on child object: {playerCollider.gameObject.name}"); + _sharedPlayerCollider = GetComponentInChildren(); + 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!"); } } - - if (playerCollider == null) + + // Set up coroutine runner (use first instance) + if (_coroutineRunner == null) { - Debug.LogError($"[{GetType().Name}] No Collider2D found on this GameObject or its children!"); + _coroutineRunner = this; } + + // Register this instance + _allInstances.Add(this); } - - protected virtual void Update() + + private void OnDestroy() { - // Handle immunity timer - if (isImmune) + // Unregister this instance + _allInstances.Remove(this); + + // Clean up static references if this was the coroutine runner + if (_coroutineRunner == this) { - immunityTimer -= Time.deltaTime; - if (immunityTimer <= 0f) + if (_globalImmunityCoroutine != null) { - isImmune = false; - OnImmunityEnd(); + 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; + } } } - - // Check for collisions if not immune - if (!isImmune && playerCollider != null) - { - CheckForCollisions(); - } } - + /// - /// Checks for collisions with obstacle layer objects + /// Called when another collider enters this trigger collider /// - protected virtual void CheckForCollisions() + /// The other collider that entered the trigger + private void OnTriggerEnter2D(Collider2D other) { - // Get all colliders overlapping with the player - Collider2D[] overlapping = new Collider2D[10]; - ContactFilter2D filter = new ContactFilter2D(); - filter.SetLayerMask(obstacleLayerMask); - filter.useTriggers = true; + Debug.Log($"[{GetType().Name}] OnTriggerEnter2D called with collider: {other.gameObject.name} on layer: {other.gameObject.layer}"); - int count = playerCollider.Overlap(filter, overlapping); - - if (count > 0) + // Check if the other collider is on one of our obstacle layers and we're not immune + if (IsObstacleLayer(other.gameObject.layer) && !_isGloballyImmune) { - // Found collision, trigger response - OnCollisionDetected(overlapping[0]); + OnCollisionDetected(other); } } - + /// /// Called when a collision with an obstacle is detected /// /// The obstacle collider that was hit protected virtual void OnCollisionDetected(Collider2D obstacle) { - if (isImmune) return; - - // Start immunity period - isImmune = true; - immunityTimer = damageImmunityDuration; - + if (_isGloballyImmune) return; + + // Start shared immunity period + StartGlobalImmunity(); + // Call the specific collision response HandleCollisionResponse(obstacle); - - // Notify about immunity start - OnImmunityStart(); } - + + /// + /// Starts the shared immunity period across all collision behaviors + /// + 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 damage start event + OnDamageStart?.Invoke(); + } + + /// + /// Coroutine that handles the immunity timer + /// + 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 damage end event + OnDamageEnd?.Invoke(); + } + /// /// Override this method to implement specific collision response behavior /// /// The obstacle that was collided with protected abstract void HandleCollisionResponse(Collider2D obstacle); - + /// - /// Called when damage immunity starts + /// Called when damage immunity starts (called on all instances) /// protected virtual void OnImmunityStart() { Debug.Log($"[{GetType().Name}] Damage immunity started for {damageImmunityDuration} seconds"); - + // Block input if specified if (blockInputDuringImmunity) { BlockPlayerInput(); } - - // Broadcast damage start event - OnDamageStart?.Invoke(); } - + /// - /// Called when damage immunity ends + /// Called when damage immunity ends (called on all instances) /// protected virtual void OnImmunityEnd() { Debug.Log($"[{GetType().Name}] Damage immunity ended"); - + // Restore input if it was blocked if (wasInputBlocked) { RestorePlayerInput(); } - - // Broadcast damage end event - OnDamageEnd?.Invoke(); } - + /// /// Restores player input after immunity /// @@ -186,14 +272,14 @@ namespace Minigames.DivingForPictures { 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"); } } - + /// /// Blocks player input during immunity /// @@ -206,7 +292,7 @@ namespace Minigames.DivingForPictures Debug.Log($"[{GetType().Name}] Player input blocked during immunity"); } } - + /// /// Updates the PlayerController's internal target to match current position /// @@ -217,22 +303,57 @@ namespace Minigames.DivingForPictures // 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); } } } - + /// - /// Public property to check if player is currently immune + /// Checks if the given layer is included in our obstacle layer mask /// - public bool IsImmune => isImmune; - + /// The layer to check + /// True if the layer is included in the obstacle layer mask + private bool IsObstacleLayer(int layer) + { + return (obstacleLayerMask.value & (1 << layer)) != 0; + } + /// - /// Remaining immunity time + /// Public property to check if player is currently immune (shared across all instances) /// - public float RemainingImmunityTime => immunityTimer; + public static bool IsImmune => _isGloballyImmune; + + /// + /// Public method to manually end immunity (affects all collision behaviors) + /// + 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(); + } + } + + OnDamageEnd?.Invoke(); + } + } } } diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerController.cs b/Assets/Scripts/Minigames/DivingForPictures/PlayerController.cs index 899fea46..06f2392b 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/PlayerController.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/PlayerController.cs @@ -31,7 +31,7 @@ namespace Minigames.DivingForPictures /// public void OnTap(Vector2 worldPosition) { - Debug.Log($"[EndlessDescenderController] OnTap at {worldPosition}"); + // Debug.Log($"[EndlessDescenderController] OnTap at {worldPosition}"); _targetFingerX = Mathf.Clamp(worldPosition.x, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax); _isTouchActive = true; } @@ -41,7 +41,7 @@ namespace Minigames.DivingForPictures /// public void OnHoldStart(Vector2 worldPosition) { - Debug.Log($"[EndlessDescenderController] OnHoldStart at {worldPosition}"); + // Debug.Log($"[EndlessDescenderController] OnHoldStart at {worldPosition}"); _targetFingerX = Mathf.Clamp(worldPosition.x, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax); _isTouchActive = true; } @@ -51,7 +51,7 @@ namespace Minigames.DivingForPictures /// public void OnHoldMove(Vector2 worldPosition) { - Debug.Log($"[EndlessDescenderController] OnHoldMove at {worldPosition}"); + // Debug.Log($"[EndlessDescenderController] OnHoldMove at {worldPosition}"); _targetFingerX = Mathf.Clamp(worldPosition.x, GameManager.Instance.EndlessDescenderClampXMin, GameManager.Instance.EndlessDescenderClampXMax); } @@ -60,7 +60,7 @@ namespace Minigames.DivingForPictures /// public void OnHoldEnd(Vector2 worldPosition) { - Debug.Log($"[EndlessDescenderController] OnHoldEnd at {worldPosition}"); + // Debug.Log($"[EndlessDescenderController] OnHoldEnd at {worldPosition}"); _isTouchActive = false; } diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs b/Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs deleted file mode 100644 index 678084a5..00000000 --- a/Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs +++ /dev/null @@ -1,180 +0,0 @@ -using UnityEngine; - -namespace Minigames.DivingForPictures -{ - /// - /// Collision behavior that grants temporary immunity and allows the player to phase through obstacles. - /// During immunity, the player can pass through all obstacles without further collision detection. - /// - public class PlayerPhaseCollisionBehavior : PlayerCollisionBehavior - { - [Header("Phase Settings")] - [Tooltip("Whether to disable the player's collider during immunity to allow phasing through obstacles")] - [SerializeField] private bool disableColliderDuringImmunity = true; - - [Tooltip("How fast the player sprite blinks during phase mode (seconds between blinks)")] - [SerializeField] private float visualFeedbackBlinkRate = 0.1f; - - private SpriteRenderer _spriteRenderer; - private bool _originalColliderState; - private bool _isBlinking; - private float _blinkTimer; - private Color _originalColor; - private float _originalAlpha; - - protected override void Awake() - { - base.Awake(); - - // Get sprite renderer from player character - if (playerCharacter != null) - { - _spriteRenderer = playerCharacter.GetComponent(); - } - - if (_spriteRenderer != null) - { - _originalColor = _spriteRenderer.color; - _originalAlpha = _originalColor.a; - } - - if (playerCollider != null) - { - _originalColliderState = playerCollider.enabled; - } - } - - protected override void Update() - { - base.Update(); - - // Handle visual feedback blinking during immunity - if (_isBlinking && _spriteRenderer != null) - { - _blinkTimer -= Time.deltaTime; - - if (_blinkTimer <= 0f) - { - // Toggle visibility - Color currentColor = _spriteRenderer.color; - currentColor.a = currentColor.a > 0.5f ? 0.3f : _originalAlpha; - _spriteRenderer.color = currentColor; - - _blinkTimer = visualFeedbackBlinkRate; - } - } - } - - protected override void CheckForCollisions() - { - // Override to skip collision detection entirely during immunity if collider is disabled - if (isImmune && disableColliderDuringImmunity) - { - return; // Skip collision detection completely - } - - base.CheckForCollisions(); - } - - protected override void HandleCollisionResponse(Collider2D obstacle) - { - Debug.Log("[PlayerPhaseCollisionBehavior] Collision detected - entering phase mode"); - - // No immediate physical response - just start immunity period - // The immunity will be handled by the base class - } - - protected override void OnImmunityStart() - { - base.OnImmunityStart(); - - // Disable collider to allow phasing through obstacles - if (disableColliderDuringImmunity && playerCollider != null) - { - playerCollider.enabled = false; - Debug.Log("[PlayerPhaseCollisionBehavior] Collider disabled - entering phase mode"); - } - - // Start visual feedback - StartVisualFeedback(); - } - - protected override void OnImmunityEnd() - { - base.OnImmunityEnd(); - - // Re-enable collider - if (playerCollider != null) - { - playerCollider.enabled = _originalColliderState; - Debug.Log("[PlayerPhaseCollisionBehavior] Collider re-enabled - exiting phase mode"); - } - - // Stop visual feedback - StopVisualFeedback(); - } - - /// - /// Starts the visual feedback to indicate immunity/phase mode - /// - private void StartVisualFeedback() - { - if (_spriteRenderer != null) - { - _isBlinking = true; - _blinkTimer = 0f; - - // Start with reduced opacity - Color immunityColor = _originalColor; - immunityColor.a = 0.3f; - _spriteRenderer.color = immunityColor; - } - } - - /// - /// Stops the visual feedback and restores original appearance - /// - private void StopVisualFeedback() - { - _isBlinking = false; - - if (_spriteRenderer != null) - { - // Restore original color and alpha - _spriteRenderer.color = _originalColor; - } - } - - /// - /// Public method to toggle collider behavior during immunity - /// - public void SetColliderDisabling(bool disable) - { - disableColliderDuringImmunity = disable; - } - - /// - /// Check if player is currently in phase mode - /// - public bool IsPhasing => isImmune && disableColliderDuringImmunity; - - /// - /// Manually trigger phase mode (useful for testing or special abilities) - /// - public void TriggerPhaseMode(float duration = -1f) - { - if (duration > 0f) - { - isImmune = true; - immunityTimer = duration; - OnImmunityStart(); - } - else - { - isImmune = true; - immunityTimer = damageImmunityDuration; - OnImmunityStart(); - } - } - } -} diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs.meta b/Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs.meta deleted file mode 100644 index 52c2563c..00000000 --- a/Assets/Scripts/Minigames/DivingForPictures/PlayerPhaseCollisionBehavior.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: d849a517ce3a41249ae9f37d2722cefa -timeCreated: 1758109641 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerBumpCollisionBehavior.cs b/Assets/Scripts/Minigames/DivingForPictures/TileBumpCollision.cs similarity index 53% rename from Assets/Scripts/Minigames/DivingForPictures/PlayerBumpCollisionBehavior.cs rename to Assets/Scripts/Minigames/DivingForPictures/TileBumpCollision.cs index 3f928944..ab9efb77 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/PlayerBumpCollisionBehavior.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/TileBumpCollision.cs @@ -1,12 +1,13 @@ using UnityEngine; +using System.Collections; namespace Minigames.DivingForPictures { /// /// Collision behavior that bumps the player toward the center of the trench. - /// Provides two modes: impulse (force-based push) or smooth movement to center. + /// Uses trigger-based collision detection with coroutine-based bump timing. /// - public class PlayerBumpCollisionBehavior : PlayerCollisionBehavior + public class TileBumpCollision : PlayerCollisionBehavior { [Header("Bump Settings")] [Tooltip("Type of bump response: Impulse pushes with force, SmoothToCenter moves directly to center")] @@ -31,55 +32,9 @@ namespace Minigames.DivingForPictures } private bool _isBumping; - private float _bumpTimer; - private float _bumpStartX; - private float _bumpTargetX; - private float _bumpDuration; + private Coroutine _bumpCoroutine; private bool _bumpInputBlocked; // Tracks bump-specific input blocking - protected override void Update() - { - base.Update(); - - // Handle bump movement - if (_isBumping) - { - _bumpTimer -= Time.deltaTime; - - if (_bumpTimer <= 0f) - { - // Bump finished - _isBumping = false; - if (_bumpInputBlocked) - { - RestoreBumpInput(); - } - - // Ensure we end exactly at target - if (playerCharacter != null) - { - Vector3 currentPos = playerCharacter.transform.position; - playerCharacter.transform.position = new Vector3(_bumpTargetX, currentPos.y, currentPos.z); - } - } - else - { - // Apply bump movement - float progress = 1f - (_bumpTimer / _bumpDuration); - float curveValue = bumpCurve.Evaluate(progress); - - float currentX = Mathf.Lerp(_bumpStartX, _bumpTargetX, curveValue); - - // 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); - } - } - } - } - protected override void HandleCollisionResponse(Collider2D obstacle) { switch (bumpMode) @@ -93,7 +48,7 @@ namespace Minigames.DivingForPictures break; } - Debug.Log($"[PlayerBumpCollisionBehavior] Collision handled with {bumpMode} mode"); + Debug.Log($"[TileBumpCollision] Collision handled with {bumpMode} mode"); } /// @@ -118,14 +73,11 @@ namespace Minigames.DivingForPictures targetX = 0f; } - // Set bump parameters - _bumpStartX = currentX; - _bumpTargetX = targetX; - _bumpDuration = 0.5f; // Fixed duration for impulse + float bumpDuration = 0.5f; // Fixed duration for impulse - StartBump(); + StartBump(currentX, targetX, bumpDuration); - Debug.Log($"[PlayerBumpCollisionBehavior] Starting impulse bump from X={_bumpStartX} to X={_bumpTargetX} (force={bumpForce})"); + Debug.Log($"[TileBumpCollision] Starting impulse bump from X={currentX} to X={targetX} (force={bumpForce})"); } /// @@ -138,31 +90,85 @@ namespace Minigames.DivingForPictures float currentX = playerCharacter.transform.position.x; float distanceToCenter = Mathf.Abs(currentX); - // Set bump parameters - _bumpStartX = currentX; - _bumpTargetX = 0f; // Always move to center - _bumpDuration = distanceToCenter / smoothMoveSpeed; // Duration based on distance and speed + float targetX = 0f; // Always move to center + float bumpDuration = distanceToCenter / smoothMoveSpeed; // Duration based on distance and speed - StartBump(); + StartBump(currentX, targetX, bumpDuration); - Debug.Log($"[PlayerBumpCollisionBehavior] Starting smooth move to center from X={_bumpStartX} (speed={smoothMoveSpeed}, duration={_bumpDuration:F2}s)"); + Debug.Log($"[TileBumpCollision] Starting smooth move to center from X={currentX} (speed={smoothMoveSpeed}, duration={bumpDuration:F2}s)"); } /// - /// Common bump initialization + /// Common bump initialization using coroutines /// - private void StartBump() + private void StartBump(float startX, float targetX, float duration) { + // Stop any existing bump + if (_bumpCoroutine != null) + { + StopCoroutine(_bumpCoroutine); + _bumpCoroutine = null; + } + _isBumping = true; - _bumpTimer = _bumpDuration; // Block player input if enabled (use bump-specific blocking) if (blockInputDuringBump && playerController != null && playerController.enabled) { playerController.enabled = false; _bumpInputBlocked = true; - Debug.Log("[PlayerBumpCollisionBehavior] Player input blocked during bump"); + Debug.Log("[TileBumpCollision] Player input blocked during bump"); } + + // Start bump coroutine + _bumpCoroutine = StartCoroutine(BumpCoroutine(startX, targetX, duration)); + } + + /// + /// Coroutine that handles the bump movement over time + /// + private IEnumerator BumpCoroutine(float startX, float targetX, float duration) + { + float elapsedTime = 0f; + + while (elapsedTime < duration) + { + elapsedTime += Time.deltaTime; + + // Calculate progress and apply curve + float progress = elapsedTime / duration; + float curveValue = bumpCurve.Evaluate(progress); + + // Interpolate position + float currentX = Mathf.Lerp(startX, targetX, curveValue); + + // 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); + } + + 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 + _isBumping = false; + _bumpCoroutine = null; + + if (_bumpInputBlocked) + { + RestoreBumpInput(); + } + + Debug.Log("[TileBumpCollision] Bump movement completed"); } /// @@ -178,22 +184,56 @@ namespace Minigames.DivingForPictures // Update the controller's target position to current position to prevent snapping UpdateControllerTarget(); - Debug.Log("[PlayerBumpCollisionBehavior] Player input restored after bump"); + Debug.Log("[TileBumpCollision] Player input restored after bump"); } } + /// + /// Override to handle bump-specific input blocking during immunity + /// + 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(); + } + } + + /// + /// Override to handle immunity end and bump cleanup + /// protected override void OnImmunityEnd() { base.OnImmunityEnd(); // Stop any ongoing bump if immunity ends - if (_isBumping) + if (_isBumping && _bumpCoroutine != null) { + StopCoroutine(_bumpCoroutine); + _bumpCoroutine = null; _isBumping = false; + if (_bumpInputBlocked) { RestoreBumpInput(); } + + Debug.Log("[TileBumpCollision] Bump interrupted by immunity end"); + } + } + + /// + /// Called when component is destroyed - cleanup coroutines + /// + private void OnDestroy() + { + if (_bumpCoroutine != null) + { + StopCoroutine(_bumpCoroutine); + _bumpCoroutine = null; } } @@ -230,5 +270,25 @@ namespace Minigames.DivingForPictures /// Check if input is currently blocked by bump /// public bool IsBumpInputBlocked => _bumpInputBlocked; + + /// + /// Public method to manually stop bump movement + /// + public void StopBump() + { + if (_isBumping && _bumpCoroutine != null) + { + StopCoroutine(_bumpCoroutine); + _bumpCoroutine = null; + _isBumping = false; + + if (_bumpInputBlocked) + { + RestoreBumpInput(); + } + + Debug.Log("[TileBumpCollision] Bump manually stopped"); + } + } } } diff --git a/Assets/Scripts/Minigames/DivingForPictures/PlayerBumpCollisionBehavior.cs.meta b/Assets/Scripts/Minigames/DivingForPictures/TileBumpCollision.cs.meta similarity index 100% rename from Assets/Scripts/Minigames/DivingForPictures/PlayerBumpCollisionBehavior.cs.meta rename to Assets/Scripts/Minigames/DivingForPictures/TileBumpCollision.cs.meta