Create a diving minigame MVP (#6)

- Obstacles
- Tiles
- Object pooling
- Monster spawns
- Scoring
- Minigame End

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com>
Reviewed-on: #6
This commit is contained in:
2025-09-22 12:16:32 +00:00
parent 46755fecb3
commit 5305c20b00
24 changed files with 3466 additions and 165 deletions

View File

@@ -1,7 +1,9 @@
using UnityEngine;
using System.Collections.Generic;
using System;
using System.Collections;
using UnityEngine.Events;
using UnityEngine.Playables;
namespace Minigames.DivingForPictures
{
@@ -31,6 +33,20 @@ namespace Minigames.DivingForPictures
[Tooltip("Additional points per depth unit")]
[SerializeField] private int depthMultiplier = 10;
[Header("Rope Damage System")]
[Tooltip("Ropes that will break one by one as player takes damage")]
[SerializeField] private RopeBreaker[] playerRopes;
[Header("Surfacing Settings")]
[Tooltip("Duration in seconds for speed transition when surfacing")]
[SerializeField] private float speedTransitionDuration = 2.0f;
[Tooltip("Factor to multiply speed by when surfacing (usually 1.0 for same speed)")]
[SerializeField] private float surfacingSpeedFactor = 3.0f;
[Tooltip("How long to continue spawning tiles after surfacing begins (seconds)")]
[SerializeField] private float surfacingSpawnDelay = 5.0f;
[Tooltip("Reference to the PlayableDirector that will play the surfacing timeline")]
[SerializeField] private PlayableDirector surfacingTimeline;
// Private state variables
private int playerScore = 0;
private float currentSpawnProbability;
@@ -38,14 +54,32 @@ namespace Minigames.DivingForPictures
private float timeSinceLastSpawn = 0f;
private List<Monster> activeMonsters = new List<Monster>();
// Velocity management
// Velocity state tracking
private float _currentVelocityFactor = 1.0f; // 1.0 = normal descent speed, -1.0 * surfacingSpeedFactor = full surfacing speed
private Coroutine _velocityTransitionCoroutine;
private Coroutine _surfacingSequenceCoroutine;
// Public properties
public int PlayerScore => playerScore;
public float CurrentVelocityFactor => _currentVelocityFactor;
// Events
public event Action<int> OnScoreChanged;
public event Action<Monster> OnMonsterSpawned;
public event Action<Monster, int> OnPictureTaken;
public event Action<float> OnSpawnProbabilityChanged;
public event Action OnGameOver;
public event Action<int> OnRopeBroken; // Passes remaining ropes count
public event Action<float> OnVelocityFactorChanged;
// Private state variables for rope system
private int currentRopeIndex = 0;
private bool isGameOver = false;
private bool _isSurfacing = false;
// Used to track if we're currently surfacing
public bool IsSurfacing => _isSurfacing;
private void Awake()
{
@@ -64,6 +98,18 @@ namespace Minigames.DivingForPictures
{
Debug.LogWarning("No TrenchTileSpawner found in scene. Monster spawning won't work.");
}
// Subscribe to player damage events
PlayerCollisionBehavior.OnDamageTaken += OnPlayerDamageTaken;
// Validate rope references
ValidateRopeReferences();
}
private void OnDestroy()
{
// Unsubscribe from events when the manager is destroyed
PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken;
}
private void Update()
@@ -92,6 +138,9 @@ namespace Minigames.DivingForPictures
if (spawnPoints.Length == 0) return;
// If we're surfacing, don't spawn new monsters
if (_isSurfacing) return;
bool forceSpawn = timeSinceLastSpawn >= guaranteedSpawnTime;
bool onCooldown = timeSinceLastSpawn < spawnCooldown;
@@ -176,6 +225,335 @@ namespace Minigames.DivingForPictures
monster.OnMonsterDespawned -= OnMonsterDespawned;
}
/// <summary>
/// Called when the player takes damage from any collision
/// </summary>
private void OnPlayerDamageTaken()
{
if (isGameOver) return;
// Break the next rope in sequence
BreakNextRope();
// Check if all ropes are broken
if (currentRopeIndex >= playerRopes.Length)
{
TriggerGameOver();
}
else
{
// Notify listeners about rope break and remaining ropes
int remainingRopes = playerRopes.Length - currentRopeIndex;
OnRopeBroken?.Invoke(remainingRopes);
Debug.Log($"[DivingGameManager] Rope broken! {remainingRopes} ropes remaining.");
}
}
/// <summary>
/// Breaks the next available rope in the sequence
/// </summary>
private void BreakNextRope()
{
if (currentRopeIndex < playerRopes.Length)
{
RopeBreaker ropeToBreak = playerRopes[currentRopeIndex];
if (ropeToBreak != null)
{
// Let the RopeBreaker component handle the breaking, effects, and sounds
ropeToBreak.BreakRope();
}
else
{
Debug.LogWarning($"[DivingGameManager] Rope at index {currentRopeIndex} is null!");
}
// Move to the next rope regardless if current was null
currentRopeIndex++;
}
}
/// <summary>
/// Manually break a rope (for testing or external events)
/// </summary>
public void ForceBreakRope()
{
if (!isGameOver)
{
OnPlayerDamageTaken();
}
}
/// <summary>
/// Triggers game over state when all ropes are broken
/// </summary>
private void TriggerGameOver()
{
if (isGameOver) return;
isGameOver = true;
Debug.Log("[DivingGameManager] Game Over! All ropes broken. Starting surfacing sequence...");
// Fire game over event
OnGameOver?.Invoke();
// Start surfacing instead of directly ending the game
StartSurfacing();
}
/// <summary>
/// Validates rope references and logs warnings if any are missing
/// </summary>
private void ValidateRopeReferences()
{
if (playerRopes == null || playerRopes.Length == 0)
{
Debug.LogWarning("[DivingGameManager] No ropes assigned to break! Damage system won't work properly.");
return;
}
for (int i = 0; i < playerRopes.Length; i++)
{
if (playerRopes[i] == null)
{
Debug.LogWarning($"[DivingGameManager] Rope at index {i} is null!");
}
}
}
/// <summary>
/// Resets the rope system for a new game
/// </summary>
public void ResetRopeSystem()
{
// Reset rope state
currentRopeIndex = 0;
isGameOver = false;
// Restore all broken ropes
if (playerRopes != null)
{
foreach (var rope in playerRopes)
{
if (rope != null)
{
rope.RestoreRope();
}
}
}
Debug.Log("[DivingGameManager] Rope system reset.");
}
/// <summary>
/// Starts the surfacing mode - reverses trench direction and adjusts all spawned entities
/// </summary>
public void StartSurfacing()
{
if (_isSurfacing) return; // Already surfacing
_isSurfacing = true;
// 1. Initiate smooth velocity transition to surfacing speed
float targetVelocityFactor = -1.0f * surfacingSpeedFactor;
SetVelocityFactor(targetVelocityFactor);
// 2. Find and notify trench tile spawner about direction change (for spawning/despawning logic)
TrenchTileSpawner tileSpawner = FindFirstObjectByType<TrenchTileSpawner>();
if (tileSpawner != null)
{
// Subscribe to velocity changes if not already subscribed
OnVelocityFactorChanged -= tileSpawner.OnVelocityFactorChanged;
OnVelocityFactorChanged += tileSpawner.OnVelocityFactorChanged;
// Subscribe to the last tile event
tileSpawner.onLastTileLeft.RemoveListener(OnLastTileLeft);
tileSpawner.onLastTileLeft.AddListener(OnLastTileLeft);
// Tell spawner to reverse spawn/despawn logic
tileSpawner.StartSurfacing();
// Immediately send current velocity factor
tileSpawner.OnVelocityFactorChanged(_currentVelocityFactor);
}
// Handle the Rock object - disable components and animate it falling offscreen
GameObject rockObject = GameObject.FindGameObjectWithTag("Rock");
if (rockObject != null)
{
// Disable all components except Transform on the rock object (not its children)
foreach (Component component in rockObject.GetComponents<Component>())
{
if (!(component is Transform))
{
if (component is Behaviour behaviour)
{
behaviour.enabled = false;
}
}
}
// Start coroutine to animate the rock falling offscreen
StartCoroutine(MoveRockOffscreen(rockObject.transform));
Debug.Log("[DivingGameManager] Disabled rock components and animating it offscreen");
}
// Handle the Player object - disable components and reset X position
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
if (playerObject != null)
{
// Disable all components except Transform and Animator on the player object (not its children)
foreach (Component component in playerObject.GetComponents<Component>())
{
if (!(component is Transform) && !(component is Animator))
{
if (component is Behaviour behaviour)
{
behaviour.enabled = false;
}
}
}
// Start coroutine to reset X position to 0 over 1 second
StartCoroutine(ResetPlayerPosition(playerObject.transform));
Debug.Log("[DivingGameManager] Disabled player components (keeping Animator) and resetting position");
}
// 3. Find bubble spawner and slow down existing bubbles (no velocity management needed)
BubbleSpawner bubbleSpawner = FindFirstObjectByType<BubbleSpawner>();
if (bubbleSpawner != null)
{
bubbleSpawner.StartSurfacing();
}
// 4. Find obstacle spawner and set up for velocity changes
ObstacleSpawner obstacleSpawner = FindFirstObjectByType<ObstacleSpawner>();
if (obstacleSpawner != null)
{
// Subscribe to velocity changes
OnVelocityFactorChanged -= obstacleSpawner.OnVelocityFactorChanged;
OnVelocityFactorChanged += obstacleSpawner.OnVelocityFactorChanged;
// Tell spawner to reverse spawn/despawn logic
obstacleSpawner.StartSurfacing();
// Immediately send current velocity factor
obstacleSpawner.OnVelocityFactorChanged(_currentVelocityFactor);
}
// Start the surfacing sequence coroutine
if (_surfacingSequenceCoroutine != null)
{
StopCoroutine(_surfacingSequenceCoroutine);
}
_surfacingSequenceCoroutine = StartCoroutine(SurfacingSequence());
Debug.Log($"[DivingGameManager] Started surfacing with target velocity factor: {targetVelocityFactor}");
}
/// <summary>
/// Coroutine to animate the rock falling below the screen
/// </summary>
private IEnumerator MoveRockOffscreen(Transform rockTransform)
{
Vector3 startPosition = rockTransform.position;
// Calculate position below the screen
Camera mainCamera = Camera.main;
if (mainCamera == null)
{
Debug.LogWarning("[DivingGameManager] Cannot find main camera to calculate offscreen position");
yield break;
}
// Get a position below the bottom of the screen
Vector3 offscreenPosition = mainCamera.ViewportToWorldPoint(new Vector3(0.5f, -0.2f, mainCamera.nearClipPlane));
Vector3 targetPosition = new Vector3(startPosition.x, offscreenPosition.y, startPosition.z);
float duration = 2.0f; // Animation duration in seconds
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// Use an easing function that accelerates to simulate falling
float easedT = t * t; // Quadratic easing
rockTransform.position = Vector3.Lerp(startPosition, targetPosition, easedT);
yield return null;
}
// Ensure final position is exactly at target
rockTransform.position = targetPosition;
}
/// <summary>
/// Coroutine to reset the player's X position to 0 over time
/// </summary>
private IEnumerator ResetPlayerPosition(Transform playerTransform)
{
Vector3 startPosition = playerTransform.position;
Vector3 targetPosition = new Vector3(0f, startPosition.y, startPosition.z);
float duration = 1.0f; // Reset duration in seconds (as requested)
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// Use smooth step for more natural movement
float smoothT = Mathf.SmoothStep(0f, 1f, t);
playerTransform.position = Vector3.Lerp(startPosition, targetPosition, smoothT);
yield return null;
}
// Ensure final position is exactly at target
playerTransform.position = targetPosition;
}
/// <summary>
/// Coroutine to handle the surfacing sequence timing
/// </summary>
private IEnumerator SurfacingSequence()
{
// Wait for the configured delay
yield return new WaitForSeconds(surfacingSpawnDelay);
// Find tile spawner and tell it to stop spawning
TrenchTileSpawner tileSpawner = FindFirstObjectByType<TrenchTileSpawner>();
if (tileSpawner != null)
{
// Tell it to stop spawning new tiles
tileSpawner.StopSpawning();
Debug.Log("[DivingGameManager] Stopped spawning new tiles after delay");
}
}
/// <summary>
/// Called when the last tile leaves the screen
/// </summary>
private void OnLastTileLeft()
{
// Play the timeline
if (surfacingTimeline != null)
{
surfacingTimeline.Play();
Debug.Log("[DivingGameManager] Last tile left the screen, playing timeline");
}
else
{
Debug.LogWarning("[DivingGameManager] No surfacing timeline assigned!");
}
}
// Call this when the game ends
public void EndGame()
{
@@ -193,5 +571,49 @@ namespace Minigames.DivingForPictures
// Final score could be saved to player prefs or other persistence
Debug.Log($"Final Score: {playerScore}");
}
/// <summary>
/// Starts a smooth transition to the new velocity factor
/// </summary>
/// <param name="targetFactor">Target velocity factor (e.g., -1.0 for surfacing speed)</param>
public void SetVelocityFactor(float targetFactor)
{
if (_velocityTransitionCoroutine != null)
{
StopCoroutine(_velocityTransitionCoroutine);
}
_velocityTransitionCoroutine = StartCoroutine(TransitionVelocityFactor(targetFactor));
}
/// <summary>
/// Coroutine to smoothly transition the velocity factor over time
/// </summary>
private IEnumerator<WaitForEndOfFrame> TransitionVelocityFactor(float targetFactor)
{
float startFactor = _currentVelocityFactor;
float elapsed = 0f;
while (elapsed < speedTransitionDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / speedTransitionDuration);
// Smooth step interpolation
float smoothStep = t * t * (3f - 2f * t);
_currentVelocityFactor = Mathf.Lerp(startFactor, targetFactor, smoothStep);
// Notify listeners about the velocity factor change
OnVelocityFactorChanged?.Invoke(_currentVelocityFactor);
yield return null;
}
_currentVelocityFactor = targetFactor;
// Final assignment to ensure exact target value
OnVelocityFactorChanged?.Invoke(_currentVelocityFactor);
}
}
}