This commit is contained in:
@@ -0,0 +1,717 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ShrinkTutorial
|
||||
{
|
||||
[DefaultExecutionOrder(-1500)]
|
||||
public class ShrinkTutorialManager : MonoBehaviour
|
||||
{
|
||||
private static ShrinkTutorialManager _instance;
|
||||
|
||||
[SerializeField] private ShrinkTutorialSettings settingsOverride;
|
||||
|
||||
private readonly List<string> _pendingTutorialIds = new();
|
||||
private readonly HashSet<string> _completedTutorials = new();
|
||||
private readonly HashSet<string> _skippedTutorials = new();
|
||||
private readonly List<RaycastResult> _pointerRaycastResults = new();
|
||||
|
||||
private IShrinkTutorialStorage _storage;
|
||||
private Canvas _canvas;
|
||||
private ShrinkTutorialMask _mask;
|
||||
private ShrinkTutorialDialog _dialog;
|
||||
|
||||
private Coroutine _runner;
|
||||
private ShrinkTutorialData _currentTutorial;
|
||||
private int _currentStepIndex = -1;
|
||||
private bool _isPaused;
|
||||
private bool _skipRequested;
|
||||
private bool _stepCompleted;
|
||||
private bool _stepCompletionQueued;
|
||||
private float _stepAutoTimer;
|
||||
private string _receivedCustomEvent = string.Empty;
|
||||
|
||||
public static ShrinkTutorialManager Instance => _instance;
|
||||
|
||||
public bool IsRunning => _currentTutorial != null;
|
||||
public string CurrentTutorialId => _currentTutorial ? _currentTutorial.tutorialId : string.Empty;
|
||||
|
||||
public event Action<string> OnTutorialStarted;
|
||||
public event Action<string> OnTutorialCompleted;
|
||||
public event Action<string> OnTutorialSkipped;
|
||||
public event Action<string, int> OnStepChanged;
|
||||
public event Action<ShrinkTutorialSignalArgs> OnSignalTriggered;
|
||||
|
||||
public static ShrinkTutorialManager EnsureInstance(ShrinkTutorialSettings settings = null)
|
||||
{
|
||||
if (_instance)
|
||||
return _instance;
|
||||
|
||||
var existing = FindObjectOfType<ShrinkTutorialManager>();
|
||||
if (existing)
|
||||
return existing;
|
||||
|
||||
var host = new GameObject("ShrinkTutorialManager");
|
||||
var manager = host.AddComponent<ShrinkTutorialManager>();
|
||||
manager.settingsOverride = settings;
|
||||
return manager;
|
||||
}
|
||||
|
||||
public static void ResetStaticState()
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
public bool HasCompleted(string tutorialId)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(tutorialId) && _completedTutorials.Contains(tutorialId);
|
||||
}
|
||||
|
||||
public bool HasSkipped(string tutorialId)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(tutorialId) && _skippedTutorials.Contains(tutorialId);
|
||||
}
|
||||
|
||||
public void ClearProgress(string tutorialId)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tutorialId))
|
||||
return;
|
||||
|
||||
var changed = _completedTutorials.Remove(tutorialId);
|
||||
changed = _skippedTutorials.Remove(tutorialId) || changed;
|
||||
if (changed)
|
||||
SaveProgress();
|
||||
}
|
||||
|
||||
public void StartTutorial(string tutorialId)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tutorialId) || HasCompleted(tutorialId))
|
||||
return;
|
||||
|
||||
if (!TryGetDatabaseTutorial(tutorialId, out var tutorial))
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkTutorial] 未找到教程: {tutorialId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ArePrerequisitesSatisfied(tutorial) || _currentTutorial)
|
||||
{
|
||||
EnqueueTutorial(tutorialId);
|
||||
return;
|
||||
}
|
||||
|
||||
LogDebug($"StartTutorial -> {tutorialId}");
|
||||
_runner = StartCoroutine(RunTutorialRoutine(tutorial));
|
||||
}
|
||||
|
||||
public void CompleteStep(string customEventName = "")
|
||||
{
|
||||
if (_currentTutorial)
|
||||
{
|
||||
LogDebug($"CompleteStep signal received, tutorial={_currentTutorial.tutorialId}, step={_currentStepIndex}, event={customEventName}");
|
||||
_receivedCustomEvent = customEventName ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void SkipCurrent()
|
||||
{
|
||||
if (!_currentTutorial)
|
||||
return;
|
||||
|
||||
_skipRequested = true;
|
||||
_stepCompleted = true;
|
||||
}
|
||||
|
||||
public void PauseCurrent()
|
||||
{
|
||||
if (!_currentTutorial)
|
||||
return;
|
||||
|
||||
_isPaused = true;
|
||||
SetUiVisible(false);
|
||||
}
|
||||
|
||||
public void ResumeCurrent()
|
||||
{
|
||||
if (!_currentTutorial)
|
||||
return;
|
||||
|
||||
_isPaused = false;
|
||||
SetUiVisible(true);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
_instance = this;
|
||||
InitializeIfNeeded();
|
||||
}
|
||||
|
||||
private void InitializeIfNeeded()
|
||||
{
|
||||
if (_storage != null)
|
||||
return;
|
||||
|
||||
var settings = ResolveSettings();
|
||||
_storage = new PlayerPrefsShrinkTutorialStorage(settings.playerPrefsStorageKey);
|
||||
|
||||
var progress = _storage.Load();
|
||||
_completedTutorials.Clear();
|
||||
_skippedTutorials.Clear();
|
||||
|
||||
foreach (var tutorialId in progress.completedTutorials)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tutorialId))
|
||||
_completedTutorials.Add(tutorialId);
|
||||
}
|
||||
|
||||
foreach (var tutorialId in progress.skippedTutorials)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tutorialId))
|
||||
_skippedTutorials.Add(tutorialId);
|
||||
}
|
||||
|
||||
if (settings.dontDestroyOnLoad)
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
EnsureEventSystem();
|
||||
EnsureUi();
|
||||
}
|
||||
|
||||
private IEnumerator RunTutorialRoutine(ShrinkTutorialData tutorial)
|
||||
{
|
||||
_currentTutorial = tutorial;
|
||||
OnTutorialStarted?.Invoke(tutorial.tutorialId);
|
||||
|
||||
for (var stepIndex = 0; stepIndex < tutorial.steps.Count; stepIndex++)
|
||||
{
|
||||
_currentStepIndex = stepIndex;
|
||||
_skipRequested = false;
|
||||
_stepCompleted = false;
|
||||
_stepCompletionQueued = false;
|
||||
_stepAutoTimer = tutorial.steps[stepIndex].autoDelay;
|
||||
_receivedCustomEvent = string.Empty;
|
||||
|
||||
var step = tutorial.steps[stepIndex];
|
||||
LogDebug(
|
||||
$"Enter step {stepIndex}, condition={step.completeCondition}, targetMode={step.targetMode}, " +
|
||||
$"targetPath={step.targetPath}, anchorId={step.anchorId}, customEvent={step.customEventName}");
|
||||
OnStepChanged?.Invoke(tutorial.tutorialId, stepIndex);
|
||||
|
||||
yield return WaitWhilePaused();
|
||||
|
||||
Transform targetTransform = null;
|
||||
yield return ResolveTargetRoutine(step, value => targetTransform = value);
|
||||
|
||||
if (step.RequiresTarget() && !targetTransform)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkTutorial] 教程 {tutorial.tutorialId} 第 {stepIndex + 1} 步缺少目标,已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetTransform)
|
||||
LogDebug($"Resolved target for step {stepIndex}: {GetTransformPath(targetTransform)}");
|
||||
|
||||
EmitSignal(step.enterSignal);
|
||||
_dialog.SetSkipVisible(ResolveSettings().showSkipButton);
|
||||
|
||||
while (!_stepCompleted && !_skipRequested)
|
||||
{
|
||||
yield return WaitWhilePaused();
|
||||
|
||||
UpdateStepVisuals(step, targetTransform);
|
||||
TickStepCompletion(step, targetTransform);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
EmitSignal(step.exitSignal);
|
||||
_dialog.Hide();
|
||||
|
||||
if (_skipRequested)
|
||||
{
|
||||
MarkSkipped(tutorial.tutorialId);
|
||||
CleanupAfterTutorial();
|
||||
OnTutorialSkipped?.Invoke(tutorial.tutorialId);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
MarkCompleted(tutorial.tutorialId);
|
||||
OnTutorialCompleted?.Invoke(tutorial.tutorialId);
|
||||
CleanupAfterTutorial();
|
||||
}
|
||||
|
||||
private IEnumerator ResolveTargetRoutine(ShrinkTutorialStep step, Action<Transform> assignResult)
|
||||
{
|
||||
var elapsed = 0f;
|
||||
var target = ShrinkTutorialTargetUtility.ResolveTarget(step);
|
||||
|
||||
while (!target && step.waitForTarget)
|
||||
{
|
||||
if (step.waitTimeout > 0f && elapsed >= step.waitTimeout)
|
||||
break;
|
||||
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
target = ShrinkTutorialTargetUtility.ResolveTarget(step);
|
||||
}
|
||||
|
||||
assignResult?.Invoke(target);
|
||||
}
|
||||
|
||||
private IEnumerator WaitWhilePaused()
|
||||
{
|
||||
while (_isPaused)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private void TickStepCompletion(ShrinkTutorialStep step, Transform targetTransform)
|
||||
{
|
||||
switch (step.completeCondition)
|
||||
{
|
||||
case ShrinkTutorialCompleteCondition.Auto:
|
||||
_stepAutoTimer -= Time.unscaledDeltaTime;
|
||||
if (_stepAutoTimer <= 0f)
|
||||
{
|
||||
LogDebug($"Auto step completed, tutorial={_currentTutorial?.tutorialId}, step={_currentStepIndex}");
|
||||
_stepCompleted = true;
|
||||
}
|
||||
break;
|
||||
case ShrinkTutorialCompleteCondition.AnyClick:
|
||||
if (WasPrimaryPointerPressedOutsideDialog())
|
||||
{
|
||||
LogDebug($"AnyClick step completed, tutorial={_currentTutorial?.tutorialId}, step={_currentStepIndex}");
|
||||
_stepCompleted = true;
|
||||
}
|
||||
break;
|
||||
case ShrinkTutorialCompleteCondition.ClickTarget:
|
||||
if (targetTransform &&
|
||||
ShrinkTutorialTargetUtility.TryBuildScreenRect(targetTransform, out var targetRect) &&
|
||||
WasPrimaryPointerReleased(targetRect, step.maskShape, out var screenPosition) &&
|
||||
WasPointerReleaseRoutedToTarget(targetTransform, screenPosition))
|
||||
{
|
||||
LogDebug(
|
||||
$"ClickTarget candidate accepted, tutorial={_currentTutorial?.tutorialId}, step={_currentStepIndex}, " +
|
||||
$"pointer={screenPosition}, target={GetTransformPath(targetTransform)}");
|
||||
QueueStepCompletion();
|
||||
}
|
||||
break;
|
||||
case ShrinkTutorialCompleteCondition.CustomEvent:
|
||||
case ShrinkTutorialCompleteCondition.DragToTarget:
|
||||
if (MatchesCustomEvent(step.customEventName))
|
||||
{
|
||||
LogDebug(
|
||||
$"CustomEvent step completed, tutorial={_currentTutorial?.tutorialId}, step={_currentStepIndex}, event={step.customEventName}");
|
||||
_stepCompleted = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStepVisuals(ShrinkTutorialStep step, Transform targetTransform)
|
||||
{
|
||||
Rect targetRect = default;
|
||||
var hasTargetRect = targetTransform && ShrinkTutorialTargetUtility.TryBuildScreenRect(targetTransform, out targetRect);
|
||||
|
||||
if (hasTargetRect && step.maskShape != ShrinkTutorialMaskShape.None)
|
||||
_mask.SetHighlight(targetRect, step.maskShape, step.maskPadding);
|
||||
else
|
||||
_mask.ClearHighlight();
|
||||
|
||||
var tutorialText = ShrinkTutorialLocalization.Resolve(step.localizedTextKey, step.fallbackText);
|
||||
_dialog.Show(tutorialText, hasTargetRect ? targetRect : (Rect?)null, step.dialogAnchor, step.dialogOffset, step.showArrow);
|
||||
}
|
||||
|
||||
private bool MatchesCustomEvent(string expectedEventName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_receivedCustomEvent))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(expectedEventName))
|
||||
{
|
||||
_receivedCustomEvent = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.Equals(expectedEventName, _receivedCustomEvent, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
_receivedCustomEvent = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool WasPrimaryPointerPressedOutsideDialog()
|
||||
{
|
||||
var pressed = TryGetPointerDownPosition(out var screenPosition);
|
||||
return pressed && !_dialog.ContainsScreenPoint(screenPosition);
|
||||
}
|
||||
|
||||
private bool WasPrimaryPointerReleased(Rect targetRect, ShrinkTutorialMaskShape maskShape, out Vector2 screenPosition)
|
||||
{
|
||||
if (!TryGetPointerUpPosition(out screenPosition))
|
||||
return false;
|
||||
|
||||
if (_dialog.ContainsScreenPoint(screenPosition))
|
||||
{
|
||||
LogDebug($"Pointer release ignored because it hit dialog, pointer={screenPosition}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var insideTarget = maskShape == ShrinkTutorialMaskShape.Circle
|
||||
? IsPointInsideCircle(targetRect, screenPosition)
|
||||
: targetRect.Contains(screenPosition);
|
||||
if (!insideTarget)
|
||||
LogDebug($"Pointer release outside target, pointer={screenPosition}, rect={targetRect}");
|
||||
|
||||
return insideTarget;
|
||||
}
|
||||
|
||||
private static bool IsPointInsideCircle(Rect targetRect, Vector2 screenPosition)
|
||||
{
|
||||
var center = targetRect.center;
|
||||
var radius = Mathf.Min(targetRect.width, targetRect.height) * 0.5f;
|
||||
return Vector2.SqrMagnitude(screenPosition - center) <= radius * radius;
|
||||
}
|
||||
|
||||
private static bool TryGetPointerDownPosition(out Vector2 screenPosition)
|
||||
{
|
||||
if (Input.touchCount > 0)
|
||||
{
|
||||
var touch = Input.GetTouch(0);
|
||||
if (touch.phase == TouchPhase.Began)
|
||||
{
|
||||
screenPosition = touch.position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
screenPosition = Input.mousePosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
screenPosition = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetPointerUpPosition(out Vector2 screenPosition)
|
||||
{
|
||||
if (Input.touchCount > 0)
|
||||
{
|
||||
var touch = Input.GetTouch(0);
|
||||
if (touch.phase == TouchPhase.Ended)
|
||||
{
|
||||
screenPosition = touch.position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonUp(0))
|
||||
{
|
||||
screenPosition = Input.mousePosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
screenPosition = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool WasPointerReleaseRoutedToTarget(Transform targetTransform, Vector2 screenPosition)
|
||||
{
|
||||
var eventSystem = EventSystem.current;
|
||||
if (!eventSystem)
|
||||
return true;
|
||||
|
||||
_pointerRaycastResults.Clear();
|
||||
var pointerEventData = new PointerEventData(eventSystem)
|
||||
{
|
||||
position = screenPosition
|
||||
};
|
||||
eventSystem.RaycastAll(pointerEventData, _pointerRaycastResults);
|
||||
|
||||
foreach (var result in _pointerRaycastResults)
|
||||
{
|
||||
var hitTransform = result.gameObject ? result.gameObject.transform : null;
|
||||
if (IsSameOrNestedTarget(hitTransform, targetTransform))
|
||||
{
|
||||
LogDebug(
|
||||
$"Raycast matched target, pointer={screenPosition}, target={GetTransformPath(targetTransform)}, " +
|
||||
$"hit={GetTransformPath(hitTransform)}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var hitSummary = _pointerRaycastResults.Count == 0
|
||||
? "<none>"
|
||||
: string.Join(" | ", _pointerRaycastResults.Take(5).Select(result =>
|
||||
{
|
||||
var hitTransform = result.gameObject ? result.gameObject.transform : null;
|
||||
return GetTransformPath(hitTransform);
|
||||
}));
|
||||
LogDebug(
|
||||
$"Raycast missed target, pointer={screenPosition}, target={GetTransformPath(targetTransform)}, hits={hitSummary}");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSameOrNestedTarget(Transform hitTransform, Transform targetTransform)
|
||||
{
|
||||
return hitTransform &&
|
||||
targetTransform &&
|
||||
(hitTransform == targetTransform ||
|
||||
hitTransform.IsChildOf(targetTransform) ||
|
||||
targetTransform.IsChildOf(hitTransform));
|
||||
}
|
||||
|
||||
private void QueueStepCompletion()
|
||||
{
|
||||
if (_stepCompletionQueued || !_currentTutorial)
|
||||
return;
|
||||
|
||||
LogDebug($"QueueStepCompletion, tutorial={_currentTutorial.tutorialId}, step={_currentStepIndex}");
|
||||
_stepCompletionQueued = true;
|
||||
StartCoroutine(CompleteStepAtEndOfFrame(_currentTutorial.tutorialId, _currentStepIndex));
|
||||
}
|
||||
|
||||
private IEnumerator CompleteStepAtEndOfFrame(string tutorialId, int stepIndex)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
_stepCompletionQueued = false;
|
||||
|
||||
if (_currentTutorial &&
|
||||
!_skipRequested &&
|
||||
!_stepCompleted &&
|
||||
string.Equals(_currentTutorial.tutorialId, tutorialId, StringComparison.Ordinal) &&
|
||||
_currentStepIndex == stepIndex)
|
||||
{
|
||||
LogDebug($"Step completed at end of frame, tutorial={tutorialId}, step={stepIndex}");
|
||||
_stepCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitSignal(string signalName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(signalName) || !_currentTutorial)
|
||||
return;
|
||||
|
||||
OnSignalTriggered?.Invoke(new ShrinkTutorialSignalArgs(
|
||||
_currentTutorial.tutorialId,
|
||||
_currentStepIndex,
|
||||
signalName));
|
||||
}
|
||||
|
||||
private void CleanupAfterTutorial()
|
||||
{
|
||||
_mask.ClearHighlight();
|
||||
_dialog.Hide();
|
||||
_currentTutorial = null;
|
||||
_currentStepIndex = -1;
|
||||
_runner = null;
|
||||
_stepCompletionQueued = false;
|
||||
|
||||
TryStartQueuedTutorial();
|
||||
}
|
||||
|
||||
private void EnsureUi()
|
||||
{
|
||||
if (_canvas && _mask && _dialog)
|
||||
return;
|
||||
|
||||
var settings = ResolveSettings();
|
||||
var canvasObject = new GameObject("ShrinkTutorialCanvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler),
|
||||
typeof(GraphicRaycaster));
|
||||
canvasObject.transform.SetParent(transform, false);
|
||||
|
||||
_canvas = canvasObject.GetComponent<Canvas>();
|
||||
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
_canvas.sortingOrder = settings.canvasSortingOrder;
|
||||
|
||||
var scaler = canvasObject.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
var maskObject = new GameObject("Mask", typeof(RectTransform), typeof(CanvasRenderer), typeof(ShrinkTutorialMask));
|
||||
maskObject.transform.SetParent(canvasObject.transform, false);
|
||||
StretchFull(maskObject.GetComponent<RectTransform>());
|
||||
_mask = maskObject.GetComponent<ShrinkTutorialMask>();
|
||||
_mask.color = settings.maskColor;
|
||||
|
||||
var dialogObject = new GameObject("Dialog", typeof(RectTransform), typeof(ShrinkTutorialDialog));
|
||||
dialogObject.transform.SetParent(canvasObject.transform, false);
|
||||
_dialog = dialogObject.GetComponent<ShrinkTutorialDialog>();
|
||||
_dialog.Initialize(canvasObject.transform, SkipCurrent);
|
||||
_dialog.Hide();
|
||||
}
|
||||
|
||||
private void EnsureEventSystem()
|
||||
{
|
||||
if (FindObjectOfType<EventSystem>())
|
||||
return;
|
||||
|
||||
var eventSystemObject = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
|
||||
DontDestroyOnLoad(eventSystemObject);
|
||||
}
|
||||
|
||||
private bool TryGetDatabaseTutorial(string tutorialId, out ShrinkTutorialData tutorial)
|
||||
{
|
||||
tutorial = null;
|
||||
var settings = ResolveSettings();
|
||||
return settings.database && settings.database.TryGetTutorial(tutorialId, out tutorial);
|
||||
}
|
||||
|
||||
private ShrinkTutorialSettings ResolveSettings()
|
||||
{
|
||||
return settingsOverride ? settingsOverride : ShrinkTutorialSettings.Instance;
|
||||
}
|
||||
|
||||
private bool ArePrerequisitesSatisfied(ShrinkTutorialData tutorial)
|
||||
{
|
||||
if (!tutorial || tutorial.prerequisiteIds == null)
|
||||
return true;
|
||||
|
||||
foreach (var prerequisiteId in tutorial.prerequisiteIds)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prerequisiteId))
|
||||
continue;
|
||||
|
||||
if (!_completedTutorials.Contains(prerequisiteId))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EnqueueTutorial(string tutorialId)
|
||||
{
|
||||
if (_pendingTutorialIds.Contains(tutorialId))
|
||||
return;
|
||||
|
||||
_pendingTutorialIds.Add(tutorialId);
|
||||
_pendingTutorialIds.Sort(ComparePendingTutorial);
|
||||
}
|
||||
|
||||
private int ComparePendingTutorial(string leftId, string rightId)
|
||||
{
|
||||
var leftPriority = TryGetDatabaseTutorial(leftId, out var leftTutorial) ? leftTutorial.priority : 0;
|
||||
var rightPriority = TryGetDatabaseTutorial(rightId, out var rightTutorial) ? rightTutorial.priority : 0;
|
||||
return rightPriority.CompareTo(leftPriority);
|
||||
}
|
||||
|
||||
private void TryStartQueuedTutorial()
|
||||
{
|
||||
for (var i = 0; i < _pendingTutorialIds.Count; i++)
|
||||
{
|
||||
var tutorialId = _pendingTutorialIds[i];
|
||||
if (!TryGetDatabaseTutorial(tutorialId, out var tutorial) || !ArePrerequisitesSatisfied(tutorial))
|
||||
continue;
|
||||
|
||||
_pendingTutorialIds.RemoveAt(i);
|
||||
StartTutorial(tutorialId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkCompleted(string tutorialId)
|
||||
{
|
||||
_completedTutorials.Add(tutorialId);
|
||||
_skippedTutorials.Remove(tutorialId);
|
||||
SaveProgress();
|
||||
}
|
||||
|
||||
private void MarkSkipped(string tutorialId)
|
||||
{
|
||||
_skippedTutorials.Add(tutorialId);
|
||||
SaveProgress();
|
||||
}
|
||||
|
||||
private void SaveProgress()
|
||||
{
|
||||
_storage.Save(new ShrinkTutorialProgress
|
||||
{
|
||||
completedTutorials = new List<string>(_completedTutorials),
|
||||
skippedTutorials = new List<string>(_skippedTutorials)
|
||||
});
|
||||
}
|
||||
|
||||
private void SetUiVisible(bool visible)
|
||||
{
|
||||
if (_canvas)
|
||||
_canvas.enabled = visible;
|
||||
}
|
||||
|
||||
private static void StretchFull(RectTransform rectTransform)
|
||||
{
|
||||
rectTransform.anchorMin = Vector2.zero;
|
||||
rectTransform.anchorMax = Vector2.one;
|
||||
rectTransform.offsetMin = Vector2.zero;
|
||||
rectTransform.offsetMax = Vector2.zero;
|
||||
rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
||||
}
|
||||
|
||||
[System.Diagnostics.Conditional("UNITY_EDITOR")]
|
||||
private static void LogDebug(string message)
|
||||
{
|
||||
if (!IsDebugLoggingEnabled())
|
||||
return;
|
||||
|
||||
Debug.Log($"[ShrinkTutorial][Debug] {message}");
|
||||
}
|
||||
|
||||
[System.Diagnostics.Conditional("UNITY_EDITOR")]
|
||||
private static void LogDebug(string format, params object[] args)
|
||||
{
|
||||
if (!IsDebugLoggingEnabled())
|
||||
return;
|
||||
|
||||
Debug.LogFormat("[ShrinkTutorial][Debug] " + format, args);
|
||||
}
|
||||
|
||||
private static bool IsDebugLoggingEnabled()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetTransformPath(Transform transform)
|
||||
{
|
||||
if (!transform)
|
||||
return "<null>";
|
||||
|
||||
var path = transform.name;
|
||||
while (transform.parent)
|
||||
{
|
||||
transform = transform.parent;
|
||||
path = $"{transform.name}/{path}";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ShrinkTutorialSignalArgs
|
||||
{
|
||||
public ShrinkTutorialSignalArgs(string tutorialId, int stepIndex, string signalName)
|
||||
{
|
||||
TutorialId = tutorialId;
|
||||
StepIndex = stepIndex;
|
||||
SignalName = signalName;
|
||||
}
|
||||
|
||||
public string TutorialId { get; }
|
||||
public int StepIndex { get; }
|
||||
public string SignalName { get; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user