Revamp game pausing and input handling. Fix minigame tutorial and end sequence. (#39)
- Revamp pausing and centralize management in GameManager - Switch Pause implementation to be counter-based to solve corner case of multiple pause requests - Remove duplicated Pause logic from other components - Add pausing when browsing the card album - Fully deliver the exclusive UI implementation - Spruce up the MiniGame tutorial with correct pausing, hiding other UI - Correctly unpause after showing tutorial - Fix minigame ending sequence. The cinematic correctly plays only once now - Replaying the minigame works Co-authored-by: Michal Adam Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: #39
This commit is contained in:
112
Assets/Scripts/UI/BlinkingCanvasGroup.cs
Normal file
112
Assets/Scripts/UI/BlinkingCanvasGroup.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using Pixelplacement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a CanvasGroup (if missing) and plays a continuous blinking tween on its alpha.
|
||||
/// Attach to any GameObject to make it pulse/fade in and out.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class BlinkingCanvasGroup : MonoBehaviour
|
||||
{
|
||||
[Header("Blink Settings")] [Tooltip("Minimum alpha value during blink")] [Range(0f, 1f)]
|
||||
public float minAlpha;
|
||||
|
||||
[Tooltip("Maximum alpha value during blink")] [Range(0f, 1f)]
|
||||
public float maxAlpha = 1f;
|
||||
|
||||
[Tooltip("Duration of one leg (min->max or max->min)")]
|
||||
public float legDuration = 0.5f;
|
||||
|
||||
[Tooltip("Delay before starting the blinking")]
|
||||
public float startDelay;
|
||||
|
||||
[Tooltip("Whether the tween should obey Time.timeScale (false = unscaled)")]
|
||||
public bool obeyTimescale;
|
||||
|
||||
[Tooltip("Optional animation curve for easing. Leave null for default ease in/out.")]
|
||||
public AnimationCurve easeCurve;
|
||||
|
||||
// Internal
|
||||
private CanvasGroup _canvasGroup;
|
||||
private Pixelplacement.TweenSystem.TweenBase _activeTween;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Ensure we have a CanvasGroup
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
// Ensure starting alpha
|
||||
_canvasGroup.alpha = minAlpha;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartBlinking();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
// If re-enabled, ensure tween is running
|
||||
if (_activeTween == null)
|
||||
StartBlinking();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
StopBlinking();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopBlinking();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the continuous blinking tween.
|
||||
/// </summary>
|
||||
public void StartBlinking()
|
||||
{
|
||||
StopBlinking();
|
||||
|
||||
if (_canvasGroup == null) return;
|
||||
|
||||
// Use PingPong-like behavior by chaining two opposite legs with LoopType.Loop
|
||||
// Simpler: Tween.CanvasGroupAlpha has an overload that sets startAlpha then end.
|
||||
// We'll tween min->max then set LoopType.PingPong if available. The API supports LoopType.PingPong.
|
||||
|
||||
// Start from minAlpha to maxAlpha
|
||||
_canvasGroup.alpha = minAlpha;
|
||||
|
||||
// Use the Tween.Value overload for float with obeyTimescale parameter
|
||||
_activeTween = Tween.Value(minAlpha, maxAlpha, v => _canvasGroup.alpha = v, legDuration, startDelay,
|
||||
easeCurve ?? Tween.EaseInOut, Tween.LoopType.PingPong, null, null, obeyTimescale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the blinking tween if running.
|
||||
/// </summary>
|
||||
public void StopBlinking()
|
||||
{
|
||||
if (_activeTween != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_activeTween.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Some versions of the tween API may not expose Cancel; ignore safely.
|
||||
}
|
||||
|
||||
_activeTween = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/BlinkingCanvasGroup.cs.meta
Normal file
3
Assets/Scripts/UI/BlinkingCanvasGroup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b75de64fb2fd4ff0b869bf469e2becea
|
||||
timeCreated: 1761299308
|
||||
@@ -1,39 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI.CardSystem
|
||||
{
|
||||
public class BackpackInput : MonoBehaviour, ITouchInputConsumer
|
||||
{
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnTap(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnHoldMove(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnHoldEnd(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7ea50a695c58944799b4f27a9014301
|
||||
@@ -2,7 +2,6 @@
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Data.CardSystem;
|
||||
using Input;
|
||||
using Pixelplacement;
|
||||
using UI.Core;
|
||||
using UnityEngine;
|
||||
@@ -33,8 +32,7 @@ namespace UI.CardSystem
|
||||
public GameObject BackpackIcon => backpackIcon;
|
||||
private UIPageController PageController => UIPageController.Instance;
|
||||
private CardSystemManager _cardManager;
|
||||
private bool _isInitialized = false;
|
||||
private bool _hasUnseenCards = false;
|
||||
private bool _hasUnseenCards;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -44,9 +42,6 @@ namespace UI.CardSystem
|
||||
backpackButton.onClick.AddListener(OnBackpackButtonClicked);
|
||||
}
|
||||
|
||||
// Initially show only the backpack icon
|
||||
ShowOnlyBackpackIcon();
|
||||
|
||||
// Hide notification dot initially
|
||||
if (boosterNotificationDot != null)
|
||||
boosterNotificationDot.gameObject.SetActive(false);
|
||||
@@ -57,8 +52,18 @@ namespace UI.CardSystem
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
// Initially show only the backpack icon
|
||||
ShowOnlyBackpackIcon();
|
||||
|
||||
// Initialize pages and hide them
|
||||
InitializePages();
|
||||
|
||||
// React to global UI hide/show events (top-page only) by toggling this GameObject
|
||||
if (UIPageController.Instance != null)
|
||||
{
|
||||
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
|
||||
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -77,8 +82,6 @@ namespace UI.CardSystem
|
||||
// Initialize UI with current values
|
||||
UpdateBoosterCount(_cardManager.GetBoosterPackCount());
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -103,6 +106,13 @@ namespace UI.CardSystem
|
||||
{
|
||||
PageController.OnPageChanged -= OnPageChanged;
|
||||
}
|
||||
|
||||
// Unsubscribe from global UI hide/show events
|
||||
if (UIPageController.Instance != null)
|
||||
{
|
||||
UIPageController.Instance.OnAllUIHidden -= HandleAllUIHidden;
|
||||
UIPageController.Instance.OnAllUIShown -= HandleAllUIShown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -142,10 +152,6 @@ namespace UI.CardSystem
|
||||
if (notificationSound != null)
|
||||
notificationSound.Play();
|
||||
|
||||
// Eat input so the characters don't walk around
|
||||
var backpackInput = backpackIcon.gameObject.GetComponentInParent<BackpackInput>();
|
||||
InputManager.Instance.RegisterOverrideConsumer(backpackInput);
|
||||
|
||||
PageController.PushPage(mainMenuPage);
|
||||
|
||||
// Clear notification for unseen cards when opening menu
|
||||
@@ -159,6 +165,8 @@ namespace UI.CardSystem
|
||||
{
|
||||
backpackButton.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
GameManager.Instance.RequestPause(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -170,8 +178,7 @@ namespace UI.CardSystem
|
||||
if (newPage == null)
|
||||
{
|
||||
ShowOnlyBackpackIcon();
|
||||
var backpackInput = backpackIcon.gameObject.GetComponentInParent<BackpackInput>();
|
||||
InputManager.Instance.UnregisterOverrideConsumer(backpackInput);
|
||||
GameManager.Instance.ReleasePause(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -315,5 +322,19 @@ namespace UI.CardSystem
|
||||
// Just log the upgrade event without showing a notification
|
||||
Logging.Debug($"[CardAlbumUI] Card upgraded: {card.Name} to {card.Rarity}");
|
||||
}
|
||||
|
||||
// Handlers for UI controller hide/show events — toggle this GameObject active state
|
||||
private void HandleAllUIHidden()
|
||||
{
|
||||
// Ensure UI returns to backpack-only state before deactivating so we don't leave the game paused
|
||||
ShowOnlyBackpackIcon();
|
||||
backpackButton.gameObject.SetActive(false);
|
||||
|
||||
}
|
||||
|
||||
private void HandleAllUIShown()
|
||||
{
|
||||
backpackButton.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ namespace UI.Core
|
||||
// Event fired when the page stack changes
|
||||
public event Action<UIPage> OnPageChanged;
|
||||
|
||||
// Events fired when the controller hides or shows all UI pages
|
||||
public event Action OnAllUIHidden;
|
||||
public event Action OnAllUIShown;
|
||||
|
||||
private PlayerInput _playerInput;
|
||||
private InputAction _cancelAction;
|
||||
|
||||
@@ -141,5 +145,39 @@ namespace UI.Core
|
||||
OnPageChanged?.Invoke(null);
|
||||
Logging.Debug("[UIPageController] Cleared page stack");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide all currently stacked UI pages by calling TransitionOut on each.
|
||||
/// This does not modify the page stack; it simply asks pages to transition out
|
||||
/// so UI elements can be hidden. Subscribers can react to <see cref="OnAllUIHidden"/>.
|
||||
/// </summary>
|
||||
public void HideAllUI()
|
||||
{
|
||||
// Only transition out the top (active) page — maintains stack semantics.
|
||||
var current = CurrentPage;
|
||||
if (current != null)
|
||||
{
|
||||
current.TransitionOut();
|
||||
}
|
||||
|
||||
OnAllUIHidden?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show all currently stacked UI pages by calling TransitionIn on each from bottom to top.
|
||||
/// This does not modify the page stack; it asks pages to transition in so UI elements become visible.
|
||||
/// Subscribers can react to <see cref="OnAllUIShown"/>.
|
||||
/// </summary>
|
||||
public void ShowAllUI()
|
||||
{
|
||||
// Only transition in the top (active) page — maintains stack semantics.
|
||||
var current = CurrentPage;
|
||||
if (current != null)
|
||||
{
|
||||
current.TransitionIn();
|
||||
}
|
||||
|
||||
OnAllUIShown?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Input;
|
||||
using Bootstrap;
|
||||
using UI.Core;
|
||||
using Pixelplacement;
|
||||
@@ -12,7 +11,6 @@ namespace UI
|
||||
public class PauseMenu : UIPage
|
||||
{
|
||||
private static PauseMenu _instance;
|
||||
private static bool _isQuitting;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the PauseMenu. No longer creates an instance if one doesn't exist.
|
||||
@@ -24,15 +22,6 @@ namespace UI
|
||||
[SerializeField] private GameObject pauseButton;
|
||||
[SerializeField] private CanvasGroup canvasGroup;
|
||||
|
||||
public event Action OnGamePaused;
|
||||
public event Action OnGameResumed;
|
||||
|
||||
private bool _isPaused = false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the game is currently paused
|
||||
/// </summary>
|
||||
public bool IsPaused => _isPaused;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -57,6 +46,13 @@ namespace UI
|
||||
// Subscribe to scene loaded events
|
||||
SceneManagerService.Instance.SceneLoadCompleted += SetPauseMenuByLevel;
|
||||
|
||||
// Also react to global UI hide/show events from the page controller
|
||||
if (UIPageController.Instance != null)
|
||||
{
|
||||
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
|
||||
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
|
||||
}
|
||||
|
||||
// SceneManagerService subscription moved to InitializePostBoot
|
||||
|
||||
// Set initial state based on current scene
|
||||
@@ -72,11 +68,11 @@ namespace UI
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= SetPauseMenuByLevel;
|
||||
}
|
||||
}
|
||||
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
_isQuitting = true;
|
||||
if (UIPageController.Instance != null)
|
||||
{
|
||||
UIPageController.Instance.OnAllUIHidden -= HandleAllUIHidden;
|
||||
UIPageController.Instance.OnAllUIShown -= HandleAllUIShown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,7 +81,7 @@ namespace UI
|
||||
/// <param name="levelName">The name of the level/scene</param>
|
||||
public void SetPauseMenuByLevel(string levelName)
|
||||
{
|
||||
HidePauseMenu(false);
|
||||
HidePauseMenu();
|
||||
// TODO: Implement level-based pause menu visibility logic if needed
|
||||
/*if (string.IsNullOrEmpty(levelName))
|
||||
return;
|
||||
@@ -103,7 +99,6 @@ namespace UI
|
||||
/// </summary>
|
||||
public void ShowPauseMenu()
|
||||
{
|
||||
if (_isPaused) return;
|
||||
if (UIPageController.Instance != null)
|
||||
{
|
||||
UIPageController.Instance.PushPage(this);
|
||||
@@ -128,9 +123,9 @@ namespace UI
|
||||
/// <summary>
|
||||
/// Hides the pause menu and shows the pause button. Sets input mode to Game.
|
||||
/// </summary>
|
||||
public void HidePauseMenu(bool resetInput = true)
|
||||
public void HidePauseMenu()
|
||||
{
|
||||
if (!_isPaused)
|
||||
if (!GameManager.Instance.IsPaused)
|
||||
{
|
||||
// Ensure UI is hidden if somehow active without state
|
||||
if (pauseMenuPanel != null) pauseMenuPanel.SetActive(false);
|
||||
@@ -148,7 +143,6 @@ namespace UI
|
||||
pauseMenuPanel.SetActive(false);
|
||||
if (pauseButton != null)
|
||||
pauseButton.SetActive(true);
|
||||
EndPauseSideEffects(resetInput);
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.alpha = 0f;
|
||||
@@ -159,29 +153,31 @@ namespace UI
|
||||
}
|
||||
}
|
||||
|
||||
public void HidePauseMenuAndResumeGame()
|
||||
{
|
||||
HidePauseMenu();
|
||||
EndPauseSideEffects();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the game by hiding the pause menu.
|
||||
/// </summary>
|
||||
public void ResumeGame()
|
||||
{
|
||||
HidePauseMenu();
|
||||
HidePauseMenuAndResumeGame();
|
||||
}
|
||||
|
||||
private void BeginPauseSideEffects()
|
||||
{
|
||||
_isPaused = true;
|
||||
if (pauseButton != null) pauseButton.SetActive(false);
|
||||
InputManager.Instance.SetInputMode(InputMode.UI);
|
||||
OnGamePaused?.Invoke();
|
||||
GameManager.Instance.RequestPause(this);
|
||||
Logging.Debug("[PauseMenu] Game Paused");
|
||||
}
|
||||
|
||||
private void EndPauseSideEffects(bool invokeEvent)
|
||||
private void EndPauseSideEffects()
|
||||
{
|
||||
_isPaused = false;
|
||||
if (pauseButton != null) pauseButton.SetActive(true);
|
||||
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
|
||||
if (invokeEvent) OnGameResumed?.Invoke();
|
||||
GameManager.Instance.ReleasePause(this);
|
||||
Logging.Debug("[PauseMenu] Game Resumed");
|
||||
}
|
||||
|
||||
@@ -189,6 +185,8 @@ namespace UI
|
||||
{
|
||||
// Ensure the panel root is active
|
||||
if (pauseMenuPanel != null) pauseMenuPanel.SetActive(true);
|
||||
// Pause side effects should run immediately (hide button, set input mode, etc.).
|
||||
// The tween itself must run in unscaled time so it still animates while the game is paused.
|
||||
BeginPauseSideEffects();
|
||||
|
||||
if (canvasGroup != null)
|
||||
@@ -196,7 +194,16 @@ namespace UI
|
||||
canvasGroup.interactable = true;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
canvasGroup.alpha = 0f;
|
||||
Tween.Value(0f, 1f, v => canvasGroup.alpha = v, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, onComplete);
|
||||
// pass obeyTimescale = false so this tween runs even when Time.timeScale == 0
|
||||
Tween.Value(0f, 1f, (v) =>
|
||||
{
|
||||
Logging.Debug($"[PauseMenu] Tweening pause menu alpha: {v}");
|
||||
canvasGroup.alpha = v;
|
||||
}, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, () =>
|
||||
{
|
||||
Logging.Debug("[PauseMenu] Finished tweening pause menu in.");
|
||||
onComplete?.Invoke();
|
||||
}, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -210,16 +217,17 @@ namespace UI
|
||||
{
|
||||
canvasGroup.interactable = false;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
// Run out-tween in unscaled time as well so the fade completes while paused.
|
||||
Tween.Value(canvasGroup.alpha, 0f, v => canvasGroup.alpha = v, transitionDuration, 0f, Tween.EaseInOut, Tween.LoopType.None, null, () =>
|
||||
{
|
||||
EndPauseSideEffects(true);
|
||||
if (pauseMenuPanel != null) pauseMenuPanel.SetActive(false);
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
{
|
||||
EndPauseSideEffects();
|
||||
if (pauseMenuPanel != null) pauseMenuPanel.SetActive(false);
|
||||
onComplete?.Invoke();
|
||||
}, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
EndPauseSideEffects(true);
|
||||
EndPauseSideEffects();
|
||||
if (pauseMenuPanel != null) pauseMenuPanel.SetActive(false);
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
@@ -261,7 +269,7 @@ namespace UI
|
||||
public async void LoadLevel(int levelSelection)
|
||||
{
|
||||
// Hide the pause menu before loading a new level
|
||||
HidePauseMenu();
|
||||
HidePauseMenuAndResumeGame();
|
||||
|
||||
// Replace with the actual scene name as set in Build Settings
|
||||
var progress = new Progress<float>(p => Logging.Debug($"Loading progress: {p * 100:F0}%"));
|
||||
@@ -278,5 +286,42 @@ namespace UI
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handlers for UI controller hide/show events — make these safe with respect to transitions and pause state
|
||||
private void HandleAllUIHidden()
|
||||
{
|
||||
var parent = transform.parent?.gameObject ?? gameObject;
|
||||
|
||||
// If we're currently transitioning, wait for the transition out to complete before deactivating
|
||||
if (_isTransitioning)
|
||||
{
|
||||
Action handler = null;
|
||||
handler = () =>
|
||||
{
|
||||
OnTransitionOutCompleted -= handler;
|
||||
parent.SetActive(false);
|
||||
};
|
||||
OnTransitionOutCompleted += handler;
|
||||
return;
|
||||
}
|
||||
|
||||
// If this page is visible, request a proper hide so transition/out side-effects run (e.g. releasing pause)
|
||||
if (_isVisible)
|
||||
{
|
||||
HidePauseMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise it's safe to immediately deactivate
|
||||
parent.SetActive(false);
|
||||
}
|
||||
|
||||
private void HandleAllUIShown()
|
||||
{
|
||||
var parent = transform.parent?.gameObject ?? gameObject;
|
||||
|
||||
// Just ensure the parent is active. Do not force pause or transitions here.
|
||||
parent.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +1,205 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Pixelplacement;
|
||||
using Minigames.DivingForPictures;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Input;
|
||||
using UnityEngine.Events;
|
||||
using Pixelplacement;
|
||||
using UI.Core;
|
||||
using UnityEngine;
|
||||
|
||||
public class DivingTutorial : MonoBehaviour, ITouchInputConsumer
|
||||
namespace UI.Tutorial
|
||||
{
|
||||
private StateMachine stateMachine;
|
||||
public DivingGameManager divingGameManager;
|
||||
public bool playTutorial;
|
||||
|
||||
// gating for input until current state's animation finishes first loop
|
||||
private bool canAcceptInput = false;
|
||||
private Coroutine waitLoopCoroutine;
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
public class DivingTutorial : MonoBehaviour, ITouchInputConsumer
|
||||
{
|
||||
if (playTutorial == true)
|
||||
private StateMachine _stateMachine;
|
||||
public bool playTutorial;
|
||||
|
||||
// gating for input until current state's animation finishes first loop
|
||||
[SerializeField] private GameObject tapPrompt;
|
||||
|
||||
private bool _canAcceptInput;
|
||||
private Coroutine _waitLoopCoroutine;
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
InitializeTutorial();
|
||||
BootCompletionService.RegisterInitAction(InitializeTutorial);
|
||||
|
||||
// Ensure prompt is hidden initially (even before tutorial initialization)
|
||||
if (tapPrompt != null)
|
||||
tapPrompt.SetActive(false);
|
||||
}
|
||||
else { RemoveTutorial(); }
|
||||
}
|
||||
|
||||
void InitializeTutorial()
|
||||
{
|
||||
stateMachine = GetComponentInChildren<StateMachine>();
|
||||
divingGameManager.Pause();
|
||||
InputManager.Instance.RegisterOverrideConsumer(this);
|
||||
stateMachine.OnLastStateExited.AddListener(RemoveTutorial);
|
||||
|
||||
// prepare gating for the initial active state
|
||||
SetupInputGateForCurrentState();
|
||||
}
|
||||
|
||||
void RemoveTutorial()
|
||||
{
|
||||
Debug.Log("Remove me!");
|
||||
if (waitLoopCoroutine != null)
|
||||
void InitializeTutorial()
|
||||
{
|
||||
StopCoroutine(waitLoopCoroutine);
|
||||
waitLoopCoroutine = null;
|
||||
}
|
||||
InputManager.Instance.UnregisterOverrideConsumer(this);
|
||||
divingGameManager.DoResume();
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
public void OnTap(Vector2 position)
|
||||
{
|
||||
if (!canAcceptInput) return; // block taps until allowed
|
||||
|
||||
// consume this tap and immediately block further taps
|
||||
canAcceptInput = false;
|
||||
|
||||
// move to next state
|
||||
stateMachine.Next(true);
|
||||
|
||||
// after the state changes, set up gating for the new active state's animation
|
||||
SetupInputGateForCurrentState();
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnHoldMove(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnHoldEnd(Vector2 position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
private void SetupInputGateForCurrentState()
|
||||
{
|
||||
if (waitLoopCoroutine != null)
|
||||
{
|
||||
StopCoroutine(waitLoopCoroutine);
|
||||
waitLoopCoroutine = null;
|
||||
}
|
||||
waitLoopCoroutine = StartCoroutine(WaitForFirstLoopOnActiveState());
|
||||
}
|
||||
|
||||
private IEnumerator WaitForFirstLoopOnActiveState()
|
||||
{
|
||||
// wait a frame to ensure StateMachine has activated the correct state GameObject
|
||||
yield return null;
|
||||
|
||||
// find the active child under the StateMachine (the current state)
|
||||
Transform smTransform = stateMachine != null ? stateMachine.transform : transform;
|
||||
Transform activeState = null;
|
||||
for (int i = 0; i < smTransform.childCount; i++)
|
||||
{
|
||||
var child = smTransform.GetChild(i);
|
||||
if (child.gameObject.activeInHierarchy)
|
||||
if (playTutorial)
|
||||
{
|
||||
activeState = child;
|
||||
break;
|
||||
// pause the game, hide UI, and register for input overrides
|
||||
GameManager.Instance.RequestPause(this);
|
||||
UIPageController.Instance.HideAllUI();
|
||||
InputManager.Instance.RegisterOverrideConsumer(this);
|
||||
|
||||
// Setup references
|
||||
_stateMachine = GetComponentInChildren<StateMachine>();
|
||||
_stateMachine.OnLastStateExited.AddListener(RemoveTutorial);
|
||||
|
||||
// prepare gating for the initial active state
|
||||
SetupInputGateForCurrentState();
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveTutorial();
|
||||
}
|
||||
}
|
||||
|
||||
if (activeState == null)
|
||||
void RemoveTutorial()
|
||||
{
|
||||
// if we can't find an active state, fail open: allow input
|
||||
canAcceptInput = true;
|
||||
yield break;
|
||||
}
|
||||
|
||||
// look for a legacy Animation component on the active state
|
||||
var anim = activeState.GetComponent<Animation>();
|
||||
if (anim == null)
|
||||
{
|
||||
// no animation to wait for; allow input immediately
|
||||
canAcceptInput = true;
|
||||
yield break;
|
||||
}
|
||||
|
||||
// determine a clip/state to observe
|
||||
string clipName = anim.clip != null ? anim.clip.name : null;
|
||||
AnimationState observedState = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(clipName))
|
||||
{
|
||||
observedState = anim[clipName];
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback: take the first enabled state in the Animation
|
||||
foreach (AnimationState st in anim)
|
||||
Debug.Log("Remove me!");
|
||||
if (_waitLoopCoroutine != null)
|
||||
{
|
||||
observedState = st;
|
||||
break;
|
||||
StopCoroutine(_waitLoopCoroutine);
|
||||
_waitLoopCoroutine = null;
|
||||
}
|
||||
|
||||
// Unpause, unregister input, and show UI
|
||||
InputManager.Instance.UnregisterOverrideConsumer(this);
|
||||
UIPageController.Instance.ShowAllUI();
|
||||
GameManager.Instance.ReleasePause(this);
|
||||
|
||||
// hide prompt if present
|
||||
if (tapPrompt != null)
|
||||
tapPrompt.SetActive(false);
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
public void OnTap(Vector2 position)
|
||||
{
|
||||
if (!_canAcceptInput) return; // block taps until allowed
|
||||
|
||||
// consume this tap and immediately block further taps
|
||||
SetInputEnabled(false);
|
||||
|
||||
// move to next state
|
||||
_stateMachine.Next(true);
|
||||
|
||||
// after the state changes, set up gating for the new active state's animation
|
||||
SetupInputGateForCurrentState();
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnHoldMove(Vector2 position)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnHoldEnd(Vector2 position)
|
||||
{
|
||||
}
|
||||
|
||||
// centralize enabling/disabling input and the tap prompt
|
||||
private void SetInputEnabled(bool allow)
|
||||
{
|
||||
_canAcceptInput = allow;
|
||||
if (tapPrompt != null)
|
||||
{
|
||||
tapPrompt.SetActive(allow);
|
||||
}
|
||||
}
|
||||
|
||||
if (observedState == null)
|
||||
private void SetupInputGateForCurrentState()
|
||||
{
|
||||
// nothing to observe; allow input
|
||||
canAcceptInput = true;
|
||||
yield break;
|
||||
if (_waitLoopCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_waitLoopCoroutine);
|
||||
_waitLoopCoroutine = null;
|
||||
}
|
||||
_waitLoopCoroutine = StartCoroutine(WaitForFirstLoopOnActiveState());
|
||||
}
|
||||
|
||||
// wait until the animation starts playing the observed clip
|
||||
float safetyTimer = 0f;
|
||||
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy && !anim.IsPlaying(observedState.name) && safetyTimer < 2f)
|
||||
private IEnumerator WaitForFirstLoopOnActiveState()
|
||||
{
|
||||
safetyTimer += Time.deltaTime;
|
||||
// wait a frame to ensure StateMachine has activated the correct state GameObject
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// wait until the first loop completes (normalizedTime >= 1)
|
||||
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy)
|
||||
{
|
||||
// if state changed (not playing anymore), allow input to avoid deadlock
|
||||
if (!anim.IsPlaying(observedState.name)) break;
|
||||
|
||||
if (observedState.normalizedTime >= 1f)
|
||||
// find the active child under the StateMachine (the current state)
|
||||
Transform smTransform = _stateMachine != null ? _stateMachine.transform : transform;
|
||||
Transform activeState = null;
|
||||
for (int i = 0; i < smTransform.childCount; i++)
|
||||
{
|
||||
break;
|
||||
var child = smTransform.GetChild(i);
|
||||
if (child.gameObject.activeInHierarchy)
|
||||
{
|
||||
activeState = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
|
||||
canAcceptInput = true;
|
||||
waitLoopCoroutine = null;
|
||||
if (activeState == null)
|
||||
{
|
||||
// if we can't find an active state, fail open: allow input
|
||||
SetInputEnabled(true);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// look for a legacy Animation component on the active state
|
||||
var anim = activeState.GetComponent<Animation>();
|
||||
if (anim == null)
|
||||
{
|
||||
// no animation to wait for; allow input immediately
|
||||
SetInputEnabled(true);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// determine a clip/state to observe
|
||||
string clipName = anim.clip != null ? anim.clip.name : null;
|
||||
AnimationState observedState = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(clipName))
|
||||
{
|
||||
observedState = anim[clipName];
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback: take the first enabled state in the Animation
|
||||
foreach (AnimationState st in anim)
|
||||
{
|
||||
observedState = st;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (observedState == null)
|
||||
{
|
||||
// nothing to observe; allow input
|
||||
SetInputEnabled(true);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// wait until the animation starts playing the observed clip
|
||||
float safetyTimer = 0f;
|
||||
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy && !anim.IsPlaying(observedState.name) && safetyTimer < 2f)
|
||||
{
|
||||
safetyTimer += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// wait until the first loop completes (normalizedTime >= 1)
|
||||
while (anim.isActiveAndEnabled && activeState.gameObject.activeInHierarchy)
|
||||
{
|
||||
// if state changed (not playing anymore), allow input to avoid deadlock
|
||||
if (!anim.IsPlaying(observedState.name)) break;
|
||||
|
||||
if (observedState.normalizedTime >= 1f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetInputEnabled(true);
|
||||
_waitLoopCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user