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

@@ -26,10 +26,15 @@ namespace Minigames.DivingForPictures
public int initialPoolSize = 10;
public int maxPoolSize = 30;
[Header("Surfacing Settings")]
[Tooltip("Factor to multiply bubble speed by when surfacing (0.5 = half speed)")]
[SerializeField] private float surfacingSpeedFactor = 0.5f;
private float _timer;
private float _nextSpawnInterval;
private BubblePool _bubblePool;
private Camera _mainCamera; // Cache camera reference
private bool _isSurfacing = false;
void Awake()
{
@@ -98,7 +103,18 @@ namespace Minigames.DivingForPictures
}
// Randomize bubble properties
bubble.speed = Random.Range(speedRange.x, speedRange.y);
float baseSpeed = Random.Range(speedRange.x, speedRange.y);
// Apply surfacing speed reduction if needed
if (_isSurfacing)
{
bubble.speed = baseSpeed * surfacingSpeedFactor;
}
else
{
bubble.speed = baseSpeed;
}
bubble.wobbleSpeed = Random.Range(wobbleSpeedRange.x, wobbleSpeedRange.y);
// Set base scale (initial size) for the bubble
@@ -119,6 +135,25 @@ namespace Minigames.DivingForPictures
bubble.SetWobbleScaleLimits(wobbleMinScale, wobbleMaxScale);
}
/// <summary>
/// Start surfacing mode - slow down all bubbles
/// </summary>
public void StartSurfacing()
{
if (_isSurfacing) return; // Already surfacing
_isSurfacing = true;
// Slow down all existing bubbles
Bubble[] activeBubbles = FindObjectsOfType<Bubble>();
foreach (Bubble bubble in activeBubbles)
{
bubble.speed *= surfacingSpeedFactor;
}
Debug.Log($"[BubbleSpawner] Started surfacing mode. Bubbles slowed to {surfacingSpeedFactor * 100}% speed.");
}
/// <summary>
/// Logs the current pool statistics for debugging
/// </summary>

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);
}
}
}

View File

@@ -44,8 +44,12 @@ namespace Minigames.DivingForPictures
private Collider2D _collider;
private Camera _mainCamera;
private float _screenTop;
private float _screenBottom; // Added to track bottom of screen
private Coroutine _movementCoroutine;
private Coroutine _offScreenCheckCoroutine;
private bool _isSurfacing = false; // Flag to track surfacing state
private float _velocityFactor = 1.0f; // Current velocity factor from game manager
private float _baseMoveSpeed; // Original move speed before velocity factor is applied
private void Awake()
{
@@ -62,6 +66,7 @@ namespace Minigames.DivingForPictures
}
_mainCamera = Camera.main;
_baseMoveSpeed = moveSpeed; // Store original speed
}
private void OnEnable()
@@ -105,6 +110,30 @@ namespace Minigames.DivingForPictures
}
}
/// <summary>
/// Called when the velocity factor changes from the DivingGameManager via ObstacleSpawner
/// </summary>
public void OnVelocityFactorChanged(float velocityFactor)
{
_velocityFactor = velocityFactor;
// Update actual move speed based on velocity factor and base speed
// We use Abs for magnitude and Sign for direction
moveSpeed = _baseMoveSpeed * Mathf.Abs(_velocityFactor);
// Restart movement with new speed if needed
if (enableMovement && gameObject.activeInHierarchy)
{
if (_movementCoroutine != null)
{
StopCoroutine(_movementCoroutine);
}
_movementCoroutine = StartCoroutine(MovementCoroutine());
}
Debug.Log($"[FloatingObstacle] {gameObject.name} velocity factor updated to {_velocityFactor:F2}, speed: {moveSpeed:F2}");
}
/// <summary>
/// Coroutine that handles obstacle movement
/// </summary>
@@ -112,8 +141,12 @@ namespace Minigames.DivingForPictures
{
while (enabled && gameObject.activeInHierarchy)
{
// Move the obstacle upward
transform.position += Vector3.up * (moveSpeed * Time.deltaTime);
// Use velocity factor sign to determine direction
Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor);
float speed = moveSpeed * Time.deltaTime;
// Apply movement in correct direction
transform.position += direction * speed;
// Wait for next frame
yield return null;
@@ -165,6 +198,9 @@ namespace Minigames.DivingForPictures
Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.transform.position.z));
_screenTop = topWorldPoint.y;
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.transform.position.z));
_screenBottom = bottomWorldPoint.y;
// Check if obstacle is significantly above screen top (obstacles move upward)
// Use a larger buffer to ensure obstacles are truly off-screen before returning to pool
if (transform.position.y > _screenTop + 5f)
@@ -172,6 +208,11 @@ namespace Minigames.DivingForPictures
Debug.Log($"[FloatingObstacle] {gameObject.name} off-screen at Y:{transform.position.y:F2}, screen top:{_screenTop:F2}");
ReturnToPool();
}
else if (transform.position.y < _screenBottom - 5f) // Added check for bottom screen edge
{
Debug.Log($"[FloatingObstacle] {gameObject.name} below screen at Y:{transform.position.y:F2}, screen bottom:{_screenBottom:F2}");
ReturnToPool();
}
}
/// <summary>
@@ -279,5 +320,20 @@ namespace Minigames.DivingForPictures
StartObstacleBehavior();
}
}
/// <summary>
/// Sets surfacing mode, which reverses obstacle movement direction
/// </summary>
public void StartSurfacing()
{
if (_isSurfacing) return; // Already surfacing
_isSurfacing = true;
// Reverse movement speed (already handled by ObstacleSpawner, but this ensures consistency)
moveSpeed *= -1;
Debug.Log($"[FloatingObstacle] {gameObject.name} started surfacing with speed: {moveSpeed}");
}
}
}

View File

@@ -75,6 +75,8 @@ namespace Minigames.DivingForPictures
private Coroutine _spawnCoroutine;
private readonly List<GameObject> _activeObstacles = new List<GameObject>();
private int _obstacleCounter = 0; // Counter for unique obstacle naming
private bool _isSurfacing = false; // Flag to track surfacing state
private float _velocityFactor = 1.0f; // Current velocity factor from the game manager
private void Awake()
{
@@ -250,6 +252,13 @@ namespace Minigames.DivingForPictures
/// </summary>
private void TrySpawnObstacle()
{
// Don't spawn new obstacles when surfacing
if (_isSurfacing)
{
Debug.Log("[ObstacleSpawner] Skipping obstacle spawn - currently surfacing");
return;
}
Debug.Log($"[ObstacleSpawner] TrySpawnObstacle called at {Time.time:F2}");
if (obstaclePrefabs == null || obstaclePrefabs.Count == 0)
@@ -338,25 +347,53 @@ namespace Minigames.DivingForPictures
return;
}
Debug.Log($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}");
// FORCE ACTIVATION - bypass pool issues
if (!obstacle.activeInHierarchy)
{
Debug.LogWarning($"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!");
obstacle.SetActive(true);
Debug.Log($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}");
}
// Important: Set position/parent/rotation BEFORE activation to avoid visual glitches
obstacle.transform.position = position;
obstacle.transform.rotation = prefab.transform.rotation;
obstacle.transform.SetParent(transform);
Debug.Log($"[ObstacleSpawner] After positioning, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}");
Debug.Log($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}");
// ENHANCED FORCE ACTIVATION - more robust approach
if (!obstacle.activeInHierarchy)
{
Debug.LogWarning($"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!");
// Configure obstacle BEFORE activation
ConfigureObstacle(obstacle, prefabIndex);
// Force activate the obstacle
obstacle.SetActive(true);
// Double-check activation status
if (!obstacle.activeInHierarchy)
{
Debug.LogError($"[ObstacleSpawner] CRITICAL ERROR: Failed to activate {obstacle.name} after multiple attempts!");
// Last resort: try to instantiate a new one instead
GameObject newObstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
if (newObstacle != null)
{
obstacle = newObstacle;
ConfigureObstacle(obstacle, prefabIndex);
}
}
Debug.Log($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}");
}
else
{
// Still configure if already active
ConfigureObstacle(obstacle, prefabIndex);
}
}
else
{
Debug.Log($"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)");
obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
// Configure the newly instantiated obstacle
ConfigureObstacle(obstacle, prefabIndex);
}
// Assign unique name with counter
@@ -364,10 +401,6 @@ namespace Minigames.DivingForPictures
string oldName = obstacle.name;
obstacle.name = $"Obstacle{_obstacleCounter:D3}";
Debug.Log($"[ObstacleSpawner] Renamed obstacle from '{oldName}' to '{obstacle.name}', active state: {obstacle.activeInHierarchy}");
// Configure the obstacle
ConfigureObstacle(obstacle, prefabIndex);
Debug.Log($"[ObstacleSpawner] After configuration, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}");
// Track active obstacles
_activeObstacles.Add(obstacle);
@@ -459,6 +492,55 @@ namespace Minigames.DivingForPictures
CalculateScreenBounds();
}
/// <summary>
/// Called when the velocity factor changes from the DivingGameManager
/// </summary>
public void OnVelocityFactorChanged(float velocityFactor)
{
_velocityFactor = velocityFactor;
// Update all active obstacles with the new velocity factor
foreach (GameObject obstacle in _activeObstacles)
{
if (obstacle != null)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
obstacleComponent.OnVelocityFactorChanged(velocityFactor);
}
}
}
Debug.Log($"[ObstacleSpawner] Velocity factor updated to {_velocityFactor:F2}, propagated to {_activeObstacles.Count} active obstacles");
}
/// <summary>
/// Start surfacing mode - reverse direction of existing obstacles and stop spawning new ones
/// </summary>
public void StartSurfacing()
{
if (_isSurfacing) return; // Already surfacing
_isSurfacing = true;
// Notify obstacles about surfacing state (for direction-based logic)
foreach (GameObject obstacle in _activeObstacles)
{
if (obstacle != null)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
// Call StartSurfacing on the obstacle component itself
obstacleComponent.StartSurfacing();
}
}
}
Debug.Log($"[ObstacleSpawner] Started surfacing mode for {_activeObstacles.Count} active obstacles");
}
/// <summary>
/// Gets the count of currently active obstacles
/// </summary>

View File

@@ -0,0 +1,331 @@
using UnityEngine;
using System.Collections;
using GogoGaga.OptimizedRopesAndCables;
/// <summary>
/// Component that allows breaking a rope in half.
/// Attach this to the same GameObject that has a Rope and LineRenderer component.
/// </summary>
public class RopeBreaker : MonoBehaviour
{
[Header("Break Settings")]
[Tooltip("Position along rope where break occurs (0-1)")]
[Range(0f, 1f)]
[SerializeField] private float breakPosition = 0.5f;
[Tooltip("Effect to spawn at break point (optional)")]
[SerializeField] private GameObject breakEffect;
[Tooltip("Sound to play when rope breaks (optional)")]
[SerializeField] private AudioClip breakSound;
[Header("Physics Settings")]
[Tooltip("Follow speed for the rope physics simulation")]
[SerializeField] private float ropeFollowSpeed = 5f;
[Tooltip("Trailing amount for the rope physics simulation")]
[SerializeField] private float ropeTrailing = 0.2f;
[Tooltip("Gravity strength applied to hanging rope end")]
[SerializeField] private float ropeGravityStrength = 9.8f;
[Tooltip("How strongly the rope tries to hang vertically")]
[SerializeField] private float ropeVerticalHangStrength = 2f;
[Tooltip("Damping for physics movement (higher = less bouncy)")]
[SerializeField] private float ropeDamping = 0.3f;
[Tooltip("Initial separation distance between rope ends when broken")]
[SerializeField] private float initialSeparationDistance = 0.1f;
[Tooltip("Initial downward impulse for falling rope end")]
[SerializeField] private float initialFallImpulse = 2.0f;
// Private references
private Rope originalRope;
private LineRenderer originalLineRenderer;
private GameObject firstHalfRope;
private GameObject secondHalfRope;
private Rope firstHalfRopeComponent;
private Rope secondHalfRopeComponent;
private Transform breakPointTransform;
private Transform secondBreakTransform;
private void Awake()
{
// Get references to the required components
originalRope = GetComponent<Rope>();
originalLineRenderer = GetComponent<LineRenderer>();
if (originalRope == null || originalLineRenderer == null)
{
Debug.LogError("RopeBreaker requires both Rope and LineRenderer components on the same GameObject");
enabled = false;
}
}
/// <summary>
/// Breaks the rope at the specified position.
/// </summary>
/// <param name="breakPositionOverride">Optional override for break position (0-1)</param>
/// <returns>True if rope was broken successfully, false otherwise</returns>
public bool BreakRope(float? breakPositionOverride = null)
{
if (originalRope == null || !originalRope.StartPoint || !originalRope.EndPoint)
{
Debug.LogError("Cannot break rope: Missing rope component or endpoints");
return false;
}
// Use override position if provided
float breakPos = breakPositionOverride ?? breakPosition;
breakPos = Mathf.Clamp01(breakPos);
// Get the world position at the break point
Vector3 breakPointPosition = originalRope.GetPointAt(breakPos);
// Create a transform at the break point to use as an anchor
CreateBreakPointTransform(breakPointPosition);
// Create two new rope GameObjects
CreateRopeSegments(breakPointPosition);
// Hide the original rope
originalLineRenderer.enabled = false;
// Play effects
PlayBreakEffects(breakPointPosition);
return true;
}
/// <summary>
/// Creates a transform at the break point to use as an anchor
/// </summary>
private void CreateBreakPointTransform(Vector3 breakPointPosition)
{
// Store references to the original rope endpoints
Transform originalStartPoint = originalRope.StartPoint;
Transform originalEndPoint = originalRope.EndPoint;
// Create a new GameObject for the break point (attached to Player)
GameObject breakPointObj = new GameObject("RopeBreakPoint");
breakPointTransform = breakPointObj.transform;
breakPointTransform.position = breakPointPosition;
breakPointTransform.SetParent(transform.parent); // Parent to the same parent as the rope
// Add the physics follower component to the break point
RopeEndPhysicsFollower follower = breakPointObj.AddComponent<RopeEndPhysicsFollower>();
// Set specific transform to follow instead of using tag
follower.SetTargetTransform(originalStartPoint);
follower.canFall = false; // Player rope end doesn't fall
follower.followSpeed = ropeFollowSpeed;
follower.trailing = ropeTrailing;
// Create second break point (for the rock-attached end)
GameObject secondBreakObj = new GameObject("RopeBreakPoint_Second");
secondBreakTransform = secondBreakObj.transform;
secondBreakTransform.position = breakPointPosition;
secondBreakTransform.SetParent(transform.parent);
// Add physics behavior to second break point
RopeEndPhysicsFollower secondFollower = secondBreakObj.AddComponent<RopeEndPhysicsFollower>();
// Set specific transform to follow instead of using tag
secondFollower.SetTargetTransform(originalEndPoint);
secondFollower.canFall = true; // Rock end can fall
secondFollower.followSpeed = ropeFollowSpeed;
secondFollower.trailing = ropeTrailing;
secondFollower.gravityStrength = ropeGravityStrength;
secondFollower.verticalHangStrength = ropeVerticalHangStrength;
secondFollower.damping = ropeDamping;
secondFollower.initialFallImpulse = initialFallImpulse;
// Create initial separation
Vector3 direction = (originalEndPoint.position - breakPointPosition).normalized;
if (direction.magnitude < 0.01f) direction = Vector3.down;
breakPointTransform.position -= direction * initialSeparationDistance * 0.5f;
secondBreakTransform.position += direction * initialSeparationDistance * 0.5f;
}
/// <summary>
/// Creates two new rope GameObjects for the broken segments
/// </summary>
private void CreateRopeSegments(Vector3 breakPointPosition)
{
// Create the first half rope (from start to break point)
firstHalfRope = new GameObject("Rope_FirstHalf");
firstHalfRope.transform.position = transform.position;
firstHalfRope.transform.rotation = transform.rotation;
firstHalfRope.transform.SetParent(transform.parent);
// Add Rope component which automatically adds LineRenderer due to RequireComponent
firstHalfRopeComponent = firstHalfRope.AddComponent<Rope>();
// Get the LineRenderer that was automatically added
LineRenderer firstLineRenderer = firstHalfRope.GetComponent<LineRenderer>();
if (firstLineRenderer == null)
{
// Only add if somehow not created (shouldn't happen, but safety check)
firstLineRenderer = firstHalfRope.AddComponent<LineRenderer>();
}
CopyLineRendererProperties(originalLineRenderer, firstLineRenderer);
// Create the second half rope (from break point to end)
secondHalfRope = new GameObject("Rope_SecondHalf");
secondHalfRope.transform.position = transform.position;
secondHalfRope.transform.rotation = transform.rotation;
secondHalfRope.transform.SetParent(transform.parent);
// Add Rope component which automatically adds LineRenderer due to RequireComponent
secondHalfRopeComponent = secondHalfRope.AddComponent<Rope>();
// Get the LineRenderer that was automatically added
LineRenderer secondLineRenderer = secondHalfRope.GetComponent<LineRenderer>();
if (secondLineRenderer == null)
{
// Only add if somehow not created (shouldn't happen, but safety check)
secondLineRenderer = secondHalfRope.AddComponent<LineRenderer>();
}
CopyLineRendererProperties(originalLineRenderer, secondLineRenderer);
// Configure the first half rope
firstHalfRopeComponent.SetStartPoint(originalRope.StartPoint);
firstHalfRopeComponent.SetEndPoint(breakPointTransform, false); // Don't recalculate yet
// Copy properties from original rope
CopyRopeProperties(originalRope, firstHalfRopeComponent);
// Explicitly initialize the rope
firstHalfRopeComponent.Initialize();
// Now force recalculation after initialization
firstHalfRopeComponent.RecalculateRope();
// Configure the second half rope - REVERSED: Rock (End) is now Start, Break point is now End
secondHalfRopeComponent.SetStartPoint(originalRope.EndPoint);
secondHalfRopeComponent.SetEndPoint(secondBreakTransform, false); // Don't recalculate yet
// Copy properties from original rope
CopyRopeProperties(originalRope, secondHalfRopeComponent);
// Explicitly initialize the rope
secondHalfRopeComponent.Initialize();
// Now force recalculation after initialization
secondHalfRopeComponent.RecalculateRope();
// Set explicit rope length constraints on the physics followers
// This needs to be done after the rope segments are created so we have the correct rope lengths
RopeEndPhysicsFollower playerFollower = breakPointTransform.GetComponent<RopeEndPhysicsFollower>();
if (playerFollower != null)
{
playerFollower.SetMaxDistance(firstHalfRopeComponent.ropeLength);
}
RopeEndPhysicsFollower rockFollower = secondBreakTransform.GetComponent<RopeEndPhysicsFollower>();
if (rockFollower != null)
{
rockFollower.SetMaxDistance(secondHalfRopeComponent.ropeLength);
}
}
/// <summary>
/// Copies properties from one LineRenderer to another
/// </summary>
private void CopyLineRendererProperties(LineRenderer source, LineRenderer destination)
{
// Copy material
destination.material = source.material;
// Copy colors
destination.startColor = source.startColor;
destination.endColor = source.endColor;
// Copy width
destination.startWidth = source.startWidth;
destination.endWidth = source.endWidth;
// Copy other properties
destination.numCornerVertices = source.numCornerVertices;
destination.numCapVertices = source.numCapVertices;
destination.alignment = source.alignment;
destination.textureMode = source.textureMode;
destination.generateLightingData = source.generateLightingData;
destination.useWorldSpace = source.useWorldSpace;
destination.loop = source.loop;
destination.sortingLayerID = source.sortingLayerID;
destination.sortingOrder = source.sortingOrder;
}
/// <summary>
/// Copies properties from one Rope to another
/// </summary>
private void CopyRopeProperties(Rope source, Rope destination)
{
destination.linePoints = source.linePoints;
destination.stiffness = source.stiffness;
destination.damping = source.damping;
destination.ropeLength = source.ropeLength / 2f; // Halve the rope length for each segment
destination.ropeWidth = source.ropeWidth;
destination.midPointWeight = source.midPointWeight;
destination.midPointPosition = source.midPointPosition;
// Recalculate the rope to update its appearance
destination.RecalculateRope();
}
/// <summary>
/// Plays visual and audio effects at the break point
/// </summary>
private void PlayBreakEffects(Vector3 breakPointPosition)
{
// Spawn break effect if assigned
if (breakEffect != null)
{
Instantiate(breakEffect, breakPointPosition, Quaternion.identity);
}
// Play break sound if assigned
if (breakSound != null)
{
AudioSource.PlayClipAtPoint(breakSound, breakPointPosition);
}
}
/// <summary>
/// Restores the original rope and cleans up the broken pieces
/// </summary>
public void RestoreRope()
{
// Re-enable the original rope
if (originalLineRenderer != null)
{
originalLineRenderer.enabled = true;
}
// Clean up the broken rope pieces
if (firstHalfRope != null)
{
Destroy(firstHalfRope);
}
if (secondHalfRope != null)
{
Destroy(secondHalfRope);
}
// Clean up both break points
if (breakPointTransform != null)
{
Destroy(breakPointTransform.gameObject);
}
if (secondBreakTransform != null)
{
Destroy(secondBreakTransform.gameObject);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 30919200017f4879867c3b6289429924
timeCreated: 1758466190

View File

@@ -0,0 +1,306 @@
using GogoGaga.OptimizedRopesAndCables;
using UnityEngine;
public class RopeEndPhysicsFollower : MonoBehaviour
{
[Header("Target Settings")]
[Tooltip("Transform this endpoint should follow")]
public Transform targetTransform;
[Tooltip("Tag of the object this endpoint should follow (only used if targetTransform is not set)")]
public string targetTag;
[Tooltip("How quickly the endpoint follows the target when not using physics")]
public float followSpeed = 5f;
[Tooltip("How much trailing (0 = instant, 1 = very slow)")]
public float trailing = 0.2f;
[Header("Physics Simulation")]
[Tooltip("Gravity strength")]
public float gravityStrength = 9.8f;
[Tooltip("How strongly the rope attempts to hang vertically")]
public float verticalHangStrength = 2f;
[Tooltip("Damping for physics movement (higher = less bouncy)")]
public float damping = 0.3f;
[Tooltip("Initial downward impulse when enabled")]
public float initialFallImpulse = 2.0f;
[Tooltip("Whether this end can fall with gravity (false for player-attached ends)")]
public bool canFall = true;
// Private variables
private Transform target;
private Vector2 physicsVelocity;
private Vector2 offset;
private Vector3 lastTargetPosition;
private bool initialized = false;
private bool debugLog = true;
// Rope reference to get the actual rope length
private Rope attachedRope;
private float maxDistance;
void Start()
{
// Find the Rope component to determine the maximum distance
FindAttachedRope();
// Use targetTransform if set, otherwise try to find by tag
if (targetTransform != null)
{
target = targetTransform;
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Using assigned target transform: {target.name}");
}
else if (!string.IsNullOrEmpty(targetTag))
{
GameObject found = GameObject.FindGameObjectWithTag(targetTag);
if (found)
{
target = found.transform;
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Found target by tag '{targetTag}': {target.name}");
}
}
// Initialize offset and velocities
if (target)
{
// Only store horizontal offset, not vertical for physics simulation
Vector2 offsetVec = transform.position - target.position;
offset.x = offsetVec.x;
offset.y = 0; // Don't preserve vertical offset for gravity simulation
lastTargetPosition = target.position;
// Apply initial falling impulse if this end can fall
if (canFall)
{
physicsVelocity = new Vector2(0, -initialFallImpulse);
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Initialized with target: {target.name}, initial Y velocity: {physicsVelocity.y}");
}
}
else
{
offset = Vector2.zero;
lastTargetPosition = transform.position;
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] No target found");
}
initialized = true;
}
void Update()
{
if (!target) return;
// Calculate deltaTime for physics stability
float deltaTime = Time.deltaTime;
// Get target velocity
Vector3 targetVelocity = (target.position - lastTargetPosition) / deltaTime;
lastTargetPosition = target.position;
// Current position relative to target
Vector2 relativePos = new Vector2(
transform.position.x - target.position.x,
transform.position.y - target.position.y
);
// Get the straight-line distance between target and this transform
float currentDistance = relativePos.magnitude;
// Normalized direction from target to this transform
Vector2 directionToTarget = relativePos.normalized;
// Apply forces based on whether this end can fall
if (canFall)
{
// 1. Gravity - always pulls down
physicsVelocity.y -= gravityStrength * deltaTime;
}
// 2. Vertical hanging force - try to align X with target when stationary
if (Mathf.Abs(targetVelocity.x) < 0.1f)
{
// Use the actual X position of the target as the desired X position
float xOffset = transform.position.x - target.position.x;
physicsVelocity.x -= xOffset * verticalHangStrength * deltaTime;
// Debug log to track vertical hanging behavior
if (debugLog && Time.frameCount % 120 == 0)
{
Debug.Log($"[RopeEndPhysicsFollower] Vertical hanging: target X={target.position.x}, my X={transform.position.x}, offset={xOffset}");
}
}
// 3. Rope length constraint - apply a force toward the target if we're exceeding the rope length
if (currentDistance > maxDistance)
{
// Calculate constraint force proportional to how much we're exceeding the rope length
float exceededDistance = currentDistance - maxDistance;
// Apply a stronger constraint force the more we exceed the max distance
Vector2 constraintForce = -directionToTarget * exceededDistance * 10f;
// Apply to velocity
physicsVelocity += constraintForce * deltaTime;
if (debugLog && Time.frameCount % 60 == 0)
{
Debug.Log($"[RopeEndPhysicsFollower] Exceeding max distance: {exceededDistance}, applying constraint");
}
}
// Apply damping to physics velocity
physicsVelocity *= (1f - damping * deltaTime);
// Log physics state periodically for debugging
if (debugLog && Time.frameCount % 60 == 0)
{
Debug.Log($"[RopeEndPhysicsFollower] Y position: {transform.position.y}, Y velocity: {physicsVelocity.y}, Distance: {currentDistance}/{maxDistance}");
}
// Apply physics velocity to position
Vector3 newPos = transform.position;
newPos.x += physicsVelocity.x * deltaTime;
// Only apply vertical movement if this end can fall
if (canFall)
{
newPos.y += physicsVelocity.y * deltaTime;
}
transform.position = newPos;
// Final distance check - hard constraint to ensure we never exceed the rope length
// This prevents numerical instability from causing the rope to stretch
float finalDistance = Vector2.Distance(
new Vector2(transform.position.x, transform.position.y),
new Vector2(target.position.x, target.position.y)
);
if (finalDistance > maxDistance)
{
// Calculate the direction from target to this transform
Vector2 direction = new Vector2(
transform.position.x - target.position.x,
transform.position.y - target.position.y
).normalized;
// Set position to be exactly at the maximum distance
Vector3 constrainedPos = new Vector3(
target.position.x + direction.x * maxDistance,
target.position.y + direction.y * maxDistance,
transform.position.z
);
transform.position = constrainedPos;
}
}
private void FindAttachedRope()
{
// Find the Rope component on the same GameObject or parent
attachedRope = GetComponent<Rope>();
if (attachedRope == null)
{
attachedRope = GetComponentInParent<Rope>();
}
// If we still couldn't find it, look for it on a child GameObject
if (attachedRope == null)
{
attachedRope = GetComponentInChildren<Rope>();
}
// Look for a rope attached to this endpoint
if (attachedRope == null)
{
// Find any rope that has this transform as an endpoint
Rope[] allRopes = FindObjectsOfType<Rope>();
foreach (var rope in allRopes)
{
if (rope.EndPoint == transform || rope.StartPoint == transform)
{
attachedRope = rope;
break;
}
}
}
// Set max distance based on rope length if we found a rope
if (attachedRope != null)
{
maxDistance = attachedRope.ropeLength;
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Found attached rope with length: {maxDistance}");
}
else
{
// Default fallback value if no rope is found
maxDistance = 2f;
if (debugLog) Debug.Log("[RopeEndPhysicsFollower] No attached rope found, using default max distance");
}
}
public void SetTargetTransform(Transform newTarget)
{
targetTransform = newTarget;
target = newTarget;
if (target != null)
{
lastTargetPosition = target.position;
// Only update horizontal offset to maintain current vertical position
if (initialized)
{
Vector2 newOffset = transform.position - target.position;
offset.x = newOffset.x;
// Don't update offset.y to allow gravity to work
}
else
{
Vector2 newOffset = transform.position - target.position;
offset.x = newOffset.x;
offset.y = 0; // Don't preserve vertical offset for gravity simulation
}
// Apply initial falling impulse if this end can fall
if (canFall)
{
physicsVelocity = new Vector2(physicsVelocity.x, -initialFallImpulse);
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Reset Y velocity to {physicsVelocity.y} after target change to {target.name}");
}
}
}
// Original tag-based method kept for backward compatibility
public void SetTargetTag(string tag)
{
targetTag = tag;
GameObject found = GameObject.FindGameObjectWithTag(targetTag);
if (found)
{
SetTargetTransform(found.transform);
}
}
// Debug method to force reset the physics
public void ForceResetPhysics()
{
if (target)
{
// Reset velocity with a strong downward impulse
physicsVelocity = new Vector2(0, -initialFallImpulse * 2f);
// Reset position to be at the same level as the target
Vector3 pos = transform.position;
pos.y = target.position.y;
transform.position = pos;
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Physics forcibly reset, new Y velocity: {physicsVelocity.y}");
}
}
// Method to manually set the maximum distance
public void SetMaxDistance(float distance)
{
maxDistance = distance;
if (debugLog) Debug.Log($"[RopeEndPhysicsFollower] Max distance manually set to: {maxDistance}");
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bc5a0e35b5e74474b4241fae08971e7a
timeCreated: 1758485385

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
@@ -48,13 +49,31 @@ namespace Minigames.DivingForPictures
private float _screenBottom;
private float _screenTop;
private TrenchTilePool _tilePool;
// Current velocity for tile movement
private float _currentVelocity;
// Interval for velocity calculations (seconds)
[SerializeField] private float velocityCalculationInterval = 0.5f;
private const float TileSpawnZ = -1f;
private const float DefaultTileHeight = 5f;
// Direction state
private bool _isSurfacing = false;
private bool _stopSpawning = false;
// Event triggered when the last tile leaves the screen after stopping spawning
public UnityEvent onLastTileLeft = new UnityEvent();
// Velocity management
private float _baseMoveSpeed;
private float _velocityFactor = 1.0f;
private void Awake()
{
_mainCamera = Camera.main;
_baseMoveSpeed = moveSpeed; // Store the original base speed
// Calculate tile heights for each prefab
CalculateTileHeights();
@@ -89,15 +108,57 @@ namespace Minigames.DivingForPictures
{
CalculateScreenBounds();
SpawnInitialTiles();
// Initialize velocity and start the velocity calculation coroutine
_currentVelocity = moveSpeed * Time.fixedDeltaTime;
StartCoroutine(VelocityCalculationRoutine());
}
private void Update()
{
MoveTiles();
HandleMovement();
HandleTileDestruction();
HandleTileSpawning();
HandleSpeedRamping();
}
/// <summary>
/// Gets the current velocity of the tiles
/// </summary>
/// <returns>The current upward velocity of the tiles</returns>
public float GetCurrentVelocity()
{
return _currentVelocity;
}
/// <summary>
/// Sets a custom velocity, overriding the calculated one
/// </summary>
/// <param name="velocity">The new velocity value</param>
public void SetVelocity(float velocity)
{
_currentVelocity = velocity;
}
/// <summary>
/// Coroutine that periodically calculates the velocity based on game state
/// </summary>
private IEnumerator VelocityCalculationRoutine()
{
while (true)
{
CalculateVelocity();
yield return new WaitForSeconds(velocityCalculationInterval);
}
}
/// <summary>
/// Calculates the current velocity based on move speed
/// </summary>
private void CalculateVelocity()
{
_currentVelocity = moveSpeed * Time.fixedDeltaTime;
}
/// <summary>
/// Calculate height values for all tile prefabs
@@ -170,9 +231,25 @@ namespace Minigames.DivingForPictures
/// </summary>
private void SpawnInitialTiles()
{
// Calculate starting Y position - moved 2 tiles up from the bottom of the screen
float startingY = _screenBottom;
// If we have prefab tiles with known heights, use their average height for offset calculation
float tileHeightEstimate = DefaultTileHeight;
if (tilePrefabs != null && tilePrefabs.Count > 0 && tilePrefabs[0] != null)
{
if (_tileHeights.TryGetValue(tilePrefabs[0], out float height))
{
tileHeightEstimate = height;
}
}
// Move starting position up by 2 tile heights
startingY += tileHeightEstimate * 2;
for (int i = 0; i < initialTileCount; i++)
{
float y = _screenBottom;
float y = startingY;
// Calculate proper Y position based on previous tiles
if (i > 0 && _activeTiles.Count > 0)
{
@@ -206,16 +283,61 @@ namespace Minigames.DivingForPictures
}
/// <summary>
/// Move all active tiles upward
/// Called when the velocity factor changes from the DivingGameManager
/// </summary>
private void MoveTiles()
public void OnVelocityFactorChanged(float velocityFactor)
{
_velocityFactor = velocityFactor;
// Update the actual move speed based on the velocity factor
// This keeps the original move speed intact for game logic
moveSpeed = _baseMoveSpeed * Mathf.Abs(_velocityFactor);
// Recalculate velocity immediately
CalculateVelocity();
Debug.Log($"[TrenchTileSpawner] Velocity factor updated to {_velocityFactor:F2}, moveSpeed: {moveSpeed:F2}");
}
/// <summary>
/// Reverses direction to start surfacing
/// </summary>
public void StartSurfacing()
{
if (_isSurfacing) return; // Already surfacing
// Set surfacing flag for spawn/despawn logic
_isSurfacing = true;
// Reverse the active tiles array to maintain consistent indexing logic
_activeTiles.Reverse();
Debug.Log("[TrenchTileSpawner] Started surfacing - reversed array order");
}
/// <summary>
/// Stops spawning new tiles
/// </summary>
public void StopSpawning()
{
_stopSpawning = true;
}
/// <summary>
/// Handles the movement of all active tiles based on the current velocity
/// </summary>
private void HandleMovement()
{
float moveDelta = moveSpeed * Time.deltaTime;
foreach (var tile in _activeTiles)
{
if (tile != null)
{
tile.transform.position += Vector3.up * moveDelta;
// Use velocity factor to determine direction
Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor);
float speed = _currentVelocity;
// Apply movement
tile.transform.position += direction * speed;
}
}
}
@@ -235,7 +357,20 @@ namespace Minigames.DivingForPictures
}
float tileHeight = GetTileHeight(topTile);
if (topTile.transform.position.y - tileHeight / 2 > _screenTop + tileSpawnBuffer)
bool shouldDestroy;
if (_isSurfacing)
{
// When surfacing, destroy tiles at the bottom
shouldDestroy = topTile.transform.position.y + tileHeight / 2 < _screenBottom - tileSpawnBuffer;
}
else
{
// When descending, destroy tiles at the top
shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + tileSpawnBuffer;
}
if (shouldDestroy)
{
_activeTiles.RemoveAt(0);
onTileDestroyed?.Invoke(topTile);
@@ -265,7 +400,15 @@ namespace Minigames.DivingForPictures
/// </summary>
private void HandleTileSpawning()
{
if (_activeTiles.Count == 0) return;
if (_activeTiles.Count == 0)
{
// If we have no active tiles and spawning is stopped, trigger the event
if (_stopSpawning)
{
onLastTileLeft.Invoke();
}
return;
}
GameObject bottomTile = _activeTiles[^1];
if (bottomTile == null)
@@ -274,15 +417,60 @@ namespace Minigames.DivingForPictures
return;
}
// Get the tile height once to use in all calculations
float tileHeight = GetTileHeight(bottomTile);
float bottomEdge = bottomTile.transform.position.y - tileHeight / 2;
if (bottomEdge > _screenBottom - tileSpawnBuffer)
// If we're in stop spawning mode, don't spawn new tiles
if (_stopSpawning)
{
// Check if this is the last tile, and if it's about to leave the screen
if (_activeTiles.Count == 1)
{
bool isLastTileLeaving;
if (_isSurfacing)
{
// When surfacing, check if the bottom of the tile is above the top of the screen
isLastTileLeaving = bottomTile.transform.position.y - tileHeight / 2 > _screenTop + tileSpawnBuffer;
}
else
{
// When descending, check if the top of the tile is below the bottom of the screen
isLastTileLeaving = bottomTile.transform.position.y + tileHeight / 2 < _screenBottom - tileSpawnBuffer;
}
if (isLastTileLeaving)
{
onLastTileLeft.Invoke();
}
}
return;
}
bool shouldSpawn;
float newY;
if (_isSurfacing)
{
// When surfacing, spawn new tiles at the top
float topEdge = bottomTile.transform.position.y + tileHeight / 2;
shouldSpawn = topEdge < _screenTop + tileSpawnBuffer;
newY = bottomTile.transform.position.y + tileHeight;
}
else
{
// When descending, spawn new tiles at the bottom
float bottomEdge = bottomTile.transform.position.y - tileHeight / 2;
shouldSpawn = bottomEdge > _screenBottom - tileSpawnBuffer;
newY = bottomTile.transform.position.y - tileHeight;
}
if (shouldSpawn)
{
float newY = bottomTile.transform.position.y - tileHeight;
SpawnTileAtY(newY);
}
}
/// <summary>
/// Handle increasing the movement speed over time
/// </summary>