Entitas学习[一]Hello World

ECS是个面向数据的编程思想, Entitas是这个思想下的一个优秀的框架,按照框架规矩写,在程序规模比较大的时候,也容易进行代码的扩展和维护,适合面向对象编程但不熟悉设计模式又不想学习设计模式的人使用,但它本身与面向对象比是一个完全不同的开发理念,所以即使是写了几年的OOP程序员开始接触到Entitas一段时间也会比较懵,这一般是因为网上的教程普遍没有清晰解释Entitas的具体内容。

Entitas是一种写法上的约束框架,而没有集成常用功能模块,商用也比较少,同样是ECS写法的并且继承了一些常用成熟功能模块的在商业公司中使用多一些的框架有ET框架。两者都能同一套代码在服务器端和客户端使用。

按照国际惯例,这里根据siki学院的Entitas教程初级篇写的第一篇用Entitas的HelloWorld教程。为了较好使用框架,后续应该还会有教程的其他Demo的记录,记录自己的理解,方便回顾想起。

Unity中,Entitas的总体开发思想是,任意组件(C)组合成实体(E),系统(S)被这些实体的一些变化触发起一些操作,或者在程序运行时的必要的时候对这些实体进行操作,如游戏开始阶段,游戏每帧运行时,一些Unity场景的结束阶段等。

代码

先看下整个HelloWorld中的代码


[Game]
public class LogComponent : IComponent
{
    public string message;
}
public class LogSystem : ReactiveSystem<GameEntity>
{

    public LogSystem(Contexts context) : base(context.game)
    {
    }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Log);
    }

    protected override bool Filter(GameEntity entity)
    {
        return true;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity entity in entities)
        {
            Debug.Log(entity.log.message);
        }
    }
}
public class InitSystem : IInitializeSystem
{
    private readonly GameContext _gameContext;
    
    public InitSystem(Contexts contexts)
    {
        _gameContext = contexts.game;
    }
        
    
    public void Initialize()
    {
        _gameContext.CreateEntity().AddLog("Hello World");
    }
}
public class AddGameSystem : Feature
{
    public AddGameSystem(Contexts contexts) : base("AddGameSystem")
    {
        Add(new InitSystem(contexts));

        Add(new LogSystem(contexts));
    }
}
public class GameController : MonoBehaviour
{
    private Systems _systems;
    
    // Start is called before the first frame update
    void Start()
    {
        var context = Contexts.sharedInstance;
        _systems = new Feature("Systems").Add(new AddGameSystem(context));
        _systems.Initialize();
    }

    // Update is called once per frame
    void Update()
    {
        _systems.Execute();
        _systems.Cleanup();
    }
}

Component

ECS中的C,习惯叫组件,Component部分的代码纯粹是数据的集合,不包括数据的处理逻辑,体现的思想是数据与表现分离这个开发界中公认的开发思想。HelloWorld中的Component部分如下所示:

using System.Collections;
using System.Collections.Generic;
using Entitas;
using UnityEngine;


[Game]
public class LogComponent : IComponent
{
    public string message;
}

Entity

ECS中的E,Entity表示一个实体,类似于Unity的GameObject概念, 本身是空的,因为挂载了组件而表示不同的对象, 在HelloWorld这个Demo中,创建了一个Entity,并且在其身上添加了个组件LogComponent 。

Context

中文名习惯叫上下文,Context在Entitas中是个大的容器,里面装载了大量的Entity

在默认设置中,只有两个Context,GameContext,和InputContext
Entitas学习[一]Hello World
在Component的代码中

[Game]这个标签说明了这个Component是只存在于 Game这个Context下的,在Input这个Context是找不到带有这个组件的Entity实体的。

注意在点击了绿色按钮Generate后中括号中才会出现Game的提示

System

ECS中的S,是逻辑处理部分。System中的操作对象是Entity,对一类的Entity进行一些逻辑处理操作。
下面是第一个System

using System.Collections;
using System.Collections.Generic;
using Entitas;
using UnityEngine;

public class LogSystem : ReactiveSystem<GameEntity>
{

    public LogSystem(Contexts context) : base(context.game)
    {
    }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Log);
    }

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasLog;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity entity in entities)
        {
            Debug.Log(entity.log.message);
        }
    }
}

这是个ReactiveSystem,ReactiveSystem表示对一些感兴趣事件的发生会作出反应,感兴趣的事写在了GetTrigger函数中,当一些Entity发生了GetTrigger里面表示的事件之后,就会触发这个System,然后这个System用 Filter函数对发生了这个事件的Entity进行再一次的条件过滤,过滤掉不符合的Entity,然后将通过过滤的Entity放到Execute函数中执行操作。

开头

public class LogSystem : ReactiveSystem<GameEntity>
{
 public LogSystem(Contexts context) : base(context.game)
    {
    }

是个固定写法,
GameEntity对应了context.game,因为GameEntity只在context.game存在
在点击了Generate之后才会有GameEntity和context.game生成,

GetTrigger

protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Log);
    }

这里

context.CreateCollector(GameMatcher.Log);

GetTrigger是关于组件的变化的触发条件函数,表示指定组件的指定操作会引起这个System的注意
这里表示了所有有关于Log组件事件发生的Entity都会被带入到这个System,包括添加Log组件,移除组件,替换组件内容等。

Filter

protected override bool Filter(GameEntity entity)
    {
        return entity.hasLog;
    }

这里看起来和GetTrigger中的判断重复了,实际上Filter函数里面的判断逻辑更加偏向于业务逻辑,例如一些条件判断,这里Filter中return true也能进行HelloWorld打印,因为在移除组件的时候这个System也会被触发,return true这样写的话Execute函数里面的函数访问就会报空报错。

.hasLog是组件自动生成的代码,代表拥有某组件,当然这个.hasLog只针对于GameEntity才有用,因为GameEntity只存在于Game这个Context中,Log这个组件在定义时候声明了,所以也是。以后会遇见各种.hasXXX。都是同样的道理。

Entitas的写法可以很方便地让里面每个System只做一件事情,而每个System只做一件事情对于System的替换是很容易的,将原来的System取消注册添加,相同的条件下使用不同处理的其他System即可。

protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity entity in entities)
        {
            Debug.Log(entity.log.message);
        }
    }

在实体通过了上面两个函数的筛选之后会到这个函数中,
这里遍历所有通过筛选的实体并打印其log组件的信息

下面是第二个System


using System.Collections;
using System.Collections.Generic;
using Entitas;
using UnityEngine;

public class InitSystem : IInitializeSystem
{
    private readonly GameContext _gameContext;
    
    public InitSystem(Contexts contexts)
    {
        _gameContext = contexts.game;
    }
        
    
    public void Initialize()
    {
        _gameContext.CreateEntity().AddLog("Hello World");
    }
}

InitSystem在游戏刚运行的时候调用,这里找到GameContext 并在其下创建实体然后添加Log组件,因为Log组件是在GameContext下的所以要这样做,在Initialize()函数执行之后,LogSystem GetTrigger 会被触发,然后GetTrigger 通过就会执行其Filter,再通过就会执行Execute。

Entitas中System总共有分五种类型,

->.IInitializeSystem只在初始化时调用一次。初始化实现可以写在Initialize方法中。

->.IExecuteSystem会在每帧执行一次。执行逻辑可以写在Execute方法中。

->.ICleanupSystem会在别的System完成后,每帧执行一次。回收逻辑可以写在Cleanup方法中。

->.ReactiveSystem会在Group有变化时执行一次。执行逻辑可以写在Execute方法中。

->Feature会将上述的System进行整合以及创建。

其他的类型的在使用中有遇到再详细记录。

开端

只有这些是不够的,框架内部并没有跟着游戏运行自动启动整个框架的代码。
还需要添加一个Feature

public class AddGameSystem : Feature
{
    public AddGameSystem(Contexts contexts) : base("AddGameSystem")
    {
        Add(new LogSystem(contexts));
        Add(new InitSystem(contexts));
    }
}

Feature也是System,是框架内已经写好的某一类System,看作是一类System的集合,主要将需要的各类型的System添加于其下,Feature的添加是固定写法,用Entitas写的各类项目都会有这种脚本作为总的系统汇总注册。这里目前来看添加的顺序没有什么影响。

最后是创建一个脚本将Entitas框架与Unity关联

using System.Collections;
using System.Collections.Generic;
using Entitas;
using UnityEngine;

public class GameController : MonoBehaviour
{
    private Systems _systems;
    
    // Start is called before the first frame update
    void Start()
    {
        var context = Contexts.sharedInstance;
        _systems = new Feature("Systems").Add(new AddGameSystem(context));
        _systems.Initialize();
    }

    // Update is called once per frame
    void Update()
    {
        _systems.Execute();
        _systems.Cleanup();
    }
}

这个关联脚本也是Entitas框架的固有写法,将Entitas里面的System与生命周期绑定。用Entitas写的各类项目都会有这种脚本作为关联,不同的只是_systems = new Feature("Systems").Add(new AddGameSystem(context));这句话,项目大时可能会有几个Feature添加进去

参考

Entitas深入研究(1):环境安装和HelloWorld
Unity手游实战:从0开始SLG——ECS战斗(一)ECS设计思想
Unity手游实战:从0开始SLG——ECS战斗(二)Entitas插件
Unity手游实战:从0开始SLG——ECS战斗(三)逻辑与表现分离
Unity手游实战:从0开始SLG——ECS战斗(四)实战ECS架构和优化
RomanZhu/Endless-Runner-Entitas-ECS

上一篇:机器学习&数据挖掘笔记_23(PGM练习七:CRF中参数的学习)


下一篇:Redis 中 String 类型的内存开销比较大