前几篇一直在讲 EasyECS 为什么存在、AoS、SoA 和 CPU Cache。
从这一篇开始,不再只讲原理。
我们直接把 EasyECS 跑起来。
如果已经有一个普通的struct,最基本的接入实际上只需要:
[ECS]然后剩下的大量代码都由 Source Generator 自动生成。
项目地址
EasyECS 本身就是MyFramework仓库中的一个独立 Unity Package。
MyFramework 主仓库:
https://github.com/ZHOURUIH/MyFrameworkEasyECS 的 Unity Package Manager 地址:
https://github.com/ZHOURUIH/MyFramework.git?path=/Packages/com.zhourui.easyecs当前版本:
EasyECS 1.1.0第一步:安装 EasyECS
打开 Unity:
Window → Package Manager → + → Add package from git URL填入:
https://github.com/ZHOURUIH/MyFramework.git?path=/Packages/com.zhourui.easyecs安装完成以后,就可以:
using EasyECS;第二步:定义一个普通 Struct
先写一个最普通的角色数据:
public struct RoleData { public int mHP; public float mSpeed; public float mPositionX; public float mPositionY; public int mID; public int mModelID; public int mCamp; }如果按照传统方式使用,通常就是:
List<RoleData> roles = new();现在我们只做一个修改:
using EasyECS; [ECS] public struct RoleData { public int mHP; public float mSpeed; public float mPositionX; public float mPositionY; public int mID; public int mModelID; public int mCamp; }增加:
[ECS]以后,这个 Struct 就会被 EasyECS 的 Source Generator 处理。
第三步:创建 ECSList
编译完成以后,EasyECS 会根据:
RoleData自动生成:
RoleDataECSList所以可以直接写:
RoleDataECSList roles = new RoleDataECSList();也可以提前指定 Capacity:
RoleDataECSList roles = new RoleDataECSList(1024);如果大概知道数据量,我一般更建议提前给 Capacity。
原因和List<T>类似:
避免不断 Resize。
第四步:Add 数据
添加数据仍然是非常熟悉的写法:
roles.Add(new RoleData { mHP = 100, mSpeed = 5.0f, mPositionX = 10.0f, mPositionY = 20.0f, mID = 1001, mModelID = 2001, mCamp = 1, });还可以继续:
roles.Add(new RoleData { mHP = 200, mSpeed = 6.0f, mID = 1002, });使用体验和:
List<RoleData>.Add()非常接近。
但底层存储已经不再是简单的:
RoleData[]第五步:访问数据
普通List<struct>有一个经常让人觉得麻烦的地方。
例如:
RoleData role = roles[index]; role.mHP -= 10; roles[index] = role;因为普通 struct 是值类型,取出来以后得到的是副本。
EasyECS 生成的 ECSList Indexer 返回的则是:
RoleDataRef所以可以直接:
RoleDataRef role = roles[0]; role.mHP -= 10; role.mPositionX += role.mSpeed;修改会直接作用到底层 Storage。
甚至可以写:
roles[0].mHP -= 10;不需要:
RoleData temp = ... ... roles[0] = temp;这也是 EasyECS 希望保留的使用体验之一。
[ECS]背后发生了什么?
代码中只写了:
[ECS] public struct RoleData但 Source Generator 实际会生成一整套相关类型。
其中比较重要的包括:
RoleDataStorage RoleDataRef RoleDataECSList RoleDataECSDictionary<TKey> Direct Column例如RoleDataECSList负责:
Add Insert RemoveAt RemoveAtSwapBack Clear Indexer Capacity Dispose所以开发者不需要自己去维护:
int[] hp; float[] speed; float[] positionX; float[] positionY;也不需要自己处理这些数组之间的同步。
默认情况下,字段会进入 SoA
如果 Struct 使用:
[ECS]那么字段默认按照 ECS 字段处理。
例如:
[ECS] public struct RoleData { public int mHP; public float mSpeed; public float mPositionX; public float mPositionY; }从数据布局上可以简单理解成:
mHP[] mSpeed[] mPositionX[] mPositionY[]也就是前面几篇一直讲的 SoA。
不想拆开的字段怎么办?
实际项目中,并不是所有字段都需要 SoA。
例如:
HP Speed Position可能每帧都要处理。
但:
ID ModelID Camp可能很少参与批量计算。
那么可以写成:
[ECS] public struct RoleData { public int mHP; public float mSpeed; public float mPositionX; public float mPositionY; [NotECS] public int mID; [NotECS] public int mModelID; [NotECS] public int mCamp; }这样:
HP / Speed / Position走 SoA。
而:
ID / ModelID / Camp继续保留 AoS。
这就是 EasyECS 的:
SoA + AoS Hybrid Storage。
具体规则下一篇会专门讲。
用完记得 Dispose
ECSList实现了:
IDisposable所以生命周期结束以后需要:
roles.Dispose();例如组件自己持有:
private RoleDataECSList mRoleList; private void Awake() { mRoleList = new RoleDataECSList(1024); } private void OnDestroy() { mRoleList?.Dispose(); }这是因为 EasyECS 在合适的 Backend 下会使用 Native Storage。
它和普通:
List<T>只依赖 GC 回收不是一回事。
一个最小完整例子
把前面的内容放在一起:
using EasyECS; using UnityEngine; [ECS] public struct RoleData { public int mHP; public float mSpeed; public float mPositionX; public float mPositionY; [NotECS] public int mID; } public class EasyECSTest : MonoBehaviour { private RoleDataECSList mRoles; private void Start() { mRoles = new RoleDataECSList(1024); mRoles.Add(new RoleData { mHP = 100, mSpeed = 5.0f, mID = 1001, }); RoleDataRef role = mRoles[0]; role.mHP -= 10; role.mPositionX += role.mSpeed; Debug.Log($"HP:{role.mHP},X:{role.mPositionX}"); } private void OnDestroy() { mRoles?.Dispose(); } }到这里,其实已经完成 EasyECS 最基本的接入了。
写在最后
EasyECS 最基础的使用流程可以压缩成四步:
安装 Package ↓ struct 添加 [ECS] ↓ 使用生成的 xxxECSList ↓ 结束时 Dispose最重要的是,开发者仍然维护:
public struct RoleData而不是手工维护一堆:
int[] float[] float[] float[]EasyECS 把这些重复又容易出错的工作交给 Source Generator。
不过这里马上会出现一个问题:
是不是加了
[ECS]以后所有字段都必须变成 SoA?
当然不是。
下一篇:
[ECS]和[NotECS]不只是两个标签:EasyECS 的数据布局规则详解
我们会正式进入 EasyECS 的 Hybrid Storage,看看一个 Struct 里怎么同时存在 AoS 和 SoA。