Files
AppleHillsProduction/Assets/Scripts/Minigames/DivingForPictures/PictureCamera/CameraViewfinderManager.cs

884 lines
38 KiB
C#

using UnityEngine;
using System;
using System.Collections;
using AppleHills.Core.Settings;
using Core;
using UnityEngine.Audio;
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;
public static CameraViewfinderManager Instance
{
get
{
if (_instance == null && Application.isPlaying)
{
_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;
private AudioSource _audioSource;
// 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;
// New field to store the current input mode
private PhotoInputModes currentInputMode;
public AudioResource flashSound;
public AudioResource focusSound;
// 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 Start()
{
settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
mainCamera = UnityEngine.Camera.main;
_audioSource = GetComponent<AudioSource>();
// Get the photo input mode from settings
currentInputMode = settings.PhotoInputMode;
}
/// <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();
InitializeViewfinder(viewfinderComponent);
}
// 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;
Logging.Warning("[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);
Logging.Debug(
$"[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
Logging.Debug($"[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)
{
Logging.Warning($"[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)
{
Logging.Debug($"[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
Logging.Debug(
$"[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)
{
viewfinderComponent.RemoveInputOverride();
// Unsubscribe from all viewfinder events
if (viewfinderComponent != null)
{
viewfinderComponent.OnViewfinderTapped -= HandleViewfinderTapped;
viewfinderComponent.OnViewfinderTapped -= HandleViewfinderTappedDuringAnimation;
viewfinderComponent.OnViewfinderHoldStarted -= HandleViewfinderHoldStarted;
viewfinderComponent.OnViewfinderHoldEnded -= HandleViewfinderHoldEnded;
}
Destroy(viewfinderInstance);
viewfinderInstance = null;
viewfinderComponent = null;
viewfinderRectTransform = null;
Logging.Debug("[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();
InitializeViewfinder(viewfinderComponent);
}
// 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;
Logging.Debug($"[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;
PlayFocusSound();
// 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;
}
// Initialize viewfinder with appropriate event handlers based on input mode
if (viewfinderComponent != null)
{
viewfinderComponent.Initialize(this);
viewfinderComponent.SetupInputOverride();
InitializeViewfinder(viewfinderComponent);
// For animation sequence, we need additional handling for tapping during animation
if (currentInputMode == PhotoInputModes.Tap)
{
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
Logging.Debug($"[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)
{
Logging.Debug($"[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)
{
Logging.Debug($"[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
HideViewfinder();
}
else
{
// Regular tap handling when not animating
HandleViewfinderTapped();
}
}
/// <summary>
/// Initialize the viewfinder with appropriate event handlers based on input mode
/// </summary>
/// <param name="viewfinder">The viewfinder component to initialize</param>
public void InitializeViewfinder(Viewfinder viewfinder)
{
if (viewfinder != null)
{
// Clean up any existing event subscriptions
viewfinder.OnViewfinderTapped -= HandleViewfinderTapped;
viewfinder.OnViewfinderHoldStarted -= HandleViewfinderHoldStarted;
viewfinder.OnViewfinderHoldEnded -= HandleViewfinderHoldEnded;
// Set up handlers based on the current input mode
switch (currentInputMode)
{
case PhotoInputModes.Tap:
// For tap mode, only subscribe to tap events
viewfinder.OnViewfinderTapped += HandleViewfinderTapped;
break;
case PhotoInputModes.Hold:
// For hold mode, subscribe to hold start/end events
viewfinder.OnViewfinderHoldStarted += HandleViewfinderHoldStarted;
viewfinder.OnViewfinderHoldEnded += HandleViewfinderHoldEnded;
break;
}
}
}
/// <summary>
/// Handles hold start event from the viewfinder
/// </summary>
private void HandleViewfinderHoldStarted()
{
if (currentInputMode == PhotoInputModes.Hold)
{
// Start the photo sequence when hold begins (same behavior as first tap in tap mode)
OnViewfinderTapped?.Invoke();
Logging.Debug("[CameraViewfinderManager] Hold started - initiating photo sequence");
}
}
/// <summary>
/// Handles hold end event from the viewfinder
/// </summary>
private void HandleViewfinderHoldEnded()
{
if (currentInputMode == PhotoInputModes.Hold && isAnimating)
{
// Complete the sequence when hold ends (same behavior as second tap in tap mode)
OnViewfinderTappedDuringAnimation?.Invoke(currentProximity);
Logging.Debug("[CameraViewfinderManager] Hold ended - completing photo sequence with proximity: " + currentProximity);
// Complete the animation immediately
if (animationCoroutine != null)
{
StopCoroutine(animationCoroutine);
animationCoroutine = null;
}
// Fire completed event
OnAnimationCompleted?.Invoke();
isAnimating = false;
isReversePhase = false;
HideViewfinder();
}
}
public void PlayShutterSound()
{
_audioSource.Stop();
_audioSource.resource = flashSound;
_audioSource.Play();
}
private void PlayFocusSound()
{
_audioSource.Stop();
_audioSource.resource = focusSound;
_audioSource.Play();
}
}
}