恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Unity ECS入门:用Entitas理清数据与逻辑,摆脱MonoBehaviour混乱

  • 首页
  • 资讯中心
  • /
  • Unity ECS入门:用Entitas理清数据与逻辑,摆脱MonoBehaviour混乱

相关资讯

探矿RAG知识库:从乱码清洗到高精度检索的完整实践 2026/10/5 12:31:03
基于A3C强化学习的网络流量异常识别:KDDCup99数据处理与模型调参实战 2026/10/5 12:31:03
基于深度学习的垃圾分类项目实战:从解压到部署全流程 2026/10/5 12:31:03

最新资讯

Java 项目实战: 外卖平台优化-Nginx目录结构与conf配置文件体系
Java 项目实战: 外卖平台优化-Nginx配置文件结构与块层级
Ubuntu安装Anaconda并配置环境
【工业篇02】CNC加工中心综合效能评估:CRITIC-TOPSIS四维量化(精度/换刀/故障/可用度)
Java 项目实战: 外卖平台优化-Nginx部署静态资源与location匹配
LaTeX2e graphics bundle 完全指南:color、graphics、graphicx 与配套工具包深度解析

今日推荐

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单
YOLOv5 OBB旋转框训练实战:从DOTA数据准备到调参避坑全流程
Zeron 终端、Worktree 与 Diff 面板:像 IDE 一样查看并驱动你的代码变更

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Unity ECS入门:用Entitas理清数据与逻辑,摆脱MonoBehaviour混乱

发布时间:2026/10/5 12:31:03
Unity ECS入门:用Entitas理清数据与逻辑,摆脱MonoBehaviour混乱 经常有朋友问我Unity 项目脚本一多各种 Manager 越挂越多改一个功能就像牵一发动全身怎么办我的建议通常是先别急着继续堆代码去看看 ECS 这套思想然后从 Entitas 插件入手实操一把。Entitas 是 Unity 社区里用得最广的纯 C# ECS 框架插件下载、导入、Hello World 跑通其实半天就能完成。真正花时间的是把脑子从“GameObject 组件思维”切到“数据加逻辑分离”的频道上。这篇文不聊虚的直接带你走一遍从下载到能跑的完整流程顺便把新手最容易卡住的几个坑提前排掉。无论你是做玩法逻辑、数字孪生、串口通信还是和 PLC 联调只要项目里状态变多、对象变杂ECS 这套组织方式都能让你的代码结构清爽一大截。Entitas 就是目前最适合用来入门 ECS 的跳板因为它的学习曲线比官方 DOTS 平缓得多而且网上资料多、案例扎实。1. ECS 到底是什么意思先忘了你熟悉的 GameObject 那一套1.1 一次“改需求”引发的思考我以前带项目时最怕的不是写新功能而是改老逻辑。比如说角色脚本里血量、移动、攻击、音效、动画状态全都堆在一个 MonoBehaviour 里。需求一变说“所有 AI 敌人也要能播放脚步声但不要跟着玩家移动”你就得在这个大类里加开关、加判断、加状态分支。改完这一处还得担心另一处被影响。时间一长每个脚本都变成了一个“啥都能干”的瑞士军刀。后来接触 Entitas 才发现ECS 解决的正是这个痛点它逼你把“这个对象是谁”和“这个对象能干什么”分开。你要移动一个角色不是去调用角色身上的 Move 方法而是往一个实体上挂一份“移动数据”然后系统会自动扫到所有挂了这份数据的实体统一处理它们。需求变了那就给 AI 敌人加一份移动速度数据不加就不动逻辑全在系统里互不打扰。这种拆分方式第一次接触会觉得绕但一旦跑通一个小例子你就会发现它特别适合信息密度高的项目。尤其是现在做数字孪生、做多设备联调的项目状态同步逻辑又碎又多传统写法光维护“哪个类负责更新哪个字段”就能把人绕晕。ECS 的“数据归数据、逻辑归逻辑”反而是最不容易出乱子的组织方式。1.2 Entity、Component、System 三个词的白话解释ECS 的三个字母分别对应 Entity、Component、System我用厨房流水线来类比。Entity 是实体但实体本身不存任何逻辑它更像一个标签或编号。你厨房里有一份订单订单号就是 Entity。订单本身不负责做菜也不负责买菜它只负责把“是谁点的、点了什么菜”串起来。Component 是组件也就是纯数据。对应到订单上就是“宫保鸡丁一份”“少放辣”“加急”这些具体条目。在 Entitas 里一个组件就是一个类里面只有字段没有方法。比如位置组件就存一个 Vector3速度组件就存一个 float显示组件就存一个 GameObject 引用。组件不带逻辑这是整个 ECS 最核心的约束。System 是系统也就是逻辑处理器。厨师是系统他只关心“现在有哪些订单需要做宫保鸡丁”拿到数据就去炒。在代码里一个 System 就是一个类类里面有 Execute 方法方法里遍历符合条件的所有实体读取组件数据、修改组件数据。系统之间天然解耦一个系统只负责一件事。这就是 ECS 的全部核心实体只是身份组件只是数据逻辑全部收敛到系统里。听起来简单但实际写代码时需要刻意练习尤其是“组件里不写方法”这条老 Unity 程序员特别容易忍不住。1.3 为什么这套设计能变快缓存、复用、说白了就是流水线ECS 快的原因有两层。第一层是数据访问方式变了。传统写法里每个 GameObject 上的组件分散在内存各处你要遍历一百个角色就要去一百个不同地方拿数据。而 ECS 系统在遍历时只关心自己需要的那几类组件数据排布更紧凑CPU 缓存命中率自然更高。Entitas 虽然没做到官方 DOTS 那种极致块状内存但在普通玩法项目里的性能表现已经比到处 GetComponent 强很多。第二层是逻辑复用。以前你要让子弹、敌人、NPC 都能被推动得给它们分别写脚本或者搞抽象基类。在 ECS 里不用折腾继承谁需要被移动就往谁身上加一份 MoveSpeed 组件同一个移动系统自动接管。这就是流水线思维不同工件经过同一道工序工序本身只认“料”不认“工件是谁”。并行也是 ECS 的潜在优势。不同系统如果操作的是不同组件集合互不依赖就可以并行执行。Entitas 本身默认是单线程的但你已经用上了“数据分离”的骨架以后切到官方 DOTS 或者自己上 Job System迁移成本会低很多。思想通了换工具只是时间问题。2. 为什么选 Entitas不是官方 DOTS 买不起而是它有性价比2.1 官方 DOTS 与 Entitas 的定位差异Unity 官方这几年一直在推 DOTS核心是 Entities 包、Job System 和 Burst 编译器。官方方案天花板确实高能压榨多核 CPU适合大规模实体计算的场景。但代价也很明显学习曲线陡你得同时理解 ECS 思想、Job 的调度规则、Burst 的编写约束还要处理 Entities 相关 API 的版本变化。很多朋友一看官方教程就劝退不是看不懂是“要补的课太多”。Entitas 恰好是个轻量替代品纯 C# 实现不依赖 Unity 新版本特性老项目也能用。你把 Entitas 玩明白了ECS 的核心概念就通了再去看官方 DOTS至少不会被 Entity、Component、System 这些名词吓到。另外有个现实问题如果你做的是中小型项目逻辑复杂度远没到需要多线程满负荷运转的地步DOTS 的极致性能你用不上反而徒增配置成本。Entitas 这种“能把自己板板正正组织起来”的框架性价比反而更高。2.2 Entitas 的核心优势和适用人群Entitas 最大的优势是代码生成。你定义好组件类点击一次 Generate它就会自动生成实体访问器、Matcher、Context代码类型安全洗IDE 智能提示也跟得上。写entity.AddMoveSpeed(2f)比手写字典、反射查组件靠谱多了编译期就能查错省掉的调试时间非常可观。其次Entitas 是老牌开源项目GitHub 上仓库是 Entitas-CSharpMIT 协议免费商用。社区里案例多Stack Overflow 和 Unity 论坛里的讨论也多遇到问题基本都能搜到答案。这对入门非常重要因为 ECS 和传统开发思维差异大没有足够样本参考很容易自己绕进死胡同。适用人群也清晰刚接触 ECS 想低成本入门的 Unity 开发者、被面向对象继承搞到头疼的中型项目团队、想在老工程里局部引入新架构的人。当然如果你做的是超大规模同屏战斗、海量 AI 模拟这类对性能有极致要求的项目最终还是要看官方 DOTS但先用 Entitas 建立心智模型绝对不亏。2.3 开始编码前必须认识的四个关键对象Entitas 里四个核心概念先混个脸熟后文示例里会反复出现。Context 是上下文容器。你可以把 Context 理解成一个“实体仓库”它管理着一批实体的创建、销毁和查询。最常见的是 GameContext 和 InputContext前者放游戏对象后者放输入状态。在代码里你通过Contexts.sharedInstance.game拿到游戏上下文。Entity 是上下文里的实体。它本身只是索引通过CreateEntity()创建通过AddXxx挂组件通过hasXxx判断组件通过ReplaceXxx更新组件。Group 是实体分组。它根据组件匹配规则自动维护一个实体集合。比如“所有同时拥有 MoveSpeed 和 Position 的实体”就是一个 Group系统通过GetGroup获取到它然后遍历GetEntities()拿到当前所有匹配实体。新增实体或移除组件时Group 会自动更新不用手动维护。Collector 和 ReactiveSystem 是事件响应机制。Collector 负责收集“某个组件发生变化”的实体ReactiveSystem 则是专门处理这批变化的系统。例如“位置组件被替换”的实体会被自动收集然后喂给 SyncTransformSystem 去同步显示。掌握了这四个对象Entitas 的地基就算打完了。3. 插件下载与导入这一步卡住的人最多3.1 从哪里下载 EntitasEntitas 的下载地址最靠谱的就是 GitHub 仓库github.com/sschmid/Entitas-CSharp。进到 Releases 页面下载最新稳定版压缩包。注意区分源码包和发布包格式有些版本提供的是Entitas.zip里面是直接可以拖进 Unity 的目录有些版本提供的只是源码归档需要你自己把Entitas、Entitas.CodeGeneration等文件夹挑出来。以前 Unity Asset Store 上也有免费版但后来维护节奏不稳定版本偏老新工程容易踩兼容坑。我个人的建议是优先 GitHub 拿最新稳定版毕竟社区维护和 issue 反馈都集中在那里。下载完成后先解压别急着拖进工程先看一眼根目录的 README。里面通常会写清楚当前版本要求的 Unity 版本范围以及是否需要额外的生成器依赖。这一步能帮你提前避开一半的导入报错。3.2 导入 Unity 工程与目录结构说明导入方式有两种本质都一样把源码放进工程。最简单粗暴的方式是把解压后包含Entitas.dll或者Entitas源码目录的文件夹整体拖到 Assets 下。如果下载的是源码版你会在文件夹里看到类似Entitas/、Entitas.CodeGeneration/、Entitas.Utils/等结构全部拖进去即可。拖进去之后 Unity 会触发编译这时候先不要做任何操作等 Console 窗口安静下来。如果编译通过菜单栏顶部会出现Entitas菜单。如果没有十有八九是解压了错误的文件夹或者把测试工程也拖了进来。目录结构上Entitas 源码和你的业务代码最好分开放。我习惯在 Assets 下建一个ThirdParty/Entitas用来放框架源码然后建一个独立的Scripts/Generated来放自动生成的代码。另外 Entitas 包里自带了一些 Demo 场景和示例 Context新手期不建议删掉等你能独立跑通自己的最小示例后再清理也不迟。3.3 打开工程第一次生成代码这一步是整个 Entitas 入门流程里最容易卡住的环节。组件定义好之后不会自动生成实体访问器必须手动执行菜单里的生成命令。打开菜单栏Entitas Preferences先看Code Generation面板。确认 Target Folder 指向一个存在的目录比如Assets/Generated如果目录不存在先创建好。再看生成器列表尽量保持默认勾选尤其是组件的代码生成器取消勾选会导致访问器不生成。设置没问题后点菜单栏Entitas Generate。执行完看一眼 Console 面板正常情况会有生成日志且 Assets 下会多出一个Generated文件夹里面有Contexts.cs、GameEntity.cs、GameMatcher.cs等文件。这些就是自动生成的强类型访问器。之后只要改了组件定义比如加字段、加新组件都要重新 Generate 一次不然新代码引用不到旧访问器编译直接报红。3.4 怎么判断导入成功判断导入成功有四个信号全中才算稳。第一Unity 编译无红错。第二菜单栏出现Entitas菜单项。第三执行 Generate 后生成Generated文件夹且文件内容不是空的。第四你在组件类里写[Game]这种 Context 特性时IDE 能识别出来。如果前面几步都正常但写组件时提示找不到Game这个命名空间或类型说明 Context 定义缺失是生成前置条件没满足。常见原因是安装包里自带的 Context 定义文件被手动删了或者第一次生成前故意清理了工程。这时候回看 3.3确认输出目录和生成器配置都没问题重新 Generate 一般能解决。4. 十五分钟跑通一个最小示例让一个方块自己往前走4.1 定义组件先把“数据”从脚本里拆出来我们做一个最简单的功能场景里创建一个方块让它以固定速度沿 X 轴移动。用传统写法你会创建一个 MoveCube 脚本在 Update 里写transform.position Vector3.right * speed * Time.deltaTime。用 Entitas我们把这个需求拆成三个组件。在 Assets 下新建一个Scripts/Components文件夹创建以下三个 C# 文件using Entitas; using UnityEngine; [Game] public sealed class MoveSpeedComponent : IComponent { public float value; } [Game] public sealed class PositionComponent : IComponent { public Vector3 value; } [Game] public sealed class ViewComponent : IComponent { public GameObject value; }注意命名规则类名最后一个词必须是以Component结尾Entitas 生成代码时会自动去掉这个后缀生成entity.moveSpeed、entity.position、entity.view这样的访问器。组件里只放字段不写方法。这是规矩也是后面所有自动化的基础。写完这三个组件后打开菜单栏Entitas Generate生成一次代码。现在你可以打开Generated/GameEntity.cs看一眼里面已经自动生成了AddPosition、ReplacePosition、AddView这些方法。4.2 写一个移动系统该处理哪些实体系统负责真正的逻辑。我们创建一个 MoveSystem目标是遍历所有“同时拥有 MoveSpeed 和 Position”的实体把 Position 往 X 轴方向推。using Entitas; using UnityEngine; public sealed class MoveSystem : IExecuteSystem { private readonly IGroupGameEntity _movers; public MoveSystem(Contexts contexts) { _movers contexts.game.GetGroup( GameMatcher.AllOf(GameMatcher.MoveSpeed, GameMatcher.Position) ); } public void Execute() { foreach (var entity in _movers.GetEntities()) { entity.ReplacePosition( entity.position.value Vector3.right * entity.moveSpeed.value * Time.deltaTime ); } } }这段代码的核心在于GetGroup和GameMatcher.AllOf。它的意思非常直白只要实体同时带着移动速度和位置我就把它纳入处理范围。新增实体、移除组件时Group 自动更新不需要你手动维护列表。这里必须强调的是用ReplacePosition而不是直接改字段。为什么因为 Entitas 的响应系统靠“组件被替换”来感知变化。如果你直接写entity.position.value xxxReactiveSystem 是收不到信号的。改用ReplacePosition每次替换都会触发事件后面的同步系统才能及时工作。4.3 创建实体把数据和表现粘起来有了组件和系统接下来要往上下文里放一个实体给它挂上移动速度、初始位置再把场景里的方块绑定到 View 组件上。在场景里创建一个空物体挂一个新的GameController脚本using Entitas; using UnityEngine; public class GameController : MonoBehaviour { private Systems _systems; private Contexts _contexts; private void Start() { _contexts Contexts.sharedInstance; var cube GameObject.CreatePrimitive(PrimitiveType.Cube); cube.name MoveCube; var entity _contexts.game.CreateEntity(); entity.AddPosition(Vector3.zero); entity.AddMoveSpeed(2f); entity.AddView(cube); _systems new Feature(Game) .Add(new MoveSystem(_contexts)) .Add(new SyncTransformSystem(_contexts)); _systems.Initialize(); } private void Update() { _systems.Execute(); _systems.Cleanup(); } private void OnDestroy() { _systems.TearDown(); _contexts.Reset(); } }这里有几个设计细节值得解释为什么方块要单独绑定 View 组件而不是让实体持有 Transform 引用其实你也可以持有 Transform但把它包装成 View 组件会让“表现层”和“逻辑层”的界限更清晰。实体管的是数据View 组件只是逻辑层和渲染层的一座桥。后面你要换模型、换特效只需要改 View 的同步逻辑数据系统完全不用动。4.4 用 Feature 把系统串起来上面 Start 里写的Feature(Game)是 Entitas 提供的一个系统组容器它负责按顺序执行所有子系统的生命周期。在 Feature 里 Add 的系统会在每次Execute()时按加入顺序依次执行Cleanup()则处理一些系统内部需要清理的临时数据。关于执行顺序有个实际建议像 MoveSystem 这种修改纯数据的系统尽量排在渲染同步系统前面。如果你先同步了 Transform再改 Position画面就会慢一帧。Feature 本身就是有序的你 Add 的顺序就是执行顺序按“逻辑在前、表现在后”的节奏来排。另外_systems.Initialize()只需要调用一次放在 Start 里Execute()需要每帧调用放在 Update 里场景销毁时调用TearDown()并Reset()上下文避免 Play 模式反复进出时数据残留。4.5 用 ReactiveSystem 把位置同步到 Transform最后一块拼图是同步显示。MoveSystem 一直在改实体的 Position 数据但场景里的方块不会自己跟着动必须有人把 Position 写给 Transform。我们用 ReactiveSystem 来实现它只响应“Position 被替换”的实体。using System.Collections.Generic; using Entitas; using UnityEngine; public sealed class SyncTransformSystem : ReactiveSystemGameEntity { public SyncTransformSystem(Contexts contexts) : base(contexts.game) { } protected override ICollectorGameEntity GetTrigger(IContextGameEntity context) { return context.CreateCollector(GameMatcher.Position); } protected override bool Filter(GameEntity entity) { return entity.hasPosition entity.hasView; } protected override void Execute(ListGameEntity entities) { foreach (var entity in entities) { entity.view.value.transform.position entity.position.value; } } }GetTrigger 定义了这个系统关心什么变化所有Position组件被添加、替换或移除的实体都会被收集。Filter 是二次过滤我要确保这个实体不仅有位置还绑定了 View否则同步时拿到空引用就会报错。这段逻辑跑起来后你就能看到方块匀速往前移动了。数据在 MoveSystem 里被修改事件被 ReactiveSystem 感知最终同步到显示层。整个过程里没有谁调用谁全靠组件变化“驱动”这正是 ECS 最有魅力的地方。5. 新手上路最容易踩的坑每条都是真金白银换来的5.1 没生成代码就开写报一堆“找不到 Game”我刚接触 Entitas 时犯过这个错组件写好了System 也写好了一编译满屏The type or namespace name Game could not be found。原因是访问器没生成GameMatcher、GameEntity、Contexts这些类还不存在。遇到这类报错先别怀疑代码写错回头执行一遍Entitas Generate然后等编译完再看。每次新增或修改组件都必须重新 Generate 一次这是使用 Entitas 的基本纪律。养成习惯后你会发现报错率直线下降。还有一个相关的坑Generated 文件夹如果被 .gitignore 忽略了别人拉代码后再打开工程会因为没有生成文件而编译失败。解决方案有两个要么把 Generated 纳入版本控制要么写一个启动时自动生成的编辑器脚本。新手期建议直接提交省事。5.2 ReactiveSystem 不执行先检查 Trigger 和 FilterReactiveSystem 不触发是新手最常见的问题。我之前写过一次GetTrigger 返回GameMatcher.Position实体确实有 Position但 Execute 就是不走。查了半天发现 Filter 里要求同时有View而那个实体恰好没绑 View所以被过滤掉了。排查思路很简单先确认 Trigger 里监听了哪个组件变化再确认 Filter 里的条件是不是过严。调试时可以在 Execute 开头加一行 Debug.Log把实体数量打出来如果数量为 0说明实体没被收集问题在 Trigger如果数量大于 0 但实际没同步问题在 Filter 或者同步逻辑本身。另一个容易被忽略的点如果你通过直接改字段来更新组件而不是用ReplaceXxx方法ReactiveSystem 是感知不到的。所有你要被监听的组件变更都必须走“替换组件”这条路。5.3 组件里放 GameObject 到底行不行很多刚接触 ECS 的人会纠结纯 ECS 要求组件只放数据但我的实体必须要显示啊GameObject 引用放哪我的实践结论是可以放但要把它定义成“表现引用组件”并且所有对 GameObject 的读写都收敛到同步类系统里。你可以在ViewComponent里存 Transform、存 Animator、存 ParticleSystem但如果一个组件又存坐标又存动画又存音效那等于回到了 MonoBehaviour 堆代码的老路。把引用组件和逻辑数据分开核心收益是测试方便。逻辑系统不依赖场景对象纯数据驱动单元测试好写。表现层系统再够薄一点换 UI、换模型都不影响核心逻辑。5.4 静态 Contexts 在切换场景和播放模式时的残留问题Contexts.sharedInstance是静态的这意味着它不会随着场景切换自动清空。如果你在场景里创建了一堆实体退出 Play 模式或者切换场景后这些实体还在静态上下文里。如果你在 Unity 的 Player 设置里关闭了 Domain Reload这种残留会更明显。重进 Play 模式时上一次的实体和组件数据还在导致各种奇奇怪怪的重复生成问题。我的习惯是在 GameController 的 OnDestroy 里调用_contexts.Reset()把上下文数据清干净。如果项目里多个场景都要用 Entitas最好做一个全局管理器统一处理上下文的初始化和清理不要让每个场景自己单独建。5.5 给新手的四条实操建议第一别急着改造老项目。单独建一个小工程把上面这个“方块移动”的示例跑通再慢慢加需求。第二组件类的字段尽量少一个组件只表达一个维度的数据粒度太小可以合并粒度太大后面拆起来想哭。第三一个 System 只做一件事MoveSystem 就只负责移动SyncTransformSystem 就只负责同步显示别写成“能跑就行”的万能系统。第四命名规范从一开始就定好组件后缀 Component系统后缀 SystemContext 名全工程统一代码生成后你会感谢这个好习惯。Entitas 这套框架入门只要半天真正难的是把命令式的“我要他走”变成声明式的“他带上了移动数据”。等你想明白这一点再回头看官方 DOTS很多概念会瞬间通透。我现在做复杂玩法系统时依然会在脑内先拆一遍 Entity、Component、System再决定工程结构这个习惯就是从 Entitas 时代养成的一直受用到现在。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号