ECS架构 Entitas-CSharp学习之路(一)

郎宏逸
2023-12-01

断断续续2个月完成了自己的游戏demo,却有种删除整个项目的冲突,架构太混乱,已经完全不想加东西,加一个功能就会因为发现自己之前留了太多屎,到后面不得不为之前的屎埋坑,必须做出改变。我需要学习一下其他人的代码和架构,查找了一下github几个star比较高的unity框架,发现了它——Entitas,最近知乎unity讨论比较热乎的ECS架构。
那么就玩玩吧,反正我一个学生党很闲很作,而且多学学也是好的,就当为学习unity2018将出的ECS架构做个预热,更何况对于这个开源项目,无论是文档,还是教程游戏demo都很丰富,很适合我这种不容易读懂源码的菜鸡。

环境安装

https://github.com/sschmid/Entitas-CSharp/wiki/Unity-Installation-Guide
直接按官方文档做,我下载的版本是1.4。

注意Generate 前要把Project Path内的文本改掉,改成你C#项目文件的扩展名,比如我是将Assembly-CSharp.csproj改成了ESC.csproj,也就是我Vs生成的.csproj文件的文件名

Hello World

https://github.com/sschmid/Entitas-CSharp/wiki/Unity-Tutorial-Hello-World
依旧只是按官方文档做,我这篇可不是教程(因为没人看),只是自己记录学习中的思考和趟坑,同时通过这种一边学习一边总结的方法加强学习效果

创建组件

在Sources文件下创建Components文件夹,接着在Components文件夹创建DebugMessageComponent脚本,继承组件必须要继承的 IComponent接口,给类加上[Game]特性,然后回到unity,Generated 。

根据官方api文档描述,组件类还加一些属性。不过这里没加,类里面只有一个字符串数据,其他代码都会自动生成

看一下生成的代码

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Entitas.CodeGeneration.Plugins.ComponentEntityApiGenerator.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
public partial class GameEntity {

    public DebugMessageComponent debugMessage { get { return (DebugMessageComponent)GetComponent(GameComponentsLookup.DebugMessage); } }
    public bool hasDebugMessage { get { return HasComponent(GameComponentsLookup.DebugMessage); } }

    public void AddDebugMessage(string newMessage) {
        var index = GameComponentsLookup.DebugMessage;
        var component = CreateComponent<DebugMessageComponent>(index);
        component.message = newMessage;
        AddComponent(index, component);
    }

    public void ReplaceDebugMessage(string newMessage) {
        var index = GameComponentsLookup.DebugMessage;
        var component = CreateComponent<DebugMessageComponent>(index);
        component.message = newMessage;
        ReplaceComponent(index, component);
    }

    public void RemoveDebugMessage() {
        RemoveComponent(GameComponentsLookup.DebugMessage);
    }
}

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by Entitas.CodeGeneration.Plugins.ComponentMatcherApiGenerator.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
public sealed partial class GameMatcher {

    static Entitas.IMatcher<GameEntity> _matcherDebugMessage;

    public static Entitas.IMatcher<GameEntity> DebugMessage {
        get {
            if (_matcherDebugMessage == null) {
                var matcher = (Entitas.Matcher<GameEntity>)Entitas.Matcher<GameEntity>.AllOf(GameComponentsLookup.DebugMessage);
                matcher.componentNames = GameComponentsLookup.componentNames;
                _matcherDebugMessage = matcher;
            }

            return _matcherDebugMessage;
        }
    }
}

文件名比之前添加的组件类名多了个”Game”,文件内有2个分部类,
实际上是给GameEntity类增加了 debugMessage属性,(hasDebugMessage,AddDebugMessage,ReplaceDebugMessage,RemoveDebugMessage)四个方法,这些个方法一看名字就知道啥意思了,再看另一个密封类GameMatcher ,看不懂!!实体匹配?我瞎猜的,以后再研究。

创建反应系统

在Sources文件下创建Systems文件夹,接着在Systems文件夹创建DebugMessageSystem脚本,继承ReactiveSystem< GameEntity>这个抽象类,作为一个反应系统,用来监听变化,哇!这个比自己写事件监听机制作舒服多了

using System.Collections.Generic;
using UnityEngine;
using Entitas;
public class DebugMessageSystem : ReactiveSystem<GameEntity>
{
     public DebugMessageSystem(Contexts contexts) : base(contexts.game)
     {
     }

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

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

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

ReactiveSystem< GameEntity>接口需要手动实现3个方法GetTrigger,Filter,Execute。
GetTrigger是利用之前生成的GameMatcher进行筛选实体,Filter?笨木头大佬说是以后作第二步筛选用的。
至于 Execute就很明显了,做该做的事,比如这里是打印。

创建初始化系统

Systems文件下创建HelloWorldSystem脚本,继承IInitializeSystem,这个脚本是为让程序开始时生成”hello world”,并利用AddDebugMessage将”hello world”改变实体DebugMessage属性,从而触发反应系统。

创建功能

为了方便保持系统组织,需要专门创建一个系统作为一个功能管理其他系统。
所以接下来创建TutorialSystems脚本,继承Feature类,并为这个类制定构造函数,构造函数后面还要跟着: base(“Tutorial Systems”),这样做除了构造父类外,还使用一个string作为功能的name。大概是为了区分功能用(瞎猜的)。

构造函数内利用Add方法加入功能所需要管理的系统,用以制定各系统执行的顺序,这个貌似可以解决unity一些方法执行顺序不清的问题。

MonoBehaviour

终于需要将脚本和unity关联了
创建GameController,负责创建,初始化,执行系统
然后运行系统,hello world

测试反应系统

尝试改变DontDestroyOnLoad /Game下实体的属性,发现每次改变都会输出新的message
尝试手动创建一个实体,更改实体属性,发现2个实体不相互影响

系统清理器

之前在GameController的update函数里加入了_systems.Cleanup();这是专门执行系统清理器的。在系统执行完后销毁系统。

执行系统

Entitas还可以通过这个系统专门监听Input事件来执行要做的事

最后的测试

我给所有系统以及MonoBehaviour加入了log,查看执行顺序
最后的log结果

MonoBehaviour Start() Begin
TutorialSystems Init
HelloWorldSystem Constructor
LogMouseClickSystem Init
DebugMessageSystem GetTrigger
CleanupDebugMessageSystem Init
HelloWorldSystem Init
MonoBehaviour Start() End

初始化正常

MonoBehaviour Update() Begin
DebugMessageSystem Filter
Hello World!
CleanupDebugMessageSystem Clean
MonoBehaviour Update() End

update正常

MonoBehaviour Update() Begin
DebugMessageSystem Filter
DebugMessageSystem Filter
Left Mouse Button Clicked
Right Mouse Button Clicked
CleanupDebugMessageSystem Clean//可以看出清理系统是同时清理了2个
MonoBehaviour Update() End

我们还注意到在DontDestroyOnLoad下存在着可重用实体,当同时点击左右键会有2个可重用实体,Entitas这样做是为减少垃圾回收和内存分配。

Over,Hello World正式结束。
每当学习新技术总是那么令人兴奋,正因为如此才要做程序啊啊。

 类似资料: