lcy
2025-11-06 09bc892c7283df8757a07b646d5af21ddaa263d1
Main/System/UIBase/UIBase.cs
@@ -9,9 +9,9 @@
public enum UILayer
{
    Static, // 静态UI 适合做 战斗 主界面
    Bottom, // 主界面
    Mid,    // 功能窗口
    System,  // 网络弹窗/其他重要弹窗
    Bottom, // 部分界面特殊处理层级用
    Mid,    // 大部分功能窗口都放这层,便于跳转上下层管理(一个界面可以同时存在多个)
    System,  // 网络弹窗,信息提示等,其他重要弹窗
    Loading,    // 加载界面
}
@@ -24,7 +24,9 @@
    SlideFromTop, // 从顶部滑入
    SlideFromBottom, // 从底部滑入
    SlideFromLeft, // 从左侧滑入
    SlideFromRight // 从右侧滑入
    SlideFromRight, // 从右侧滑入
    ScaleOverInOut,// 缩放根据曲线
}
[RequireComponent(typeof(Canvas))]
@@ -37,39 +39,58 @@
    // UI基本属性
    [SerializeField] public UILayer uiLayer = UILayer.Mid;
    [SerializeField][HideInInspector] public string uiName;
    [SerializeField] public bool isMainUI = false;
    [SerializeField] public bool supportParentChildRelation = true; // 新增:是否支持父子关系
    [SerializeField] public bool isMainUI = false;   //同时勾选supportParentChildRelation 会当作新的父节点,但不作为子节点
    // 是否支持父子关系,即UI拥有的上下级链式关系
    // 拥有父子关系 在关闭父界面的时候 子界面会连同一起关闭 子界面打开时
    // 在非特定情况下 都要拥有父子关系 (一般来说功能都要有父子关系 例外的是例如系统弹窗
    // 附加说明:功能基本做在Middle层
    [SerializeField] public bool supportParentChildRelation = true;
    // 持久化相关
    [SerializeField] public bool isPersistent = false;
    [SerializeField][HideInInspector] public int maxIdleRounds = 20;
    // 动画相关
    [SerializeField] public UIAnimationType openAnimationType = UIAnimationType.None;
    [SerializeField] public UIAnimationType closeAnimationType = UIAnimationType.None;
    [SerializeField]/*[HideInInspector]*/ public float animeDuration = 0.2f;
    [SerializeField]public TweenCurve scaleOverInOutCurve;
    [SerializeField][HideInInspector] public Ease animationEase = Ease.OutQuad; // 确保使用 DG.Tweening.Ease
    // 运行时状态
    [HideInInspector] public int lastUsedRound = 0;
    [HideInInspector] public UIBase parentUI;
    [HideInInspector] public GameObject rootNode; // 根节点
    // 子UI管理
    [HideInInspector] public List<UIBase> childrenUI = new List<UIBase>();
    [Header("所有UI排版应该在此节点内层")]
    [SerializeField] protected RectTransform _rectTransform; //界面默认添加根节点用于表现界面开启关闭动画,或者设置适配用
    //遮罩组件在界面开发中生成,遮罩开关和点击空白为其中的组件特性
    //  打开遮罩
    [Header("遮罩(透明)开关")]
    [SerializeField] public bool openMask = false;
    //  点击空白区域关闭界面
    //  默认点击空白区域关闭界面
    [Header("点击空白关闭")]
    [SerializeField] public bool clickEmptySpaceClose = false;
    private GameObject screenMask = null;
    public GameObject screenMask = null;
    private Button btnClickEmptyClose;
    public Action btnClickEmptyCloseEvent = null;   //提供点击空白区域关闭界面的回调
    protected int functionOrder = 0;
    //  跟OneLevelWin联动 实际上是需要继承自OneLevelWin才能生效的值 使用需要注意
    int m_FunctionOrder = 0;
    public int functionOrder
    {
        get { return m_FunctionOrder; }
        set { m_FunctionOrder = value; }
    }
    // 内部状态
    protected bool isActive = false;
@@ -79,70 +100,59 @@
    // 组件引用
    protected Canvas canvas;
    protected CanvasGroup canvasGroup;
    protected RectTransform _rectTransform; //界面默认添加根节点用于表现界面开启关闭动画
    // 动画相关
    protected Vector3 originalPosition;
    protected Sequence currentAnimation;
    private CanvasScaler canvasScaler;
    public CanvasScaler canvasScaler
    {
        get;
        private set;
    }
    public const int SafeHeightUp = 50;
    public const int SafeHeightDown = 30;
    #endregion
    #region Unity生命周期
    protected virtual void Awake()
    {
        CreateRootNode();
        //  防止有人不写base.InitComponent引发错误 所以拆分
        InitComponentInternal();
        // 在Awake中进行基本初始化
        InitComponent();
        try
        {
            InitComponentInternal();
        }
        catch (Exception e)
        {
            Debug.LogError($"{uiName}界面的InitComponentInternal报错: {e.StackTrace}");
        }
        try
        {
            InitComponent();
        }
        catch (Exception e)
        {
            Debug.LogError($"{uiName}界面的InitComponent报错: {e.StackTrace}");
        }
        // 保存原始值用于动画
        if (_rectTransform != null)
        {
            if (Screen.height / Screen.width > 1.8)//宽屏需要适配
            {
                //上下各间隔SafeWidth
                _rectTransform.offsetMax = new Vector2(0, -SafeHeightUp);  //上
                _rectTransform.offsetMin = new Vector2(0, SafeHeightDown);   //下
            }
            originalPosition = _rectTransform.anchoredPosition;
        }
        ApplySettings();
        if (openMask)
        {
            screenMask = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/ScreenMask"), transform);
            screenMask.transform.localScale = Vector3.one;
            screenMask.transform.localPosition = Vector3.zero;
        if (screenMask != null)
            screenMask.transform.SetAsFirstSibling();
        }
    }
        InitClickEmptySpaceBtn();
    private void CreateRootNode()
    {
        if (openAnimationType == UIAnimationType.None && closeAnimationType == UIAnimationType.None)
            return;
        List<Transform> children = new List<Transform>();
        foreach (Transform child in transform)
        {
            children.Add(child);
        }
        rootNode = new GameObject("WindowRoot");
        rootNode.transform.SetParent(transform, false);
        rootNode.layer = LayerMask.NameToLayer("UI");
        _rectTransform = rootNode.AddMissingComponent<RectTransform>();
        //设置成拉伸效果,和父容器保持同样大小自动适配
        _rectTransform.anchorMin = Vector2.zero;
        _rectTransform.anchorMax = Vector2.one;
        _rectTransform.pivot = new Vector2(0.5f, 0.5f);
        _rectTransform.anchoredPosition = Vector2.zero;
        _rectTransform.sizeDelta = Vector2.zero; // 设置为0,表示拉伸到父容器大小
        foreach (Transform child in children)
        {
            child.SetParent(rootNode.transform, false);
        }
    }
    protected virtual void Start()
@@ -150,20 +160,38 @@
        // 子类可以重写此方法进行额外初始化
    }
    protected async UniTask ApplySettings()
    {
    protected async UniTask ApplyClickEmptySpaceClose()
    {
        if (clickEmptySpaceClose)
        {
            //延迟创建会导致层级在ScreenMask之上
            GameObject goBtnESC = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/ClickEmptyCloseMask"), transform);
            btnClickEmptyClose = goBtnESC.GetComponent<Button>();
            btnClickEmptyClose.AddListener(CloseWindow);
            btnClickEmptyClose.transform.SetAsFirstSibling();
            await UniTask.DelayFrame(5);
            btnClickEmptyClose = goBtnESC.GetComponent<Button>();
            btnClickEmptyClose.AddListener(CloseWindow);
            //延迟x帧后可点击,防止点击过快立即关闭了
            await UniTask.Delay(200);
            btnClickEmptyClose.enabled = true;
        }
    }
    private void InitClickEmptySpaceBtn()
    {
        if (!clickEmptySpaceClose)
        {
            return;
        }
        btnClickEmptyClose = screenMask.GetComponent<Button>();
        btnClickEmptyClose.AddListener(() =>
        {
            if (btnClickEmptyCloseEvent != null)
            {
                btnClickEmptyCloseEvent();
            }
            else
            {
                CloseWindow();
            }
        });
        btnClickEmptyClose.enabled = false;
    }
@@ -198,8 +226,10 @@
        // 设置Canvas属性
        canvas.overrideSorting = true;
        canvas.worldCamera = CameraManager.uiCamera;
        canvas.pixelPerfect = false;
        canvas.sortingLayerID = SortingLayer.NameToID("UI"); // 确保使用正确的排序层
        // 获取或添加CanvasGroup组件
        canvasGroup = GetComponent<CanvasGroup>();
@@ -215,6 +245,9 @@
        }
        canvasScaler = GetComponent<CanvasScaler>();
        canvasScaler.referenceResolution = Constants.DESIGN_RESOLUTION;
        canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        canvasScaler.matchWidthOrHeight = 0;
    }
    // 获取必要的组件
@@ -245,20 +278,66 @@
    // 打开UI
    public void HandleOpen()
    {
        OnPreOpen();
        // 如果正在播放动画,先停止
        if (_rectTransform == null)
        {
            Debug.LogError($"界面: {uiName} 需要设置根节点_rectTransform ");
            return;
        }
        if ((clickEmptySpaceClose || openMask) && screenMask == null)
        {
            Debug.LogError($"界面: {uiName} 缺少遮罩 ");
            return;
        }
        try
        {
            OnPreOpen();
        }
        catch (Exception e)
        {
            Debug.LogError($"{uiName}界面的OnPreOpen报错: {e.StackTrace}");
        }
        StopCurrentAnimation();
        // 重置关闭标记
        isClosing = false;
        gameObject.SetActive(true);
        isActive = true;
        // 根据动画类型播放打开动画
        PlayOpenAnimation();
        OnOpen();
        // // 如果后续需要统一处理刘海或者小游戏的界面适配问题
        // _rectTransform.offsetMin = new Vector2(0, 10);  //下方
        // _rectTransform.offsetMax = new Vector2(0, -50); //上方
        try
        {
            OnOpen();
        }
        catch (Exception e)
        {
            Debug.LogError($"{uiName}界面的OnOpen报错: {e.StackTrace}");
        }
        ApplyClickEmptySpaceClose();
        ExecuteNextFrame(() =>
        {
            try
            {
                NextFrameAfterOpen();
            }
            catch (Exception e)
            {
                Debug.LogError($"{uiName}界面的NextFrameAfterOpen报错: {e.StackTrace}");
            }
        });
    }
    protected virtual void NextFrameAfterOpen()
    {
    }
    // 关闭UI - 修改后的方法
@@ -266,33 +345,50 @@
    {
        // 如果已经在关闭过程中,直接返回
        if (isClosing) return;
        OnPreClose();
        // 如果正在播放动画,先停止
        if (clickEmptySpaceClose)
            btnClickEmptyClose.enabled = false;
        try
        {
            OnPreClose();
        }
        catch (Exception e)
        {
            Debug.LogError($"{uiName}界面的OnPreClose报错: {e.StackTrace}");
        }
        StopCurrentAnimation();
        // 设置关闭标记
        isClosing = true;
        isActive = false;
        // 禁用交互但保持可见
        if (canvasGroup != null)
        {
            canvasGroup.interactable = false;
            canvasGroup.blocksRaycasts = false;
        }
        // 根据动画类型播放关闭动画
        PlayCloseAnimation();
        // 调用关闭回调
        OnClose();
        // 如果没有关闭动画,直接禁用游戏对象
        try
        {
            OnClose();
        }
        catch (Exception e)
        {
            Debug.LogError($"{uiName}界面的OnClose报错: {e.StackTrace}");
        }
        if (closeAnimationType == UIAnimationType.None)
        {
            CompleteClose();
            try
            {
                CompleteClose();
            }
            catch (Exception e)
            {
                Debug.LogError($"{uiName}界面的CompleteClose报错: {e.StackTrace}");
            }
        }
        // 否则在动画完成后禁用游戏对象(在PlayCloseAnimation中处理)
    }
@@ -307,6 +403,12 @@
    public virtual void CloseWindow()
    {
        UIManager.Instance.CloseWindow(this, false);
    }
    public async UniTask DelayCloseWindow(int delayTime = 30)
    {
        await UniTask.Delay(delayTime);
        CloseWindow();
    }
    // 刷新UI
@@ -338,148 +440,27 @@
    /// <summary>
    /// 播放UI特效
    /// </summary>
    /// <param name="effectName">特效资源名称</param>
    /// <param name="id">特效资源名称</param>
    /// <param name="parent">特效父节点,默认为当前UI</param>
    /// <param name="autoDestroy">是否自动销毁,默认为true</param>
    /// <param name="destroyDelay">自动销毁延迟时间,默认为5秒</param>
    /// <returns>特效游戏对象</returns>
    public GameObject PlayUIEffect(int id, Transform parent = null, bool autoDestroy = true, float destroyDelay = 5f)
    public UIEffectPlayer PlayUIEffect(int id, Transform parent = null)
    {
        // 使用默认值
        if (parent == null) parent = transform;
        EffectConfig effectCfg = EffectConfig.Get(id);
        if (null == effectCfg)
        return UIEffectPlayer.CreateEffect(id, parent, false);
    }
    public int GetSortingOrder()
    {
        if (null != canvas)
        {
            return null;
            return canvas.sortingOrder;
        }
        // 加载特效资源
        var effectPrefab = ResManager.Instance.LoadAsset<GameObject>("UIEffect/" + effectCfg.packageName, effectCfg.fxName);
        if (effectPrefab == null)
        {
            Debug.LogError($"加载UI特效失败: {effectCfg.packageName}");
            return null;
        }
        // 实例化特效
        GameObject effectObj = Instantiate(effectPrefab, parent);
        effectObj.name = $"Effect_{effectCfg.packageName}";
        // 添加特效穿透阻挡器
        EffectPenetrationBlocker blocker = effectObj.AddComponent<EffectPenetrationBlocker>();
        blocker.parentCanvas = canvas;
        //  延迟一帧才生效
        this.DelayFrame(blocker.UpdateSortingOrder);
        // blocker.UpdateSortingOrder();
        // 自动销毁
        if (autoDestroy)
        {
            Destroy(effectObj, destroyDelay);
        }
        return effectObj;
        return 0;
    }
    
    /// <summary>
    /// 在两个UI元素之间播放特效(按照sortingOrder的中间值)
    /// </summary>
    /// <param name="effectName">特效资源名称</param>
    /// <param name="frontElement">前景UI元素(Image或RawImage)</param>
    /// <param name="backElement">背景UI元素(Image或RawImage)</param>
    /// <param name="autoDestroy">是否自动销毁,默认为true</param>
    /// <param name="destroyDelay">自动销毁延迟时间,默认为5秒</param>
    /// <returns>特效游戏对象</returns>
    public async UniTask<GameObject> PlayEffectBetweenUIElements(string effectName, Graphic frontElement, Graphic backElement, bool autoDestroy = true, float destroyDelay = 5f)
    {
        if (frontElement == null || backElement == null)
        {
            Debug.LogError("前景或背景UI元素为空");
            return null;
        }
        // 确保UI元素在当前UIBase的Canvas下
        if (frontElement.canvas != canvas || backElement.canvas != canvas)
        {
            Debug.LogError("UI元素不在当前UIBase的Canvas下");
            return null;
        }
        // 加载特效资源
        GameObject effectPrefab = ResManager.Instance.LoadAsset<GameObject>("UIEffect", effectName);
        if (effectPrefab == null)
        {
            Debug.LogError($"加载UI特效失败: {effectName}");
            return null;
        }
        // 创建一个新的GameObject作为特效容器
        GameObject container = new GameObject($"EffectContainer_{effectName}");
        container.transform.SetParent(transform, false);
        // 设置容器位置
        RectTransform containerRect = container.AddComponent<RectTransform>();
        containerRect.anchorMin = new Vector2(0.5f, 0.5f);
        containerRect.anchorMax = new Vector2(0.5f, 0.5f);
        containerRect.pivot = new Vector2(0.5f, 0.5f);
        containerRect.anchoredPosition = Vector2.zero;
        containerRect.sizeDelta = new Vector2(100, 100); // 默认大小,可以根据需要调整
        // 获取前景和背景元素的siblingIndex
        int frontIndex = frontElement.transform.GetSiblingIndex();
        int backIndex = backElement.transform.GetSiblingIndex();
        // 设置特效容器的siblingIndex在两者之间
        if (frontIndex > backIndex)
        {
            // 前景在背景之后,特效应该在中间
            container.transform.SetSiblingIndex((frontIndex + backIndex) / 2 + 1);
        }
        else
        {
            // 背景在前景之后,特效应该在中间
            container.transform.SetSiblingIndex((frontIndex + backIndex) / 2);
        }
        // 实例化特效
        GameObject effectObj = Instantiate(effectPrefab, container.transform);
        effectObj.name = $"Effect_{effectName}";
        // 添加特效穿透阻挡器
        EffectPenetrationBlocker blocker = effectObj.AddComponent<EffectPenetrationBlocker>();
        // 直接设置特效渲染器的排序顺序
        Renderer[] renderers = effectObj.GetComponentsInChildren<Renderer>(true);
        foreach (Renderer renderer in renderers)
        {
            renderer.sortingOrder = canvas.sortingOrder;
            renderer.sortingLayerName = canvas.sortingLayerName;
        }
        // 设置粒子系统渲染器的排序顺序
        ParticleSystem[] particleSystems = effectObj.GetComponentsInChildren<ParticleSystem>(true);
        foreach (ParticleSystem ps in particleSystems)
        {
            ParticleSystemRenderer psRenderer = ps.GetComponent<ParticleSystemRenderer>();
            if (psRenderer != null)
            {
                psRenderer.sortingOrder = canvas.sortingOrder;
                psRenderer.sortingLayerName = canvas.sortingLayerName;
            }
        }
        // 自动销毁
        if (autoDestroy)
        {
            Destroy(container, destroyDelay);
        }
        return effectObj;
    }
    #endregion
    public bool raycastTarget
@@ -509,7 +490,8 @@
    // 播放打开动画
    protected virtual void PlayOpenAnimation()
    {
        //禁用交互会引起点透问题, 后续解决可以考虑EventSystem
        canvasGroup.blocksRaycasts = true;
        if (openAnimationType == UIAnimationType.None)
        {
@@ -528,12 +510,11 @@
                if (canvasGroup != null)
                {
                    canvasGroup.alpha = 0f;
                    canvasGroup.interactable = false;
                    canvasGroup.blocksRaycasts = false;
                    // canvasGroup.blocksRaycasts = false;
                }
                if (canvasScaler != null)
                if (_rectTransform != null)
                {
                    canvasScaler.scaleFactor = 1f;
                    _rectTransform.localScale = Vector3.one;
                }
                break;
@@ -541,12 +522,11 @@
                if (canvasGroup != null)
                {
                    canvasGroup.alpha = 1f;
                    canvasGroup.interactable = false;
                    canvasGroup.blocksRaycasts = false;
                    // canvasGroup.blocksRaycasts = false;
                }
                if (canvasScaler != null)
                if (_rectTransform != null)
                {
                    canvasScaler.scaleFactor = 0.3f;
                    _rectTransform.localScale = Vector3.one * 0.3f;
                }
                break;
@@ -589,6 +569,18 @@
                    _rectTransform.anchoredPosition = startPos;
                }
                break;
            case UIAnimationType.ScaleOverInOut:
                if (canvasGroup != null)
                {
                    canvasGroup.alpha = 1f;
                    // 禁用交互会引起点透问题
                    // canvasGroup.blocksRaycasts = false;
                }
                if (_rectTransform != null)
                {
                    _rectTransform.localScale = Vector3.one * 0.3f;
                }
                break;
        }
        try
@@ -609,7 +601,7 @@
                case UIAnimationType.ScaleInOut:
                    if (_rectTransform != null)
                    {
                        currentAnimation.Append(DOVirtual.Float(0.3f, 1f, animeDuration, (value) => {canvasScaler.scaleFactor = value;}).SetEase(animationEase));
                        currentAnimation.Append(DOVirtual.Float(0.3f, 1f, animeDuration, (value) => {_rectTransform.localScale = Vector3.one * value;}).SetEase(animationEase));
                    }
                    break;
@@ -622,35 +614,53 @@
                        currentAnimation.Append(_rectTransform.DOAnchorPos(originalPosition, animeDuration).SetEase(animationEase));
                    }
                    break;
                case UIAnimationType.ScaleOverInOut:
                    if (_rectTransform != null)
                    {
                        float startScale = scaleOverInOutCurve.curve.Evaluate(0f);
                        _rectTransform.localScale = Vector3.one * startScale;
                        currentAnimation.Append(
                            DOTween.To(
                                () => _rectTransform.localScale.x,
                                (value) => _rectTransform.localScale = Vector3.one * value,
                                1f,
                                animeDuration
                            )
                            .SetEase(scaleOverInOutCurve.curve)
                            .OnComplete(() => _rectTransform.localScale = Vector3.one) // 确保最终值1正确
                        );
                    }
                    break;
            }
            // 动画完成后的回调
            currentAnimation.OnComplete(() =>
            {
                isAnimating = false;
                _ResetToBegin();
                OnOpenAnimationComplete();
                // 启用交互
                if (canvasGroup != null)
                {
                    canvasGroup.interactable = true;
                    canvasGroup.blocksRaycasts = true;
                }
            });
            // currentAnimation.ingoreTimeScale = true;
            currentAnimation.Play();
        }
        catch (System.Exception e)
        {
            Debug.LogError($"播放打开动画时出错: {e.Message}");
            Debug.LogError($"播放打开动画时出错: {e.StackTrace}");
            // 出错时确保UI可见并可交互
            if (canvasGroup != null)
            {
                canvasGroup.alpha = 1f;
                canvasGroup.interactable = true;
                canvasGroup.blocksRaycasts = true;
            }
            isAnimating = false;
@@ -667,12 +677,11 @@
        if (canvasGroup != null)
        {
            canvasGroup.alpha = 1f;
            canvasGroup.interactable = true;
            canvasGroup.blocksRaycasts = true;
        }
        if (canvasScaler != null)
        if (_rectTransform != null)
        {
            canvasScaler.scaleFactor = 1f;
            _rectTransform.localScale = Vector3.one;
        }
        if (_rectTransform != null)
@@ -713,9 +722,10 @@
                    break;
                case UIAnimationType.ScaleInOut:
                case UIAnimationType.ScaleOverInOut:
                    if (_rectTransform != null)
                    {
                        currentAnimation.Append(DOVirtual.Float(1f, 0.3f, animeDuration, (value) => {canvasScaler.scaleFactor = value;}).SetEase(animationEase));
                        currentAnimation.Append(DOVirtual.Float(1f, 0.3f, animeDuration, (value) => {_rectTransform.localScale = Vector3.one * value;}).SetEase(animationEase));
                    }
                    break;
@@ -760,11 +770,16 @@
            currentAnimation.OnComplete(() =>
            {
                isAnimating = false;
                // 动画完成后,完成关闭过程
                if (isClosing)
                {
                    CompleteClose();
                    try
                    {
                        CompleteClose();
                    }
                    catch (Exception e)
                    {
                        Debug.LogError($"{uiName}界面的CompleteClose报错: {e.StackTrace}");
                    }
                }
            });
@@ -772,7 +787,7 @@
        }
        catch (System.Exception e)
        {
            Debug.LogError($"播放关闭动画时出错: {e.Message}");
            Debug.LogError($"播放关闭动画时出错: {e.StackTrace}");
            
            // 出错时直接完成关闭
            isAnimating = false;