using Exoa.Utils; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.UI; namespace Exoa.TutorialEngine { public class TutorialController : MonoBehaviour { private enum State { Inactive, Loading, Playing, FadingOut, Delaying, Completed }; private static State tutorialState; private static bool isSkippable; private static bool autoNext = true; private static float delay = 0; private TutorialSession session; private List steps; [Header("UI")] public TutorialPopup popup; public Button hiddenBtn; public RectTransform hiddenBtnRt; public RectTransform mask; private int currentStep = -1; public float maskScale = 1.2f; public float nextTime; public Image bg; public static TutorialController instance; private bool retried; private float size1Duration = 0.2f; private GameObject currentTarget; private Vector2 currentEndPositionValue; private float currentEndSizeValue; private float currentEndPositionChangeTime; private GameObject lastFocusedObject; private Transform lastFocusedObjectParent; private int lastFocusedObjectSibling; [Header("BACKGROUND")] public Color initBGColor = new Color(0, 0, 0, .7f); public Color normalBGColor = new Color(0, 0, 0, .7f); private Color currentBgColor; [Header("ANIMATION")] public Springs popupMoveSettings; private Vector2Spring popupMoveSpring; public Springs maskMoveSettings; private Vector2Spring maskMoveSpring; public Springs maskSize1Settings; public Springs maskSize2Settings; private Vector2Spring maskSizeSpring; public Springs bgColorSettings; private Vector4Spring bgColorSpring; private RenderMode canvasRenderMode; private Camera canvasRenderCamera; #if TUTORIAL_ENGINE_LOCALIZATION [Header("LOCALIZAION")] public string tableName = "New Table"; #endif [Header("DEBUG MODE")] public bool debug; /// /// Option to display the close button or not, to skip the tutorial /// public static bool IsSkippable { get { return isSkippable; } set { isSkippable = value; } } /// /// Getter to check if the tutorial is still playing /// public static bool IsTutorialActive { get { return tutorialState == State.Playing; } } /// /// Getter to check if the tutorial is still playing /// public static bool IsTutorialComplete { get { return tutorialState == State.Completed; } } /// /// Automatically go to the next step after a step is done /// If false, the tutorial will hide until you call NextStep() /// public static bool AutoNext { get => autoNext; set => autoNext = value; } /// /// Add a delay between steps /// public static float Delay { get => delay; set => delay = value; } private void OnDestroy() { TutorialEvents.OnTutorialLoaded -= OnTutorialLoeaded; } void Awake() { if (instance != null) throw new Exception("TutorialController alraedy creaeted"); instance = this; canvasRenderCamera = GetCamera(); popup.gameObject.SetActive(false); popup.closeBtn.onClick.AddListener(OnClosePopup); } private void OnClosePopup() { HideTutorial(); } void Start() { bg.material.color = initBGColor; if (TutorialLoader.instance.tutorialLoaded) OnTutorialLoeaded(); TutorialEvents.OnTutorialLoaded += OnTutorialLoeaded; } private void OnTutorialLoeaded() { steps = new List(); if (TutorialLoader.instance != null && TutorialLoader.instance.currentTutorial.tutorial_steps != null) steps.AddRange(TutorialLoader.instance.currentTutorial.tutorial_steps); tutorialState = State.Playing; currentStep = -1; if (steps.Count > 0) { ShowTutorial(); ForceNext(); } else { HideTutorial(); } } private void OnClickNext() { Next(); } private void Update() { if (debug) Debug.Log("tutorialState:" + tutorialState); if (tutorialState == State.Inactive || tutorialState == State.Completed) { return; } if (tutorialState == State.Delaying) { if (nextTime < Time.time - delay) { ShowTutorial(); ForceNext(); } } if (tutorialState == State.FadingOut) { Vector2 targetOutSize = new Vector2(7000, 7000); mask.sizeDelta = maskSize1Settings.UpdateSpring(ref maskSizeSpring, targetOutSize); popup.PopupRt.anchoredPosition = popupMoveSettings.UpdateSpring(ref popupMoveSpring, targetOutSize); if (Vector2.Distance(targetOutSize, mask.sizeDelta) < 100) { if (steps == null || currentStep >= steps.Count - 1) { if (debug) Debug.Log("Invoke OnTutorialComplete"); tutorialState = State.Completed; TutorialEvents.OnTutorialComplete?.Invoke(); } else tutorialState = State.Inactive; bg.gameObject.SetActive(false); mask.gameObject.SetActive(false); popup.Hide(); return; } } if (tutorialState == State.Playing) { if (currentTarget != null) { Rect targetRect2D = GetObjectCanvasRect(currentTarget); float targetSize = Mathf.Max(targetRect2D.width, targetRect2D.height); if (currentEndSizeValue != targetSize) { currentEndSizeValue = targetSize; } if (currentEndPositionValue != targetRect2D.position) { currentEndPositionValue = targetRect2D.position; } mask.anchoredPosition = maskMoveSettings.UpdateSpring(ref maskMoveSpring, currentEndPositionValue); Vector2 targetMaskSize = Vector2.one * currentEndSizeValue * maskScale; if (currentEndPositionChangeTime > Time.time - size1Duration) { targetMaskSize = new Vector2(.3f, 2f) * currentEndSizeValue * maskScale; mask.sizeDelta = maskSize1Settings.UpdateSpring(ref maskSizeSpring, targetMaskSize); } else { mask.sizeDelta = maskSize2Settings.UpdateSpring(ref maskSizeSpring, targetMaskSize); } Vector2 popupPositon = popup.CalculatePopupPosition(targetRect2D); popup.PopupRt.anchoredPosition = popupMoveSettings.UpdateSpring(ref popupMoveSpring, popupPositon); hiddenBtnRt.anchoredPosition = currentEndPositionValue; hiddenBtnRt.sizeDelta = targetMaskSize; } else { popup.PopupRt.anchoredPosition = popupMoveSettings.UpdateSpring(ref popupMoveSpring, Vector2.zero); } Vector4 newColorV = bgColorSettings.UpdateSpring(ref bgColorSpring, currentBgColor.ToVector4()); bg.color = newColorV.ToColor(); } } /// /// Jump to the next tutorial step /// public void Next() { InternalNext(false); } /// /// Jump to the next tutorial step /// public void ForceNext() { InternalNext(true); } private void InternalNext(bool force = false) { if (debug) Debug.Log("Next force:" + force + " autoNext:" + autoNext + " delay:" + delay); if (!autoNext && !IsTutorialActive) { ShowTutorial(); } if (!autoNext && !force) { HideTutorial(); return; } else if (delay > 0 && !force) { HideTutorial(); nextTime = Time.time; tutorialState = State.Delaying; popup.Hide(); return; } if (debug) print("Next Tutorial Step currentStep:" + currentStep + " steps.Count:" + steps.Count); currentStep++; if (steps == null || currentStep >= steps.Count) { HideTutorial(); return; } TutorialSession.TutorialStep s = steps[currentStep]; popup.SetStep(s); currentTarget = null; if (s.target_obj != "") currentTarget = GameObject.Find(s.target_obj); bool differentObject = currentTarget != lastFocusedObject; if (currentTarget != null) { lastFocusedObject = currentTarget; lastFocusedObjectParent = currentTarget.transform.parent; lastFocusedObjectSibling = currentTarget.transform.GetSiblingIndex(); Rect rect2D = GetObjectCanvasRect(currentTarget); float size = Mathf.Max(rect2D.width, rect2D.height); Vector3 dir = new Vector3(rect2D.position.x, rect2D.position.y, 0) - mask.anchoredPosition3D; Debug.DrawRay(mask.position, dir, Color.yellow, 10); mask.rotation = Quaternion.LookRotation(mask.forward, dir); currentEndPositionValue = rect2D.position; currentEndPositionChangeTime = differentObject ? Time.time : Time.time - size1Duration; currentEndSizeValue = size; RectTransform rt = currentTarget.GetComponent(); Button btn = currentTarget.GetComponent