using System.Collections;
|
using System.Collections.Generic;
|
using UnityEngine;
|
using DG.Tweening;
|
using UnityEngine.UI; // DOTween 插件引用
|
using Cysharp.Threading.Tasks;
|
using System;
|
|
public enum UILayer
|
{
|
Static, // 静态UI 适合做 常驻用的如 主界面
|
Bottom, // 最底层 适合做 主界面上面的 一级窗口 一级界面
|
Mid, // 适合做 二级窗口 二级界面
|
Top, // 适合做提示弹窗
|
System // 网络弹窗/其他重要弹窗/系统公告
|
}
|
|
public enum UIAnimationType
|
{
|
None, // 无动画
|
FadeInOut, // 淡入淡出
|
ScaleInOut, // 缩放
|
SlideFromTop, // 从顶部滑入
|
SlideFromBottom, // 从底部滑入
|
SlideFromLeft, // 从左侧滑入
|
SlideFromRight // 从右侧滑入
|
}
|
|
[RequireComponent(typeof(Canvas))]
|
[RequireComponent(typeof(CanvasGroup))]
|
[RequireComponent(typeof(CanvasScaler))]
|
public class UIBase : MonoBehaviour
|
{
|
#region 属性和变量
|
|
// 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 isPersistent = false;
|
[SerializeField][HideInInspector] public int maxIdleRounds = 20;
|
|
// 动画相关
|
[SerializeField] public UIAnimationType openAnimationType = UIAnimationType.None;
|
[SerializeField] public UIAnimationType closeAnimationType = UIAnimationType.None;
|
[SerializeField][HideInInspector] public float animationDuration = 0.3f;
|
[SerializeField][HideInInspector] public Ease animationEase = Ease.OutQuad; // 确保使用 DG.Tweening.Ease
|
|
// 运行时状态
|
[HideInInspector] public int lastUsedRound = 0;
|
[HideInInspector] public UIBase parentUI;
|
|
// 子UI管理
|
[HideInInspector] public List<UIBase> childrenUI = new List<UIBase>();
|
|
// 打开遮罩
|
[SerializeField] public bool openMask = false;
|
|
// 点击空白区域关闭界面
|
[SerializeField] public bool clickEmptySpaceClose = false;
|
|
private GameObject screenMask = null;
|
|
private Button btnClickEmptyClose;
|
|
protected int functionOrder = 0;
|
|
// 内部状态
|
protected bool isActive = false;
|
protected bool isAnimating = false;
|
protected bool isClosing = false; // 新增:标记是否正在关闭
|
|
// 组件引用
|
protected Canvas canvas;
|
protected CanvasGroup canvasGroup;
|
protected RectTransform rectTransform;
|
|
// 动画相关
|
protected Vector3 originalScale;
|
protected Vector3 originalPosition;
|
protected Sequence currentAnimation;
|
|
#endregion
|
|
#region Unity生命周期
|
|
protected virtual void Awake()
|
{
|
// 确保 DOTween 已初始化
|
DOTween.SetTweensCapacity(500, 50);
|
|
// 在Awake中进行基本初始化
|
InitComponent();
|
|
// 保存原始值用于动画
|
if (rectTransform != null)
|
{
|
originalScale = rectTransform.localScale;
|
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;
|
screenMask.transform.SetAsFirstSibling();
|
}
|
}
|
|
protected virtual void Start()
|
{
|
// 子类可以重写此方法进行额外初始化
|
}
|
|
protected async UniTask ApplySettings()
|
{
|
await UniTask.DelayFrame(5);
|
|
if (null != transform)
|
{
|
if (clickEmptySpaceClose)
|
{
|
GameObject goBtnESC = GameObject.Instantiate(Resources.Load<GameObject>("Prefabs/ClickEmptyCloseMask"), transform);
|
// Load
|
btnClickEmptyClose = goBtnESC.GetComponent<Button>();
|
btnClickEmptyClose.AddListener(CloseWindow);
|
btnClickEmptyClose.transform.SetAsFirstSibling();
|
}
|
}
|
}
|
|
protected async void ExecuteNextFrame(Action _action)
|
{
|
await UniTask.DelayFrame(1);
|
_action?.Invoke();
|
}
|
|
protected virtual void OnDestroy()
|
{
|
// 确保动画被正确清理
|
if (currentAnimation != null)
|
{
|
currentAnimation.Kill();
|
currentAnimation = null;
|
}
|
}
|
|
#endregion
|
|
#region 初始化方法
|
|
// 获取必要的组件
|
protected virtual void InitComponent()
|
{
|
// 获取或添加Canvas组件
|
canvas = GetComponent<Canvas>();
|
if (canvas == null)
|
{
|
canvas = gameObject.AddComponent<Canvas>();
|
}
|
|
// 设置Canvas属性
|
canvas.overrideSorting = true;
|
|
// 获取或添加CanvasGroup组件
|
canvasGroup = GetComponent<CanvasGroup>();
|
if (canvasGroup == null)
|
{
|
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
}
|
|
// 添加GraphicRaycaster组件(如果没有)
|
if (GetComponent<UnityEngine.UI.GraphicRaycaster>() == null)
|
{
|
gameObject.AddComponent<UnityEngine.UI.GraphicRaycaster>();
|
}
|
|
// 获取RectTransform组件
|
rectTransform = GetComponent<RectTransform>();
|
}
|
|
#endregion
|
|
#region UI操作方法
|
|
// 设置UI层级
|
public void SetSortingOrder(int order)
|
{
|
if (canvas != null)
|
{
|
canvas.sortingOrder = order;
|
}
|
}
|
|
protected virtual void OnPreOpen()
|
{
|
// 子类可以重写此方法进行额外的预打开操作
|
}
|
|
protected virtual void OnPreClose()
|
{
|
// 子类可以重写此方法进行额外的预关闭操作
|
}
|
|
// 打开UI
|
public void HandleOpen()
|
{
|
OnPreOpen();
|
// 如果正在播放动画,先停止
|
StopCurrentAnimation();
|
|
// 重置关闭标记
|
isClosing = false;
|
|
gameObject.SetActive(true);
|
isActive = true;
|
|
// 根据动画类型播放打开动画
|
PlayOpenAnimation();
|
|
OnOpen();
|
}
|
|
// 关闭UI - 修改后的方法
|
public void HandleClose()
|
{
|
// 如果已经在关闭过程中,直接返回
|
if (isClosing) return;
|
|
OnPreClose();
|
|
// 如果正在播放动画,先停止
|
StopCurrentAnimation();
|
|
// 设置关闭标记
|
isClosing = true;
|
isActive = false;
|
|
// 禁用交互但保持可见
|
if (canvasGroup != null)
|
{
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
|
// 根据动画类型播放关闭动画
|
PlayCloseAnimation();
|
|
// 调用关闭回调
|
OnClose();
|
|
// 如果没有关闭动画,直接禁用游戏对象
|
if (closeAnimationType == UIAnimationType.None)
|
{
|
CompleteClose();
|
}
|
// 否则在动画完成后禁用游戏对象(在PlayCloseAnimation中处理)
|
}
|
|
// 完成关闭过程 - 新增方法
|
protected virtual void CompleteClose()
|
{
|
gameObject.SetActive(false);
|
isClosing = false;
|
}
|
|
public virtual void CloseWindow()
|
{
|
UIManager.Instance.CloseWindow(this, false);
|
}
|
|
// 刷新UI
|
public virtual void Refresh()
|
{
|
// 子类可以重写此方法实现UI刷新逻辑
|
}
|
|
// 销毁UI
|
public virtual void Destroy()
|
{
|
// 停止所有动画
|
StopCurrentAnimation();
|
|
ClearChildrenUI();
|
|
if (parentUI != null)
|
{
|
parentUI.RemoveChildUI(this);
|
}
|
|
Destroy(gameObject);
|
}
|
|
#endregion
|
|
#region 特效相关
|
|
/// <summary>
|
/// 播放UI特效
|
/// </summary>
|
/// <param name="effectName">特效资源名称</param>
|
/// <param name="parent">特效父节点,默认为当前UI</param>
|
/// <param name="autoDestroy">是否自动销毁,默认为true</param>
|
/// <param name="destroyDelay">自动销毁延迟时间,默认为5秒</param>
|
/// <returns>特效游戏对象</returns>
|
public async UniTask<GameObject> PlayUIEffect(int id, Transform parent = null, bool autoDestroy = true, float destroyDelay = 5f)
|
{
|
// 使用默认值
|
if (parent == null) parent = transform;
|
|
EffectConfig effectCfg = EffectConfig.Get(id);
|
|
if (null == effectCfg)
|
{
|
return null;
|
}
|
|
// 加载特效资源
|
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;
|
blocker.UpdateSortingOrder();
|
|
// 自动销毁
|
if (autoDestroy)
|
{
|
Destroy(effectObj, destroyDelay);
|
}
|
|
return effectObj;
|
}
|
|
/// <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
|
{
|
get;
|
set;
|
} = true;
|
|
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
|
{
|
return raycastTarget;
|
}
|
|
#region 动画方法
|
|
// 停止当前正在播放的动画
|
protected void StopCurrentAnimation()
|
{
|
if (currentAnimation != null && currentAnimation.IsActive() && !currentAnimation.IsComplete())
|
{
|
currentAnimation.Kill(false); // 不要完成动画,直接停止
|
currentAnimation = null;
|
}
|
isAnimating = false;
|
}
|
|
// 播放打开动画
|
protected virtual void PlayOpenAnimation()
|
{
|
if (openAnimationType == UIAnimationType.None)
|
{
|
// 无动画,直接启用交互
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 1f;
|
canvasGroup.interactable = true;
|
canvasGroup.blocksRaycasts = true;
|
}
|
return;
|
}
|
|
isAnimating = true;
|
|
// 初始化动画前的状态
|
switch (openAnimationType)
|
{
|
case UIAnimationType.FadeInOut:
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 0f;
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
break;
|
|
case UIAnimationType.ScaleInOut:
|
if (rectTransform != null)
|
{
|
rectTransform.localScale = Vector3.zero;
|
}
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 0f;
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
break;
|
|
case UIAnimationType.SlideFromTop:
|
if (rectTransform != null)
|
{
|
Vector2 startPos = originalPosition;
|
startPos.y = Screen.height;
|
rectTransform.anchoredPosition = startPos;
|
}
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 0f;
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
break;
|
|
case UIAnimationType.SlideFromBottom:
|
if (rectTransform != null)
|
{
|
Vector2 startPos = originalPosition;
|
startPos.y = -Screen.height;
|
rectTransform.anchoredPosition = startPos;
|
}
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 0f;
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
break;
|
|
case UIAnimationType.SlideFromLeft:
|
if (rectTransform != null)
|
{
|
Vector2 startPos = originalPosition;
|
startPos.x = -Screen.width;
|
rectTransform.anchoredPosition = startPos;
|
}
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 0f;
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
break;
|
|
case UIAnimationType.SlideFromRight:
|
if (rectTransform != null)
|
{
|
Vector2 startPos = originalPosition;
|
startPos.x = Screen.width;
|
rectTransform.anchoredPosition = startPos;
|
}
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 0f;
|
canvasGroup.interactable = false;
|
canvasGroup.blocksRaycasts = false;
|
}
|
break;
|
}
|
|
try
|
{
|
// 创建动画序列
|
currentAnimation = DOTween.Sequence();
|
|
// 添加动画
|
switch (openAnimationType)
|
{
|
case UIAnimationType.FadeInOut:
|
if (canvasGroup != null)
|
{
|
currentAnimation.Append(canvasGroup.DOFade(1f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.ScaleInOut:
|
if (rectTransform != null)
|
{
|
currentAnimation.Append(rectTransform.DOScale(originalScale, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(1f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.SlideFromTop:
|
case UIAnimationType.SlideFromBottom:
|
case UIAnimationType.SlideFromLeft:
|
case UIAnimationType.SlideFromRight:
|
if (rectTransform != null)
|
{
|
currentAnimation.Append(rectTransform.DOAnchorPos(originalPosition, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(1f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
}
|
|
// 动画完成后的回调
|
currentAnimation.OnComplete(() =>
|
{
|
isAnimating = false;
|
|
// 启用交互
|
if (canvasGroup != null)
|
{
|
canvasGroup.interactable = true;
|
canvasGroup.blocksRaycasts = true;
|
}
|
});
|
}
|
catch (System.Exception e)
|
{
|
Debug.LogError($"播放打开动画时出错: {e.Message}");
|
|
// 出错时确保UI可见并可交互
|
if (canvasGroup != null)
|
{
|
canvasGroup.alpha = 1f;
|
canvasGroup.interactable = true;
|
canvasGroup.blocksRaycasts = true;
|
}
|
isAnimating = false;
|
}
|
}
|
|
// 播放关闭动画 - 修改后的方法
|
protected virtual void PlayCloseAnimation()
|
{
|
if (closeAnimationType == UIAnimationType.None)
|
{
|
// 无动画,直接返回,让HandleClose方法处理
|
return;
|
}
|
|
isAnimating = true;
|
|
try
|
{
|
// 创建动画序列
|
currentAnimation = DOTween.Sequence();
|
|
// 添加动画
|
switch (closeAnimationType)
|
{
|
case UIAnimationType.FadeInOut:
|
if (canvasGroup != null)
|
{
|
currentAnimation.Append(canvasGroup.DOFade(0f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.ScaleInOut:
|
if (rectTransform != null)
|
{
|
currentAnimation.Append(rectTransform.DOScale(Vector3.zero, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(0f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.SlideFromTop:
|
if (rectTransform != null)
|
{
|
Vector2 endPos = originalPosition;
|
endPos.y = Screen.height;
|
currentAnimation.Append(rectTransform.DOAnchorPos(endPos, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(0f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.SlideFromBottom:
|
if (rectTransform != null)
|
{
|
Vector2 endPos = originalPosition;
|
endPos.y = -Screen.height;
|
currentAnimation.Append(rectTransform.DOAnchorPos(endPos, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(0f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.SlideFromLeft:
|
if (rectTransform != null)
|
{
|
Vector2 endPos = originalPosition;
|
endPos.x = -Screen.width;
|
currentAnimation.Append(rectTransform.DOAnchorPos(endPos, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(0f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
|
case UIAnimationType.SlideFromRight:
|
if (rectTransform != null)
|
{
|
Vector2 endPos = originalPosition;
|
endPos.x = Screen.width;
|
currentAnimation.Append(rectTransform.DOAnchorPos(endPos, animationDuration).SetEase(animationEase));
|
}
|
if (canvasGroup != null)
|
{
|
currentAnimation.Join(canvasGroup.DOFade(0f, animationDuration).SetEase(animationEase));
|
}
|
break;
|
}
|
|
// 动画完成后的回调 - 修改为调用CompleteClose
|
currentAnimation.OnComplete(() =>
|
{
|
isAnimating = false;
|
|
// 动画完成后,完成关闭过程
|
if (isClosing)
|
{
|
CompleteClose();
|
}
|
});
|
}
|
catch (System.Exception e)
|
{
|
Debug.LogError($"播放关闭动画时出错: {e.Message}");
|
|
// 出错时直接完成关闭
|
isAnimating = false;
|
CompleteClose();
|
}
|
}
|
|
#endregion
|
|
#region 子UI管理
|
|
// 添加子UI
|
public void AddChildUI(UIBase childUI)
|
{
|
if (childUI != null && !childrenUI.Contains(childUI))
|
{
|
childrenUI.Add(childUI);
|
childUI.parentUI = this;
|
}
|
}
|
|
// 移除子UI
|
public void RemoveChildUI(UIBase childUI)
|
{
|
if (childUI != null && childrenUI.Contains(childUI))
|
{
|
childrenUI.Remove(childUI);
|
childUI.parentUI = null;
|
}
|
}
|
|
// 清空所有子UI
|
public void ClearChildrenUI()
|
{
|
for (int i = childrenUI.Count - 1; i >= 0; i--)
|
{
|
if (childrenUI[i] != null)
|
{
|
childrenUI[i].parentUI = null;
|
}
|
}
|
|
childrenUI.Clear();
|
}
|
|
public bool IsActive()
|
{
|
return isActive;
|
}
|
|
#endregion
|
|
#region 回调方法
|
|
// UI打开时的回调
|
protected virtual void OnOpen()
|
{
|
// 子类可以重写此方法实现打开UI时的逻辑
|
}
|
|
// UI关闭时的回调
|
protected virtual void OnClose()
|
{
|
// 子类可以重写此方法实现关闭UI时的逻辑
|
}
|
|
#endregion
|
}
|