【Unity】 HTFramework框架(三十五)ECS模式

更新日期:2020年6月8日。
Github源码:[点我获取源码]

ECS

ECS - 实体-组件-系统,此ECS非Unity的ECS,并不一定会带来性能的提升,只是基于ECS的思想,建立在Unity现有的组件模式之上,以ECS模式进行开发可以避开项目后期繁重的继承链,提升开发速度和质量、以及项目稳定性。

HTFramework的ECS(HTECS)保持与Unity官方的ECS相同的开发模式,在HTECS中可以尝试将编码习惯逐渐脱离OOP,在未来Unity DOTS盛起的大趋势下可以无缝转接,且HTECS基于Unity源生的组件结构,使得HTECS的组件和实体足够透明,你可以像控制MonoBehaviour那样去控制他们。

使用ECS

新建组件(ECS中的C)

ECS的组件类必须满足以下条件:
1.继承至ECS_Component
2.标记ComponentName特性(选择性,用来在检视面板快速识别一个组件的功能)。
3.标记DisallowMultipleComponent特性(选择性,但建议始终标记,因为同类型的组件即使挂在一个实体上,也只会有一个组件生效)。

推荐使用快捷创建方式,所有条件会自动帮你填充:
在这里插入图片描述
如下,我新建了一个RotateComponent旋转组件:

    /// <summary>
    /// 旋转组件
    /// </summary>
    [DisallowMultipleComponent]
    [ComponentName("旋转组件")]
    public sealed class RotateComponent : ECS_Component
    {
        /// <summary>
        /// 旋转轴
        /// </summary>
        public Vector3 Axle = new Vector3(0, 1, 0);
        /// <summary>
        /// 旋转速度
        /// </summary>
        public float Speed = 1;
    }

我再新建一个PositionComponent位置组件:

    /// <summary>
    /// 位置组件
    /// </summary>
    [DisallowMultipleComponent]
    [ComponentName("位置组件")]
    public sealed class PositionComponent : ECS_Component
    {

    }

位置组件里什么也没有,这里偷懒一下暂时就用Unity源生的Transform替换了。

再新建一个InputComponent输入组件,用来接收输入:

    /// <summary>
    /// 输入组件
    /// </summary>
    [DisallowMultipleComponent]
    [ComponentName("输入组件")]
    public sealed class InputComponent : ECS_Component
    {

    }

新建系统(ECS中的S)

ECS的系统类必须满足以下条件:
1.继承至ECS_System
2.标记SystemName特性(选择性,用来在检视面板快速识别一个系统的功能)。
3.标记StarComponent特性(表明此系统所关注的组件类型,如无此特性标记,此系统自动无效)。

推荐使用快捷创建方式,所有条件会自动帮你填充:

在这里插入图片描述
如下,我新建了一个RotateSystem旋转系统:

    /// <summary>
    /// 旋转系统(只关注拥有PositionComponent、RotateComponent组件的实体)
    /// </summary>
    [StarComponent(typeof(PositionComponent), typeof(RotateComponent))]
    [SystemName("旋转系统")]
    public sealed class RotateSystem : ECS_System
    {
        /// <summary>
        /// 系统逻辑更新
        /// </summary>
        /// <param name="entities">系统关注的所有实体</param>
        public override void OnUpdate(HashSet<ECS_Entity> entities)
        {
        }
    }

看下代码,你会发现RotateSystemStarComponent特性标记了两个类型,分别是PositionComponentRotateComponent,这表示RotateSystem只会关注同时拥有这两个组件的实体,RotateSystemOnUpdate会每帧呼叫(除非这个系统已禁用),他的参数entities就是他所关注的所有实体,换句话说,entities中的每一个实体均包含有PositionComponentRotateComponent组件。

再新建一个InputSystem输入系统,用来处理我们的输入:

    /// <summary>
    /// 输入系统(只关注拥有InputComponent组件的实体)
    /// </summary>
    [StarComponent(typeof(InputComponent))]
    [SystemName("输入系统")]
    public sealed class InputSystem : ECS_System
    {
        /// <summary>
        /// 系统逻辑更新
        /// </summary>
        /// <param name="entities">系统关注的所有实体</param>
        public override void OnUpdate(HashSet<ECS_Entity> entities)
        {
        }
    }

新建实体(ECS中的E)

任何GameObject只要挂上ECS_Entity组件,他就成为了一个实体,可以通过以下两种方式生成实体:

1.编辑模式下静态生成:

为一个GameObject直接挂载组件ECS_Entity,然后点击按钮Generate ID生成ID即可,此ID在整个ECS环境中将是独一无二的。
在这里插入图片描述
也可以选中一个GameObject后,点击菜单栏选项 HTFramework -> ECS -> Mark As To Entity,快捷完成此操作。
在这里插入图片描述

2.运行模式下动态生成:

动态生成实体必须调用如下接口:

ECS_Entity entity = ECS_Entity.CreateEntity(target);

传入的参数target为此实体挂载的GameObject对象。

不能直接使用AddComponent来挂载实体组件,这会导致ID无效,从而实体无效。

为实体挂载组件

为实体挂载组件可以使用静态方式也可以使用动态方式,就像添加一个MonoBehaviour一样。

我们直接使用鼠标拖拽将PositionComponentRotateComponent添加到我们生成的实体上:
在这里插入图片描述

Inspector检视面板

也可以点击ECS_Entity面板的Open In Inspector按钮打开检视面板,在检视面板添加或删除组件,更可以直观的在检视面板查看此实体将会被哪些系统所关注(对于实体挂载的组件非常多的情况下,这可以快速的判断出实体的功效)。
在这里插入图片描述
窗口的Components面板检索所有组件,Systems面板检索所有系统,比如此处,我们可以看到当前实体挂载的所有组件,以及将来会关注这个实体的系统,点击系统栏右侧的Apply To Star按钮,可以快捷应用当前实体到此系统所关注的状态,也即是将此系统所关注的组件全部附加到当前实体上。

旋转实体

1.编写系统逻辑

接下来我们在RotateSystemOnUpdate中加入如下代码:

        /// <summary>
        /// 系统逻辑更新
        /// </summary>
        /// <param name="entities">系统关注的所有实体</param>
        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);
            }
        }

2.运行

我们直接运行入口场景,便可以发现我们的实体(挂载PositionComponentRotateComponent的)已经自动旋转起来了,接下来我们想要让输入来控制实体的旋转。

输入控制旋转实体

1.指令

HTECS为了降低多系统之间功能代码相互覆盖的耦合度(比如这里输入系统将会关联到旋转系统),统一使用Order(指令)来驱动各个系统,指令分为ID指令对象,对于一些简单的指令,可能只需要ID就可以了,不需要独立的指令对象(当然这不是强制性的,如果不想用,你可以完全当做没看到这个东西)。

新建指令类:(快捷创建方式)

在这里插入图片描述

/// <summary>
/// 新建指令
/// </summary>
public sealed class RotateOrder : ECS_Order
{
	
}

比如这里简单的旋转指令,他就不需要指令对象,只需要一个代表旋转指令的ID即可。

    /// <summary>
    /// 指令ID
    /// </summary>
    public enum OrderID
    {
        Rotate = 1
    }

指令可以发送到一个实体上,也可以随时给这个实体撤销指令。

2.编写系统逻辑

接下来我们来改写RotateSystemOnUpdate方法:

        /// <summary>
        /// 系统逻辑更新
        /// </summary>
        /// <param name="entities">系统关注的所有实体</param>
        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);
                }
            }
        }

同时编写InputSystemOnUpdate逻辑:

        /// <summary>
        /// 系统逻辑更新
        /// </summary>
        /// <param name="entities">系统关注的所有实体</param>
        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(指令)对象,用以描述指令的具体细节或参数(比如发起攻击指令了,该攻击谁?)。

3.运行

我们直接运行入口场景,按住空格键,便可以发现我们的实体(挂载PositionComponentRotateComponentInputComponent的)已经自动旋转起来了,释放空格键,停止旋转。

除此之外,我们会发现,未挂载InputComponent组件的不会旋转,因为输入系统不会关注他,自然不会给他发送旋转指令,未同时挂载PositionComponentRotateComponent组件的也不会旋转,因为旋转系统不会关注他,自然也不会为他附加旋转逻辑。

ECS Dirty

由于整个ECS环境是实时变化的(当然对于一些特殊的项目,也可能场景只有几个实体且不会增删),无论组件的增删,还是实体的增删,这些都会导致一个系统很可能不再继续关注他之前所关注的实体(比如我在运行时动态移除了某个实体上的InputComponent组件,那么输入系统将不再关注这个实体),所以这就需要ECS环境重新去检测所有的系统,找到他们所关注的实体,这个过程叫做ECS Dirty,只有在ECS环境处于Dirty状态时,才会触发这个过程,且框架底层会尽可能少的去触发ECS Dirty,除非在必要的时刻。

运行时检视面板

在编辑器中运行时将会出现运行时检视面板(Runtime Data),主要用以调试或数据监测,目前面板如下:
在这里插入图片描述
1.展示当前环境中的所有ECS系统。

  • IsEnabled:是否激活系统。
  • 显示当前系统所关注的所有实体列表。
  • Remove按钮:移除当前系统对此实体的关注。
  • Set Dirty按钮:手动设置ECS环境为Dirty状态。

猜你喜欢

转载自blog.csdn.net/qq992817263/article/details/106619485