MayHeCome/Assets/Exoa/TutorialEngine/Scripts/Controllers/TutorialController.cs
2024-12-18 17:55:34 +08:00

484 lines
17 KiB
C#

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<TutorialSession.TutorialStep> 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;
/// <summary>
/// Option to display the close button or not, to skip the tutorial
/// </summary>
public static bool IsSkippable { get { return isSkippable; } set { isSkippable = value; } }
/// <summary>
/// Getter to check if the tutorial is still playing
/// </summary>
public static bool IsTutorialActive { get { return tutorialState == State.Playing; } }
/// <summary>
/// Getter to check if the tutorial is still playing
/// </summary>
public static bool IsTutorialComplete { get { return tutorialState == State.Completed; } }
/// <summary>
/// Automatically go to the next step after a step is done
/// If false, the tutorial will hide until you call NextStep()
/// </summary>
public static bool AutoNext { get => autoNext; set => autoNext = value; }
/// <summary>
/// Add a delay between steps
/// </summary>
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<TutorialSession.TutorialStep>();
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();
}
}
/// <summary>
/// Jump to the next tutorial step
/// </summary>
public void Next()
{
InternalNext(false);
}
/// <summary>
/// Jump to the next tutorial step
/// </summary>
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<RectTransform>();
Button btn = currentTarget.GetComponent<Button>();
IPointerClickHandler IPCH = currentTarget.GetComponent<IPointerClickHandler>();
hiddenBtn.onClick.RemoveAllListeners();
if (s.isClickable && rt != null && btn != null)
{
hiddenBtn.gameObject.SetActive(true);
if (btn != null)
{
hiddenBtn.onClick.AddListener(btn.onClick.Invoke);
}
if (IPCH != null)
{
currentTarget.gameObject.AddComponent<ClickInterceptor>().OnClicked.AddListener(() =>
{
Destroy(currentTarget.gameObject.AddComponent<ClickInterceptor>());
});
hiddenBtn.onClick.AddListener(currentTarget.gameObject.AddComponent<ClickInterceptor>().OnClicked.Invoke);
}
}
else
{
hiddenBtn.gameObject.SetActive(false);
}
if (s.isReplacingNextButton && ((rt != null && btn != null )||IPCH != null))
{
popup.nextBtn.gameObject.SetActive(false);
hiddenBtn.onClick.AddListener(popup.nextBtn.onClick.Invoke);
}
else
{
popup.nextBtn.gameObject.SetActive(true);
}
TutorialEvents.OnTutorialFocus?.Invoke(s.target_obj, rect2D.center);
TutorialEvents.OnTutorialProgress?.Invoke(currentStep, steps.Count);
}
else if (!retried && !string.IsNullOrEmpty(s.target_obj))
{
retried = true;
if (debug) Debug.Log("RETRYING CANNOT FIND " + s.target_obj);
// Retry in .2f seconds
currentStep--;
Invoke("Next", .2f);
}
else
{
if (!string.IsNullOrEmpty(s.target_obj) && debug)
Debug.Log("CANNOT FIND " + s.target_obj);
if (!string.IsNullOrEmpty(s.sendMessage) && debug)
{
Debug.Log("Calling " + s.sendMessage + " on popup");
popup.SendMessage(s.sendMessage, SendMessageOptions.DontRequireReceiver);
}
currentEndPositionValue = Vector2.zero;
currentEndPositionChangeTime = Time.time;
popup.nextBtn.gameObject.SetActive(true);
hiddenBtn.gameObject.SetActive(false);
mask.sizeDelta = (Vector2.zero);
mask.anchoredPosition = (Vector2.zero);
TutorialEvents.OnTutorialProgress?.Invoke(currentStep, steps.Count);
}
currentBgColor = currentStep == 0 ? initBGColor : normalBGColor;
}
private void ShowTutorial()
{
popup.RemoveListeners();
popup.OnClickNext.AddListener(OnClickNext);
popup.closeBtn.gameObject.SetActive(IsSkippable);
popup.gameObject.SetActive(true);
popup.createBackground = false;
#if TUTORIAL_ENGINE_LOCALIZATION
popup.tableName = tableName;
#endif
popup.Init();
popup.Center();
popup.Open();
bg.gameObject.SetActive(true);
mask.gameObject.SetActive(true);
mask.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 200);
mask.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 200);
mask.anchoredPosition = new Vector2(0, 1000);
hiddenBtn.gameObject.SetActive(false);
tutorialState = State.Playing;
}
/// <summary>
/// Close the tutorial
/// </summary>
public void HideTutorial()
{
tutorialState = State.FadingOut;
popup.RemoveListeners();
hiddenBtn.onClick.RemoveAllListeners();
hiddenBtn.gameObject.SetActive(false);
}
/**
* Get Object's bounds in the Canvas space
**/
private Rect GetObjectCanvasRect(GameObject obj)
{
RectTransform objRect = obj.GetComponent<RectTransform>();
Renderer objRenderer = obj.GetComponent<Renderer>();
Collider objCollider = obj.GetComponentInChildren<Collider>();
Rect newRect = new Rect();
if (objRect != null)
{
newRect = objRect.GetRectFromOtherParent(mask.parent as RectTransform);
}
else if (objRenderer != null)
{
newRect = objRenderer.GetScreenRect(canvasRenderCamera);
newRect = (mask.parent as RectTransform).ScreenRectToRectTransform(newRect,
canvasRenderMode == RenderMode.ScreenSpaceOverlay ? null : canvasRenderCamera);
}
else if (objCollider != null)
{
newRect = objCollider.GetScreenRect(canvasRenderCamera);
newRect = (mask.parent as RectTransform).ScreenRectToRectTransform(newRect,
canvasRenderMode == RenderMode.ScreenSpaceOverlay ? null : canvasRenderCamera);
}
return newRect;
}
private Camera GetCamera()
{
Camera cam = Camera.main;
// first find the canvas
Canvas canvas = GetComponentInParent<Canvas>();
if (canvas == null)
{
canvas = transform.root.GetComponent<Canvas>();
}
if (canvas != null)
{
canvasRenderMode = canvas.renderMode;
if (canvas.worldCamera != null)
cam = canvas.worldCamera;
}
return cam;
}
}
public class ClickInterceptor : MonoBehaviour, IPointerClickHandler
{
// 定义点击事件
public UnityEvent OnClicked;
// 实现点击处理方法
public void OnPointerClick(PointerEventData eventData)
{
// 触发点击事件
OnClicked?.Invoke();
}
}
}