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