Unity EasyECS 五分钟快速上手:只加一个 [ECS]

前几篇一直在讲 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。

相关推荐
SmalBox1 天前
【节点】[Twirl节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[Triplanar节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[TilingAndOffset节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox4 天前
【节点】[Spherize节点]原理解析与实际应用
unity3d·游戏开发·图形学
Behavior5 天前
Unity游戏之Jenkins的安装部署应用
unity3d·游戏开发
SmalBox5 天前
【节点】[Rotate节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox6 天前
【节点】[RadialShear节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox7 天前
【节点】[PolarCoordinates节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox8 天前
【节点】[PixelateUV节点]原理解析与实际应用
unity3d·游戏开发·图形学