Screenshots in Diving Minigame

This commit is contained in:
Michal Pikulski
2025-12-18 10:52:12 +01:00
parent 4e992ccf53
commit ca56e748ba
22 changed files with 1516 additions and 143 deletions

View File

@@ -1,4 +1,4 @@
using AppleHills.Core.Interfaces;
using AppleHills.Core.Interfaces;
using Cinematics;
using Core;
using Core.Lifecycle;
@@ -9,6 +9,7 @@ using System.Collections;
using System.Collections.Generic;
using Core.Settings;
using Minigames.DivingForPictures.Bubbles;
using Minigames.DivingForPictures.Screenshot;
using UI.Core;
using UnityEngine;
using UnityEngine.Playables;
@@ -55,7 +56,9 @@ namespace Minigames.DivingForPictures
// Public properties
public int PlayerScore => _playerScore;
public float CurrentVelocityFactor => _currentVelocityFactor;
public int picturesTaken;
// Delegate to DivingScreenshotManager for backward compatibility
public int picturesTaken => DivingScreenshotManager.Instance != null ? DivingScreenshotManager.Instance.ScreenshotCount : 0;
// Events
public event Action<int> OnScoreChanged;
@@ -284,23 +287,7 @@ namespace Minigames.DivingForPictures
}
}
private void DoPictureTaken(Monster.Monster monster)
{
// Calculate points based on depth
int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * _settings.DepthMultiplier);
int pointsAwarded = _settings.BasePoints + depthBonus;
// Add score
_playerScore += pointsAwarded;
// Add number of pictures taken
picturesTaken += 1;
// Fire events
OnScoreChanged?.Invoke(picturesTaken);
OnPictureTaken?.Invoke(monster, picturesTaken);
}
// Photo logic moved to DivingScreenshotManager
/// <summary>
/// Called when the player takes damage from any collision
@@ -667,8 +654,13 @@ namespace Minigames.DivingForPictures
_activeMonsters.Clear();
// Calculate booster pack reward based on pictures taken
int boosterPackCount = CalculateBoosterPackReward();
Logging.Debug($"[DivingGameManager] Pictures taken: {picturesTaken}, awarding {boosterPackCount} booster pack(s)");
int boosterPackCount = DivingScreenshotManager.Instance != null
? DivingScreenshotManager.Instance.CalculateBoosterPackReward()
: 1;
int screenshotCount = DivingScreenshotManager.Instance != null
? DivingScreenshotManager.Instance.ScreenshotCount
: 0;
Logging.Debug($"[DivingGameManager] Pictures taken: {screenshotCount}, awarding {boosterPackCount} booster pack(s)");
// Show UI and grant booster packs using new async method
UIPageController.Instance.ShowAllUI();
@@ -700,29 +692,7 @@ namespace Minigames.DivingForPictures
Logging.Debug($"Final Score: {_playerScore}");
}
/// <summary>
/// Calculates how many booster packs to award based on the number of pictures taken.
/// </summary>
/// <returns>Number of booster packs to award</returns>
private int CalculateBoosterPackReward()
{
if (picturesTaken <= 3)
{
return 1;
}
else if (picturesTaken <= 5)
{
return 2;
}
else if (picturesTaken <= 10)
{
return 3;
}
else
{
return 4;
}
}
// Booster reward calculation moved to DivingScreenshotManager
/// <summary>
/// Starts a smooth transition to the new velocity factor
@@ -949,66 +919,49 @@ namespace Minigames.DivingForPictures
{
if (!_isPhotoSequenceActive || _currentPhotoTarget == null)
return;
// Stop the viewfinder animation if it's still running
if (viewfinderManager != null)
// Start coroutine to handle screenshot and cleanup
StartCoroutine(TakePictureCoroutine());
}
/// <summary>
/// Coroutine that takes screenshot and waits for completion before cleanup
/// </summary>
private IEnumerator TakePictureCoroutine()
{
// Take screenshot and WAIT for it to complete
if (DivingScreenshotManager.Instance != null)
{
viewfinderManager.StopViewfinderAnimation();
yield return DivingScreenshotManager.Instance.TakeScreenshotAsync(_currentPhotoTarget, _capturedProximity);
}
else
{
Logging.Warning("[DivingGameManager] DivingScreenshotManager not found!");
}
// Play shutter sound
if (cameraViewfinderManager != null)
{
cameraViewfinderManager.PlayShutterSound();
}
// Notify the monster that its picture was taken
_currentPhotoTarget.NotifyPictureTaken();
// Calculate score based on proximity and depth
CalculateScore(_currentPhotoTarget, _capturedProximity);
//Trigger the Flash Effect
if (flashRef != null)
if (_currentPhotoTarget != null)
{
var flash = flashRef.GetComponent<FlashBehaviour>();
if (flash != null)
{
flash.TriggerFlash();
cameraViewfinderManager.PlayShutterSound();
}
_currentPhotoTarget.NotifyPictureTaken();
}
// NOW destroy the viewfinder (screenshot is complete)
if (viewfinderManager != null)
{
viewfinderManager.StopViewfinderAnimation();
}
// 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 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;
Logging.Debug($"[DivingGameManager] Picture score calculation: base={proximityBonus} (proximity={proximity:F2}), " +
$"depth bonus={depthBonus}, total={pointsAwarded}");
// Add score
_playerScore += pointsAwarded;
// Add pictures taken
picturesTaken += 1;
// Fire events
OnScoreChanged?.Invoke(picturesTaken);
OnPictureTaken?.Invoke(monster, picturesTaken);
}
// Score calculation moved to DivingScreenshotManager
/// <summary>
/// Handles completion of the viewfinder animation

View File

@@ -1,23 +1,59 @@
using UnityEngine;
using TMPro;
using Minigames.DivingForPictures.Screenshot;
namespace Minigames.DivingForPictures
{
public class DivingScoreUI : MonoBehaviour
{
private static DivingScoreUI _instance;
public static DivingScoreUI Instance
{
get
{
if (_instance == null && Application.isPlaying)
{
_instance = FindAnyObjectByType<DivingScoreUI>();
}
return _instance;
}
}
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private GameObject scorePopupPrefab;
[SerializeField] private Transform popupParent;
[Header("Animation Target")]
[SerializeField] private Transform photoIconTarget;
/// <summary>
/// The photo icon transform that flyaway animations should target
/// </summary>
public Transform PhotoIconTarget => photoIconTarget;
private void Awake()
{
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Debug.LogWarning("[DivingScoreUI] Multiple instances detected. Keeping first instance.");
}
}
private void Start()
{
// Subscribe to events
DivingGameManager.Instance.OnScoreChanged += UpdateScoreDisplay;
DivingGameManager.Instance.OnPictureTaken += ShowScorePopup;
// Subscribe to screenshot manager events
if (DivingScreenshotManager.Instance != null)
{
DivingScreenshotManager.Instance.OnScreenshotCountChanged += UpdateScoreDisplay;
}
// Initialize display
UpdateScoreDisplay(DivingGameManager.Instance.picturesTaken);
UpdateScoreDisplay(0);
// Create popup parent if needed
if (popupParent == null)
@@ -29,10 +65,9 @@ namespace Minigames.DivingForPictures
private void OnDestroy()
{
// Unsubscribe from events
if (DivingGameManager.Instance)
if (DivingScreenshotManager.Instance != null)
{
DivingGameManager.Instance.OnScoreChanged -= UpdateScoreDisplay;
DivingGameManager.Instance.OnPictureTaken -= ShowScorePopup;
DivingScreenshotManager.Instance.OnScreenshotCountChanged -= UpdateScoreDisplay;
}
}
@@ -43,23 +78,5 @@ namespace Minigames.DivingForPictures
scoreText.text = $"x {score}";
}
}
private void ShowScorePopup(Monster.Monster monster, int points)
{
if (scorePopupPrefab == null) return;
// Create popup at monster position
GameObject popup = Instantiate(scorePopupPrefab, monster.transform.position, Quaternion.identity, popupParent);
// Find text component and set value
TextMeshProUGUI popupText = popup.GetComponentInChildren<TextMeshProUGUI>();
if (popupText != null)
{
popupText.text = $"+{points}";
}
// Auto-destroy after delay
Destroy(popup, 2f);
}
}
}

View File

@@ -42,6 +42,12 @@ namespace Minigames.DivingForPictures.PictureCamera
private Viewfinder viewfinderComponent;
private UnityEngine.Camera mainCamera;
private AudioSource _audioSource;
/// <summary>
/// The currently active Viewfinder component.
/// Exposed for screenshot system to access capture area and flyaway animation.
/// </summary>
public Viewfinder CurrentViewfinder => viewfinderComponent;
// Animation state
private float animationProgress = 0f;

View File

@@ -3,17 +3,30 @@ using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Input;
using UnityEngine.Serialization;
using Utils.UI;
namespace Minigames.DivingForPictures.PictureCamera
{
/// <summary>
/// Attached to the viewfinder UI prefab. Handles tap detection and other viewfinder-specific operations.
/// Also exposes screenshot-related references for the diving minigame.
/// </summary>
public class Viewfinder : MonoBehaviour, ITouchInputConsumer
{
[SerializeField]
private Image[] viewfinderImages; // Array of Image components to tint based on proximity
[Header("Screenshot Components")]
[FormerlySerializedAs("_captureArea")]
[SerializeField] private RectTransform captureArea;
[FormerlySerializedAs("_flyawayAnimation")]
[SerializeField] private ScreenshotFlyawayAnimation flyawayAnimation;
[Header("UI Elements to Hide During Screenshot")]
[Tooltip("UI elements (like frame, crosshairs) that should be hidden during screenshot capture")]
[SerializeField] private GameObject[] uiElementsToHideDuringCapture;
// Events
public event System.Action OnViewfinderTapped;
public event System.Action OnViewfinderHoldStarted;
@@ -24,9 +37,41 @@ namespace Minigames.DivingForPictures.PictureCamera
private RectTransform _rectTransform;
private CameraViewfinderManager _viewfinderManager;
// Public properties for screenshot system
public RectTransform CaptureArea
{
get
{
if (captureArea == null)
{
captureArea = GetComponent<RectTransform>();
}
return captureArea;
}
}
public ScreenshotFlyawayAnimation FlyawayAnimation => flyawayAnimation;
/// <summary>
/// UI elements to hide during screenshot capture (frame, crosshairs, etc.)
/// </summary>
public GameObject[] UIElementsToHideDuringCapture => uiElementsToHideDuringCapture;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
// Auto-assign capture area to self if not set
if (captureArea == null)
{
captureArea = GetComponent<RectTransform>();
}
// Auto-find flyaway animation in children
if (flyawayAnimation == null)
{
flyawayAnimation = GetComponentInChildren<ScreenshotFlyawayAnimation>(includeInactive: true);
}
}
/// <summary>

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa94031c3f274ab2afd0d0b2d4cef32f
timeCreated: 1766012726

View File

@@ -0,0 +1,286 @@
using System;
using System.Collections;
using Core;
using Core.Settings;
using UnityEngine;
using Utils;
using Utils.UI;
namespace Minigames.DivingForPictures.Screenshot
{
/// <summary>
/// Singleton manager for diving minigame screenshot functionality.
/// Provides easy-to-use TakeScreenshot() and TakeScreenshotAsync() methods.
/// Integrates with PhotoManager for disk storage and ScreenshotFlyawayAnimation for visual feedback.
/// </summary>
public class DivingScreenshotManager : MonoBehaviour
{
private static DivingScreenshotManager _instance;
public static DivingScreenshotManager Instance
{
get
{
if (_instance == null && Application.isPlaying)
{
_instance = FindAnyObjectByType<DivingScreenshotManager>();
if (_instance == null)
{
GameObject go = new GameObject("DivingScreenshotManager");
_instance = go.AddComponent<DivingScreenshotManager>();
}
}
return _instance;
}
}
[Header("Screenshot Settings")]
[SerializeField] private GameObject[] _uiElementsToHide;
[Header("Flash Effect")]
[SerializeField] private GameObject _flashEffect;
// State tracking
private int _screenshotCount;
private int _playerScore;
private bool _isCapturing;
// Settings reference
private IDivingMinigameSettings _settings;
// Public properties
public int ScreenshotCount => _screenshotCount;
public int PlayerScore => _playerScore;
public bool IsCapturing => _isCapturing;
// Events
public event Action<int> OnScreenshotCountChanged;
public event Action<int> OnScoreChanged;
public event Action<string, Texture2D> OnScreenshotCaptured; // photoId, texture
private void Awake()
{
if (_instance == null)
{
_instance = this;
}
else if (_instance != this)
{
Destroy(gameObject);
return;
}
}
private void Start()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
}
/// <summary>
/// Take a screenshot using coroutine. Simple synchronous call.
/// </summary>
/// <param name="monster">Optional monster reference for score calculation</param>
/// <param name="proximity">Optional proximity score (0-1)</param>
public void TakeScreenshot(Monster.Monster monster = null, float proximity = 0f)
{
if (_isCapturing)
{
Logging.Warning("[DivingScreenshotManager] Already capturing a screenshot!");
return;
}
StartCoroutine(TakeScreenshotCoroutine(monster, proximity));
}
/// <summary>
/// Take a screenshot asynchronously. Returns when capture is complete.
/// </summary>
/// <param name="monster">Optional monster reference for score calculation</param>
/// <param name="proximity">Optional proximity score (0-1)</param>
public Coroutine TakeScreenshotAsync(Monster.Monster monster = null, float proximity = 0f)
{
if (_isCapturing)
{
Logging.Warning("[DivingScreenshotManager] Already capturing a screenshot!");
return null;
}
return StartCoroutine(TakeScreenshotCoroutine(monster, proximity));
}
/// <summary>
/// Internal coroutine that handles the screenshot capture process
/// </summary>
private IEnumerator TakeScreenshotCoroutine(Monster.Monster monster, float proximity)
{
_isCapturing = true;
// Get viewfinder and validate references
var viewfinderManager = PictureCamera.CameraViewfinderManager.Instance;
if (viewfinderManager == null || viewfinderManager.CurrentViewfinder == null)
{
Logging.Error("[DivingScreenshotManager] No active viewfinder found!");
_isCapturing = false;
yield break;
}
var viewfinder = viewfinderManager.CurrentViewfinder;
RectTransform captureArea = viewfinder?.CaptureArea;
if (captureArea == null)
{
Logging.Error($"[DivingScreenshotManager] Capture area not configured! Viewfinder: {viewfinder?.gameObject.name}");
_isCapturing = false;
yield break;
}
// Show flash effect
if (_flashEffect != null)
{
_flashEffect.SetActive(true);
yield return new WaitForSeconds(0.1f);
_flashEffect.SetActive(false);
}
// Combine global UI elements with viewfinder-specific ones
GameObject[] combinedUIToHide = CombineUIElementsToHide(_uiElementsToHide, viewfinder.UIElementsToHideDuringCapture);
// Capture and save using PhotoManager
bool captureSuccess = false;
string savedPhotoId = null;
Texture2D capturedTexture = null;
yield return PhotoManager.CaptureAndSaveCoroutine(
CaptureType.DivingMinigame,
captureArea,
combinedUIToHide,
onSuccess: (photoId) =>
{
savedPhotoId = photoId;
captureSuccess = true;
// Load the texture back for animation
capturedTexture = PhotoManager.LoadPhoto(CaptureType.DivingMinigame, photoId);
Logging.Debug($"[DivingScreenshotManager] Screenshot saved: {photoId}");
},
onFailure: (error) =>
{
Logging.Error($"[DivingScreenshotManager] Screenshot failed: {error}");
captureSuccess = false;
}
);
if (!captureSuccess)
{
_isCapturing = false;
yield break;
}
// Calculate score
int earnedScore = CalculateScore(monster, proximity);
_playerScore += earnedScore;
_screenshotCount++;
// Fire events
OnScreenshotCountChanged?.Invoke(_screenshotCount);
OnScoreChanged?.Invoke(_screenshotCount); // Pass screenshot count for UI compatibility
OnScreenshotCaptured?.Invoke(savedPhotoId, capturedTexture);
Logging.Debug($"[DivingScreenshotManager] Score: +{earnedScore} (Total: {_playerScore}, Count: {_screenshotCount})");
// Trigger flyaway animation with target from DivingScoreUI
if (capturedTexture != null && viewfinder.FlyawayAnimation != null)
{
Transform photoIconTarget = DivingScoreUI.Instance?.PhotoIconTarget;
viewfinder.FlyawayAnimation.PlayAnimation(capturedTexture, photoIconTarget);
}
_isCapturing = false;
}
/// <summary>
/// Calculate score based on monster depth and proximity
/// </summary>
private int CalculateScore(Monster.Monster monster, float proximity)
{
if (_settings == null)
{
return 100; // Default fallback
}
int baseScore = _settings.BasePoints;
// Add depth bonus if monster reference provided
if (monster != null)
{
int depthBonus = Mathf.FloorToInt(Mathf.Abs(monster.transform.position.y) * _settings.DepthMultiplier);
baseScore += depthBonus;
}
// Add proximity bonus (0-1 scale, multiply by some factor)
int proximityBonus = Mathf.RoundToInt(proximity * 50f);
baseScore += proximityBonus;
return baseScore;
}
/// <summary>
/// Get booster pack count based on screenshot count (for end game reward)
/// </summary>
public int CalculateBoosterPackReward()
{
if (_screenshotCount <= 3)
{
return 1;
}
else if (_screenshotCount <= 5)
{
return 2;
}
else if (_screenshotCount <= 10)
{
return 3;
}
else
{
return 4;
}
}
/// <summary>
/// Reset state for new game
/// </summary>
public void ResetState()
{
_screenshotCount = 0;
_playerScore = 0;
_isCapturing = false;
}
/// <summary>
/// Combines global UI elements with viewfinder-specific UI elements
/// </summary>
private GameObject[] CombineUIElementsToHide(GameObject[] globalElements, GameObject[] viewfinderElements)
{
if (globalElements == null && viewfinderElements == null)
return null;
if (globalElements == null)
return viewfinderElements;
if (viewfinderElements == null)
return globalElements;
// Combine both arrays
GameObject[] combined = new GameObject[globalElements.Length + viewfinderElements.Length];
globalElements.CopyTo(combined, 0);
viewfinderElements.CopyTo(combined, globalElements.Length);
return combined;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 78476907cb4b488da7a1526a8453b577
timeCreated: 1766012726

View File

@@ -37,11 +37,18 @@ namespace UI
protected override void DoTransitionIn(Action onComplete)
{
// Update score when showing the screen
if (DivingGameManager.Instance != null)
int finalScore = 0;
if (Minigames.DivingForPictures.Screenshot.DivingScreenshotManager.Instance != null)
{
int finalScore = DivingGameManager.Instance.picturesTaken;
finalScoreText.text = $"x {finalScore}";
finalScore = Minigames.DivingForPictures.Screenshot.DivingScreenshotManager.Instance.ScreenshotCount;
}
else if (DivingGameManager.Instance != null)
{
// Fallback for backward compatibility
finalScore = DivingGameManager.Instance.picturesTaken;
}
finalScoreText.text = $"x {finalScore}";
if (canvasGroup != null)
{

View File

@@ -57,8 +57,11 @@ namespace Utils
}
}
// Wait for UI to hide
// Wait for UI to hide and render to complete
yield return new WaitForEndOfFrame();
yield return null; // Wait one more frame for good measure
Logging.Debug($"[PhotoManager] About to capture. CaptureArea: {captureArea.name}");
// Capture photo
bool captureComplete = false;
@@ -68,6 +71,7 @@ namespace Utils
{
capturedPhoto = texture;
captureComplete = true;
Logging.Debug($"[PhotoManager] Capture callback received. Texture: {(texture != null ? $"{texture.width}x{texture.height}" : "NULL")}");
}, mainCamera, clampToScreenBounds);
// Wait for capture to complete
@@ -129,6 +133,12 @@ namespace Utils
if (mainCamera == null) mainCamera = Camera.main;
// Debug: Log RectTransform details
Logging.Debug($"[PhotoManager] CaptureArea RectTransform: {captureArea.name}");
Logging.Debug($"[PhotoManager] CaptureArea anchoredPosition: {captureArea.anchoredPosition}");
Logging.Debug($"[PhotoManager] CaptureArea sizeDelta: {captureArea.sizeDelta}");
Logging.Debug($"[PhotoManager] CaptureArea rect: {captureArea.rect}");
// Use ScreenSpaceUtility to convert RectTransform to screen rect
Rect screenRect = ScreenSpaceUtility.RectTransformToScreenRect(
captureArea,
@@ -138,6 +148,15 @@ namespace Utils
);
Logging.Debug($"[PhotoManager] Capturing area: pos={screenRect.position}, size={screenRect.size}");
Logging.Debug($"[PhotoManager] Screen dimensions: {Screen.width}x{Screen.height}");
// Validate screenRect
if (screenRect.width <= 0 || screenRect.height <= 0)
{
Logging.Error($"[PhotoManager] Invalid screen rect calculated! width={screenRect.width}, height={screenRect.height}");
onComplete?.Invoke(null);
return;
}
// Use Screenshot Helper's Capture method
ScreenshotHelper.Instance.Capture(

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6b23cacf87f94639aad3b8e25b596ac3
timeCreated: 1766012758

View File

@@ -0,0 +1,217 @@
using UnityEngine;
using UnityEngine.UI;
using Pixelplacement;
namespace Utils.UI
{
/// <summary>
/// UI component that animates a screenshot flying away and disappearing.
/// Uses Pixelplacement Tween library for smooth animations with curves.
/// Attach to a UI prefab with frame (Image) and picture (Image) components.
/// </summary>
public class ScreenshotFlyawayAnimation : MonoBehaviour
{
[Header("UI Components")]
[SerializeField] private Image _frame;
[SerializeField] private Image _picture;
[Header("Animation Settings")]
[Tooltip("Target offset from starting position (in UI space)")]
[SerializeField] private Vector2 _targetOffset = new Vector2(0, 300f);
[Tooltip("Number of full rotations during animation")]
[SerializeField] private float _spinRotations = 2f;
[Tooltip("Duration of the entire animation in seconds")]
[SerializeField] private float _duration = 1.5f;
[Tooltip("Movement curve for position animation")]
[SerializeField] private AnimationCurve _moveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[Tooltip("Scale curve for shrinking animation")]
[SerializeField] private AnimationCurve _scaleCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
[Tooltip("Rotation curve for spinning animation")]
[SerializeField] private AnimationCurve _rotationCurve = AnimationCurve.Linear(0, 0, 1, 1);
private RectTransform _rectTransform;
private bool _isAnimating;
private Transform _targetTransform; // Target to fly toward (optional)
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
// Hide initially - will be shown when animation plays
gameObject.SetActive(false);
// Validate components
if (_frame == null)
{
Debug.LogWarning("[ScreenshotFlyawayAnimation] Frame image not assigned!");
}
if (_picture == null)
{
Debug.LogWarning("[ScreenshotFlyawayAnimation] Picture image not assigned!");
}
}
/// <summary>
/// Start the flyaway animation with the captured screenshot texture.
/// Unparents to root canvas to survive viewfinder destruction.
/// </summary>
/// <param name="photo">The screenshot texture to display</param>
/// <param name="target">Optional target transform to fly toward (if null, uses _targetOffset)</param>
public void PlayAnimation(Texture2D photo, Transform target = null)
{
if (_isAnimating)
{
Debug.LogWarning("[ScreenshotFlyawayAnimation] Animation already playing!");
return;
}
if (photo == null)
{
Debug.LogError("[ScreenshotFlyawayAnimation] Photo texture is null!");
Destroy(gameObject);
return;
}
// Store target for StartAnimation
_targetTransform = target;
// Show the GameObject before playing animation
gameObject.SetActive(true);
// Unparent to root canvas to survive viewfinder destruction
Canvas rootCanvas = GetComponentInParent<Canvas>();
if (rootCanvas != null)
{
rootCanvas = rootCanvas.rootCanvas;
transform.SetParent(rootCanvas.transform, worldPositionStays: true);
}
// Assign texture to picture image
if (_picture != null)
{
Sprite photoSprite = Sprite.Create(
photo,
new Rect(0, 0, photo.width, photo.height),
new Vector2(0.5f, 0.5f)
);
_picture.sprite = photoSprite;
}
_isAnimating = true;
StartAnimation();
}
/// <summary>
/// Execute the animation using Pixelplacement Tween
/// </summary>
private void StartAnimation()
{
if (_rectTransform == null) return;
// Get starting position
Vector2 startPosition = _rectTransform.anchoredPosition;
Vector2 endPosition;
// Calculate end position based on target or offset
if (_targetTransform != null)
{
// Convert target world position to local anchored position
RectTransform targetRect = _targetTransform as RectTransform;
if (targetRect != null && _rectTransform.parent is RectTransform parentRect)
{
// Convert target's position to parent's local space
Vector2 targetLocalPos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentRect,
RectTransformUtility.WorldToScreenPoint(null, targetRect.position),
null,
out targetLocalPos
);
endPosition = targetLocalPos;
}
else
{
// Fallback: use offset if conversion fails
endPosition = startPosition + _targetOffset;
}
}
else
{
// Use offset if no target provided
endPosition = startPosition + _targetOffset;
}
// Start rotation at 0
Vector3 startRotation = Vector3.zero;
Vector3 endRotation = new Vector3(0, 0, 360f * _spinRotations);
// Start scale at 1
Vector3 startScale = Vector3.one;
Vector3 endScale = Vector3.zero;
// Animate position using AnchoredPosition for UI
Tween.AnchoredPosition(
_rectTransform,
startPosition,
endPosition,
_duration,
0f, // no delay
_moveCurve,
Tween.LoopType.None,
null, // no start callback
OnAnimationComplete // complete callback
);
// Animate scale
Tween.LocalScale(
transform,
startScale,
endScale,
_duration,
0f,
_scaleCurve
);
// Animate rotation (Z-axis spin)
Tween.Rotation(
transform,
Quaternion.Euler(startRotation),
Quaternion.Euler(endRotation),
_duration,
0f,
_rotationCurve,
Tween.LoopType.None,
null,
null,
true // obey timescale
);
}
/// <summary>
/// Called when animation completes - destroy the GameObject
/// </summary>
private void OnAnimationComplete()
{
_isAnimating = false;
// Clean up sprite to avoid memory leak
if (_picture != null && _picture.sprite != null)
{
Sprite sprite = _picture.sprite;
_picture.sprite = null;
Destroy(sprite.texture);
Destroy(sprite);
}
// Destroy the animation GameObject
Destroy(gameObject);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5600bf58913e468e883989b2cca4b0a8
timeCreated: 1766012758