ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter游戏鸿蒙化适配:dartemis ECS架构实战与踩坑记录

Flutter游戏鸿蒙化适配:dartemis ECS架构实战与踩坑记录 Flutter 游戏项目里dartemis 是我用得比较顺手的 Dart 版 ECS 库但真正把它跑在鸿蒙设备上却没有想象中那么轻松。项目组第一次把战斗原型丢到真机上时报错、闪退、渲染花屏接踵而来排查到最后问题几乎都集中在“第三方库到底怎么在鸿蒙的 Flutter 引擎里被正确编译和调度”上。这篇笔记会从 ECS 架构的功能拆分开始讲清楚 dartemis 里的 Entity、Component、System 到底怎么用再把我做鸿蒙化适配时的工程配置、运行参数、踩坑记录整理出来。适合两类人看一类是用 Flutter 写游戏或复杂交互、想把逻辑和 UI 剥离开的开发者另一类是正在把现有 Flutter 应用搬到鸿蒙上、遇到第三方纯 Dart 库无法编译或运行异常的开发者。文章不涉及商业 SDK 推广只是个人项目经验的复盘。1. 项目全景为什么要做 dartemis 的鸿蒙化适配1.1 从 Flutter 到鸿蒙当前跨端现状我接触鸿蒙化 Flutter 的时间不算早项目初期还在用常规的 Flutter 三端工程写玩法原型等打到真机验收阶段发现目标设备全部是鸿蒙系统才开始认真对待适配问题。目前常见的做法是使用面向 OpenHarmony 的 Flutter SDK 分支来构建鸿蒙应用Dart 代码仍然由 Flutter 引擎解释执行外面套一层鸿蒙应用壳最终打包成 HAP 或 HAP 内的模块。开发者在 pubspec 里写的依赖只要不涉及原生平台差异就有很大概率直接跑通。dartemis 恰好是这种“很有概率直接跑通”的库因为它没有依赖任何 Android 或 iOS 原生代码底层只是 Dart 的集合、队列和泛型工具。真正的难点反而在工程侧鸿蒙化的 Flutter 工具链版本可能与 pub.dev 上的 SDK 约束不一致第三方库传递依赖也可能和鸿蒙壳工程产生编译冲突。所以我建议把“鸿蒙化适配”拆成两层看第一层是让 Flutter 引擎在鸿蒙设备上跑起来第二层是让目标 Dart 库在引擎内正常编译、实例化、持续运行。dartemis 属于第二层但第二层的很多问题往往由第一层的基础配置引发。对于刚上手的人来说生态现状最大的感受是命令和工具还没完全统一。有人用 DevEco Studio 直接打开生成的 ohos 目录有人用命令行 hvigor 打包也有人沿用 flutter build 自动触发鸿蒙构建。不同版本的 Flutter SDK 分支对命令的支持程度不一样这时候不要急着怀疑 dartemis先把一个空的 Flutter 工程跑通再逐步加入 ECS 相关代码定位问题的成本会低很多。1.2 ECS 架构为什么适合游戏和数据密集型场景传统面向对象建模喜欢把“一个玩家”封装成一个对象玩家拥有的位置、血量、技能、动画状态全部挂在同一个类里。玩法复杂以后这个类会越来越大而且不同系统对同一份数据的访问路径五花八门改一处逻辑容易牵连一片 UI。ECS 的核心思想完全反过来玩家不再是一个大对象而是“一堆组件数据”加“一组系统逻辑”的组合。位置是 Position血量是 Health技能是 Skill它们各自是独立的数据块移动系统只负责读取 Position 和 Velocity伤害系统只负责读取 Health 和 Attack。把数据从对象里抽出来的好处在大量实体同时更新时尤其明显。一个弹幕类游戏里屏幕上有上千颗子弹如果每颗子弹都是一个包含几十个字段的独立对象创建和遍历的开销都很高。ECS 会让同类型的组件连续存储在内存里系统按组件数组批量扫描CPU 缓存的命中率会比到处跳转指针高不少。我见过 Unity ECS 和 Bevy 的很多项目都因此获得明显性能提升dartemis 虽然在纯 Dart 环境里没有做到那种极致的 cache-friendly但架构思想上完全一致用在 Flutter 游戏里也能让逻辑层清晰很多。还有一层价值是数据资产的可治理性。当所有实体状态都集中由几组 Component 描述时存档、回放、网络同步都变得很好做因为组件本身就是一份可序列化的数据。我的项目里把玩家背包、敌人波次、关卡进度全部建模成 Component测试时直接构造一份 JSON 就能复现特定战斗场景这在以前硬编码在类字段里的写法下几乎不可能。1.3 dartemis 在 Flutter 项目中的定位dartemis 移植自 Java 世界的 Artemis 框架保留了核心概念World 负责管理实体和系统Entity 只是实体标识Component 是纯数据System 才是行为逻辑的载体。在 Flutter 项目里我最常用的方式是让 dartemis 成为整个玩法层的“数据中台”Widget 代码不直接持有组件对象而是通过桥接层读取需要展示的数据。这样定位之后Flutter 自身的状态管理就只承担页面交互和局部 UI 状态比如弹窗开关、当前选中项、表单输入内容而战斗数据、单位位置、AI 状态、资源产出这些高频变动的数据统一交给 dartemis 的 World 管理。两者之间有明确的边界只有被 UI 需要的数据才会通过 ValueNotifier 或 Stream 暴露出去没有被消费的数据继续留在 ECS 内部避免 Flutter 组件因为无关数据变化而频繁重建。为什么我当时要专门为 dartemis 做鸿蒙化适配而不是临时换一套别的 ECS 方案因为项目里已经基于 dartemis 写了十几个系统包括移动、碰撞、技能、掉落、任务、成就整体代码量不小重写成本太高。从经验来看只要这个库是纯 Dart 实现鸿蒙化的核心工作就集中在对编译环境、同步机制和生命周期兼容性的调整上而不是去重写业务逻辑。2. 掌控数据资产ECS 核心概念与 dartemis 数据建模2.1 Entity、Component、System 三者如何协作用一个厨房类比来快速建立概念Component 是放在台面上的食材System 是厨师手里的菜谱Entity 是一口锅。锅里可以放任意组合的食材食材之间互相不知道对方的存在厨师按照菜谱扫描符合条件的锅对里面的食材做处理。比如“移动系统”只关心“有位置且有速度”的锅它遍历所有这样的锅把速度加到位置上其他食材一概不管。在 dartemis 里Entity 本质上只是一个轻量标识内部绑定了它自己关联的 Component 类型。创建实体后通过 ComponentMapper 给实体添加组件实例。系统通过 Aspect 声明自己关心哪些组件组合World 每次调用 process 方法时会按顺序执行所有已注册系统的 update 逻辑。这样设计带来的直接好处是新增玩法不需要修改已有实体类只需要定义新的 Component 和新的 System然后让 World 创建对应实体即可。实际项目中我习惯把 Component 定义成非常薄的“数据盒子”一般只包含字段、构造函数和必要的 getter不写业务方法。任何“计算血量”“判断碰撞”这类逻辑都放进 System。这条纪律能让数据资产变得可序列化也能让多人协同开发时冲突面积大幅缩小。团队成员不会因为改了某个方法而影响到另一个系统的假设。2.2 用 dartemis 定义组件与实体先定义两个最简单的组件位置和速度。代码如下import package:dartemis/dartemis.dart; class Position extends Component { double x; double y; Position(this.x, this.y); } class Velocity extends Component { double vx; double vy; Velocity(this.vx, this.vy); }然后创建一个 World手动注册组件 Mapper并创建实体void main() { final world World(); final positionMapper world.getMapperPosition(); final velocityMapper world.getMapperVelocity(); final player world.createEntity() ..add(positionMapper.create(100, 200)) ..add(velocityMapper.create(5, 0)); world.process(); }这里的createEntity只是拿到了一个空实体通过add方法把组件实例挂上去。dartemis 的 World 会在内部维护组件索引后续系统可以从对应 Mapper 直接读取该实体上的组件。需要注意不同版本 dartemis 对createEntity的返回类型和链式调用支持略有差异如果直接复制这段代码报类型错误去 pub 页面看下当前版本 API 即可。我通常还会封装一个工厂函数来批量创建实体比如创建一队敌人ListEntity spawnEnemies(World world, int count) { final posMapper world.getMapperPosition(); final hpMapper world.getMapperHealth(); return List.generate(count, (i) { final enemy world.createEntity(); enemy.add(posMapper.create(100.0 i * 40, 50.0)); enemy.add(hpMapper.create(10)); return enemy; }); }这种工厂函数让实体生成逻辑集中在一处也方便后续接入关卡配置表。我用配置文件驱动实体生成后新增兵种基本不用改 Dart 代码只要在 JSON 里声明组件字段就行。2.3 数据访问与查询Aspect、Mapper、Bag 等 API 实践System 要高效查询实体依赖 dartemis 的 Aspect 和 Mapper。Aspect 用来声明“这个系统需要哪些组件”比如移动系统需要同时拥有 Position 和 Velocityclass MovementSystem extends EntityProcessingSystem { MovementSystem() : super(Aspect.all([Position, Velocity])); late ComponentMapperPosition positionMapper; late ComponentMapperVelocity velocityMapper; override void initialize() { super.initialize(); positionMapper world.getMapperPosition(); velocityMapper world.getMapperVelocity(); } override void process(Entity entity) { final position positionMapper.get(entity); final velocity velocityMapper.get(entity); position.x velocity.vx; position.y velocity.vy; } }Aspect.all表示系统只处理同时满足这些组件的实体每次 World 扫描时内部会维护候选实体列表。除 all 之外还有 one、exclude 等组合方式比如“有攻击力但不需要渲染”的系统。建议用 all 搭配 exclude 来精确描述系统输入这样你能保证系统内不需要反复判断组件是否存在。Mapper 是访问组件的主要入口mapper.get(entity)返回该实体上的组件实例。如果实体不满足条件通常返回 null 或抛异常我一般会在系统流程里先确保 Aspect 已经把候选列表过滤干净。dartemis 内部使用 Bag 这类无序容器来保存候选实体遍历时没有顺序保证所以不要在系统逻辑里依赖实体插入顺序需要严格顺序的地方应该自己维护排序列表而不是依赖框架的遍历顺序。实际开发里为了不让 System 显得过于臃肿我会把“查组件”和“改组件”的逻辑分别封装成小工具函数。比如一个moveEntity(entity, dx, dy)函数内部只改 Position外部所有系统都能复用。这样既保留了 ECS 的数据优势又不用每个 System 都写一堆重复的 Mapper 读取代码。3. 精密 ECS 架构治理从杂牌状态管理到系统性调度3.1 组件通信与 Flutter Widget 之间的桥接ECS 解决了数据与逻辑的划分但 Flutter 页面还需要把这些数据展示出来。直接让 Widget 持有 Component 引用是最糟糕的做法因为组件更新频率和 Widget 重建频率完全不在同一个节奏上。我采用的方案是在 ECS 系统和 Flutter UI 之间加一层“桥接对象”桥接对象内部持有 ValueNotifier只在 ECS 数据变化时更新需要暴露的数据。举一个实战例子主界面上方的玩家血条需要监听 Health 组件变化。我在桥接类里维护一个ValueNotifierint由一个专门的 UI Sync System 在每帧或每间隔时间读取 Health 组件并同步值class HealthBridge { final ValueNotifierint currentHp ValueNotifier(0); void sync(ComponentMapperHealth healthMapper, Entity player) { final health healthMapper.get(player); currentHp.value health.value; } }Widget 侧使用ValueListenableBuilder监听数据变化时自动更新不会整个页面重建。这个方法的核心原则是ECS 是数据源桥接对象是只读透传Widget 是纯展示层。如果哪块 UI 想要往 ECS 里写数据应该通过事件或命令发送给系统而不是让 UI 直接修改组件字段。在实际项目里桥接层数量不多通常就十来组分别对应玩家血条、位置指示、背包列表、战斗日志等。这些桥接对象统一注册到一个EcsBridgeManager里随着 World 初始化而初始化在页面销毁时统一释放避免 ValueNotifier 泄漏。这种做法也天然适配 Flutter 组件通信你想让两个兄弟 Widget 共享同一个桥接对象就往上层放一个 Provider或者用 InheritedWidget 把它传给子树。3.2 System 调度策略与生命周期管理一个玩法级项目会同时存在多个 System比如移动、碰撞、技能冷却、AI 行为、掉落生成。它们之间既有先后关系也可能在同一帧里互相影响。移动系统必须先于碰撞系统运行否则碰撞检测会基于上一帧的位置做计算看起来慢半拍。dartemis 没有强制规定所有系统必须用同一种顺序我习惯把系统按阶段分组输入阶段、逻辑阶段、物理阶段、渲染阶段然后在初始化时按这个顺序注册。final world World() ..addSystem(InputSystem()) ..addSystem(MovementSystem()) ..addSystem(CollisionSystem()) ..addSystem(DamageSystem()) ..addSystem(DropSystem()) ..addSystem(UiSyncSystem());World 初始化后每帧调用一次world.process()即可。如果希望在帧率不稳定时逻辑仍然平滑我会给逻辑系统传入自定义 deltaTime而不是依赖真实帧间隔。需要注意dartemis 里的系统对象一旦被注册进 World生命周期就交给 World 管理当实体被移除时相关的系统回调也会处理移除事件。生命周期方面的坑我踩过最深的是“资源释放”。如果一个实体绑定音频组件实体被销毁后如果不显式停止音频声音会一直播放。正确的做法是在系统的removed回调里做清理或者在实体进入回收队列前统一触发一个 DestroySystem由它负责调用所有需要释放的外部资源。鸿蒙和 Android 在这点上没有区别只是真机上音频和纹理资源泄漏的表现更明显连续进出几十次战斗场景就会看到内存持续上涨。3.3 Widget 数据同步与 Provider 的取舍很多 Flutter 项目喜欢把状态全交给 Provider 管理这在普通应用里很好用但放到 ECS 场景里要小心。Provider 按数据依赖关系自动重建 Widget 的特性和游戏每帧高频数值变化天然冲突。我见过有人把每个子弹的 Position 都放进 ChangeNotifier结果子弹数量一多页面直接卡成幻灯片。我现在的取舍规则是只有被展示模块订阅的状态才交给 Provider 或 ValueNotifier游戏世界内部的高频数据一律留在 ECS 里。比如“当前关卡”“玩家等级”“商店商品列表”这些低频状态适合放进 Provider它们与 ECS 的同步频率可能只有每秒几次而“角色坐标”“子弹速度”“血条数值”这类几十毫秒就会变化的数据不适合层层传递 Provider。表格做一个简单对比维度Provider / ValueNotifierdartemis ECS适合数据类型低频 UI 状态、页面配置高频游戏数据、大量实体属性更新粒度按依赖自动重建 Widget按组件读取精确定位变更调试成本较难追踪源头系统与组件边界清晰鸿蒙适配无原生依赖直接可用无原生依赖直接可用在实际工程里两者可以共存Provider 负责页面间共享的低频数据dartemis 负责玩法核心数据两者通过桥接对象连接。这也是我推荐给团队的默认结构既不用抛弃 Flutter 生态熟悉的状态管理写法也不会把所有状态压到一套体系里导致性能问题。4. 鸿蒙化适配实战构建可运行的 Flutter dartemis 工程4.1 鸿蒙化 Flutter SDK 环境准备做鸿蒙化适配之前我建议先准备一套独立的工具链目录不要和日常开发用的 Flutter SDK 混在一起。目前常见的方式是拉取 OpenHarmony 适配的 Flutter SDK 分支本地切换到对应分支然后在该 SDK 下重新编译 flutter_tools再用这个专用 SDK 来创建鸿蒙工程。大致流程如下拉取适配分支代码到本地目录例如flutter_ohos。配置环境变量PATH优先指向该目录下的bin。执行flutter doctor检查依赖重点看 Dart SDK 和编译工具链是否完整。创建新工程在支持鸿蒙平台生成的版本里执行flutter create --platformsohos my_game_project。用 DevEco Studio 打开工程下的ohos目录确认 SDK、签名、模块配置无误。如果你手头已经有一个 Flutter 工程想要增加鸿蒙平台目录可以先确认当前专用 SDK 是否支持flutter create --platformsohos .增量生成。不同分支命令的命名不完全一致有的版本仍使用flutter create --platformsharmony有的把ohos目录模板放到 examples 里。我在适配时发现最可靠的做法是先看工程根目录的README或tool脚本而不是盲猜命令。单独准备一套 SDK 的好处很明显dartemis 的 pubspec 可能声明了sdk: 3.0.0 4.0.0而普通 Flutter 官方 SDK 和鸿蒙分支 SDK 的 Dart 版本可能不一致。如果你用官方 SDK 跑flutter pub get再切到鸿蒙 SDK 编译版本约束可能直接报错。确保同一个项目始终用同一个 SDK 工作会省掉很多“在 A 机器能跑、在 B 机器跑不了”的烦恼。4.2 在鸿蒙工程中集成 dartemis工程跑通后加 dartemis 其实只是一个常规操作。在 pubspec.yaml 里添加dependencies: flutter: sdk: flutter dartemis: ^0.4.0然后执行flutter pub get。由于 dartemis 没有原生依赖这一步基本不会报错如果报错绝大多数原因是 pub 源配置或 SDK 版本约束冲突而不是库本身的问题。执行成功后在 Dart 代码里直接import package:dartemis/dartemis.dart;就能正常使用所有 ECS API。到这里很多人会以为适配已经结束了实际上还差临门一脚要把 Dart 代码打包进鸿蒙应用。在支持鸿蒙平台的 Flutter SDK 里flutter build会生成可供 DevEco Studio 打包的产物或者直接在命令行执行对应构建命令。构建产物需要被鸿蒙壳工程正确引用否则真机上启动应用时会提示找不到 Flutter 引擎或资源。我的建议是先用一个极简工程验证集成链路。在 main.dart 里只创建一个 World注册一个只打印日志的系统然后跑到鸿蒙模拟器或真机上确认日志输出。这能验证 dartemis 是否被成功编译进产物、World 是否能在鸿蒙环境下创建、System 是否能被执行。这个最小验证通过以后再逐步迁移业务系统避免一次加入大量代码后根本无法定位是哪个环节出了错。4.3 渲染与输入Impeller、纹理、触摸事件适配dartemis 本身不碰渲染但 Flutter 游戏逻辑会最终体现在画面上所以渲染引擎的适配情况也会影响项目整体体验。Impeller 作为 Flutter 的下一代渲染引擎在鸿蒙适配分支上是否开启取决于 SDK 分支的默认配置。如果项目里大量使用自定义 Shader 或复杂的 BlendMode建议先跑一个渲染压测放满屏粒子看帧率和功耗。如果出现花屏或完全空白不要第一时间怀疑 dartemis。多数情况下是渲染后端的兼容性问题这时候尝试在 flutter 配置里临时切回 Skia 渲染确认是否由 Impeller 触发。我的项目在鸿蒙真机上就遇到过文字模糊和部分纹理闪烁的问题最终切换后端后恢复正常但代价是某些性能优化无法启用。等到 SDK 分支更新后重新开启 Impeller 再验证一遍。输入方面鸿蒙设备的触摸事件会通过 Flutter 引擎框架层传递到 Dart 侧dartemis 系统并不直接监听触摸。我通常在 Flutter 的 Listener 或 GestureDetector 里接收触点然后转换为游戏内指令投递给输入队列。需要特别注意的是不要在触摸回调里直接创建大量实体或修改组件高频触碰会造成 UI 线程卡顿。正确的做法是把触摸事件先放进一个队列由 InputSystem 在world.process()时统一消费。这样输入频率再高对逻辑层来说都是一帧内的批量数据不容易出现“连点导致逻辑乱序”的竞态问题。后面在鸿蒙真机上调试时这个队列也能很方便地记录回放把输入序列存成日志就能复现某个触控场景下的 bug。5. 常见问题与排查技巧实录5.1 类库冲突与 API 差异集成过程中最容易碰到的是“名字冲突”。dartemis 里World、Entity、Component都是非常通用的名词如果项目里还有其他库或自己写的工具类也定义了同名类型import 时会报 ambiguous import。解决办法是给 dartemis 加别名import package:dartemis/dartemis.dart as ecs;这样所有 ECS 类型都带ecs.前缀虽然写代码时多打几个字符但能彻底避免命名污染。如果你只是局部冲突也可以用show或hide选择性导入。API 差异是另一个高频问题。dartemis 的历史版本中创建实体、添加组件、定义 Aspect 的写法有细微变化。我遇到过从较老版本升级后原本返回Entity的createEntity()变成了空实体加手动入库的写法。碰到这种情况直接对照 pub 页面上的 CHANGELOG 或升级文档调整即可不要盲目搜索别人的老代码。5.2 线程模型与异步调度问题ECS 强调单线程同步处理这也是它容易被理解和调试的原因之一。但 Flutter 项目里免不了要使用异步操作比如加载关卡配置、读取存档、发起网络请求。一个常见错误是把 World 对象直接传给compute或Isolate在后台线程操作实体和组件。dartemis 内部的 Mapper、Bag、实体索引都不是线程安全的多线程并发修改会导致索引错乱、组件丢失、甚至随机 crash。我现在的规范是World 永远只属于 UI 线程异步任务只处理“数据准备”拿到结果后回到主 isolate再往 World 里注入数据。比如网络下载的配置 JSON在后台 parse 完主线程里创建实体和组件。如果某个计算特别耗时比如大规模寻路或战斗回放模拟正确的做法是用后台 isolate 计算一个纯粹的模拟结果比如最终的坐标数组、伤害统计然后把结果传回来应用到 World。我见过有人试图把整场战斗放进后台 isolate 跑结果同步 UI 变得非常复杂后来还是改成“后台计算 主线程应用”的模式。5.3 内存管理与对象池调优ECS 可以降低对象创建频率但并不意味着不会产生垃圾。每次createEntity、mapper.create都会有新对象产生。在弹幕类游戏里上万发子弹一帧内生成、下一帧销毁GC 压力会非常明显。dartemis 不像 Unity ECS 那样自带托管组件数组但我们可以手动做对象池。我给子弹系统做了一个简单的池子初始化时预创建一批 Position、Velocity、SpriteInfo 组件实例实体发射时从池里取出组件并重置字段子弹销毁时把组件归还池子。这样实体 ID 和组件实例都能复用GC 压力大幅下降。组件池的命中场景最适合“高频生成、短生命周期”的实体。具体的池化代码如下class ComponentPoolT extends Component { final ListT _free []; final T Function() _factory; ComponentPool(this._factory); T obtain() { return _free.isNotEmpty ? _free.removeLast() : _factory(); } void recycle(T component) { _free.add(component); } }使用池化时要注意归还组件前必须把所有字段重置为初始值否则下一个实体拿到残留数据会造成诡异 bug。我在调试时就碰到过一颗子弹带着上一颗子弹的伤害值飞出去排查了很久才发现是池子里的组件没有重置属性。建议在recycle方法里强制调用一个 reset 回调。5.4 适配鸿蒙真机时的高频报错鸿蒙真机调试和模拟器表现并不完全相同有些问题只在真机上出现。我把常见的报错整理成一张速查表方便大家直接对照报错现象可能原因处理方式MissingPluginException某个 Flutter 插件缺少鸿蒙原生实现换成支持 ohos 的适配插件或改成纯 Dart 实现启动后白屏Flutter 引擎产物未正确打包进 HAP重新执行鸿蒙构建检查产物资源路径渲染花屏或闪烁Impeller 与 GPU 驱动兼容问题临时切换 Skia 渲染升级 SDK 分支flutter pub get卡住或失败pub 源访问异常检查网络与 pub 源配置不要随意设置代理编译报duplicate class原生依赖冲突排查重复引入的第三方库统一版本最后一类重复类冲突多发生在同时引用多个鸿蒙原生插件的场景dartemis 一般不掺和原生层但项目里其他库可能拖进来。遇到报错先看日志里的堆栈是 Dart 层还是原生层原生层的问题再进入 DevEco Studio 的编译日志排查。还有一点值得提醒鸿蒙真机上调试时打开开发者模式并允许 USB 调试正常连接后 DevEco Studio 能看到设备信息。如果 Flutter 命令行工具一直识别不到设备先确认鸿蒙专用 SDK 的 adb 相关工具链和 DevEco Studio 是否一致。我一开始总是习惯性用旧版 Android adb 去连鸿蒙设备结果经常出现设备列表为空或断连换了配套工具之后才真正顺畅起来。我个人在实际操作中的体会是ECS 的价值不是炫技而是让数据变更路径变得可预期。dartemis 作为纯 Dart 库在鸿蒙化适配中的坑远少于那些带原生代码的库只要先把 Flutter 引擎的鸿蒙运行环境理顺再用“桥接层”把 ECS 数据和 UI 隔离开整个项目的复杂度是可控的。如果你也想做类似的事情建议不要急着把全部业务迁到 ECS先从子弹、敌人这类高频实体开始试点跑通以后再逐步扩大范围。这样既验证了架构也降低了团队的上手阻力。最后再分享一个小技巧所有组件类都尽量保持纯数据、无方法唯一可以放的是 reset 和 copyWith这样不管是存盘、同步还是池化复用都会省掉无数麻烦。
返回列表