zhouqijie

软指引示例


using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

using LitJson;
using DG.Tweening;


public enum GuideStepType
{
    Simple, //GetChild (i) at eleList[i].
    Slide, //GetChild (0) slide between first and last eleList.
}

[System.Serializable]
public class GuideStep
{
    public GuideStepType type;

    public string focus;

    public string guideText;

    public bool tapToClose;

    public Transform[] elements;

    public Vector2Int[] elementReplaces;
}


public class StandaloneGuidesDataSerializer
{
    public const string prefName = "GuidedInfos";

    private static List<string> guidedInfoList = null;


    public static void GenList()
    {
        var strGuidedList = PlayerPrefs.GetString(prefName, "");
        if (strGuidedList != "")
        {
            guidedInfoList = JsonMapper.ToObject<string[]>(strGuidedList).ToList();
        }
        else
        {
            guidedInfoList = new List<string>();
        }
    }
    public static void AddGuidedInfo(string info)
    {
        if(guidedInfoList == null)
        {
            GenList();
        }

        guidedInfoList.Add(info);
        SaveData();
    }

    public static bool CheckGuidedInfo(string info)
    {
        if (guidedInfoList == null)
        {
            GenList();
        }

        return guidedInfoList.Contains(info);
    }


    //Other
    public static void SaveData()
    {
        if (guidedInfoList == null) return;

        string json = JsonMapper.ToJson(guidedInfoList.ToArray());
        PlayerPrefs.SetString(prefName, json);
    }
    public static void DeleteInfos()
    {
        if (PlayerPrefs.HasKey(prefName))
        {
            PlayerPrefs.DeleteKey(prefName);
        }
    }
}


public class SoftGuide : MonoBehaviour
{
    //serializable
    [Header("引导延迟")]
    public int delaySec;

    [Header("点击判定半径")]
    public float validateRadius = 99999;

    [Header("锁定时间")]
    public float lockTime = 2f;
    
    [Header("最大存在时间")]
    public float maxExistTime = 30f;

    [Header("下一指引")]
    public string nextGuide;

    [Header("引导步骤")]
    public GuideStep[] guideSteps;



    //coms
    private CanvasScaler canvasScaler;

    //status
    private int currentGuideIdx = -1;

    private bool startedGuide = false;      public bool StartedGuide => this.startedGuide;
    private bool guided = false;            public bool Guided => this.guided;




    /// <summary>
    /// Awake 初始化、检查问题
    /// </summary>
    void Awake()
    {
        canvasScaler = this.GetComponentInParent<CanvasScaler>();

        CheckStepLength();
    }


    /// <summary>
    /// Enable时自动开始引导
    /// </summary>
    private void OnEnable()
    {
        if (guided)
        {
            EndGuide();
            return;
        }
        if (IsGuided())
        {
            //Debug.Log("找到了已引导信息");
            EndGuide();
            return;
        }
        else
        {
            //Debug.Log("未找到已引导信息");
        }

        //Default Status
        AutoShowHide();

        //Invoke
        Invoke("Next", delaySec);
    }


    /// <summary>
    /// Update 判断点击事件
    /// </summary>
    void Update()
    {
        //未开始
        if (currentGuideIdx < 0) return;


        //可以点击结束步骤  
        if (!canClick) { return; }
        if(currentGuideIdx > -1 && guideSteps[currentGuideIdx].tapToClose)
        {
            //PC
            if (Input.GetKeyDown(KeyCode.Mouse0) || Input.GetKeyUp(KeyCode.Mouse0))
            {
                if (ValidateScreenPos(Input.mousePosition))
                {
                    Next();
                }
            }
            //Mobile
            if (Input.touchCount == 1 && (Input.touches[0].phase == TouchPhase.Began || Input.touches[0].phase == TouchPhase.Ended))
            {
                if (ValidateScreenPos(Input.touches[0].position))
                    Next();
            }
        }
    }


    /// <summary>
    ///是已经完成的引导    
    /// </summary>
    /// <returns></returns>
    public bool IsGuided()
    {
        if (finishedMarks.Contains(this.gameObject.name)) return true;

        return StandaloneGuidesDataSerializer.CheckGuidedInfo(this.gameObject.name);
    }




    /// <summary>
    /// 验证判定位置
    /// </summary>
    /// <param name="clickScreenPos"></param>
    /// <returns></returns>
    private bool ValidateScreenPos(Vector2 clickScreenPos)
    {
        Camera cam = Camera.current;
        if (Camera.current == null) cam = Camera.main;
        if (cam == null) return true;

        //radius
        float trueRadius = this.guideSteps[currentGuideIdx].type == GuideStepType.Slide ? 99999f : validateRadius;


        //position
        Vector2 fingerCanvasPos = new Vector2();
        Vector2 fingerScreenPos = new Vector2();
        switch (this.guideSteps[currentGuideIdx].type)
        {
            case GuideStepType.Simple:
                fingerScreenPos = cam.WorldToScreenPoint(this.transform.GetChild(currentGuideIdx).GetChild(0).position);
                fingerCanvasPos = UtilsScreenAndUI.ScreenPointToCanvasPos_MatchWidth(fingerScreenPos ,    this.canvasScaler ); //第一个图像位置
                break;
            default:
                break;
        }

        Vector2 clickCanvasPos = UtilsScreenAndUI.ScreenPointToCanvasPos_MatchWidth(clickScreenPos, this.canvasScaler); //第一个图像位置

        

        //validate (only simple)
        if(this.canvasScaler.GetComponent<Canvas>().renderMode == RenderMode.WorldSpace)
        {
            if ((fingerCanvasPos - clickCanvasPos).sqrMagnitude < (trueRadius * trueRadius))
                return true;
            else
                return false;
        }
        else
        {
            if ((fingerCanvasPos - clickCanvasPos).sqrMagnitude < (trueRadius * trueRadius))
                return true;
            else
                return false;
        }
        
        return true;
    }
    



    /// <summary>
    /// 检查引导数是否匹配
    /// </summary>
    private void CheckStepLength()
    {
        if (guideSteps.Length != this.transform.childCount)
            throw new UnityException("引导不匹配");
    }
    


    /// <summary>
    /// 自动隐藏其他步骤的引导
    /// </summary>
    private void AutoShowHide()
    {
        for (int i = 0; i < this.transform.childCount; i++)
        {
            if (currentGuideIdx == i)
                this.transform.GetChild(i).gameObject.SetActive(true);
            else
                this.transform.GetChild(i).gameObject.SetActive(false);
        }
    }


    /// <summary>
    /// 下一个引导
    /// </summary>
    private void Next()
    {
        if (currentGuideIdx >= guideSteps.Length) return;
        if (guided) return;



        //!! 引导互斥 -- Only Exist One
        foreach(var guide in this.transform.parent.GetComponentsInChildren<SoftGuide>(true))
        {
            if (guide != this && guide.startedGuide) // is guiding ???
            {
                guide.EndGuide();
            }
        }


        //Reset TImer
        ResetLock();

        //Start(First)
        if (!startedGuide)
            startedGuide = true;
        //NExt
        currentGuideIdx += 1;

        //Auto ShowHide
        AutoShowHide();

        //Show Next Guide
        if (currentGuideIdx < guideSteps.Length)
        {
            Debug.LogAssertion("Exist Next!!");

            switch (guideSteps[currentGuideIdx].type)
            {
                case GuideStepType.Simple:
                    {
                        for (int i = 0; i < this.transform.GetChild(currentGuideIdx).childCount; i++)
                        {
                            if (i < this.guideSteps[currentGuideIdx].elements.Length)
                            {
                                var pointer = this.transform.GetChild(currentGuideIdx).GetChild(i).GetComponent<RectTransform>();
                                var target = JudgeElementAnchorPos(guideSteps[currentGuideIdx], i, canvasScaler);
                                pointer.anchoredPosition = target;
                            }
                            else
                            {
                                //停留原位置
                            }
                        }
                    }
                    break;
                case GuideStepType.Slide:
                    {
                        for (int i = 0; i < this.transform.GetChild(currentGuideIdx).childCount; i++)
                        {
                            if(i == 0) //第一个为滑动元素
                            {
                                var slider = this.transform.GetChild(currentGuideIdx).GetChild(i).GetComponent<RectTransform>();
                                var target1 = JudgeElementAnchorPos(guideSteps[currentGuideIdx], 0, canvasScaler);
                                var target2 = JudgeElementAnchorPos(guideSteps[currentGuideIdx], guideSteps[currentGuideIdx].elements.Length - 1, canvasScaler);

                                slider.transform.DOComplete();
                                slider.anchoredPosition = target1;
                                slider.DOAnchorPos(target2, 1f).SetLoops(-1);
                            }
                            else//其余为固定元素
                            {
                                if (i < guideSteps[currentGuideIdx].elements.Length - 1) //-1:忽略最后一个(最后一个是滑动元素的末位置)
                                {
                                    var pointer = this.transform.GetChild(currentGuideIdx).GetChild(i).GetComponent<RectTransform>();
                                    var target = JudgeElementAnchorPos(guideSteps[currentGuideIdx], i, canvasScaler);
                                    pointer.anchoredPosition = target;
                                }
                                else
                                {
                                    //停留原来位置
                                }
                            }
                        }
                    }
                    break;
            }


            //auto next
            AutoNext();
        }
        //No Guide
        else
        {
            StandaloneGuidesDataSerializer.AddGuidedInfo(this.gameObject.name);
            finishedMarks.Add(this.gameObject.name);
            guided = true;
            EndGuide();
        }
    }


    /// <summary>
    /// 结束引导
    /// </summary>
    private void EndGuide()
    {
        this.gameObject.SetActive(false);

        if (!string.IsNullOrEmpty(this.nextGuide))
        {
            SoftGuide.EnableGuide(this.nextGuide);
        }
        else
        {
        }
    }

    /// <summary>
    /// 防止连点
    /// </summary>
    private bool canClick = false;
    private void ResetLock()
    {
        canClick = false;
        CancelInvoke("LockEnd");
        Invoke("LockEnd", lockTime);
    }
    private void LockEnd()
    {
        canClick = true;
    }

    //超时自动到下一步(调用Next后触发)
    private void AutoNext()
    {
        CancelInvoke("Next");
        Invoke("Next", maxExistTime);
    }




    // -------- Finish Marks Temp ---------------------  
    private static List<string> finishedMarks = new List<string>();
    // --------(Static)Enable Guide --------------------------------
    public static List<SoftGuide> softguideList = null;
    public static void EnableGuide(string guideName)
    {
        if (finishedMarks.Contains(guideName))
        {
            return;
        }

        if (StandaloneGuidesDataSerializer.CheckGuidedInfo(guideName))
        {
            SoftGuide.finishedMarks.Add(guideName);
            return;
        }

        if(softguideList == null) softguideList = Resources.FindObjectsOfTypeAll<SoftGuide>().Where(s => s.gameObject != null).ToList();


        //find target
        var targetGuide = softguideList.FirstOrDefault(g => g.gameObject.name == guideName);
        if (targetGuide == null) return;

        //enable
        targetGuide.gameObject.SetActive(true);
    }
    public static void FinishGuide(string guideName)
    {
        if (finishedMarks.Contains(guideName))
        {
            return;
        }
        if (StandaloneGuidesDataSerializer.CheckGuidedInfo(guideName))
        {
            SoftGuide.finishedMarks.Add(guideName);
            return;
        }



        if (softguideList == null) softguideList = Resources.FindObjectsOfTypeAll<SoftGuide>().Where(s => s.gameObject != null).ToList();

        //find target
        var targetGuide = softguideList.FirstOrDefault(g => g.gameObject.name == guideName);
        if (targetGuide == null) return;

        //Finish  
        StandaloneGuidesDataSerializer.AddGuidedInfo(targetGuide.gameObject.name);
        targetGuide.guided = true;
        targetGuide.EndGuide();
    }
    public static bool IsFinished(string guideName)
    {
        if (finishedMarks.Contains(guideName)) return true;

        return StandaloneGuidesDataSerializer.CheckGuidedInfo(guideName);
    }


    // --------(Static)(ApplicationCustom) DynamicGetPos ------------
    private static System.Func<Vector2>[] presets = null; 
    public static void HardCodeDynamicGetPosPreset()
    {
        presets = new System.Func<Vector2>[0];

        //presets = new System.Func<Vector2>[2];

        //presets[0] = () =>
        //{
        //    var scaler = FindObjectOfType<UIArenaCanvas>().GetComponent<CanvasScaler>();
        //    var cell = FindObjectsOfType<ArenaGridCell>().OrderByDescending(c => c.State).FirstOrDefault(c => c.team == 0);
        //    return GetAnchorPosition(cell.transform, scaler);
        //};
        //presets[1] = () =>
        //{
        //    var scaler = FindObjectOfType<UIArenaCanvas>().GetComponent<CanvasScaler>();
        //    var cell = FindObjectsOfType<ArenaGridCell>().OrderByDescending(c => c.State).LastOrDefault(c => c.team == 0);
        //    return GetAnchorPosition(cell.transform, scaler);
        //};
    }
    public static Vector2 DynamicGetAnchorPos(int presetIdx)
    {
        if (presets == null) HardCodeDynamicGetPosPreset();


        if (presets.Length > presetIdx)
            return presets[presetIdx]();
        else
            return new Vector2();
    }



    // ----------------- Utils -----------------------
    private static Vector2 JudgeElementAnchorPos(GuideStep step, int idx, CanvasScaler scaler)
    {
        if(idx > -1 && idx < step.elements.Length)
        {
            Transform element = step.elements[idx];
            Vector2Int replace = step.elementReplaces.FirstOrDefault(r => r.x == idx);

            if (element != null)
                return GetAnchorPosition(step.elements[idx].transform, scaler);
            if (replace != null)
                return DynamicGetAnchorPos(replace.y);
            else
                return new Vector2();
        }

        return default;
    }

    private static Vector2 GetAnchorPosition(Transform trans, CanvasScaler canvasScaler)
    {
        if (trans is RectTransform)
        {
            var screenPos = UtilsScreenAndUI.WorldToScreenPoint(trans.position);
            return UtilsScreenAndUI.ScreenPointToCanvasPos_MatchWidth(screenPos, canvasScaler);
        }
        else
        {
            return UtilsScreenAndUI.ScreenPointToCanvasPos_MatchWidth(UtilsScreenAndUI.WorldToScreenPoint(trans.position), canvasScaler);
        }
    }






#if UNITY_EDITOR
    
#endif

}



#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(SoftGuide))]
public class CustomSoftGuide : UnityEditor.Editor
{
    private static GUIStyle commentStyle = null;

    public override void OnInspectorGUI()
    {
        if(commentStyle == null)
        {
            commentStyle = new GUIStyle();
            commentStyle.normal.textColor = new Color(0, 0.5f, 0, 0.8f);
        }

        GUILayout.Label("新手引导系统"
            + "\n//软引导"
            + "\n//Elements:引导元素,可以是场景内的Transform也可以是UI上的RecttTransform"
            + "\n//Replace:引导元素不确定,改为通过硬编码的函数动态获取"
            + "\n//Type: 类型。可以是固定和滑动的。"
            + "\n//激活方式1:自动Enable"
            + "\n//激活方式2:其他物体挂Enabler脚本"
            + "\n//激活方式3:调用SoftGuide.EnableGuide(string guideName)静态方法"
            + "\n//激活方式4:下一指引NextGuide字段填写"
            , commentStyle);

        base.OnInspectorGUI();
    }
}

#endif