MyFramework:Unity大型项目里的 UI 生命周期应该怎么管理

做 Unity 项目时,UI 管理一开始通常都很简单。

打开界面:

bash 复制代码
panel.SetActive(true);

关闭界面:

bash 复制代码
panel.SetActive(false);

销毁界面:

bash 复制代码
Destroy(panel);

Demo 阶段这样写没有问题。

但是项目一旦变大,UI 就不会只是"显示"和"隐藏"这么简单了。

一个界面从出现到消失,背后通常还牵扯到很多事情:

bash 复制代码
Prefab 加载
节点绑定
脚本初始化
点击事件注册
输入框注册
ScrollView 初始化
对象池初始化
本地化文本收集
界面显示刷新
界面隐藏清理
动画命令中断
对象池回收
事件监听注销
资源卸载
渲染层级调整
Esc 关闭逻辑
背景模糊处理

这些事情如果都散落在各个 UI 脚本里,项目越做越大,后面就会越来越难维护。

所以 MyFramework 里有一个 LayoutManager

它解决的不是"怎么打开一个界面"这么简单的问题,而是:

一个大型 Unity 项目里,UI 从加载、显示、更新、隐藏到销毁,应该由谁来管,怎么管,管到什么程度。


一、UI 不应该只是一个 GameObject

很多项目里的 UI 管理都是围绕 GameObject 做的。

比如:

bash 复制代码
GameObject loginPanel;
GameObject mainPanel;
GameObject bagPanel;

然后业务逻辑里到处写:

bash 复制代码
loginPanel.SetActive(true);
bagPanel.SetActive(false);

这种写法最大的问题是,UI 被当成了一个普通节点。

但是在大型项目里,一个界面并不只是一个 GameObject。

它至少应该包含三层概念:

bash 复制代码
Prefab 资源
界面实例
界面逻辑脚本

在 MyFramework 里,这三层分别对应:

bash 复制代码
ResourceRef<GameObject>   // 界面 Prefab 资源引用
GameLayout                // 界面实例对象
LayoutScript              // 界面逻辑脚本

也就是说,框架不会让业务直接管理一个裸的 GameObject。

业务层看到的是 LayoutScript,框架内部管理的是 GameLayout,资源层持有的是 ResourceRef<GameObject>

这样做的好处是,界面的生命周期就可以被完整接管。

加载时,LayoutManager 负责实例化 Prefab。

初始化时,GameLayout 负责创建根节点和绑定脚本。

显示时,LayoutScript 负责刷新界面状态。

隐藏时,LayoutScript 负责清理监听和回收对象。

销毁时,GameLayout 负责销毁 UI 对象并释放资源引用。

这比单纯 SetActive 要可靠得多。


二、LayoutManager 管的是"界面资源和界面实例"

LayoutManager 里最核心的几个数据结构,大概可以理解成这样:

bash 复制代码
mLayoutRegisteList       // 所有已注册的界面信息
mLayoutList              // 当前已经加载出来的界面实例
mLayoutTypeToPath        // LayoutScript 类型到 Prefab 路径的映射
mLayoutPathToType        // Prefab 路径到 LayoutScript 类型的映射
mLoadingLayoutList       // 正在异步加载的界面
mBackBlurLayoutList      // 需要背景模糊的界面

这几个列表决定了 LayoutManager 的定位。

它不是业务导航器。

它不是页面返回栈。

它也不是具体界面的逻辑控制器。

它真正负责的是:

bash 复制代码
这个界面有没有注册
这个界面的 Prefab 在哪里
这个界面有没有加载
这个界面是否正在异步加载
这个界面当前是否显示
这个界面当前渲染顺序是多少
这个界面是否需要背景模糊
这个界面什么时候销毁

所以 LayoutManager 的职责是比较底层、比较稳定的。

它不关心玩家现在是在登录流程、主城流程、战斗流程,还是副本流程。

它只关心:

某个 LayoutScript 对应的界面资源,应该如何被加载、显示、隐藏和释放。


三、界面注册:先让框架知道这个界面是什么

MyFramework 里的界面不是随便传一个路径加载,而是先注册。

比如:

bash 复制代码
LayoutManager.registeLayout<UILogin>(LAYOUT_LIFE_CYCLE.PART_USE);
LayoutManager.registeLayoutPersist<UIMain>(null);

注册时会记录几个关键东西:

bash 复制代码
界面脚本类型
界面 Prefab 路径
界面生命周期类型
界面创建后的回调

生命周期目前主要分两类:

bash 复制代码
PERSIST     // 全局常驻
PART_USE    // 局部使用

这个设计很重要。

因为大型项目里的 UI 并不是所有界面都应该一样处理。

有些界面是全局常驻的,比如主界面、提示层、网络等待层、通用弹窗层。

这些界面可能会跨流程、跨场景存在。

有些界面只是某个流程临时使用,比如登录界面、创建角色界面、某个活动界面、副本结算界面。

这些界面在离开对应流程后就应该卸载。

所以 LayoutManager 不是只提供一个 Load,而是把界面的生命周期属性也登记进来。

后面场景流程退出时,就可以统一调用:

bash 复制代码
mLayoutManager.unloadAllPartLayout();

这样所有 PART_USE 类型的界面都会被销毁,而 PERSIST 类型的界面会继续保留。

这就是框架层生命周期管理的意义。

不是每个界面自己判断"我该不该销毁",而是注册时就把规则确定下来。


四、加载和显示要分开

很多项目里,打开界面时会把加载和显示混在一起:

bash 复制代码
LoadPanel();
ShowPanel();

看起来没问题,但实际项目里会遇到很多情况。

比如:

bash 复制代码
进入场景前提前加载 UI,但是暂时不显示
打开界面时如果已经加载过,就只显示
异步加载完成后再显示
某些界面要预加载,避免打开时卡顿
某些界面只加载一次,后续重复使用

所以 MyFramework 里提供了多种入口:

bash 复制代码
LOAD<T>();
LOAD_HIDE<T>();
LOAD_ASYNC<T>();
LOAD_ASYNC_HIDE<T>();
SHOW<T>();
HIDE<T>();
UNLOAD<T>();

这些接口背后的区别很清楚:

bash 复制代码
LOAD        加载并显示
LOAD_HIDE   加载但不显示
SHOW        已加载界面显示出来
HIDE        已加载界面隐藏起来
UNLOAD      隐藏并销毁界面

这比简单的 OpenUI 更适合大型项目。

因为大型项目里,UI 的状态不只有"打开"和"关闭"。

至少有这几种状态:

bash 复制代码
未注册
已注册但未加载
正在异步加载
已加载但隐藏
已加载且显示
正在隐藏动画中
已销毁

这些状态如果没有框架统一管理,很容易出现重复加载、异步回调错乱、隐藏后还在更新、资源提前释放等问题。

MyFramework 里还有一个 mLoadingLayoutList,就是用来避免一个界面正在异步加载时,又被同步加载。

这种保护在小项目里看起来没必要,但在实际项目里非常重要。

因为 UI 打开入口可能来自很多地方:

bash 复制代码
按钮点击
网络回包
场景流程
新手引导
系统推送
热更逻辑

没有统一保护,同一个界面被重复创建并不是小概率事件。


五、GameLayout 才是真正的界面实例

LayoutManager 负责管理所有界面,而每一个具体界面,则由 GameLayout 表示。

GameLayout 不是 MonoBehaviour,它是框架里的普通 C# 对象。

它内部持有:

bash 复制代码
mRoot               // 界面根节点
mScript             // 界面脚本
mPrefab             // Prefab 资源引用
mObjectList         // 当前界面里所有已创建的 UI 对象
mNeedUpdateList     // 当前界面里需要每帧更新的 UI 对象
mRenderOrder        // 当前界面渲染顺序
mRenderOrderType    // 渲染顺序计算方式
mBlurBack           // 是否需要背景模糊

界面加载出来以后,初始化流程大致是:

bash 复制代码
创建 LayoutScript
实例化 Prefab
创建根 myUGUICanvas
重置 RectTransform 锚点和尺寸
绑定 LayoutScript 的 Root
调用 assignWindow 绑定节点
执行自适应 Anchor
调用 LayoutScript.init
调用 LayoutScript.postInit
设置渲染顺序并刷新 UI 深度
初始化完成后默认隐藏

这里有一个关键点:

加载完成后不是立即随便显示,而是先把整个界面的内部结构初始化完整。

比如节点绑定、自适应、对象池初始化、窗口深度刷新,都是在显示之前完成的。

这样业务层拿到这个 LayoutScript 时,界面内部已经是一个完整可用的状态。


六、显示界面时,不只是 SetActive(true)

在 MyFramework 里,显示界面时确实会激活根节点。

但显示不是只有这一句。

GameLayout.setVisible(true) 的核心逻辑是:

bash 复制代码
激活根节点
调用 LayoutScript.onGameState

onGameState 这个名字很有框架特点。

它不是简单叫 onShow,而是强调:

界面显示时,应该根据当前游戏状态重新调整自己。

比如同一个主界面,在不同流程下显示内容可能不同:

bash 复制代码
主城状态
战斗状态
副本状态
观战状态
剧情状态
引导状态

界面不是只要显示出来就行,还要根据当前游戏状态重置显示内容。

所以 onGameState 里会做几类事情:

bash 复制代码
中断之前未完成的界面命令
编辑器下检查 ScrollRect 是否注册
编辑器下检查 InputField 是否注册
重置一级子窗口对象
通知激活的子窗口 onShow

这说明 MyFramework 对 UI 显示的理解不是"打开窗口",而是"窗口进入当前游戏状态"。

这个区别很重要。

大型项目里,很多 UI bug 都不是界面没打开,而是界面打开后状态不干净。

比如:

bash 复制代码
上一次打开留下的选中状态还在
上一次创建的对象池元素没回收
上一次注册的事件还在监听
上一次播放的动画命令还没结束
文本本地化对象重复注册
ScrollView 状态没有刷新

所以显示界面时必须有一个统一入口来重置状态。


七、隐藏界面时,也不只是 SetActive(false)

隐藏界面更容易被低估。

很多项目里隐藏 UI 就是:

bash 复制代码
gameObject.SetActive(false);

但这只解决了"看不见"。

它没有解决这些问题:

bash 复制代码
事件监听是否注销
输入监听是否注销
动画命令是否中断
对象池元素是否回收
子窗口是否收到隐藏通知
本地化对象是否清理

MyFramework 的 LayoutScript.onHide 会统一处理这些事情:

bash 复制代码
中断所有命令
清理本地化注册
注销事件监听
通知所有根窗口对象 onHide
回收对象池中的对象
注销按键监听

这就是 UI 生命周期管理真正有价值的地方。

隐藏界面不是简单让它不可见,而是让它从当前交互系统里退出。

一个已经隐藏的界面,不应该继续收到事件。

一个已经隐藏的界面,不应该继续响应按键。

一个已经隐藏的界面,不应该继续持有临时对象池数据。

一个已经隐藏的界面,不应该继续播放之前的动画命令。

否则项目运行久了,就会出现各种奇怪问题。

比如界面已经关了,但还在响应网络事件。

界面已经隐藏了,但还在刷新列表。

界面已经切走了,但输入框还在被输入系统管理。

界面重新打开时,对象池里残留的是上一次的数据。

这些问题靠每个业务脚本自觉处理是不可靠的。

框架必须在基类里兜底。


八、为什么返回栈不应该放在 LayoutManager 里

很多 UI 框架喜欢在 UIManager 里做一个全局返回栈。

大概是这样:

bash 复制代码
打开 A,压栈 A
打开 B,压栈 B
打开 C,压栈 C
点击返回,关闭 C
再点击返回,关闭 B

这种做法适合页面结构比较简单的 App。

但是游戏项目里的 UI 往往不是纯页面导航。

尤其是 MyFramework 这种结构里,返回关系通常不应该由 LayoutManager 统一决定,而是应该配合 GameSceneSceneProcedure 来实现。

原因很简单:

界面属于流程,而不是流程属于界面。

比如一个游戏场景里可能有这些流程:

bash 复制代码
LoginScene
 ├── ProcedureLogin
 ├── ProcedureSelectServer
 ├── ProcedureCreateRole
 └── ProcedureEnterGame

或者:

bash 复制代码
MainScene
 ├── ProcedureMainCity
 │   ├── ProcedureBag
 │   ├── ProcedureShop
 │   └── ProcedureTask
 └── ProcedureBattle
     ├── ProcedureNormalBattle
     └── ProcedureResult

SceneProcedure 是树形结构。

这意味着每个流程只需要关心自己负责的界面。

比如:

bash 复制代码
ProcedureLogin 负责登录界面
ProcedureBag 负责背包界面
ProcedureShop 负责商店界面
ProcedureBattle 负责战斗界面
ProcedureResult 负责结算界面

当流程进入时,它打开自己的 UI。

当流程退出时,它关闭或卸载自己的 UI。

当从子流程返回父流程时,父流程通过 onInitFromChild 恢复自己的状态。

当进入子流程时,父流程通过 onExitToChild 决定自己是否隐藏部分 UI。

这样每个流程的责任边界非常清楚。

LayoutManager 不需要知道"点击返回时应该回到背包还是主城"。

这个问题应该由当前 GameScene 的 SceneProcedure 决定。

因为只有 SceneProcedure 知道当前处于什么游戏流程,知道返回以后应该恢复哪些界面,隐藏哪些界面,保留哪些状态。


九、树形 SceneProcedure 比全局 UI 返回栈更适合游戏

全局 UI 返回栈的问题在于,它把所有界面都当成了同一种东西。

但游戏里的界面不是同一层级的。

比如这些界面就不是一个概念:

bash 复制代码
主界面
背包界面
弹窗确认框
战斗 HUD
副本结算
网络等待层
新手引导层
聊天窗口
系统跑马灯

如果全部塞进一个 UI 返回栈,就会很混乱。

例如:

bash 复制代码
战斗 HUD 要不要进返回栈?
网络等待层要不要进返回栈?
确认弹窗要不要进返回栈?
新手引导界面要不要进返回栈?
常驻主界面要不要进返回栈?

这些问题最后都会变成各种特殊判断。

而树形 SceneProcedure 的优势是,它天然表达了流程关系。

父流程和子流程之间有明确关系。

兄弟流程之间有明确切换逻辑。

退出到共同父节点时,框架可以只退出必要的流程。

从子流程返回父流程时,也可以走专门的恢复入口。

MyFramework 的 SceneProcedure 里有几个很关键的函数:

bash 复制代码
onInit
onInitFromChild
onExit
onExitToChild
onExitSelf
onPrepareExit

这些入口刚好对应游戏流程里的几种情况:

bash 复制代码
第一次进入流程
从子流程返回当前流程
退出当前流程进入其他流程
退出当前流程进入自己的子流程
无论进入哪里都要执行的退出逻辑
准备退出但还没真正退出

这比单纯 UI 返回栈表达能力强很多。

因为它不是在管理"界面打开顺序",而是在管理"游戏流程状态"。

UI 只是流程状态的一部分。


十、一个流程只关心自己的界面

这种设计还有一个很大的好处:

每个 SceneProcedure 只需要关心自己的 UI。

比如登录流程只写登录相关逻辑:

bash 复制代码
protected override void onInit(SceneProcedure lastProcedure)
{
    LOAD<UILogin>();
}

protected override void onExit(SceneProcedure nextProcedure)
{
    HIDE<UILogin>();
}

背包流程只写背包相关逻辑:

bash 复制代码
protected override void onInit(SceneProcedure lastProcedure)
{
    LOAD<UIBag>();
}

protected override void onExit(SceneProcedure nextProcedure)
{
    HIDE<UIBag>();
}

子流程返回时,也只处理自己的恢复逻辑:

bash 复制代码
protected override void onInitFromChild(SceneProcedure lastProcedure)
{
    SHOW<UIMain>();
}

进入子流程时,也只处理自己需要暂时退让的内容:

bash 复制代码
protected override void onExitToChild(SceneProcedure nextProcedure)
{
    HIDE<UIMainMenu>();
}

这样 UI 管理不会集中堆在一个巨大的 UIManager 里。

每个流程只管自己的界面,父流程只管父流程的界面,子流程只管子流程的界面。

这才适合大型项目长期维护。

因为大型项目最怕的不是代码多,而是所有逻辑都集中在一个地方。

如果所有 UI 打开、关闭、返回、恢复都写在 LayoutManager 或 UIManager 里,最后一定会变成一个巨大的状态机。

而 SceneProcedure 树形结构的好处是,把状态机拆开了。

每个流程节点就是一小块状态机。


十一、LayoutManager 只做通用规则,不做业务决策

这也是 MyFramework 里 LayoutManager 设计比较克制的地方。

它提供这些能力:

bash 复制代码
加载界面
异步加载界面
显示界面
隐藏界面
卸载界面
计算渲染顺序
管理 Esc 关闭
管理背景模糊
管理常驻和局部界面
管理 UI 根节点
刷新 UI 深度

但它不应该做这些事情:

bash 复制代码
当前应该进入哪个业务页面
返回按钮应该回到哪里
某个流程退出时要不要保留某个界面
某个子流程返回后父流程应该恢复什么状态

这些都属于业务流程问题。

所以正确分层应该是:

bash 复制代码
GameScene / SceneProcedure 负责流程
LayoutManager 负责界面生命周期
GameLayout 负责界面实例
LayoutScript 负责界面逻辑
WindowObject / myUGUIObject 负责具体节点

这个分层很清晰。

流程层决定"现在应该显示哪些界面"。

布局管理层负责"如何把这些界面正确显示出来"。

界面脚本层负责"这个界面显示什么内容"。

节点层负责"具体控件怎么响应和刷新"。


十二、渲染顺序也应该由框架统一管理

大型项目里,UI 层级也是一个常见问题。

比如:

bash 复制代码
主界面
普通弹窗
二级弹窗
提示层
引导层
网络等待层
GM 窗口
系统错误弹窗

如果每个界面自己写 sortingOrder,很容易乱。

MyFramework 里用 LAYOUT_ORDER 来描述渲染顺序的计算方式:

bash 复制代码
AUTO              // 普通自动层级
FIXED             // 固定层级
ALWAYS_TOP        // 固定的最上层
ALWAYS_TOP_AUTO   // 自动计算的最上层

这几个类型解决的是不同问题。

普通界面通常用 AUTO

需要固定层级的场景 UI 可以用 FIXED

提示层、引导层、最高弹窗可以用 ALWAYS_TOPALWAYS_TOP_AUTO

显示界面时,如果是自动层级,框架会重新计算当前最高层级。

这样业务层不需要到处写:

bash 复制代码
canvas.sortingOrder = 100;
canvas.sortingOrder = 200;
canvas.sortingOrder = 999;

也不需要猜哪个界面应该压过哪个界面。

框架里统一计算,才能保证规则一致。


十三、Esc 关闭不是简单关闭最后打开的界面

PC 端还有一个常见需求:按 Esc 关闭界面。

如果有全局 UI 返回栈,通常就是关闭栈顶界面。

但 MyFramework 里 Esc 的处理更接近渲染层级。

COMLayoutManagerEscHide 会维护当前显示中的界面列表,并按渲染顺序排序。

按下 Esc 时,从最上层界面往下传递:

bash 复制代码
最上层界面先收到 Esc
如果它处理了,就停止传递
如果它不处理,再传给下一层

具体是否处理,由 LayoutScript.onESCDown 决定。

默认逻辑是:

bash 复制代码
if (mEscHide)
{
    close();
}
return mEscHide;

这比全局关栈更合理。

因为 Esc 关闭的是当前用户视觉上最上层、最应该响应的界面,而不是某个抽象栈里最后记录的界面。

并且每个界面可以自己决定是否响应 Esc。

比如:

bash 复制代码
普通背包界面可以 Esc 关闭
强制剧情界面不能 Esc 关闭
网络等待界面不能 Esc 关闭
确认弹窗可以 Esc 关闭
战斗 HUD 不应该 Esc 关闭

这些判断放在界面脚本里,比放在全局管理器里更自然。


十四、背景模糊也应该跟随界面显示状态

很多项目会有这种效果:

bash 复制代码
打开弹窗时,后面的界面变模糊
关闭弹窗时,后面的界面恢复正常

如果每个弹窗自己处理背景模糊,很容易出现问题。

比如:

bash 复制代码
A 弹窗打开,背景模糊
B 弹窗又打开,背景仍然模糊
关闭 B 时,不应该取消 A 的模糊
关闭 A 时,才应该真正取消模糊

所以 MyFramework 里 LayoutManager 有一个 mBackBlurLayoutList

当显示一个需要背景模糊的界面时,把它加入列表。

当隐藏时,从列表中移除。

然后根据当前列表里最高层的模糊界面,决定哪些低层界面要切到模糊层。

这类逻辑就非常适合放在 LayoutManager。

因为它不是某个具体界面的业务,而是多个界面之间的通用显示规则。


十五、更新也要受可见性控制

UI 更新也是大型项目里容易浪费性能的地方。

很多界面隐藏以后,脚本还在跑 Update。

很多节点实际上不需要每帧刷新,却一直在 Update。

很多列表隐藏以后还在刷新数据。

MyFramework 里 GameLayout.update 会先判断:

bash 复制代码
界面是否可见
脚本是否存在
脚本是否已经初始化完成

只有可见且初始化完成的界面才会继续更新。

而且它还把更新拆成两部分:

bash 复制代码
更新需要 Update 的 UI 对象
更新 LayoutScript 自己

其中 UI 对象还有一个 mNeedUpdateList

也就是说,并不是界面里的所有节点都会无脑 Update。

只有需要更新的窗口对象才会放进更新列表。

这对大型 UI 很重要。

一个复杂界面里可能有几十甚至上百个 UI 节点,但真正需要每帧更新的可能只有几个。

框架层做这个区分,可以避免很多无意义开销。


十六、销毁时必须释放资源和清理引用

隐藏和销毁不是一回事。

隐藏只是暂时不用。

销毁是这个界面实例彻底退出。

MyFramework 里销毁界面时,会走:

bash 复制代码
LayoutManager.destroyLayout
GameLayout.destroy
LayoutScript.destroy
myUGUIObject.destroyWindow
ResourceManager.unload

LayoutScript.destroy 里还会做兜底清理:

bash 复制代码
清理本地化注册
销毁对象池
销毁窗口对象池
销毁根窗口对象
清理所有窗口对象列表
注销事件监听
注销按键监听
中断所有命令

这很关键。

因为隐藏时可能只是临时关闭,销毁时则必须彻底清理。

尤其是框架里有事件系统、命令系统、对象池、本地化、输入系统,如果销毁时没有统一兜底,很容易留下悬空引用。

大型项目里很多内存泄漏、重复回调、空对象报错,最后都能追到生命周期没管好。


十七、场景退出时统一卸载局部 UI

SceneProcedure 里有一个通用退出逻辑:

bash 复制代码
genericExit()

其中就会调用:

bash 复制代码
mLayoutManager.unloadAllPartLayout();

这代表一个很明确的规则:

场景退出时,局部界面不应该残留。

因为 PART_USE 界面本来就是某些流程临时使用的。

离开场景或者退出流程后,继续留着没有意义,还可能持有不该持有的资源。

PERSIST 界面则可以保留。

这比在每个流程里手动 UNLOAD 一堆界面更稳。

流程可以负责自己明确打开和隐藏的界面。

而场景退出这种大清理,则由框架统一兜底。


十八、总结

MyFramework 里的 UI 生命周期管理,核心不是多封装几个 ShowHide 函数。

它真正做的是把 UI 拆成了几层:

bash 复制代码
LayoutManager    管理所有界面的注册、加载、显示、隐藏、销毁
GameLayout       表示一个具体界面实例
LayoutScript     承载一个界面的业务逻辑和显示状态
myUGUIObject     表示具体 UI 节点
SceneProcedure   决定当前流程应该显示哪些界面

这里最关键的一点是:

LayoutManager 不负责业务返回栈。

返回关系、流程跳转、父子流程恢复,应该放在 GameScene + SceneProcedure 里。

因为 SceneProcedure 是树形的,每个流程只需要关心自己的界面。

父流程管父流程的 UI,子流程管子流程的 UI。

进入流程时显示自己的界面,退出流程时隐藏自己的界面,从子流程返回时恢复自己的状态。

这样 UI 生命周期和游戏流程就不会混在一起。

LayoutManager 负责通用能力:

bash 复制代码
加载
显示
隐藏
卸载
渲染顺序
Esc 关闭
背景模糊
对象更新
资源释放

SceneProcedure 负责业务流程:

bash 复制代码
现在处于哪个状态
应该进入哪个子流程
返回时回到哪个流程
这个流程应该显示哪些界面
这个流程退出时应该隐藏哪些界面

这套分层的好处是,项目越大越明显。

小项目里,一个 UIManager 什么都管也能跑。

但大型项目里,UI 管理一定要拆清楚。

界面的生命周期归 LayoutManager。

界面的业务状态归 LayoutScript。

界面的流程关系归 SceneProcedure。

只有这样,UI 才不会变成一个巨大的、谁都不敢改的全局状态机。

相关推荐
SmalBox4 小时前
【节点】[IrisOutOfBoundColorClamp节点]原理解析与实际应用
unity3d·游戏开发·图形学
Ethan的技术工坊4 小时前
Unity Windows 客户端从 Mono 切换到 IL2CPP 后,安装包体积翻倍的排查记录
unity3d
_zhourui_h_5 小时前
MyFramework Unity:TweenSequence 和 DOTween 有什么区别
unity3d
SmalBox1 天前
【节点】[IrisOffset节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[IrisLimbalRing节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[EyeSurfaceTypeDebug节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox4 天前
【节点】[CorneaRefraction节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox5 天前
【节点】[CirclePupilAnimation节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox6 天前
【节点】[Zigzag节点]原理解析与实际应用
unity3d·游戏开发·图形学