更新日期:2020年6月8日。
Github源码:[点我获取源码]
ECS - 实体-组件-系统,此ECS非Unity的ECS,并不一定会带来性能的提升,只是基于ECS的思想,建立在Unity现有的组件模式之上,以ECS模式进行开发可以避开项目后期繁重的继承链,提升开发速度和质量、以及项目稳定性。
HTFramework的ECS(HTECS)保持与Unity官方的ECS相同的开发模式,在HTECS中可以尝试将编码习惯逐渐脱离OOP,在未来Unity DOTS盛起的大趋势下可以无缝转接,且HTECS基于Unity源生的组件结构,使得HTECS的组件和实体足够透明,你可以像控制MonoBehaviour那样去控制他们。
ECS的组件类必须满足以下条件:
1.继承至ECS_Component。
2.标记ComponentName特性(选择性,用来在检视面板快速识别一个组件的功能)。
3.标记DisallowMultipleComponent特性(选择性,但建议始终标记,因为同类型的组件即使挂在一个实体上,也只会有一个组件生效)。
推荐使用快捷创建方式,所有条件会自动帮你填充:
如下,我新建了一个RotateComponent旋转组件:
///
/// 旋转组件
///
[DisallowMultipleComponent]
[ComponentName("旋转组件")]
public sealed class RotateComponent : ECS_Component
{
///
/// 旋转轴
///
public Vector3 Axle = new Vector3(0, 1, 0);
///
/// 旋转速度
///
public float Speed = 1;
}
我再新建一个PositionComponent位置组件:
///
/// 位置组件
///
[DisallowMultipleComponent]
[ComponentName("位置组件")]
public sealed class PositionComponent : ECS_Component
{
}
位置组件里什么也没有,这里偷懒一下暂时就用Unity源生的Transform替换了。
再新建一个InputComponent输入组件,用来接收输入:
///
/// 输入组件
///
[DisallowMultipleComponent]
[ComponentName("输入组件")]
public sealed class InputComponent : ECS_Component
{
}
ECS的系统类必须满足以下条件:
1.继承至ECS_System。
2.标记SystemName特性(选择性,用来在检视面板快速识别一个系统的功能)。
3.标记StarComponent特性(表明此系统所关注的组件类型,如无此特性标记,此系统自动无效)。
推荐使用快捷创建方式,所有条件会自动帮你填充:
///
/// 旋转系统(只关注拥有PositionComponent、RotateComponent组件的实体)
///
[StarComponent(typeof(PositionComponent), typeof(RotateComponent))]
[SystemName("旋转系统")]
public sealed class RotateSystem : ECS_System
{
///
/// 系统逻辑更新
///
/// 系统关注的所有实体
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
}
}
看下代码,你会发现RotateSystem的StarComponent特性标记了两个类型,分别是PositionComponent和RotateComponent,这表示RotateSystem只会关注同时拥有这两个组件的实体,RotateSystem的OnUpdate会每帧呼叫(除非这个系统已禁用),他的参数entities就是他所关注的所有实体,换句话说,entities中的每一个实体均包含有PositionComponent和RotateComponent组件。
再新建一个InputSystem输入系统,用来处理我们的输入:
///
/// 输入系统(只关注拥有InputComponent组件的实体)
///
[StarComponent(typeof(InputComponent))]
[SystemName("输入系统")]
public sealed class InputSystem : ECS_System
{
///
/// 系统逻辑更新
///
/// 系统关注的所有实体
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
}
}
任何GameObject只要挂上ECS_Entity组件,他就成为了一个实体,可以通过以下两种方式生成实体:
为一个GameObject直接挂载组件ECS_Entity,然后点击按钮Generate ID生成ID即可,此ID在整个ECS环境中将是独一无二的。
也可以选中一个GameObject后,点击菜单栏选项 HTFramework -> ECS -> Mark As To Entity,快捷完成此操作。
动态生成实体必须调用如下接口:
ECS_Entity entity = ECS_Entity.CreateEntity(target);
传入的参数target为此实体挂载的GameObject对象。
不能直接使用AddComponent来挂载实体组件,这会导致ID无效,从而实体无效。
为实体挂载组件可以使用静态方式也可以使用动态方式,就像添加一个MonoBehaviour一样。
我们直接使用鼠标拖拽将PositionComponent和RotateComponent添加到我们生成的实体上:
也可以点击ECS_Entity面板的Open In Inspector按钮打开检视面板,在检视面板添加或删除组件,更可以直观的在检视面板查看此实体将会被哪些系统所关注(对于实体挂载的组件非常多的情况下,这可以快速的判断出实体的功效)。
窗口的Components面板检索所有组件,Systems面板检索所有系统,比如此处,我们可以看到当前实体挂载的所有组件,以及将来会关注这个实体的系统,点击系统栏右侧的Apply To Star按钮,可以快捷应用当前实体到此系统所关注的状态,也即是将此系统所关注的组件全部附加到当前实体上。
接下来我们在RotateSystem的OnUpdate中加入如下代码:
///
/// 系统逻辑更新
///
/// 系统关注的所有实体
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
foreach (var entity in entities)
{
RotateComponent component = entity.Component<RotateComponent>();
entity.transform.Rotate(component.Axle, component.Speed);
}
}
我们直接运行入口场景,便可以发现我们的实体(挂载PositionComponent和RotateComponent的)已经自动旋转起来了,接下来我们想要让输入来控制实体的旋转。
HTECS为了降低多系统之间功能代码相互覆盖的耦合度(比如这里输入系统将会关联到旋转系统),统一使用Order(指令)来驱动各个系统,指令分为ID和指令对象,对于一些简单的指令,可能只需要ID就可以了,不需要独立的指令对象(当然这不是强制性的,如果不想用,你可以完全当做没看到这个东西)。
新建指令类:(快捷创建方式)
///
/// 新建指令
///
public sealed class RotateOrder : ECS_Order
{
}
比如这里简单的旋转指令,他就不需要指令对象,只需要一个代表旋转指令的ID即可。
///
/// 指令ID
///
public enum OrderID
{
Rotate = 1
}
指令可以发送到一个实体上,也可以随时给这个实体撤销指令。
接下来我们来改写RotateSystem的OnUpdate方法:
///
/// 系统逻辑更新
///
/// 系统关注的所有实体
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
foreach (var entity in entities)
{
//如果目标实体存在旋转指令,才执行旋转逻辑
if (entity.IsExistOrder((int)OrderID.Rotate))
{
RotateComponent component = entity.Component<RotateComponent>();
entity.transform.Rotate(component.Axle, component.Speed);
}
}
}
同时编写InputSystem的OnUpdate逻辑:
///
/// 系统逻辑更新
///
/// 系统关注的所有实体
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
//空格键发出旋转指令
if (Main.m_Input.GetKeyDown(KeyCode.Space))
{
foreach (var entity in entities)
{
entity.GiveOrder((int)OrderID.Rotate);
}
}
//释放时撤销指令
if (Main.m_Input.GetKeyUp(KeyCode.Space))
{
foreach (var entity in entities)
{
entity.RecedeOrder((int)OrderID.Rotate);
}
}
}
GiveOrder向一个实体发起指令,RecedeOrder撤销该实体的指令,这两个方式都支持传入一个Order(指令)对象,用以描述指令的具体细节或参数(比如发起攻击指令了,该攻击谁?)。
我们直接运行入口场景,按住空格键,便可以发现我们的实体(挂载PositionComponent和RotateComponent和InputComponent的)已经自动旋转起来了,释放空格键,停止旋转。
除此之外,我们会发现,未挂载InputComponent组件的不会旋转,因为输入系统不会关注他,自然不会给他发送旋转指令,未同时挂载PositionComponent和RotateComponent组件的也不会旋转,因为旋转系统不会关注他,自然也不会为他附加旋转逻辑。
由于整个ECS环境是实时变化的(当然对于一些特殊的项目,也可能场景只有几个实体且不会增删),无论组件的增删,还是实体的增删,这些都会导致一个系统很可能不再继续关注他之前所关注的实体(比如我在运行时动态移除了某个实体上的InputComponent组件,那么输入系统将不再关注这个实体),所以这就需要ECS环境重新去检测所有的系统,找到他们所关注的实体,这个过程叫做ECS Dirty,只有在ECS环境处于Dirty状态时,才会触发这个过程,且框架底层会尽可能少的去触发ECS Dirty,除非在必要的时刻。
在编辑器中运行时将会出现运行时检视面板(Runtime Data),主要用以调试或数据监测,目前面板如下:
1.展示当前环境中的所有ECS系统。