Working state for minigameobstacles

This commit is contained in:
2025-09-17 16:10:18 +02:00
committed by Michal Pikulski
parent 2ec5c3d855
commit 50070651c5
26 changed files with 4573 additions and 19 deletions

View File

@@ -0,0 +1,233 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Complete floating obstacle component that handles data, movement, collision detection, and pooling.
/// Obstacles move upward toward the surface and detect collisions with the player.
/// </summary>
public class FloatingObstacle : MonoBehaviour, IPoolable
{
[Header("Obstacle Properties")]
[Tooltip("Index of the prefab this obstacle was created from")]
[SerializeField] private int prefabIndex;
[Tooltip("Damage this obstacle deals to the player")]
[SerializeField] private float damage = 1f;
[Tooltip("Movement speed of this obstacle")]
[SerializeField] private float moveSpeed = 2f;
[Header("Movement")]
[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;
// Public properties
public int PrefabIndex
{
get => prefabIndex;
set => prefabIndex = value;
}
public float Damage
{
get => damage;
set => damage = value;
}
public float MoveSpeed
{
get => moveSpeed;
set => moveSpeed = value;
}
public bool HasDealtDamage => _hasDealtDamage;
// Private fields
private Collider2D _collider;
private float _collisionCheckTimer;
private bool _hasDealtDamage;
private Camera _mainCamera;
private float _screenTop;
private void Awake()
{
_collider = GetComponent<Collider2D>();
if (_collider == null)
{
_collider = GetComponentInChildren<Collider2D>();
}
if (_collider == null)
{
Debug.LogError($"[FloatingObstacle] No Collider2D found on {gameObject.name}!");
}
_mainCamera = Camera.main;
}
private void Update()
{
if (enableMovement)
{
HandleMovement();
}
HandleCollisionDetection();
CheckIfOffScreen();
}
/// <summary>
/// Moves the obstacle upward based on its speed
/// </summary>
private void HandleMovement()
{
transform.position += Vector3.up * (moveSpeed * Time.deltaTime);
}
/// <summary>
/// Checks for collisions with the player at regular intervals
/// </summary>
private void HandleCollisionDetection()
{
if (_hasDealtDamage || _collider == null) return;
_collisionCheckTimer -= Time.deltaTime;
if (_collisionCheckTimer <= 0f)
{
_collisionCheckTimer = collisionCheckInterval;
CheckForPlayerCollision();
}
}
/// <summary>
/// Checks if this obstacle is colliding with the player
/// </summary>
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]);
}
}
/// <summary>
/// Called when this obstacle collides with the player
/// </summary>
/// <param name="playerCollider">The player's collider</param>
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");
}
/// <summary>
/// Checks if the obstacle has moved off-screen and should be despawned
/// </summary>
private void CheckIfOffScreen()
{
if (_mainCamera == null) return;
// Calculate screen top if not cached
if (_screenTop == 0f)
{
Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane));
_screenTop = topWorldPoint.y;
}
// Check if obstacle is above screen
if (transform.position.y > _screenTop + 2f) // Extra buffer for safety
{
ReturnToPool();
}
}
/// <summary>
/// Returns this obstacle to the spawner's pool
/// </summary>
private void ReturnToPool()
{
if (spawner != null)
{
spawner.ReturnObstacleToPool(gameObject, prefabIndex);
}
else
{
Debug.LogWarning($"[FloatingObstacle] Cannot return {gameObject.name} to pool - missing spawner reference");
Destroy(gameObject);
}
}
/// <summary>
/// Sets the spawner reference for this obstacle
/// </summary>
/// <param name="obstacleSpawner">The spawner that created this obstacle</param>
public void SetSpawner(ObstacleSpawner obstacleSpawner)
{
spawner = obstacleSpawner;
}
/// <summary>
/// Called when the obstacle is retrieved from the pool
/// </summary>
public void OnSpawn()
{
_hasDealtDamage = false;
_collisionCheckTimer = 0f;
_screenTop = 0f; // Reset cached screen bounds
// Ensure the obstacle is active and visible
gameObject.SetActive(true);
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned");
}
/// <summary>
/// Called when the obstacle is returned to the pool
/// </summary>
public void OnDespawn()
{
_hasDealtDamage = false;
_collisionCheckTimer = 0f;
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned");
}
/// <summary>
/// Public method to manually trigger return to pool (for external systems)
/// </summary>
public void ForceReturnToPool()
{
ReturnToPool();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 32718083aef44be2a4318681fcdf5b2e
timeCreated: 1758117709

View File

@@ -0,0 +1,44 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Manages a pool of floating obstacle objects to reduce garbage collection overhead.
/// Optimized for handling a large number of different obstacle prefab types.
/// </summary>
public class ObstaclePool : MultiPrefabPool<FloatingObstacle>
{
/// <summary>
/// Returns an obstacle to the pool
/// </summary>
/// <param name="obstacle">The obstacle to return to the pool</param>
/// <param name="prefabIndex">The index of the prefab this obstacle was created from</param>
public void ReturnObstacle(GameObject obstacle, int prefabIndex)
{
if (obstacle != null)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
Return(obstacleComponent, prefabIndex);
}
else
{
Debug.LogWarning($"Attempted to return a GameObject without a FloatingObstacle component: {obstacle.name}");
Destroy(obstacle);
}
}
}
/// <summary>
/// Gets an obstacle from the pool, or creates a new one if the pool is empty
/// </summary>
/// <returns>An obstacle instance ready to use</returns>
public GameObject GetObstacle(int prefabIndex)
{
FloatingObstacle obstacleComponent = Get(prefabIndex);
return obstacleComponent.gameObject;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a53ba79246a94dc4a71d2fb0d7214cfb
timeCreated: 1758116804

View File

@@ -0,0 +1,452 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Spawns and manages mobile obstacles for the diving minigame.
/// Uses object pooling and validates spawn positions to avoid colliding with tiles.
/// </summary>
public class ObstacleSpawner : MonoBehaviour
{
[Header("Obstacle Prefabs")]
[Tooltip("List of possible obstacle prefabs to spawn")]
[SerializeField] private List<GameObject> obstaclePrefabs;
[Header("Spawn Settings")]
[Tooltip("Time interval between spawn attempts (in seconds)")]
[SerializeField] private float spawnInterval = 2f;
[Tooltip("Random variation in spawn timing (+/- seconds)")]
[SerializeField] private float spawnIntervalVariation = 0.5f;
[Tooltip("Maximum number of spawn position attempts before skipping")]
[SerializeField] private int maxSpawnAttempts = 10;
[Tooltip("Radius around spawn point to check for tile collisions")]
[SerializeField] private float spawnCollisionRadius = 1f;
[Header("Spawn Position")]
[Tooltip("How far below screen to spawn obstacles")]
[SerializeField] private float spawnDistanceBelowScreen = 2f;
[Tooltip("Horizontal spawn range (distance from center)")]
[SerializeField] private float spawnRangeX = 8f;
[Header("Obstacle Properties Randomization")]
[Tooltip("Minimum movement speed for spawned obstacles")]
[SerializeField] private float minMoveSpeed = 1f;
[Tooltip("Maximum movement speed for spawned obstacles")]
[SerializeField] private float maxMoveSpeed = 4f;
[Tooltip("Minimum damage dealt by obstacles")]
[SerializeField] private float minDamage = 0.5f;
[Tooltip("Maximum damage dealt by obstacles")]
[SerializeField] private float maxDamage = 2f;
[Header("Object Pooling")]
[Tooltip("Whether to use object pooling for obstacles")]
[SerializeField] private bool useObjectPooling = true;
[Tooltip("Maximum objects per prefab type in pool")]
[SerializeField] private int maxPerPrefabPoolSize = 3;
[Tooltip("Total maximum pool size across all prefab types")]
[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
[Header("Events")]
[Tooltip("Called when an obstacle is spawned")]
public UnityEvent<GameObject> onObstacleSpawned;
[Tooltip("Called when an obstacle is returned to pool")]
public UnityEvent<GameObject> onObstacleDestroyed;
// Private fields
private ObstaclePool _obstaclePool;
private Camera _mainCamera;
private float _screenBottom;
private Coroutine _spawnCoroutine;
private readonly List<GameObject> _activeObstacles = new List<GameObject>();
private void Awake()
{
_mainCamera = Camera.main;
// Validate obstacle prefabs
ValidateObstaclePrefabs();
if (useObjectPooling)
{
InitializeObjectPool();
}
}
private void Start()
{
CalculateScreenBounds();
StartSpawning();
}
private void OnDestroy()
{
StopSpawning();
}
/// <summary>
/// Validates that all prefabs have required components
/// </summary>
private void ValidateObstaclePrefabs()
{
for (int i = 0; i < obstaclePrefabs.Count; i++)
{
if (obstaclePrefabs[i] == null) continue;
// Check if the prefab has a FloatingObstacle component
if (obstaclePrefabs[i].GetComponent<FloatingObstacle>() == null)
{
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} does not have a FloatingObstacle component. Adding one automatically.");
obstaclePrefabs[i].AddComponent<FloatingObstacle>();
}
// Ensure the prefab is on the correct layer
if (obstaclePrefabs[i].layer != 11) // QuarryObstacle layer
{
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on QuarryObstacle layer (11). Setting layer automatically.");
SetLayerRecursively(obstaclePrefabs[i], 11);
}
}
}
/// <summary>
/// Sets the layer of a GameObject and all its children
/// </summary>
private void SetLayerRecursively(GameObject obj, int layer)
{
obj.layer = layer;
foreach (Transform child in obj.transform)
{
SetLayerRecursively(child.gameObject, layer);
}
}
/// <summary>
/// Initialize the object pool system
/// </summary>
private void InitializeObjectPool()
{
GameObject poolGO = new GameObject("ObstaclePool");
poolGO.transform.SetParent(transform);
_obstaclePool = poolGO.AddComponent<ObstaclePool>();
// Set up pool configuration
_obstaclePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize;
_obstaclePool.totalMaxPoolSize = totalMaxPoolSize;
// Convert GameObject list to FloatingObstacle list
List<FloatingObstacle> prefabObstacles = new List<FloatingObstacle>(obstaclePrefabs.Count);
foreach (var prefab in obstaclePrefabs)
{
if (prefab != null)
{
FloatingObstacle obstacleComponent = prefab.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
prefabObstacles.Add(obstacleComponent);
}
else
{
Debug.LogError($"Obstacle prefab {prefab.name} is missing a FloatingObstacle component!");
}
}
}
// Initialize the pool
_obstaclePool.Initialize(prefabObstacles);
// Periodically trim the pool
InvokeRepeating(nameof(TrimExcessPooledObstacles), 15f, 30f);
}
/// <summary>
/// Calculate screen bounds in world space
/// </summary>
private void CalculateScreenBounds()
{
if (_mainCamera == null)
{
_mainCamera = Camera.main;
if (_mainCamera == null)
{
Debug.LogError("[ObstacleSpawner] No main camera found!");
return;
}
}
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane));
_screenBottom = bottomWorldPoint.y;
}
/// <summary>
/// Starts the obstacle spawning coroutine
/// </summary>
public void StartSpawning()
{
if (_spawnCoroutine == null)
{
_spawnCoroutine = StartCoroutine(SpawnObstaclesCoroutine());
Debug.Log("[ObstacleSpawner] Started spawning obstacles");
}
}
/// <summary>
/// Stops the obstacle spawning coroutine
/// </summary>
public void StopSpawning()
{
if (_spawnCoroutine != null)
{
StopCoroutine(_spawnCoroutine);
_spawnCoroutine = null;
Debug.Log("[ObstacleSpawner] Stopped spawning obstacles");
}
}
/// <summary>
/// Main spawning coroutine that runs continuously
/// </summary>
private IEnumerator SpawnObstaclesCoroutine()
{
while (true)
{
// Calculate next spawn time with variation
float nextSpawnTime = spawnInterval + Random.Range(-spawnIntervalVariation, spawnIntervalVariation);
nextSpawnTime = Mathf.Max(0.1f, nextSpawnTime); // Ensure minimum interval
yield return new WaitForSeconds(nextSpawnTime);
// Attempt to spawn an obstacle
TrySpawnObstacle();
}
}
/// <summary>
/// Attempts to spawn an obstacle at a valid position
/// </summary>
private void TrySpawnObstacle()
{
if (obstaclePrefabs == null || obstaclePrefabs.Count == 0)
{
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs available for spawning!");
return;
}
Vector3 spawnPosition;
bool foundValidPosition = false;
// Try to find a valid spawn position
for (int attempts = 0; attempts < maxSpawnAttempts; attempts++)
{
spawnPosition = GetRandomSpawnPosition();
if (IsValidSpawnPosition(spawnPosition))
{
SpawnObstacleAt(spawnPosition);
foundValidPosition = true;
break;
}
}
if (!foundValidPosition)
{
Debug.Log($"[ObstacleSpawner] Could not find valid spawn position after {maxSpawnAttempts} attempts");
}
}
/// <summary>
/// Gets a random spawn position below the screen
/// </summary>
private Vector3 GetRandomSpawnPosition()
{
float randomX = Random.Range(-spawnRangeX, spawnRangeX);
float spawnY = _screenBottom - spawnDistanceBelowScreen;
return new Vector3(randomX, spawnY, 0f);
}
/// <summary>
/// Checks if a spawn position is valid (not colliding with tiles)
/// </summary>
private bool IsValidSpawnPosition(Vector3 position)
{
// Use OverlapCircle to check for collisions with tiles
Collider2D collision = Physics2D.OverlapCircle(position, spawnCollisionRadius, tileLayerMask);
return collision == null;
}
/// <summary>
/// Spawns an obstacle at the specified position
/// </summary>
private void SpawnObstacleAt(Vector3 position)
{
// Select random prefab
int prefabIndex = Random.Range(0, obstaclePrefabs.Count);
GameObject prefab = obstaclePrefabs[prefabIndex];
if (prefab == null)
{
Debug.LogError($"[ObstacleSpawner] Obstacle prefab at index {prefabIndex} is null!");
return;
}
GameObject obstacle;
// Spawn using pool or instantiate directly
if (useObjectPooling && _obstaclePool != null)
{
obstacle = _obstaclePool.GetObstacle(prefabIndex);
if (obstacle == null)
{
Debug.LogError("[ObstacleSpawner] Failed to get obstacle from pool!");
return;
}
obstacle.transform.position = position;
obstacle.transform.rotation = prefab.transform.rotation;
obstacle.transform.SetParent(transform);
}
else
{
obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
}
// Configure the obstacle
ConfigureObstacle(obstacle, prefabIndex);
// Track active obstacles
_activeObstacles.Add(obstacle);
// Invoke events
onObstacleSpawned?.Invoke(obstacle);
Debug.Log($"[ObstacleSpawner] Spawned obstacle {obstacle.name} at {position}");
}
/// <summary>
/// Configures an obstacle with randomized properties
/// </summary>
private void ConfigureObstacle(GameObject obstacle, int prefabIndex)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
// Set prefab index
obstacleComponent.PrefabIndex = prefabIndex;
// Randomize properties
obstacleComponent.MoveSpeed = Random.Range(minMoveSpeed, maxMoveSpeed);
obstacleComponent.Damage = Random.Range(minDamage, maxDamage);
// Set spawner reference (since FloatingObstacle has this built-in now)
obstacleComponent.SetSpawner(this);
}
}
/// <summary>
/// Returns an obstacle to the pool (called by FloatingObstacle)
/// </summary>
public void ReturnObstacleToPool(GameObject obstacle, int prefabIndex)
{
if (obstacle == null) return;
// Remove from active list
_activeObstacles.Remove(obstacle);
// Invoke events
onObstacleDestroyed?.Invoke(obstacle);
// Return to pool or destroy
if (useObjectPooling && _obstaclePool != null)
{
_obstaclePool.ReturnObstacle(obstacle, prefabIndex);
}
else
{
Destroy(obstacle);
}
}
/// <summary>
/// Called periodically to trim excess pooled obstacles
/// </summary>
private void TrimExcessPooledObstacles()
{
if (_obstaclePool != null)
{
_obstaclePool.TrimExcess();
}
}
/// <summary>
/// Public method to change spawn interval at runtime
/// </summary>
public void SetSpawnInterval(float interval)
{
spawnInterval = interval;
}
/// <summary>
/// Public method to change spawn range at runtime
/// </summary>
public void SetSpawnRange(float range)
{
spawnRangeX = range;
}
/// <summary>
/// Public method to set speed range at runtime
/// </summary>
public void SetSpeedRange(float min, float max)
{
minMoveSpeed = min;
maxMoveSpeed = max;
}
/// <summary>
/// Public method to set damage range at runtime
/// </summary>
public void SetDamageRange(float min, float max)
{
minDamage = min;
maxDamage = max;
}
/// <summary>
/// Gets the count of currently active obstacles
/// </summary>
public int ActiveObstacleCount => _activeObstacles.Count;
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
// Draw spawn area
Gizmos.color = Color.yellow;
Vector3 center = new Vector3(0f, _screenBottom - spawnDistanceBelowScreen, 0f);
Vector3 size = new Vector3(spawnRangeX * 2f, 1f, 1f);
Gizmos.DrawWireCube(center, size);
// Draw collision radius at spawn point
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(center, spawnCollisionRadius);
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 49ec62157fd945fab730193e9ea0bff7
timeCreated: 1758116903

View File

@@ -0,0 +1,242 @@
using UnityEngine;
using System.Collections;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Handles visual feedback for player damage by making the sprite renderer blink red.
/// Automatically subscribes to damage events from PlayerCollisionBehavior components.
/// </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;
private bool _isBlinking;
private bool _isShowingDamageColor;
private Coroutine _blinkCoroutine;
private Color _originalColor; // Missing field declaration
private void Awake()
{
// Auto-assign sprite renderer if not set
if (targetSpriteRenderer == null)
{
targetSpriteRenderer = GetComponent<SpriteRenderer>();
if (targetSpriteRenderer == null)
{
targetSpriteRenderer = GetComponentInChildren<SpriteRenderer>();
if (targetSpriteRenderer != null)
{
Debug.Log($"[PlayerBlinkBehavior] Found SpriteRenderer on child object: {targetSpriteRenderer.gameObject.name}");
}
}
}
if (targetSpriteRenderer == null)
{
Debug.LogError("[PlayerBlinkBehavior] No SpriteRenderer found on this GameObject or its children!");
return;
}
// Store original color
_originalColor = targetSpriteRenderer.color;
// Ensure damage color has the correct alpha
damageBlinkColor.a = damageColorAlpha;
}
private void OnEnable()
{
// Subscribe to damage events
PlayerCollisionBehavior.OnDamageStart += StartBlinking;
PlayerCollisionBehavior.OnDamageEnd += StopBlinking;
}
private void OnDisable()
{
// Unsubscribe from damage events
PlayerCollisionBehavior.OnDamageStart -= StartBlinking;
PlayerCollisionBehavior.OnDamageEnd -= StopBlinking;
// 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()
{
if (targetSpriteRenderer != null)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
}
/// <summary>
/// Updates the original color reference (useful if sprite color changes during gameplay)
/// </summary>
public void UpdateOriginalColor()
{
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)
{
StopCoroutine(_blinkCoroutine);
}
StartCoroutine(ManualBlinkCoroutine(duration));
}
/// <summary>
/// Coroutine for manually triggered blink effects
/// </summary>
private IEnumerator ManualBlinkCoroutine(float duration)
{
_isBlinking = true;
_isShowingDamageColor = false;
float elapsed = 0f;
while (elapsed < duration && targetSpriteRenderer != null)
{
// Toggle between original and damage colors
if (_isShowingDamageColor)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
else
{
targetSpriteRenderer.color = damageBlinkColor;
_isShowingDamageColor = true;
}
yield return new WaitForSeconds(blinkRate);
elapsed += blinkRate;
}
// Ensure we end with original color
RestoreOriginalColor();
_isBlinking = false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8ea29cc80524de8affe17b930cd75c1
timeCreated: 1758114229

View File

@@ -0,0 +1,234 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Collision behavior that bumps the player toward the center of the trench.
/// Provides two modes: impulse (force-based push) or smooth movement to center.
/// </summary>
public class PlayerBumpCollisionBehavior : 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 float _bumpTimer;
private float _bumpStartX;
private float _bumpTargetX;
private float _bumpDuration;
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)
{
case BumpMode.Impulse:
StartImpulseBump();
break;
case BumpMode.SmoothToCenter:
StartSmoothMoveToCenter();
break;
}
Debug.Log($"[PlayerBumpCollisionBehavior] Collision handled with {bumpMode} mode");
}
/// <summary>
/// Starts an impulse bump toward the center with force-based distance
/// </summary>
private void StartImpulseBump()
{
if (playerCharacter == null) return;
float currentX = playerCharacter.transform.position.x;
// Calculate bump distance based on force and current position
float directionToCenter = currentX > 0 ? -1f : 1f; // Direction toward center
// Calculate target position - closer to center based on bump force
float bumpDistance = bumpForce * 0.2f; // Scale factor for distance
float targetX = currentX + (directionToCenter * bumpDistance);
// Clamp to center if we would overshoot
if ((currentX > 0 && targetX < 0) || (currentX < 0 && targetX > 0))
{
targetX = 0f;
}
// Set bump parameters
_bumpStartX = currentX;
_bumpTargetX = targetX;
_bumpDuration = 0.5f; // Fixed duration for impulse
StartBump();
Debug.Log($"[PlayerBumpCollisionBehavior] Starting impulse bump from X={_bumpStartX} to X={_bumpTargetX} (force={bumpForce})");
}
/// <summary>
/// Starts smooth movement to the center
/// </summary>
private void StartSmoothMoveToCenter()
{
if (playerCharacter == null) return;
float currentX = playerCharacter.transform.position.x;
float distanceToCenter = Mathf.Abs(currentX);
// Set bump parameters
_bumpStartX = currentX;
_bumpTargetX = 0f; // Always move to center
_bumpDuration = distanceToCenter / smoothMoveSpeed; // Duration based on distance and speed
StartBump();
Debug.Log($"[PlayerBumpCollisionBehavior] Starting smooth move to center from X={_bumpStartX} (speed={smoothMoveSpeed}, duration={_bumpDuration:F2}s)");
}
/// <summary>
/// Common bump initialization
/// </summary>
private void StartBump()
{
_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");
}
}
/// <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("[PlayerBumpCollisionBehavior] Player input restored after bump");
}
}
protected override void OnImmunityEnd()
{
base.OnImmunityEnd();
// Stop any ongoing bump if immunity ends
if (_isBumping)
{
_isBumping = false;
if (_bumpInputBlocked)
{
RestoreBumpInput();
}
}
}
/// <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;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8222f0e3aeeb4fc4975aaead6cf7afbe
timeCreated: 1758109619

View File

@@ -0,0 +1,238 @@
using UnityEngine;
using System;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Base class for handling player collisions with world obstacles.
/// Detects collisions between Player layer (7) and WorldObstacle layer (6).
/// </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 - should match WorldObstacle layer")]
[SerializeField] protected LayerMask obstacleLayerMask = 1 << 6; // WorldObstacle layer
[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;
// Events for damage state changes
public static event Action OnDamageStart;
public static event Action OnDamageEnd;
/// <summary>
/// Public static method to trigger damage start event from external classes
/// </summary>
public static void TriggerDamageStart()
{
OnDamageStart?.Invoke();
}
/// <summary>
/// Public static method to trigger damage end event from external classes
/// </summary>
public static void TriggerDamageEnd()
{
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<PlayerController>();
// Look for collider on this GameObject first, then in children
playerCollider = GetComponent<Collider2D>();
if (playerCollider == null)
{
playerCollider = GetComponentInChildren<Collider2D>();
if (playerCollider != null)
{
Debug.Log($"[{GetType().Name}] Found collider on child object: {playerCollider.gameObject.name}");
}
}
if (playerCollider == null)
{
Debug.LogError($"[{GetType().Name}] No Collider2D found on this GameObject or its children!");
}
}
protected virtual void Update()
{
// Handle immunity timer
if (isImmune)
{
immunityTimer -= Time.deltaTime;
if (immunityTimer <= 0f)
{
isImmune = false;
OnImmunityEnd();
}
}
// Check for collisions if not immune
if (!isImmune && playerCollider != null)
{
CheckForCollisions();
}
}
/// <summary>
/// Checks for collisions with obstacle layer objects
/// </summary>
protected virtual void CheckForCollisions()
{
// Get all colliders overlapping with the player
Collider2D[] overlapping = new Collider2D[10];
ContactFilter2D filter = new ContactFilter2D();
filter.SetLayerMask(obstacleLayerMask);
filter.useTriggers = true;
int count = playerCollider.Overlap(filter, overlapping);
if (count > 0)
{
// Found collision, trigger response
OnCollisionDetected(overlapping[0]);
}
}
/// <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 (isImmune) return;
// Start immunity period
isImmune = true;
immunityTimer = damageImmunityDuration;
// Call the specific collision response
HandleCollisionResponse(obstacle);
// Notify about immunity start
OnImmunityStart();
}
/// <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
/// </summary>
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();
}
/// <summary>
/// Called when damage immunity ends
/// </summary>
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();
}
/// <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>
/// Public property to check if player is currently immune
/// </summary>
public bool IsImmune => isImmune;
/// <summary>
/// Remaining immunity time
/// </summary>
public float RemainingImmunityTime => immunityTimer;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e6c959bca2e24e72bf22e92439580d79
timeCreated: 1758109598

View File

@@ -0,0 +1,102 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// 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).
/// </summary>
public class PlayerDamageCollisionBehavior : PlayerCollisionBehavior
{
[Header("Damage Settings")]
[Tooltip("Base damage amount dealt by obstacles")]
[SerializeField] private float baseDamage = 1f;
[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;
// Try to get damage from the obstacle component if enabled
if (useObstacleDamageValue)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
damageAmount = obstacleComponent.Damage;
}
}
// 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}");
}
/// <summary>
/// Applies damage to the player
/// Override this method to integrate with your health/damage system
/// </summary>
/// <param name="damage">Amount of damage to apply</param>
protected virtual void ApplyDamage(float damage)
{
// 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");
// TODO: Integrate with health system when available
// Example: playerHealth.TakeDamage(damage);
}
/// <summary>
/// Override to prevent input blocking during damage immunity
/// Since obstacles pass through the player, we don't want to block input
/// </summary>
protected override void OnImmunityStart()
{
Debug.Log($"[PlayerDamageCollisionBehavior] Damage immunity started for {damageImmunityDuration} seconds");
// Don't block input for obstacle damage - let player keep moving
// Only broadcast the damage event
TriggerDamageStart();
}
/// <summary>
/// Override to handle immunity end without input restoration
/// </summary>
protected override void OnImmunityEnd()
{
Debug.Log($"[PlayerDamageCollisionBehavior] Damage immunity ended");
// Broadcast damage end event
TriggerDamageEnd();
}
/// <summary>
/// Public method to set base damage at runtime
/// </summary>
public void SetBaseDamage(float damage)
{
baseDamage = damage;
}
/// <summary>
/// Public method to toggle between base damage and obstacle-specific damage
/// </summary>
public void SetUseObstacleDamage(bool useObstacleDamage)
{
useObstacleDamageValue = useObstacleDamage;
}
}
}

View File

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

View File

@@ -0,0 +1,180 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// 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.
/// </summary>
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<SpriteRenderer>();
}
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();
}
/// <summary>
/// Starts the visual feedback to indicate immunity/phase mode
/// </summary>
private void StartVisualFeedback()
{
if (_spriteRenderer != null)
{
_isBlinking = true;
_blinkTimer = 0f;
// Start with reduced opacity
Color immunityColor = _originalColor;
immunityColor.a = 0.3f;
_spriteRenderer.color = immunityColor;
}
}
/// <summary>
/// Stops the visual feedback and restores original appearance
/// </summary>
private void StopVisualFeedback()
{
_isBlinking = false;
if (_spriteRenderer != null)
{
// Restore original color and alpha
_spriteRenderer.color = _originalColor;
}
}
/// <summary>
/// Public method to toggle collider behavior during immunity
/// </summary>
public void SetColliderDisabling(bool disable)
{
disableColliderDuringImmunity = disable;
}
/// <summary>
/// Check if player is currently in phase mode
/// </summary>
public bool IsPhasing => isImmune && disableColliderDuringImmunity;
/// <summary>
/// Manually trigger phase mode (useful for testing or special abilities)
/// </summary>
public void TriggerPhaseMode(float duration = -1f)
{
if (duration > 0f)
{
isImmune = true;
immunityTimer = duration;
OnImmunityStart();
}
else
{
isImmune = true;
immunityTimer = damageImmunityDuration;
OnImmunityStart();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d849a517ce3a41249ae9f37d2722cefa
timeCreated: 1758109641