Add backbone for card creation and implement Camera minigame mechanics

This commit is contained in:
Michal Pikulski
2025-10-10 14:31:51 +02:00
parent d9039ce655
commit 0c5546efd2
107 changed files with 10490 additions and 280 deletions

View File

@@ -20,7 +20,7 @@ namespace Minigames.DivingForPictures
private float maxScale = 1.2f;
private float baseScale = 1f;
private Camera mainCamera;
private UnityEngine.Camera mainCamera;
private BubblePool parentPool;
// Coroutine references
@@ -48,7 +48,7 @@ namespace Minigames.DivingForPictures
}
// Cache camera reference
mainCamera = Camera.main;
mainCamera = UnityEngine.Camera.main;
}
private void OnEnable()
@@ -78,7 +78,7 @@ namespace Minigames.DivingForPictures
/// <summary>
/// Resumes all bubble behaviors
/// </summary>
public void Resume()
public void DoResume()
{
if (!_isPaused) return; // Already running
@@ -94,7 +94,7 @@ namespace Minigames.DivingForPictures
/// </summary>
private void StartBubbleBehavior()
{
if (_isPaused) return; // Don't start if paused
if (_isPaused || !isActiveAndEnabled) return; // Don't start if paused
_movementCoroutine = StartCoroutine(MovementCoroutine());
_wobbleCoroutine = StartCoroutine(WobbleCoroutine());

View File

@@ -1,6 +1,5 @@
using System.Collections;
using UnityEngine;
using Pooling;
using AppleHills.Core.Settings;
using AppleHills.Core.Interfaces;
@@ -20,7 +19,7 @@ namespace Minigames.DivingForPictures
private float _timer;
private float _nextSpawnInterval;
private BubblePool _bubblePool;
private Camera _mainCamera; // Cache camera reference
private UnityEngine.Camera _mainCamera; // Cache camera reference
private bool _isSurfacing = false;
// Pause state
@@ -34,7 +33,7 @@ namespace Minigames.DivingForPictures
void Awake()
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
// Get developer settings and game settings
_devSettings = GameManager.GetDeveloperSettings<DivingDeveloperSettings>();
@@ -67,12 +66,7 @@ namespace Minigames.DivingForPictures
void Start()
{
// Register with DivingGameManager for pause/resume events
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.RegisterPausableComponent(this);
}
DivingGameManager.Instance.RegisterPausableComponent(this);
// Start spawning if not paused
StartSpawningCoroutine();
@@ -80,12 +74,7 @@ namespace Minigames.DivingForPictures
void OnDestroy()
{
// Unregister from DivingGameManager
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.UnregisterPausableComponent(this);
}
DivingGameManager.Instance.UnregisterPausableComponent(this);
// Clean up any active coroutines
StopAllCoroutines();
@@ -123,7 +112,7 @@ namespace Minigames.DivingForPictures
/// <summary>
/// Resumes the bubble spawner and all bubbles
/// </summary>
public void Resume()
public void DoResume()
{
if (!_isPaused) return; // Already running
@@ -138,7 +127,7 @@ namespace Minigames.DivingForPictures
{
if (bubble != null)
{
bubble.Resume();
bubble.DoResume();
}
}

View File

@@ -2,12 +2,15 @@
using System.Collections.Generic;
using System;
using System.Collections;
using System.Linq;
using UnityEngine.Events;
using UnityEngine.Playables;
using AppleHills.Core.Settings;
using Utility;
using AppleHills.Core.Interfaces;
using Input;
using UI;
using Minigames.DivingForPictures.PictureCamera;
namespace Minigames.DivingForPictures
{
@@ -24,9 +27,11 @@ namespace Minigames.DivingForPictures
[Header("Surfacing Settings")]
[Tooltip("Reference to the PlayableDirector that will play the surfacing timeline")]
[SerializeField] private PlayableDirector surfacingTimeline;
private CameraViewfinderManager viewfinderManager;
// Settings reference
private IDivingMinigameSettings _settings;
private IDivingMinigameSettings settings;
// Private state variables
private int playerScore = 0;
@@ -37,13 +42,13 @@ namespace Minigames.DivingForPictures
// 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;
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;
public float CurrentVelocityFactor => currentVelocityFactor;
// Events
public event Action<int> OnScoreChanged;
@@ -76,18 +81,58 @@ namespace Minigames.DivingForPictures
// IPausable implementation
public bool IsPaused => _isPaused;
// Photo sequence state
private bool _isPhotoSequenceActive = false;
private Monster _currentPhotoTarget = null;
private Dictionary<IPausable, bool> _pauseStateBackup = new Dictionary<IPausable, bool>();
private float _capturedProximity = 0f; // New: tracks how close to target the photo was taken (0-1)
// List of components to exempt from pausing during photo sequence
private List<IPausable> _exemptFromPhotoSequencePausing = new List<IPausable>();
// Photo sequence events
public event Action<Monster> OnPhotoSequenceStarted;
public event Action<Monster, float> OnPhotoSequenceCompleted; // Now includes proximity score
public event Action<float> OnPhotoSequenceProgressUpdated;
private static DivingGameManager _instance;
private static bool _isQuitting = false;
public static DivingGameManager Instance
{
get
{
if (_instance == null && Application.isPlaying && !_isQuitting)
{
_instance = FindAnyObjectByType<DivingGameManager>();
if (_instance == null)
{
var go = new GameObject("DivingGameManager");
_instance = go.AddComponent<DivingGameManager>();
}
}
return _instance;
}
}
private void Awake()
{
// Get settings from GameManager
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
if (_settings == null)
{
Debug.LogError("[DivingGameManager] Failed to load diving minigame settings!");
}
settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
currentSpawnProbability = settings?.BaseSpawnProbability ?? 0.2f;
// Initialize with base probability from settings
currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
private void OnApplicationQuit()
{
_isQuitting = true;
}
private void Start()
@@ -97,7 +142,7 @@ namespace Minigames.DivingForPictures
if (pauseMenu != null)
{
pauseMenu.OnGamePaused += Pause;
pauseMenu.OnGameResumed += Resume;
pauseMenu.OnGameResumed += DoResume;
Debug.Log("[DivingGameManager] Subscribed to PauseMenu events");
}
@@ -135,35 +180,26 @@ namespace Minigames.DivingForPictures
// Validate rope references (this doesn't depend on initialization)
ValidateRopeReferences();
}
/// <summary>
/// Initializes the game components once the orientation is correct.
/// This is called by SceneOrientationEnforcer when the device is properly oriented.
/// </summary>
public void InitializeGame()
{
// Prevent double initialization
if (_isGameInitialized) return;
viewfinderManager = CameraViewfinderManager.Instance;
Debug.Log("[DivingGameManager] Initializing game");
// Subscribe to tile spawned event
TrenchTileSpawner tileSpawner = FindFirstObjectByType<TrenchTileSpawner>();
if (tileSpawner != null)
// Subscribe to viewfinder events if found
if (viewfinderManager != null)
{
tileSpawner.onTileSpawned.AddListener(OnTileSpawned);
}
else
{
Debug.LogWarning("No TrenchTileSpawner found in scene. Monster spawning won't work.");
viewfinderManager.OnAnimationCompleted += OnViewfinderAnimationCompleted;
viewfinderManager.OnViewfinderTapped += OnViewfinderTapped;
viewfinderManager.OnProximityUpdated += OnProximityUpdated;
viewfinderManager.OnViewfinderTappedDuringAnimation += OnViewfinderTappedDuringAnimation;
viewfinderManager.OnReverseAnimationStarted += OnReverseAnimationStarted;
// Add the viewfinder manager to exempt list to ensure it keeps working during photo sequences
if (viewfinderManager is IPausable viewfinderPausable)
{
RegisterExemptFromPhotoSequencePausing(viewfinderPausable);
}
}
// Mark as initialized
_isGameInitialized = true;
// Notify all listeners that the game is initialized
OnGameInitialized?.Invoke();
OnMonsterSpawned += DoMonsterSpawned;
}
private void OnDestroy()
@@ -181,7 +217,7 @@ namespace Minigames.DivingForPictures
if (pauseMenu != null)
{
pauseMenu.OnGamePaused -= Pause;
pauseMenu.OnGameResumed -= Resume;
pauseMenu.OnGameResumed -= DoResume;
}
// Unregister from GameManager
@@ -192,6 +228,16 @@ namespace Minigames.DivingForPictures
// Unregister all pausable components
_pausableComponents.Clear();
// Unsubscribe from viewfinder events
if (viewfinderManager != null)
{
viewfinderManager.OnAnimationCompleted -= OnViewfinderAnimationCompleted;
viewfinderManager.OnViewfinderTapped -= OnViewfinderTapped;
viewfinderManager.OnProximityUpdated -= OnProximityUpdated;
viewfinderManager.OnViewfinderTappedDuringAnimation -= OnViewfinderTappedDuringAnimation;
viewfinderManager.OnReverseAnimationStarted -= OnReverseAnimationStarted;
}
}
private void Update()
@@ -200,10 +246,10 @@ namespace Minigames.DivingForPictures
// Gradually increase spawn probability over time
float previousProbability = currentSpawnProbability;
if (currentSpawnProbability < _settings.MaxSpawnProbability)
if (currentSpawnProbability < settings.MaxSpawnProbability)
{
currentSpawnProbability += _settings.ProbabilityIncreaseRate * Time.deltaTime;
currentSpawnProbability = Mathf.Min(currentSpawnProbability, _settings.MaxSpawnProbability);
currentSpawnProbability += settings.ProbabilityIncreaseRate * Time.deltaTime;
currentSpawnProbability = Mathf.Min(currentSpawnProbability, settings.MaxSpawnProbability);
// Only fire event if probability changed significantly
if (Mathf.Abs(currentSpawnProbability - previousProbability) > 0.01f)
@@ -223,8 +269,8 @@ namespace Minigames.DivingForPictures
// If we're surfacing, don't spawn new monsters
if (_isSurfacing) return;
bool forceSpawn = timeSinceLastSpawn >= _settings.GuaranteedSpawnTime;
bool onCooldown = timeSinceLastSpawn < _settings.SpawnCooldown;
bool forceSpawn = timeSinceLastSpawn >= settings.GuaranteedSpawnTime;
bool onCooldown = timeSinceLastSpawn < settings.SpawnCooldown;
// Don't spawn if on cooldown, unless forced
if (onCooldown && !forceSpawn) return;
@@ -241,7 +287,7 @@ namespace Minigames.DivingForPictures
// Reset timer and adjust probability
lastSpawnTime = Time.time;
timeSinceLastSpawn = 0f;
currentSpawnProbability = _settings.BaseSpawnProbability;
currentSpawnProbability = settings.BaseSpawnProbability;
OnSpawnProbabilityChanged?.Invoke(currentSpawnProbability);
}
}
@@ -265,14 +311,7 @@ namespace Minigames.DivingForPictures
{
// Parent the monster to the spawn point so it moves with the tile
monsterObj.transform.SetParent(spawnPoint);
// Subscribe to monster events
monster.OnPictureTaken += OnMonsterPictureTaken;
monster.OnMonsterDespawned += OnMonsterDespawned;
// Add to active monsters list
activeMonsters.Add(monster);
// Fire event
OnMonsterSpawned?.Invoke(monster);
}
@@ -283,11 +322,11 @@ namespace Minigames.DivingForPictures
}
}
private void OnMonsterPictureTaken(Monster monster)
private void DoPictureTaken(Monster monster)
{
// Calculate points based on depth
int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * _settings.DepthMultiplier);
int pointsAwarded = _settings.BasePoints + depthBonus;
int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * settings.DepthMultiplier);
int pointsAwarded = settings.BasePoints + depthBonus;
// Add score
playerScore += pointsAwarded;
@@ -296,17 +335,7 @@ namespace Minigames.DivingForPictures
OnScoreChanged?.Invoke(playerScore);
OnPictureTaken?.Invoke(monster, pointsAwarded);
}
private void OnMonsterDespawned(Monster monster)
{
// Remove from active list
activeMonsters.Remove(monster);
// Unsubscribe from events
monster.OnPictureTaken -= OnMonsterPictureTaken;
monster.OnMonsterDespawned -= OnMonsterDespawned;
}
/// <summary>
/// Called when the player takes damage from any collision
/// </summary>
@@ -438,7 +467,7 @@ namespace Minigames.DivingForPictures
_isSurfacing = true;
// 1. Initiate smooth velocity transition to surfacing speed
float targetVelocityFactor = -1.0f * _settings.SurfacingSpeedFactor;
float targetVelocityFactor = -1.0f * settings.SurfacingSpeedFactor;
SetVelocityFactor(targetVelocityFactor);
// 2. Find and notify trench tile spawner about direction change (for spawning/despawning logic)
@@ -457,7 +486,7 @@ namespace Minigames.DivingForPictures
tileSpawner.StartSurfacing();
// Immediately send current velocity factor
tileSpawner.OnVelocityFactorChanged(_currentVelocityFactor);
tileSpawner.OnVelocityFactorChanged(currentVelocityFactor);
}
// Handle the Rock object - disable components and animate it falling offscreen
@@ -525,15 +554,15 @@ namespace Minigames.DivingForPictures
obstacleSpawner.StartSurfacing();
// Immediately send current velocity factor
obstacleSpawner.OnVelocityFactorChanged(_currentVelocityFactor);
obstacleSpawner.OnVelocityFactorChanged(currentVelocityFactor);
}
// Start the surfacing sequence coroutine
if (_surfacingSequenceCoroutine != null)
if (surfacingSequenceCoroutine != null)
{
StopCoroutine(_surfacingSequenceCoroutine);
StopCoroutine(surfacingSequenceCoroutine);
}
_surfacingSequenceCoroutine = StartCoroutine(SurfacingSequence());
surfacingSequenceCoroutine = StartCoroutine(SurfacingSequence());
Debug.Log($"[DivingGameManager] Started surfacing with target velocity factor: {targetVelocityFactor}");
}
@@ -546,7 +575,7 @@ namespace Minigames.DivingForPictures
Vector3 startPosition = rockTransform.position;
// Calculate position below the screen
Camera mainCamera = Camera.main;
UnityEngine.Camera mainCamera = UnityEngine.Camera.main;
if (mainCamera == null)
{
Debug.LogWarning("[DivingGameManager] Cannot find main camera to calculate offscreen position");
@@ -609,7 +638,7 @@ namespace Minigames.DivingForPictures
private IEnumerator SurfacingSequence()
{
// Wait for the configured delay
yield return new WaitForSeconds(_settings.SurfacingSpawnDelay);
yield return new WaitForSeconds(settings.SurfacingSpawnDelay);
// Find tile spawner and tell it to stop spawning
TrenchTileSpawner tileSpawner = FindFirstObjectByType<TrenchTileSpawner>();
@@ -662,12 +691,12 @@ namespace Minigames.DivingForPictures
/// <param name="targetFactor">Target velocity factor (e.g., -1.0 for surfacing speed)</param>
public void SetVelocityFactor(float targetFactor)
{
if (_velocityTransitionCoroutine != null)
if (velocityTransitionCoroutine != null)
{
StopCoroutine(_velocityTransitionCoroutine);
StopCoroutine(velocityTransitionCoroutine);
}
_velocityTransitionCoroutine = StartCoroutine(TransitionVelocityFactor(targetFactor));
velocityTransitionCoroutine = StartCoroutine(TransitionVelocityFactor(targetFactor));
}
/// <summary>
@@ -675,29 +704,29 @@ namespace Minigames.DivingForPictures
/// </summary>
private IEnumerator<WaitForEndOfFrame> TransitionVelocityFactor(float targetFactor)
{
float startFactor = _currentVelocityFactor;
float startFactor = currentVelocityFactor;
float elapsed = 0f;
while (elapsed < _settings.SpeedTransitionDuration)
while (elapsed < settings.SpeedTransitionDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / _settings.SpeedTransitionDuration);
float t = Mathf.Clamp01(elapsed / settings.SpeedTransitionDuration);
// Smooth step interpolation
float smoothStep = t * t * (3f - 2f * t);
_currentVelocityFactor = Mathf.Lerp(startFactor, targetFactor, smoothStep);
currentVelocityFactor = Mathf.Lerp(startFactor, targetFactor, smoothStep);
// Notify listeners about the velocity factor change
OnVelocityFactorChanged?.Invoke(_currentVelocityFactor);
OnVelocityFactorChanged?.Invoke(currentVelocityFactor);
yield return null;
}
_currentVelocityFactor = targetFactor;
currentVelocityFactor = targetFactor;
// Final assignment to ensure exact target value
OnVelocityFactorChanged?.Invoke(_currentVelocityFactor);
OnVelocityFactorChanged?.Invoke(currentVelocityFactor);
}
/// <summary>
@@ -737,6 +766,11 @@ namespace Minigames.DivingForPictures
/// Pause the game and all registered components
/// </summary>
public void Pause()
{
DoPause();
}
public void DoPause(bool turnOffGameInput = true)
{
if (_isPaused) return; // Already paused
@@ -748,13 +782,17 @@ namespace Minigames.DivingForPictures
component.Pause();
}
// Change input mode to UI when menu is open
if(turnOffGameInput)
InputManager.Instance.SetInputMode(InputMode.UI);
Debug.Log($"[DivingGameManager] Game paused. Paused {_pausableComponents.Count} components.");
}
/// <summary>
/// Resume the game and all registered components
/// </summary>
public void Resume()
public void DoResume()
{
if (!_isPaused) return; // Already running
@@ -763,10 +801,281 @@ namespace Minigames.DivingForPictures
// Resume all registered components
foreach (var component in _pausableComponents)
{
component.Resume();
component.DoResume();
}
// Change input mode to UI when menu is open
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
Debug.Log($"[DivingGameManager] Game resumed. Resumed {_pausableComponents.Count} components.");
}
#region Photo Sequence Methods
/// <summary>
/// Called when the viewfinder animation phase changes to reverse (zoom out)
/// </summary>
private void OnReverseAnimationStarted()
{
Debug.Log("[DivingGameManager] Viewfinder animation entering reverse (zoom-out) phase");
}
/// <summary>
/// Called when proximity value is updated during animation
/// </summary>
private void OnProximityUpdated(float proximity)
{
// Store the current proximity value for potential use in scoring
_capturedProximity = proximity;
}
/// <summary>
/// Called when the player taps during the viewfinder animation
/// </summary>
private void OnViewfinderTappedDuringAnimation(float proximity)
{
if (!_isPhotoSequenceActive || _currentPhotoTarget == null)
return;
// Store the proximity value at the time of tap for scoring
_capturedProximity = proximity;
Debug.Log($"[DivingGameManager] Player tapped during animation! Proximity: {proximity:F2}");
// Take the picture at the current proximity
TakePicture();
}
/// <summary>
/// Takes the picture with the current proximity value for scoring
/// </summary>
private void TakePicture()
{
if (!_isPhotoSequenceActive || _currentPhotoTarget == null)
return;
// Notify the monster that its picture was taken
_currentPhotoTarget.NotifyPictureTaken();
// Calculate score based on proximity and depth
CalculateScore(_currentPhotoTarget, _capturedProximity);
// Complete the sequence
CompletePhotoSequence();
}
/// <summary>
/// Calculates the score for a picture based on proximity to target and monster depth
/// </summary>
private void CalculateScore(Monster monster, float proximity)
{
if (monster == null) return;
// Calculate base points from depth
int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * settings.DepthMultiplier);
// Apply proximity multiplier (0-100%)
float proximityMultiplier = Mathf.Clamp01(proximity); // Ensure it's in 0-1 range
int proximityBonus = Mathf.RoundToInt(settings.BasePoints * proximityMultiplier);
// Calculate total score
int pointsAwarded = settings.BasePoints + proximityBonus + depthBonus;
Debug.Log($"[DivingGameManager] Picture score calculation: base={proximityBonus} (proximity={proximity:F2}), " +
$"depth bonus={depthBonus}, total={pointsAwarded}");
// Add score
playerScore += pointsAwarded;
// Fire events
OnScoreChanged?.Invoke(playerScore);
OnPictureTaken?.Invoke(monster, pointsAwarded);
}
/// <summary>
/// Handles completion of the viewfinder animation
/// </summary>
private void OnViewfinderAnimationCompleted()
{
if (!_isPhotoSequenceActive || _currentPhotoTarget == null)
return;
// Take the picture at whatever the final proximity was
TakePicture();
}
/// <summary>
/// Cleans up and completes the picture-taking sequence
/// </summary>
private void CompletePhotoSequence()
{
if (!_isPhotoSequenceActive)
return;
// Notify listeners that photo sequence has completed (with proximity score)
if (_currentPhotoTarget != null)
{
OnPhotoSequenceCompleted?.Invoke(_currentPhotoTarget, _capturedProximity);
// We no longer despawn the monster after taking a picture
// _currentPhotoTarget.Despawn(false);
// Just mark the photo sequence as no longer in progress
_currentPhotoTarget.NotifyPictureTaken();
}
DoResume();
// Reset state
_isPhotoSequenceActive = false;
_currentPhotoTarget = null;
Debug.Log($"[DivingGameManager] Completed photo sequence with proximity score: {_capturedProximity:F2}");
}
/// <summary>
/// Register a component to be exempt from pausing during photo sequence
/// </summary>
public void RegisterExemptFromPhotoSequencePausing(IPausable component)
{
if (!_exemptFromPhotoSequencePausing.Contains(component))
{
_exemptFromPhotoSequencePausing.Add(component);
}
}
#endregion
#region Game Initialization
/// <summary>
/// Initializes the game components once the orientation is correct.
/// This is called by SceneOrientationEnforcer when the device is properly oriented.
/// </summary>
public void InitializeGame()
{
// Prevent double initialization
if (_isGameInitialized) return;
Debug.Log("[DivingGameManager] Initializing game");
// Subscribe to tile spawned event
TrenchTileSpawner tileSpawner = FindFirstObjectByType<TrenchTileSpawner>();
if (tileSpawner != null)
{
tileSpawner.onTileSpawned.AddListener(OnTileSpawned);
}
else
{
Debug.LogWarning("No TrenchTileSpawner found in scene. Monster spawning won't work.");
}
// Mark as initialized
_isGameInitialized = true;
// Notify all listeners that the game is initialized
OnGameInitialized?.Invoke();
}
#endregion
private void DoMonsterSpawned(Monster monster)
{
if (monster != null)
{
// Subscribe to monster enter/exit events
monster.OnPlayerEnterDetectionRange += OnPlayerEnterMonsterRange;
monster.OnPlayerExitDetectionRange += OnPlayerExitMonsterRange;
monster.OnMonsterDespawned += DoMonsterDespawned;
// Add to active monsters list
activeMonsters.Add(monster);
}
}
private void DoMonsterDespawned(Monster monster)
{
// Remove from active list
activeMonsters.Remove(monster);
// Unsubscribe from monster events
if (monster != null)
{
monster.OnPlayerEnterDetectionRange -= OnPlayerEnterMonsterRange;
monster.OnPlayerExitDetectionRange -= OnPlayerExitMonsterRange;
monster.OnMonsterDespawned -= DoMonsterDespawned;
}
}
// Handles player entering monster detection range
private void OnPlayerEnterMonsterRange(Monster monster)
{
if (monster != null && !_isPhotoSequenceActive)
{
// Store current target for later use
_currentPhotoTarget = monster;
// Show the full-screen viewfinder (first step in two-step process)
if (viewfinderManager != null)
{
viewfinderManager.ShowFullScreenViewfinder();
Debug.Log($"[DivingGameManager] Player entered range of monster {monster.name}, showing full-screen viewfinder");
}
}
}
// Handles player exiting monster detection range
private void OnPlayerExitMonsterRange(Monster monster)
{
// Only hide viewfinder if we're not already in a photo sequence
if (!_isPhotoSequenceActive && monster == _currentPhotoTarget)
{
// Hide the viewfinder
if (viewfinderManager != null)
{
viewfinderManager.HideViewfinder();
Debug.Log($"[DivingGameManager] Player exited range of monster {monster.name}, hiding viewfinder");
}
// Clear current target
_currentPhotoTarget = null;
}
}
// Called when the player taps on the viewfinder
private void OnViewfinderTapped()
{
// Only proceed if we have a valid target and not already in a sequence
if (_currentPhotoTarget != null && !_isPhotoSequenceActive && _currentPhotoTarget.IsPlayerInDetectionRange)
{
// Pause the game immediately
DoPause(false);
Debug.Log($"[DivingGameManager] Pausing game before starting viewfinder animation");
// Mark the photo sequence as active
_isPhotoSequenceActive = true;
// Mark monster as in photo sequence
_currentPhotoTarget.SetPhotoSequenceInProgress();
// Reset captured proximity
_capturedProximity = 0f;
// Notify listeners that photo sequence has started
OnPhotoSequenceStarted?.Invoke(_currentPhotoTarget);
// Start the complete animation sequence to target monster
if (viewfinderManager != null)
{
viewfinderManager.StartViewfinderSequence(_currentPhotoTarget.transform);
Debug.Log($"[DivingGameManager] Viewfinder tapped for monster {_currentPhotoTarget.name}, starting animation sequence");
}
else
{
Debug.LogError("[DivingGameManager] No ViewfinderManager available!");
CompletePhotoSequence();
}
}
}
}
}

View File

@@ -9,25 +9,15 @@ namespace Minigames.DivingForPictures
[SerializeField] private GameObject scorePopupPrefab;
[SerializeField] private Transform popupParent;
private DivingGameManager gameManager;
private void Start()
{
gameManager = FindFirstObjectByType<DivingGameManager>();
// Subscribe to events
DivingGameManager.Instance.OnScoreChanged += UpdateScoreDisplay;
DivingGameManager.Instance.OnPictureTaken += ShowScorePopup;
if (gameManager != null)
{
// Subscribe to events
gameManager.OnScoreChanged += UpdateScoreDisplay;
gameManager.OnPictureTaken += ShowScorePopup;
// Initialize display
UpdateScoreDisplay(gameManager.PlayerScore);
}
else
{
Debug.LogWarning("No DivingGameManager found in scene.");
}
// Initialize display
UpdateScoreDisplay(DivingGameManager.Instance.PlayerScore);
// Create popup parent if needed
if (popupParent == null)
@@ -38,12 +28,9 @@ namespace Minigames.DivingForPictures
private void OnDestroy()
{
if (gameManager != null)
{
// Unsubscribe from events
gameManager.OnScoreChanged -= UpdateScoreDisplay;
gameManager.OnPictureTaken -= ShowScorePopup;
}
// Unsubscribe from events
DivingGameManager.Instance.OnScoreChanged -= UpdateScoreDisplay;
DivingGameManager.Instance.OnPictureTaken -= ShowScorePopup;
}
private void UpdateScoreDisplay(int score)

View File

@@ -10,22 +10,29 @@ namespace Minigames.DivingForPictures
[SerializeField] private CircleCollider2D detectionCollider;
private bool pictureAlreadyTaken = false;
private Camera mainCamera;
private bool photoSequenceInProgress = false;
private UnityEngine.Camera mainCamera;
// Track if player is in detection range
private bool playerInDetectionRange = false;
// Events
public event Action<Monster> OnPictureTaken;
public event Action<Monster> OnMonsterSpawned;
public event Action<Monster> OnMonsterDespawned;
public event Action<Monster> OnPlayerEnterDetectionRange;
public event Action<Monster> OnPlayerExitDetectionRange;
// Properties
public float PictureRadius => detectionCollider != null ? detectionCollider.radius : 0f;
public bool IsPhotoSequenceInProgress => photoSequenceInProgress;
public bool IsPlayerInDetectionRange => playerInDetectionRange;
public bool IsPictureTaken => pictureAlreadyTaken;
private void Awake()
{
if (detectionCollider == null)
detectionCollider = GetComponent<CircleCollider2D>();
mainCamera = Camera.main;
mainCamera = UnityEngine.Camera.main;
// Start checking if monster is off-screen
StartCoroutine(CheckIfOffScreen());
@@ -34,7 +41,7 @@ namespace Minigames.DivingForPictures
private void OnEnable()
{
pictureAlreadyTaken = false;
OnMonsterSpawned?.Invoke(this);
photoSequenceInProgress = false;
}
private IEnumerator CheckIfOffScreen()
@@ -56,7 +63,7 @@ namespace Minigames.DivingForPictures
private bool IsVisibleToCamera()
{
if (mainCamera == null)
mainCamera = Camera.main;
mainCamera = UnityEngine.Camera.main;
if (mainCamera == null)
return false;
@@ -81,18 +88,67 @@ namespace Minigames.DivingForPictures
// Check if it's the player
if (other.CompareTag("Player") && !pictureAlreadyTaken)
{
TakePicture();
playerInDetectionRange = true;
// Fire the event so the game manager can display the viewfinder without pausing
OnPlayerEnterDetectionRange?.Invoke(this);
}
}
// Called when a picture is taken of this monster
public void TakePicture()
private void OnTriggerExit2D(Collider2D other)
{
// Check if it's the player
if (other.CompareTag("Player"))
{
playerInDetectionRange = false;
// Fire the event so the game manager can hide the viewfinder
OnPlayerExitDetectionRange?.Invoke(this);
}
}
/// <summary>
/// Mark this monster as having its photo sequence in progress
/// </summary>
public void SetPhotoSequenceInProgress(bool inProgress = true)
{
if (pictureAlreadyTaken) return;
photoSequenceInProgress = inProgress;
}
/// <summary>
/// Notify that a picture has been taken of this monster
/// This is now called by DivingGameManager instead of handling the logic internally
/// </summary>
public void NotifyPictureTaken()
{
if (pictureAlreadyTaken) return;
pictureAlreadyTaken = true;
OnPictureTaken?.Invoke(this);
photoSequenceInProgress = false;
}
/// <summary>
/// Despawn this monster (can be called externally)
/// </summary>
/// <param name="immediate">Whether to despawn immediately or after a delay</param>
public void Despawn(bool immediate = false)
{
if (immediate)
{
DespawnMonster();
}
else
{
// Add small delay before despawning
StartCoroutine(DelayedDespawn());
}
}
/// <summary>
/// Coroutine to despawn after a short delay
/// </summary>
private IEnumerator DelayedDespawn()
{
yield return new WaitForSeconds(1.0f); // 1-second delay before despawn
DespawnMonster();
}
@@ -106,20 +162,7 @@ namespace Minigames.DivingForPictures
Destroy(gameObject);
}
}
// Visualization for the picture radius in editor
private void OnDrawGizmosSelected()
{
// Get the collider in edit mode
if (detectionCollider == null)
detectionCollider = GetComponent<CircleCollider2D>();
if (detectionCollider != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, detectionCollider.radius / 2);
}
}
#if UNITY_EDITOR
// Update collider radius in editor

View File

@@ -45,7 +45,7 @@ namespace Minigames.DivingForPictures
// Private fields
private Collider2D _collider;
private Camera _mainCamera;
private UnityEngine.Camera _mainCamera;
private float _screenTop;
private float _screenBottom; // Added to track bottom of screen
private Coroutine _movementCoroutine;
@@ -78,7 +78,7 @@ namespace Minigames.DivingForPictures
Debug.LogError($"[FloatingObstacle] No Collider2D found on {gameObject.name}!");
}
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
// Get settings
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
@@ -153,7 +153,7 @@ namespace Minigames.DivingForPictures
/// <summary>
/// Resume this obstacle's movement and behavior
/// </summary>
public void Resume()
public void DoResume()
{
if (!_isPaused) return; // Already running
@@ -168,6 +168,9 @@ namespace Minigames.DivingForPictures
/// </summary>
private void StartObstacleCoroutines()
{
if (!isActiveAndEnabled)
return;
if (enableMovement && _movementCoroutine == null)
{
_movementCoroutine = StartCoroutine(MovementCoroutine());
@@ -281,7 +284,7 @@ namespace Minigames.DivingForPictures
{
if (_mainCamera == null)
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
if (_mainCamera == null) return;
}
@@ -357,7 +360,7 @@ namespace Minigames.DivingForPictures
{
// Reset all state first
_screenTop = 0f; // Reset cached screen bounds
_mainCamera = Camera.main; // Refresh camera reference
_mainCamera = UnityEngine.Camera.main; // Refresh camera reference
// Re-enable the collider for reuse
if (_collider != null)

View File

@@ -31,7 +31,7 @@ namespace Minigames.DivingForPictures
// Private fields
private ObstaclePool _obstaclePool;
private Camera _mainCamera;
private UnityEngine.Camera _mainCamera;
private float _screenBottom;
private float _spawnRangeX;
private Coroutine _spawnCoroutine;
@@ -50,7 +50,7 @@ namespace Minigames.DivingForPictures
private void Awake()
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
// Get settings from GameManager
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
@@ -84,38 +84,23 @@ namespace Minigames.DivingForPictures
private void Start()
{
// Find DivingGameManager and subscribe to its initialization event
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
DivingGameManager.Instance.OnGameInitialized += Initialize;
// Register with the DivingGameManager for pause/resume events
DivingGameManager.Instance.RegisterPausableComponent(this);
// If game is already initialized, initialize immediately
if (DivingGameManager.Instance.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.GetValue(DivingGameManager.Instance) is bool isInitialized && isInitialized)
{
gameManager.OnGameInitialized += Initialize;
// Register with the DivingGameManager for pause/resume events
gameManager.RegisterPausableComponent(this);
// If game is already initialized, initialize immediately
if (gameManager.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.GetValue(gameManager) is bool isInitialized && isInitialized)
{
Initialize();
}
}
else
{
Debug.LogWarning("[ObstacleSpawner] DivingGameManager not found. Initializing immediately.");
Initialize();
}
}
private void OnDestroy()
{
// Unregister from DivingGameManager
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.UnregisterPausableComponent(this);
}
DivingGameManager.Instance.UnregisterPausableComponent(this);
// Clean up any active coroutines
StopAllCoroutines();
@@ -172,7 +157,7 @@ namespace Minigames.DivingForPictures
/// <summary>
/// Resumes the spawner and all associated processes
/// </summary>
public void Resume()
public void DoResume()
{
if (!_isPaused) return; // Already running
@@ -192,7 +177,7 @@ namespace Minigames.DivingForPictures
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
obstacleComponent.Resume();
obstacleComponent.DoResume();
}
}
}
@@ -314,7 +299,7 @@ namespace Minigames.DivingForPictures
{
if (_mainCamera == null)
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
if (_mainCamera == null)
{
Debug.LogError("[ObstacleSpawner] No main camera found!");
@@ -751,7 +736,7 @@ namespace Minigames.DivingForPictures
if (_mainCamera == null)
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
if (_mainCamera == null)
{
yield return new WaitForSeconds(checkInterval);

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ddda7563ddf04d63ae19527266695bd2
timeCreated: 1760016807

View File

@@ -0,0 +1,776 @@
using UnityEngine;
using System;
using System.Collections;
using AppleHills.Core.Settings;
namespace Minigames.DivingForPictures.PictureCamera
{
/// <summary>
/// Manages the camera viewfinder visual representation and animation
/// Responsible only for visual aspects with no game logic
/// </summary>
public class CameraViewfinderManager : MonoBehaviour
{
// Singleton instance
private static CameraViewfinderManager _instance;
private static bool _isQuitting = false;
public static CameraViewfinderManager Instance
{
get
{
if (_instance == null && Application.isPlaying && !_isQuitting)
{
_instance = FindAnyObjectByType<CameraViewfinderManager>();
if (_instance == null)
{
var go = new GameObject("CameraViewfinderManager");
_instance = go.AddComponent<CameraViewfinderManager>();
}
}
return _instance;
}
}
[Header("References")] [Tooltip("The Canvas to spawn the viewfinder UI on")] [SerializeField]
private Canvas targetCanvas;
// References
private GameObject viewfinderInstance;
private RectTransform viewfinderRectTransform;
private Viewfinder viewfinderComponent;
private UnityEngine.Camera mainCamera;
// Animation state
private float animationProgress = 0f;
private bool isAnimating = false;
private bool isReversePhase = false; // Whether we're in the zoom-out phase
private float currentProximity = 0f; // How close we are to the target (0=far, 1=directly over target)
private Transform targetTransform;
private Coroutine animationCoroutine;
// Target position and size (calculated once at start)
private Vector3 targetScreenPosition;
private float targetViewfinderSize;
private Vector2 targetAnchoredPosition; // target position in canvas units
// Store settings
private IDivingMinigameSettings settings;
// Events for progress milestones
public event Action<float> OnProgressUpdated; // Continuous progress updates (0-1)
public event Action OnAnimationStarted;
public event Action OnAnimationCompleted;
public event Action OnReverseAnimationStarted; // New event for when zoom-out phase starts
public event Action<float> OnProximityUpdated; // New event for proximity updates
public event Action<float> OnProgressThresholdReached; // Fires at configured thresholds (25%, 50%, etc.)
public event Action OnViewfinderTapped; // Event when viewfinder is tapped during normal display
public event Action<float> OnViewfinderTappedDuringAnimation; // Event when viewfinder is tapped during animation (with proximity)
private void Awake()
{
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
private void OnApplicationQuit()
{
_isQuitting = true;
}
private void Start()
{
settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
mainCamera = UnityEngine.Camera.main;
}
/// <summary>
/// Begin the viewfinder animation targeting a specific transform
/// </summary>
/// <param name="target">The transform to focus on (usually the monster)</param>
public void StartViewfinderAnimation(Transform target)
{
if (isAnimating)
{
StopViewfinderAnimation();
}
if (settings.ViewfinderPrefab == null)
{
Debug.LogError("[CameraViewfinderManager] No viewfinder prefab assigned!");
return;
}
if (targetCanvas == null)
{
Debug.LogError("[CameraViewfinderManager] No canvas assigned!");
return;
}
targetTransform = target;
// Calculate target screen position and size only once at the start
CalculateTargetScreenPositionAndSize();
// Create viewfinder as UI element
viewfinderInstance = Instantiate(settings.ViewfinderPrefab, targetCanvas.transform);
viewfinderRectTransform = viewfinderInstance.GetComponent<RectTransform>();
viewfinderComponent = viewfinderInstance.GetComponent<Viewfinder>();
if (viewfinderRectTransform == null)
{
Debug.LogError("[CameraViewfinderManager] Viewfinder prefab doesn't have a RectTransform component!");
Destroy(viewfinderInstance);
return;
}
// Initialize viewfinder with a reference to this manager
if (viewfinderComponent != null)
{
viewfinderComponent.Initialize(this);
viewfinderComponent.SetupInputOverride();
viewfinderComponent.OnViewfinderTapped += HandleViewfinderTapped;
}
// Reset state
animationProgress = 0f;
isAnimating = true;
// Determine canvas width for CanvasScaler-consistent sizing
RectTransform canvasRect = targetCanvas.transform as RectTransform;
float canvasWidth = canvasRect != null ? canvasRect.rect.width : Screen.width;
// Initialize viewfinder size and position to full canvas width (square) at center
float startSize = canvasWidth;
viewfinderRectTransform.sizeDelta = new Vector2(startSize, startSize);
viewfinderRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.pivot = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.anchoredPosition = Vector2.zero;
// Compute target anchored position in canvas units from previously calculated screen position
targetAnchoredPosition = ScreenToAnchoredPosition(targetScreenPosition);
// Fire starting event
OnAnimationStarted?.Invoke();
// Start animation coroutine
animationCoroutine = StartCoroutine(AnimateViewfinder());
}
/// <summary>
/// Calculates the target screen position and size based on monster's sprite bounds
/// </summary>
private void CalculateTargetScreenPositionAndSize()
{
if (targetTransform == null || mainCamera == null) return;
RectTransform canvasRect = targetCanvas != null ? targetCanvas.transform as RectTransform : null;
if (canvasRect == null)
{
Debug.LogError("[CameraViewfinderManager] Target canvas RectTransform not found.");
return;
}
// Choose UI camera for coordinate conversion
Camera uiCamera = null;
if (targetCanvas.renderMode == RenderMode.ScreenSpaceCamera ||
targetCanvas.renderMode == RenderMode.WorldSpace)
{
uiCamera = targetCanvas.worldCamera;
}
// Get sprite renderer from the monster
SpriteRenderer spriteRenderer = targetTransform.GetComponent<SpriteRenderer>();
if (spriteRenderer == null || spriteRenderer.sprite == null)
{
// Fallback to transform position and default size if no sprite renderer
targetScreenPosition = mainCamera.WorldToScreenPoint(targetTransform.position);
// Convert to anchored UI position
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, targetScreenPosition, uiCamera,
out targetAnchoredPosition);
// Default size: fraction of canvas width (canvas units)
float canvasWidth = canvasRect.rect.width;
targetViewfinderSize = canvasWidth * 0.25f;
Debug.LogWarning("[CameraViewfinderManager] No SpriteRenderer found on target, using default size");
return;
}
// Calculate world bounds of the sprite and apply padding
Bounds spriteBounds = spriteRenderer.bounds;
Vector3 paddedSize = spriteBounds.size * settings.PaddingFactor;
Bounds paddedBounds = new Bounds(spriteBounds.center, paddedSize);
// Convert bounds corners to screen space
Vector3[] worldCorners = new Vector3[4];
worldCorners[0] = new Vector3(paddedBounds.min.x, paddedBounds.min.y, paddedBounds.center.z); // BL
worldCorners[1] = new Vector3(paddedBounds.max.x, paddedBounds.min.y, paddedBounds.center.z); // BR
worldCorners[2] = new Vector3(paddedBounds.min.x, paddedBounds.max.y, paddedBounds.center.z); // TL
worldCorners[3] = new Vector3(paddedBounds.max.x, paddedBounds.max.y, paddedBounds.center.z); // TR
// Convert screen-space corners to canvas local points (canvas units)
bool anyFailed = false;
Vector2[] localCorners = new Vector2[4];
for (int i = 0; i < 4; i++)
{
Vector3 screenPos = mainCamera.WorldToScreenPoint(worldCorners[i]);
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, screenPos, uiCamera,
out localCorners[i]))
{
anyFailed = true;
}
}
// Fallback: if conversion failed for some reason, keep original behavior (pixels -> clamp -> convert later)
if (anyFailed)
{
Vector2 minScreen = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxScreen = new Vector2(float.MinValue, float.MinValue);
foreach (Vector3 corner in worldCorners)
{
Vector3 sp = mainCamera.WorldToScreenPoint(corner);
minScreen.x = Mathf.Min(minScreen.x, sp.x);
minScreen.y = Mathf.Min(minScreen.y, sp.y);
maxScreen.x = Mathf.Max(maxScreen.x, sp.x);
maxScreen.y = Mathf.Max(maxScreen.y, sp.y);
}
float widthPx = maxScreen.x - minScreen.x;
float heightPx = maxScreen.y - minScreen.y;
float canvasWidth = canvasRect.rect.width;
float scaleX = Screen.width > 0 ? canvasWidth / Screen.width : 1f;
targetViewfinderSize = Mathf.Max(widthPx, heightPx) * scaleX; // approximate conversion
float minCanvas = canvasWidth * settings.MinSizePercent;
float maxCanvas = canvasWidth * settings.MaxSizePercent;
targetViewfinderSize = Mathf.Clamp(targetViewfinderSize, minCanvas, maxCanvas);
// Target position
targetScreenPosition = mainCamera.WorldToScreenPoint(spriteBounds.center);
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, targetScreenPosition, uiCamera,
out targetAnchoredPosition);
return;
}
// Compute width/height in canvas units from local corners
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
for (int i = 0; i < 4; i++)
{
minX = Mathf.Min(minX, localCorners[i].x);
minY = Mathf.Min(minY, localCorners[i].y);
maxX = Mathf.Max(maxX, localCorners[i].x);
maxY = Mathf.Max(maxY, localCorners[i].y);
}
float widthCanvas = Mathf.Max(0f, maxX - minX);
float heightCanvas = Mathf.Max(0f, maxY - minY);
float canvasSquare = Mathf.Max(widthCanvas, heightCanvas);
// Clamp using canvas width
float canvasW = canvasRect.rect.width;
float minSizeCanvas = canvasW * settings.MinSizePercent;
float maxSizeCanvas = canvasW * settings.MaxSizePercent;
targetViewfinderSize = Mathf.Clamp(canvasSquare, minSizeCanvas, maxSizeCanvas);
// Target position in both screen and canvas units
targetScreenPosition = mainCamera.WorldToScreenPoint(spriteBounds.center);
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, targetScreenPosition, uiCamera,
out targetAnchoredPosition);
Debug.Log(
$"[CameraViewfinderManager] Target size (canvas): {targetViewfinderSize}, target anchored: {targetAnchoredPosition}");
}
/// <summary>
/// Stop the current viewfinder animation and clean up
/// </summary>
public void StopViewfinderAnimation()
{
if (animationCoroutine != null)
{
StopCoroutine(animationCoroutine);
animationCoroutine = null;
}
if (viewfinderInstance != null)
{
Destroy(viewfinderInstance);
viewfinderInstance = null;
}
isAnimating = false;
targetTransform = null;
}
/// <summary>
/// Coroutine that handles the viewfinder animation
/// </summary>
private IEnumerator AnimateViewfinder()
{
RectTransform canvasRect = targetCanvas.transform as RectTransform;
float canvasWidth = canvasRect != null ? canvasRect.rect.width : Screen.width;
// IMPORTANT: We're animating from full canvas width DOWN to the target size
float startSize = canvasWidth; // Always start with full canvas width
float endSize = targetViewfinderSize; // End at the calculated target size
float elapsedTime = 0f;
bool[] thresholdTriggered = new bool[settings.ViewfinderProgressThresholds.Length];
// Debug the actual values
Debug.Log($"[CameraViewfinderManager] Animation starting: startSize={startSize}, endSize={endSize}, " +
$"current sizeDelta={viewfinderRectTransform.sizeDelta}");
// Verify the initial size is set correctly
viewfinderRectTransform.sizeDelta = new Vector2(startSize, startSize);
while (elapsedTime < settings.ViewfinderShrinkDuration)
{
if (targetTransform == null)
{
StopViewfinderAnimation();
yield break;
}
elapsedTime += Time.unscaledDeltaTime;
animationProgress = Mathf.Clamp01(elapsedTime / settings.ViewfinderShrinkDuration);
// The curve value might be inverted - ensure we're shrinking not growing
float curveValue = settings.ViewfinderShrinkCurve.Evaluate(animationProgress);
// FIX: Ensure we're interpolating from large (start) to small (end)
// This is critical - we must ensure the start and end are in the right order
float currentSize = Mathf.Lerp(startSize, endSize, curveValue);
// Additional check to ensure size is actually shrinking
if (startSize > endSize && currentSize > startSize)
{
Debug.LogWarning($"[CameraViewfinderManager] Animation curve producing wrong direction! " +
$"progress={animationProgress:F2}, curveValue={curveValue:F2}");
// Force correct behavior
curveValue = animationProgress; // Override with linear interpolation
currentSize = Mathf.Lerp(startSize, endSize, curveValue);
}
viewfinderRectTransform.sizeDelta = new Vector2(currentSize, currentSize);
// Move in UI space towards the anchored target position
Vector2 currentPos = viewfinderRectTransform.anchoredPosition;
Vector2 newPos = Vector2.Lerp(currentPos, targetAnchoredPosition,
settings.ViewfinderMoveSpeed * Time.unscaledDeltaTime);
viewfinderRectTransform.anchoredPosition = newPos;
// Log the animation state occasionally for debugging
if (animationProgress % 0.25f <= 0.01f || animationProgress >= 0.99f)
{
Debug.Log($"[CameraViewfinderManager] Animation progress: {animationProgress:F2}, " +
$"curveValue={curveValue:F2}, currentSize={currentSize:F2}, currentPos={newPos}");
}
for (int i = 0; i < settings.ViewfinderProgressThresholds.Length; i++)
{
if (!thresholdTriggered[i] && animationProgress >= settings.ViewfinderProgressThresholds[i])
{
thresholdTriggered[i] = true;
OnProgressThresholdReached?.Invoke(settings.ViewfinderProgressThresholds[i]);
}
}
OnProgressUpdated?.Invoke(animationProgress);
yield return null;
}
// Ensure we reach the exact end size
viewfinderRectTransform.sizeDelta = new Vector2(endSize, endSize);
animationProgress = 1f;
OnProgressUpdated?.Invoke(animationProgress);
OnAnimationCompleted?.Invoke();
// Log final state to confirm we reached the target size
Debug.Log(
$"[CameraViewfinderManager] Animation completed: final size={viewfinderRectTransform.sizeDelta}");
yield return new WaitForSecondsRealtime(0.5f);
StopViewfinderAnimation();
}
// Converts a screen-space point to a RectTransform anchoredPosition for the target canvas
private Vector2 ScreenToAnchoredPosition(Vector3 screenPosition)
{
if (targetCanvas == null)
{
return new Vector2(screenPosition.x, screenPosition.y);
}
RectTransform canvasRect = targetCanvas.transform as RectTransform;
Camera uiCamera = null;
if (targetCanvas.renderMode == RenderMode.ScreenSpaceCamera ||
targetCanvas.renderMode == RenderMode.WorldSpace)
{
uiCamera = targetCanvas.worldCamera;
}
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, screenPosition, uiCamera,
out localPoint);
return localPoint;
}
private void OnDestroy()
{
StopViewfinderAnimation();
if (_instance == this)
{
_instance = null;
}
}
/// <summary>
/// Handles tap event from the viewfinder and forwards it
/// </summary>
private void HandleViewfinderTapped()
{
// Forward the tap event to any listeners
OnViewfinderTapped?.Invoke();
}
/// <summary>
/// Hides the currently displayed viewfinder
/// </summary>
public void HideViewfinder()
{
if (viewfinderInstance != null)
{
// Unsubscribe from the viewfinder's tap event
if (viewfinderComponent != null)
{
viewfinderComponent.OnViewfinderTapped -= HandleViewfinderTapped;
}
Destroy(viewfinderInstance);
viewfinderInstance = null;
viewfinderComponent = null;
viewfinderRectTransform = null;
Debug.Log("[CameraViewfinderManager] Hid viewfinder");
}
}
/// <summary>
/// Shows the viewfinder in the middle of the screen at full size without animation
/// This is the first step in the two-step process, showing the UI without targeting a monster yet
/// </summary>
public void ShowFullScreenViewfinder()
{
if (viewfinderInstance != null)
{
// Already showing a viewfinder, destroy it first
Destroy(viewfinderInstance);
}
if (settings.ViewfinderPrefab == null)
{
Debug.LogError("[CameraViewfinderManager] No viewfinder prefab assigned!");
return;
}
if (targetCanvas == null)
{
Debug.LogError("[CameraViewfinderManager] No canvas assigned!");
return;
}
isAnimating = false;
// Create viewfinder as UI element
viewfinderInstance = Instantiate(settings.ViewfinderPrefab, targetCanvas.transform);
viewfinderRectTransform = viewfinderInstance.GetComponent<RectTransform>();
viewfinderComponent = viewfinderInstance.GetComponent<Viewfinder>();
if (viewfinderRectTransform == null)
{
Debug.LogError("[CameraViewfinderManager] Viewfinder prefab doesn't have a RectTransform component!");
Destroy(viewfinderInstance);
return;
}
// Initialize viewfinder with a reference to this manager
if (viewfinderComponent != null)
{
viewfinderComponent.Initialize(this);
viewfinderComponent.SetupInputOverride();
viewfinderComponent.OnViewfinderTapped += HandleViewfinderTapped;
}
// Determine canvas width for full-screen sizing
RectTransform canvasRect = targetCanvas.transform as RectTransform;
float canvasWidth = canvasRect != null ? canvasRect.rect.width : Screen.width;
// Set up the viewfinder in the center of the screen at full canvas width
viewfinderRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.pivot = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.sizeDelta = new Vector2(canvasWidth, canvasWidth);
viewfinderRectTransform.anchoredPosition = Vector2.zero;
Debug.Log($"[CameraViewfinderManager] Showed full-screen viewfinder with size {canvasWidth}");
}
/// <summary>
/// Starts the complete viewfinder animation sequence (zoom in, then zoom out)
/// </summary>
/// <param name="target">The transform to focus on (usually the monster)</param>
public void StartViewfinderSequence(Transform target)
{
if (isAnimating)
{
StopViewfinderAnimation();
}
if (settings.ViewfinderPrefab == null)
{
Debug.LogError("[CameraViewfinderManager] No viewfinder prefab assigned!");
return;
}
if (targetCanvas == null)
{
Debug.LogError("[CameraViewfinderManager] No canvas assigned!");
return;
}
targetTransform = target;
isReversePhase = false;
currentProximity = 0f;
// Calculate target screen position and size based on monster's sprite bounds
CalculateTargetScreenPositionAndSize();
// Reuse existing viewfinder instance if it exists
if (viewfinderInstance == null)
{
// Create viewfinder as UI element if it doesn't exist yet
viewfinderInstance = Instantiate(settings.ViewfinderPrefab, targetCanvas.transform);
viewfinderRectTransform = viewfinderInstance.GetComponent<RectTransform>();
viewfinderComponent = viewfinderInstance.GetComponent<Viewfinder>();
if (viewfinderRectTransform == null)
{
Debug.LogError("[CameraViewfinderManager] Viewfinder prefab doesn't have a RectTransform component!");
Destroy(viewfinderInstance);
return;
}
}
else
{
// Unsubscribe from any existing event to prevent multiple subscriptions
if (viewfinderComponent != null)
{
viewfinderComponent.OnViewfinderTapped -= HandleViewfinderTapped;
viewfinderComponent.OnViewfinderTapped -= HandleViewfinderTappedDuringAnimation;
}
// Subscribe to the viewfinder's tap event for interrupting the animation
viewfinderComponent.OnViewfinderTapped += HandleViewfinderTappedDuringAnimation;
}
// Reset state
animationProgress = 0f;
isAnimating = true;
// Determine canvas width for CanvasScaler-consistent sizing
RectTransform canvasRect = targetCanvas.transform as RectTransform;
float canvasWidth = canvasRect != null ? canvasRect.rect.width : Screen.width;
// Initialize viewfinder size and position to full canvas width (square) at center
float startSize = canvasWidth;
viewfinderRectTransform.sizeDelta = new Vector2(startSize, startSize);
viewfinderRectTransform.anchorMin = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.anchorMax = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.pivot = new Vector2(0.5f, 0.5f);
viewfinderRectTransform.anchoredPosition = Vector2.zero;
// Compute target anchored position in canvas units from previously calculated screen position
targetAnchoredPosition = ScreenToAnchoredPosition(targetScreenPosition);
// Fire starting event
OnAnimationStarted?.Invoke();
// Start sequence coroutine
animationCoroutine = StartCoroutine(AnimateViewfinderSequence());
}
/// <summary>
/// Coroutine that handles the complete viewfinder animation sequence (zoom in, then zoom out)
/// </summary>
private IEnumerator AnimateViewfinderSequence()
{
RectTransform canvasRect = targetCanvas.transform as RectTransform;
float canvasWidth = canvasRect != null ? canvasRect.rect.width : Screen.width;
// Phase 1: Zoom in (from full screen to monster)
float startSize = canvasWidth; // Always start with full canvas width
float endSize = targetViewfinderSize; // End at the calculated target size
Vector2 startPos = Vector2.zero; // Center of screen
Vector2 endPos = targetAnchoredPosition; // Target position
float elapsedTime = 0f;
bool[] thresholdTriggered = new bool[settings.ViewfinderProgressThresholds.Length];
// Debug the actual values
Debug.Log($"[CameraViewfinderManager] Animation sequence starting: startSize={startSize}, endSize={endSize}");
// Verify the initial size is set correctly
viewfinderRectTransform.sizeDelta = new Vector2(startSize, startSize);
// Phase 1: Zoom In
while (elapsedTime < settings.ViewfinderShrinkDuration && !isReversePhase)
{
if (targetTransform == null)
{
StopViewfinderAnimation();
yield break;
}
elapsedTime += Time.unscaledDeltaTime;
animationProgress = Mathf.Clamp01(elapsedTime / settings.ViewfinderShrinkDuration);
// Calculate proximity - it increases as we get closer to the target
currentProximity = animationProgress;
OnProximityUpdated?.Invoke(currentProximity);
// The curve value might be inverted - ensure we're shrinking not growing
float curveValue = settings.ViewfinderShrinkCurve.Evaluate(animationProgress);
// Interpolate size
float currentSize = Mathf.Lerp(startSize, endSize, curveValue);
viewfinderRectTransform.sizeDelta = new Vector2(currentSize, currentSize);
// Interpolate position
Vector2 currentPos = viewfinderRectTransform.anchoredPosition;
Vector2 newPos = Vector2.Lerp(startPos, endPos, curveValue);
viewfinderRectTransform.anchoredPosition = newPos;
// Log the animation state occasionally for debugging
if (animationProgress % 0.25f <= 0.01f || animationProgress >= 0.99f)
{
Debug.Log($"[CameraViewfinderManager] Animation progress: {animationProgress:F2}, " +
$"curveValue={curveValue:F2}, currentSize={currentSize:F2}, currentPos={newPos}, proximity={currentProximity:F2}");
}
for (int i = 0; i < settings.ViewfinderProgressThresholds.Length; i++)
{
if (!thresholdTriggered[i] && animationProgress >= settings.ViewfinderProgressThresholds[i])
{
thresholdTriggered[i] = true;
OnProgressThresholdReached?.Invoke(settings.ViewfinderProgressThresholds[i]);
}
}
OnProgressUpdated?.Invoke(animationProgress);
yield return null;
}
// Ensure we reach the exact end size and position for phase 1
viewfinderRectTransform.sizeDelta = new Vector2(endSize, endSize);
viewfinderRectTransform.anchoredPosition = endPos;
currentProximity = 1.0f; // At target
OnProximityUpdated?.Invoke(currentProximity);
// Brief pause at target
yield return new WaitForSecondsRealtime(0.2f);
// Start phase 2 - reverse animation
isReversePhase = true;
OnReverseAnimationStarted?.Invoke();
// Reset for reverse phase
elapsedTime = 0f;
animationProgress = 0f;
// Phase 2: Zoom Out (from monster back to full screen)
while (elapsedTime < settings.ViewfinderShrinkDuration && isReversePhase)
{
if (targetTransform == null)
{
StopViewfinderAnimation();
yield break;
}
elapsedTime += Time.unscaledDeltaTime;
animationProgress = Mathf.Clamp01(elapsedTime / settings.ViewfinderShrinkDuration);
// Calculate proximity - it decreases as we move away from target
currentProximity = 1.0f - animationProgress;
OnProximityUpdated?.Invoke(currentProximity);
// The curve value for zooming out
float curveValue = settings.ViewfinderShrinkCurve.Evaluate(animationProgress);
// Interpolate size (now growing)
float currentSize = Mathf.Lerp(endSize, startSize, curveValue);
viewfinderRectTransform.sizeDelta = new Vector2(currentSize, currentSize);
// Interpolate position (now returning to center)
Vector2 currentPos = viewfinderRectTransform.anchoredPosition;
Vector2 newPos = Vector2.Lerp(endPos, startPos, curveValue);
viewfinderRectTransform.anchoredPosition = newPos;
// Log the animation state occasionally for debugging
if (animationProgress % 0.25f <= 0.01f || animationProgress >= 0.99f)
{
Debug.Log($"[CameraViewfinderManager] Reverse animation progress: {animationProgress:F2}, " +
$"curveValue={curveValue:F2}, currentSize={currentSize:F2}, currentPos={newPos}, proximity={currentProximity:F2}");
}
OnProgressUpdated?.Invoke(animationProgress);
yield return null;
}
// Animation completed (both phases)
HandleViewfinderTappedDuringAnimation();
}
/// <summary>
/// Handle tap events during animation (for capturing at current proximity)
/// </summary>
private void HandleViewfinderTappedDuringAnimation()
{
if (isAnimating)
{
// Fire event with current proximity value for scoring
OnViewfinderTappedDuringAnimation?.Invoke(currentProximity);
// Complete the animation immediately
if (animationCoroutine != null)
{
StopCoroutine(animationCoroutine);
animationCoroutine = null;
}
// Fire completed event
OnAnimationCompleted?.Invoke();
isAnimating = false;
isReversePhase = false;
// Hide the viewfinder on the second tap
viewfinderComponent.RemoveInputOverride();
HideViewfinder();
}
else
{
// Regular tap handling when not animating
HandleViewfinderTapped();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e5a6d17776b84719a2599fdc35c7076d
timeCreated: 1760016840

View File

@@ -0,0 +1,119 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Input;
namespace Minigames.DivingForPictures.PictureCamera
{
/// <summary>
/// Attached to the viewfinder UI prefab. Handles tap detection and other viewfinder-specific operations.
/// </summary>
public class Viewfinder : MonoBehaviour, ITouchInputConsumer
{
[SerializeField]
private Image[] viewfinderImages; // Array of Image components to tint based on proximity
// Events
public event System.Action OnViewfinderTapped;
// State
private bool isActive = true;
private RectTransform _rectTransform;
private CameraViewfinderManager _viewfinderManager;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
}
/// <summary>
/// Initializes the viewfinder with a reference to its manager
/// </summary>
/// <param name="manager">Reference to the CameraViewfinderManager</param>
public void Initialize(CameraViewfinderManager manager)
{
_viewfinderManager = manager;
if (_viewfinderManager != null)
{
_viewfinderManager.OnProximityUpdated += UpdateProximityColor;
}
}
private void OnDisable()
{
// Unsubscribe from events when disabled
if (_viewfinderManager != null)
{
_viewfinderManager.OnProximityUpdated -= UpdateProximityColor;
}
}
/// <summary>
/// Updates the color of all viewfinder images based on proximity value
/// </summary>
/// <param name="proximity">Proximity value between 0 and 1 (0=far, 1=close)</param>
private void UpdateProximityColor(float proximity)
{
// Lerp between green (close) and red (far)
Color tintColor = Color.Lerp(Color.red, Color.green, proximity);
// Apply the color to all referenced image components
if (viewfinderImages != null && viewfinderImages.Length > 0)
{
foreach (Image image in viewfinderImages)
{
if (image != null)
{
image.color = tintColor;
}
}
}
}
/// <summary>
/// Enable or disable input reception
/// </summary>
public void SetActive(bool active)
{
isActive = active;
}
#region ITouchInputConsumer Implementation
public void SetupInputOverride()
{
InputManager.Instance.RegisterOverrideConsumer(this);
}
public void RemoveInputOverride()
{
InputManager.Instance.UnregisterOverrideConsumer(this);
}
public void OnTap(Vector2 position)
{
if (isActive)
{
// Fire the tap event that PhotoSequenceController will listen to
OnViewfinderTapped?.Invoke();
Debug.Log($"[MDPI] Viewfinder OnTap: {position}");
}
}
// Required interface methods (no-op implementations)
public void OnHoldStart(Vector2 position) { }
public void OnHoldMove(Vector2 position) { }
public void OnHoldEnd(Vector2 position) { }
#endregion
/// <summary>
/// Check if a point is within the viewfinder's bounds
/// </summary>
public bool ContainsPoint(Vector2 screenPoint)
{
return RectTransformUtility.RectangleContainsScreenPoint(_rectTransform, screenPoint);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4b4b818dd8cb454abe85b8edcaece1db
timeCreated: 1760032922

View File

@@ -42,23 +42,13 @@ namespace Minigames.DivingForPictures
_targetFingerX = transform.position.x;
_isTouchActive = false;
// Find DivingGameManager and subscribe to its initialization event
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
DivingGameManager.Instance.OnGameInitialized += Initialize;
// If game is already initialized, initialize immediately
if (DivingGameManager.Instance.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.GetValue(DivingGameManager.Instance) is bool isInitialized && isInitialized)
{
gameManager.OnGameInitialized += Initialize;
// If game is already initialized, initialize immediately
if (gameManager.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.GetValue(gameManager) is bool isInitialized && isInitialized)
{
Initialize();
}
}
else
{
Debug.LogWarning("[PlayerController] DivingGameManager not found. Initializing immediately.");
Initialize();
}
}
@@ -79,12 +69,7 @@ namespace Minigames.DivingForPictures
private void OnDestroy()
{
// Unsubscribe from events to prevent memory leaks
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.OnGameInitialized -= Initialize;
}
DivingGameManager.Instance.OnGameInitialized -= Initialize;
}
/// <summary>

View File

@@ -31,7 +31,7 @@ public class RopeEndPhysicsFollower : MonoBehaviour
private Vector2 offset;
private Vector3 lastTargetPosition;
private bool initialized = false;
private bool debugLog = true;
private bool debugLog = false;
// Rope reference to get the actual rope length
private Rope attachedRope;

View File

@@ -37,7 +37,7 @@ namespace Minigames.DivingForPictures
private int _spawnCounter;
private float _speedUpTimer;
private Camera _mainCamera;
private UnityEngine.Camera _mainCamera;
private float _screenBottom;
private float _screenTop;
private TrenchTilePool _tilePool;
@@ -76,7 +76,7 @@ namespace Minigames.DivingForPictures
private void Awake()
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
// Get settings from GameManager
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
@@ -125,38 +125,23 @@ namespace Minigames.DivingForPictures
private void Start()
{
// Find DivingGameManager and subscribe to its initialization event
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
DivingGameManager.Instance.OnGameInitialized += Initialize;
// Register with the DivingGameManager for pause/resume events
DivingGameManager.Instance.RegisterPausableComponent(this);
// If game is already initialized, initialize immediately
if (DivingGameManager.Instance.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.GetValue(DivingGameManager.Instance) is bool isInitialized && isInitialized)
{
gameManager.OnGameInitialized += Initialize;
// Register with the DivingGameManager for pause/resume events
gameManager.RegisterPausableComponent(this);
// If game is already initialized, initialize immediately
if (gameManager.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.GetValue(gameManager) is bool isInitialized && isInitialized)
{
Initialize();
}
}
else
{
Debug.LogWarning("[TrenchTileSpawner] DivingGameManager not found. Initializing immediately.");
Initialize();
}
}
private void OnDestroy()
{
// Unregister from DivingGameManager
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.UnregisterPausableComponent(this);
}
DivingGameManager.Instance.UnregisterPausableComponent(this);
}
/// <summary>
@@ -199,7 +184,7 @@ namespace Minigames.DivingForPictures
/// <summary>
/// Resumes the spawner and all associated processes
/// </summary>
public void Resume()
public void DoResume()
{
if (!_isPaused) return; // Already running
@@ -386,7 +371,7 @@ namespace Minigames.DivingForPictures
{
if (_mainCamera == null)
{
_mainCamera = Camera.main;
_mainCamera = UnityEngine.Camera.main;
if (_mainCamera == null)
{
Debug.LogError("No main camera found!");
@@ -904,7 +889,7 @@ namespace Minigames.DivingForPictures
if (!Application.isPlaying)
{
// Only try to calculate this if _screenBottom hasn't been set by the game
Camera editorCam = Camera.main;
UnityEngine.Camera editorCam = UnityEngine.Camera.main;
if (editorCam != null)
{
Vector3 bottom = editorCam.ViewportToWorldPoint(new Vector3(0.5f, 0f, editorCam.nearClipPlane));

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0892ac3d8370a274b8d5847ca51ac046
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
using AppleHills.Core.Interfaces;
using UnityEngine;
namespace Minigames.DivingForPictures.Utilities
{
public class BottlePauser : MonoBehaviour, IPausable
{
[SerializeField] private WobbleBehavior wobbleReference;
private bool isPaused = false;
private void Awake()
{
DivingGameManager.Instance.RegisterPausableComponent(this);
}
public void Pause()
{
wobbleReference.enabled = false;
isPaused = true;
}
public void DoResume()
{
wobbleReference.enabled = true;
isPaused = false;
}
public bool IsPaused => isPaused;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cf50e6f7c05d5a14296152b8c3fbb923

View File

@@ -0,0 +1,34 @@
using AppleHills.Core.Interfaces;
using UnityEngine;
namespace Minigames.DivingForPictures.Utilities
{
public class RockPauser : MonoBehaviour, IPausable
{
[SerializeField] private RockFollower rockReference;
[SerializeField] private WobbleBehavior rockWobbleReference;
private bool isPaused = false;
private void Awake()
{
DivingGameManager.Instance.RegisterPausableComponent(this);
}
public void Pause()
{
rockReference.enabled = false;
rockWobbleReference.enabled = false;
isPaused = true;
}
public void DoResume()
{
rockReference.enabled = true;
rockWobbleReference.enabled = true;
isPaused = false;
}
public bool IsPaused => isPaused;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a392c919de2df0340b700309e221b7b1