Appearance
当 Egret 进入深水区
本文基于大型 RPG 项目《风起江湖》的真实开发经验,完整总结 Egret 在中大型项目进入“深水区”后的结构性问题与根因。
这不是抱怨,而是理解 Egret 架构边界、制定稳定工程策略的基础。
目录
- Egret 的定位与天然边界
- 引擎层深水问题
- EUI / EXML 深水问题
- 资源管理与内存问题
- 性能与渲染体系问题
- 小游戏 / 现代构建链路兼容性
- Egret 大型项目的工程级规避策略
- 总结:不是选型错误,而是时代约束
1. Egret 的定位与天然边界
Egret 的原始目标:
- 轻量级 H5 引擎
- 以 UI 驱动的小游戏
- 简单渲染、简单资源、简单逻辑
- ES5 时代设计(非现代前端)
它不是为以下场景设计的:
- 大世界 RPG
- 多系统联动
- 复杂 UI / 多皮肤工程
- 大量资源 / 动画 / 地图管理
- 长期迭代的内容型项目
因此 Egret 在“深水区”爆的问题,不是 bug,而是架构天花板。
2. 引擎层深水问题
2.1 hitTest 性能瓶颈(全局矩阵逆变换)
Egret 的默认 hitTest 做以下操作:
- 计算 concatenated matrix
- 做逆矩阵
- 把 stage 坐标变成 local 坐标
- 每一层递归做一次
- bounds / mask / scrollRect 都要判断
当树深时,其复杂度趋近:
O(N × depth × matrix-invert)
(实际卡顿就是这么来的)
结果:
- UI 深层嵌套卡
- 大地图交互卡
- 热点节点(如技能图标)频繁 hitTest 卡
解决方案:
- 自己重写本地坐标递归 hitTest
- 非旋转节点完全不走矩阵逆变换
- 仅在叶子调用
$hitTest(stageX, stageY)兜底 - 大世界的交互完全放弃 Egret hitTest
→ 用逻辑网格/包围盒实现
这部分你已自己实现。
2.2 Graphics / Shape / Sprite 不适合大规模使用
这些节点的特点:
- 非矩形
- bounds 不固定
- hitTest 成本高
- dirty 区域不透明
大量使用会导致:
- 渲染成本升高
- hitTest 成本无法预测
结论:
UI / 场景尽量不要依赖 Graphics 做复杂内容
(序列帧或贴图更稳定)
2.3 Egret 内部的 “魔法字段 / 魔法常量”
如:
$UIComponent$Group[5](touchThrough)$maskedObject$getContentBounds()$renderNode
这些字段极其隐含,文档不全。
要做中大型项目,迟早会碰到底层。
3. EUI / EXML 深水问题
3.1 EXML → JS → eval(小游戏的大坑)
EXML 的流程:
XML → AST → JS → eval → skin class → skinParts 绑定根本问题:
- 小游戏环境(微信等)禁止 eval
- 子皮肤未加载 → eval 直接报错
- 必须预先注册所有皮肤
- 无法按需加载 EXML
- 动态皮肤几乎不可用
dev 与 release 工作流完全不同。
3.2 EXML 构建链路老旧
EXML 本质上是:
- JavaScript 动态 class
- 生命周期绑定复杂
- skinParts 绑定写死
- 必须静态注册
- 无 JSON 化
- 断点难调
- 无现代 UI DSL(对比 Cocos / Laya)
要把 EXML 改为 JSON =
重写 EUI 一半以上功能,不现实。
3.3 UI 生命周期不可控(核心问题)
UI 存在以下问题:
- removeChild 不会释放资源
Image.source = xxx无法被监控- skin → texture 的关系无法追踪
- theme → skin 的依赖完全静态化
- 皮肤引用没有回调、没有生命周期 hook
→ 想写 UI 资源引用计数,几乎不可能。
→ UI 越多,显存越难控制。
解决方法:
- UI 全部常驻内存(换稳定)
- default.thm.json 裁剪到最小
- 拆 theme → 按模块加载
- 编辑器 UI 不放入游戏包
4. 资源管理与内存问题
4.1 RES 系统 = 两个 Map(几乎只够 demo 用)
Egret 的 RES:
map<string, any>
map<string, Texture>无:
- 引用计数
- 纹理访问记录
- 图集 → 小图的依赖关系
- 自动释放
- 资源版本管理
- 常驻/临时资源区分
造成的问题:
- 显存不可控
- 资源泄漏频繁
- 图集无法安全卸载
- UI 与动画共享纹理难管理
你已自行开发:
- MovieClipManager(ref/unref)
- TTL 清理
- 访问时间戳
- 图集 ref=0→延迟 1 分钟回收
这是正确做法。
4.2 Egret 无法自动释放 UI texture
原因:
- Image.source 没有 ref/unref
- 皮肤动态替换没回调
- 关闭 UI 不会释放纹理
结果:
UI 最好全部常驻内存,否则你自己都不知道什么时候该回收。
5. 性能与渲染体系问题
5.1 renderNode 太老旧,缺乏现代优化
没有:
- batching
- dirty region
- mask 裁剪优化
- 层级裁剪
- 自定义 shader(基本不可行)
- WebGL2 特性
完全不适合做:
- 大量特效
- 复杂 UI
- 高频动画
应对:
- 简化 UI 层级
- 尽量使用贴图而非动态绘制
- 特效做成序列帧而非 shader
6. 小游戏 / 构建链路问题
6.1 微信小游戏禁止 eval → EXML 必须预编译
导致:
- dev/release 执行链路割裂
- 动态皮肤全废掉
- 默认 build 体系必须 hack
- theme 必须静态注册
6.2 Egret 构建工具停留在 ES5 时代
- manifest.json 必须手动维护
- 无现代 bundler 支持
- 引擎 build 慢、不可控
- 打包结果难调试
你最终的方案:
- tsc -w 取代 Egret build
- 手动维护 manifest
- 自己写 Promise RES API
这是最佳路线。
7. Egret 大型项目的工程级规避策略
结合《风起江湖》的实践:
7.1 UI 策略
- UI 尽量少、尽量常驻
- theme 按模块拆分
- default.thm.json 发布前裁剪
- 不动态加载 EXML
- 避免深层嵌套
7.2 hitTest 策略
- 大地图不靠显示树 hitTest
- 自定义本地递归 hitTest
- 降低矩阵逆变换频率
- 非旋转对象走快路径
7.3 资源策略
- 全局资源需要 ref/unref
- TTL + 时间戳清理
- 动画单独管理
- 图集 ref=0 延迟释放
- UI 资源常驻避免复杂性
7.4 构建策略
- 完全抛弃 Egret build
- tsc -w + 手动 manifest
- 自定义 asset pipeline
- 不依赖 EXML 的运行时加载
7.5 项目级策略
“Egret 负责渲染,不负责架构。”
系统层、任务层、剧情层、世界逻辑全部写在引擎外部。
→ Egret 只用于渲染,不参与项目复杂性。
8. 总结:不是选型错误,而是时代约束
2022 年的实际情况:
- Cocos Studio 生态混乱
- Cocos Creator UI/构建不成熟
- Laya 名气不足、风险高
- Egret 当时是 H5 里最稳的选择
你今天遇到的问题,是因为:
项目的“野心”比 Egret 的“天花板”高得多。
这不是你的错误,是 Egret 的时代属性。
最终建议
通过以下策略,你仍可以让 Egret 成为“足够稳的渲染层”:
- UI 少而稳
- 资源自建管理
- theme 裁剪
- 构建链路自控
- hitTest 重写
- 内容/系统层与引擎解耦
让 Egret 不阻碍你的大世界叙事,而只是一个可靠的渲染底板。
