【Unity/ECS】从零开始的ECS学习生活

      从OW技术分享ECS到Unity提供ECS插件,我看了两三篇关于ECS的文章,但是始终不明白这到底是个啥,最近有幸换了2019版本的unity,准备借机学习一下这个ECS。这个文章也将是我一边学一边照着其他文章(视频)敲出来的,为了方便以后自己如果真的用到了可以拿出来快速回顾一下。
      首先,我使用的unity版本为2019.3.4f1,我查了网上很多版本的关于ECS的文章,大部分都是基于2018版本写的,那个时候使用ECS插件还需要一份官方提供的XML文件,但是现在网站已经挂掉了,下载不到了。
      还好我在瞎逛github的时候,找到了一个好多星星的插件:Entitas-CSharp,然后发现这个插件已经做成了压缩包,而且有一些教学视频,我就准备通过这个插件入手,从零开始浅浅的接触一下ECS,这里我下载的是1.13.0版本的插件。

插件的全部内容

      插件的全部内容就在上面了,没有实际的代码,没有示例场景,啥都没有,只有纯插件,还好git上有相关的讲解视频。
      首先,我们需要先初始化一下插件,在工具栏中会出现一个tools页签,选择preferences可以弹出一个具体的设置界面。

工具入口
设置界面

     这里正常来说点击auto import插件就会自动帮你设置成默认的设置,但是如果你的C#Project不在默认位置的话可能会出现问题,所以要确认一下csproj的位置,然后就可以生成对应的代码了。这个代码在每次修改ECS代码或者新增代码的时候都需要重新生成。工具中有提供生成代码的快捷键,而且视频中有介绍如何使用指令生成代码或是在你的IDE中集成生成代码的功能(视频中使用的rider,我也不确定其他IDE可不可以,我只是来学学ECS,快捷生成代码还是以后再学习吧)
     然后!!我们就可以来编写一些简单的ECS代码了

1.编写第一段Entity,Component代码

     接下来,我们就要开始编写第一段ECS代码了。首先新建一个C#脚本(我是跟着视频做的,新建了一个HealthComponent脚本),然后填充第一个属性,就像下面这样:

HealthComponent

     第一个脚本就已经写好了,接下来回到Unity,运行一下刚才tools工具栏下的generator生成相关代码:

generated代码
public partial class GameEntity {

    public HealthComponent health { get { return (HealthComponent)GetComponent(GameComponentsLookup.Health); } }
    public bool hasHealth { get { return HasComponent(GameComponentsLookup.Health); } }

    public void AddHealth(float newValue) {
        var index = GameComponentsLookup.Health;
        var component = (HealthComponent)CreateComponent(index, typeof(HealthComponent));
        component.value = newValue;
        AddComponent(index, component);
    }

    public void ReplaceHealth(float newValue) {
        var index = GameComponentsLookup.Health;
        var component = (HealthComponent)CreateComponent(index, typeof(HealthComponent));
        component.value = newValue;
        ReplaceComponent(index, component);
    }

    public void RemoveHealth() {
        RemoveComponent(GameComponentsLookup.Health);
    }
}

     可以看到生成的HealthComponent脚本中包含了三个函数:AddHealth,ReplaceHealth,RemoveHealth。后面关于实体的操作应该也是通过这三个函数进行的。

     下面我们就可以使用代码创建实体了,首先我们需要先创建一个Contexts对象,然后通过这个对象进行创建Entity,创建出来的Entity可以拥有我们之前编写的组件(component):

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

public class GameController : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        var context = new Contexts();
        var e = context.game.CreateEntity();
        //给实体赋予组件功能
        e.AddHealth(100);
    }

}

     进行到这里,我们的第一段代码就已经写完了,在一个空场景中挂上这个GameController组件后运行游戏就可以在场景中看到这样的结构:

运行结果

     这就表示我们拥有一个实体,这个实体有一个组件列表,其中有一个组件名字叫Health,其中Health的值是100。这和我们刚才的代码完全吻合!
     这里我们可以发现实体被分为两大类,分别是:game和input,这个是在上面的jenny设置面板中设置的:

contexts

     在contexts中我们可以查看这个模块中有多少实体,包括正在使用的和被回收放在池子中的实体(Reusable entities),当实体被回收时会自动放入回收池中供下次使用:

contexts

2.编写第一段System代码

     上面我们已经写了ECS中的EC模块,接下来再写一段S模块的代码,今天就可以打完收工了!和Component类似,我们需要再新建一个脚本来编写System:

using Entitas;

public sealed class LogHealthSystem : IExecuteSystem
{
    readonly IGroup<GameEntity> _entities;
    public LogHealthSystem(Contexts contexts) {
        _entities = contexts.game.GetGroup(GameMatcher.Health);
    }
    public void Execute()
    {
        foreach (var e in _entities)
        {
            UnityEngine.Debug.Log(e.health.value);
        }
    }
}

     这个System的作用是把health的值打印出来,首先这个类继承自IExecuteSystem,那么需要实现Execute接口,也就是这个System的具体执行操作。然后我们可以通过函数的构造函数拿到Context(感觉这个Context是一个管理类,其中包括了game和input两个大类一样的)。然后可以通过GetGroup函数可以拿到有对应组件的实体列表。然后我们就可以在excute函数执行我们想要做的事情了。
     此外,之前在GameController中获取context是通过实例化一个新的context,除此之外还可以通过Contexts.sharedInstance拿到全局通用的Contexts。接下来我们在这里创建一个新的System,并把刚才的context传入构造函数并执行。

    void Start()
    {
        var context = Contexts.sharedInstance;
        var e = context.game.CreateEntity();
        e.AddHealth(100);
        var system = new LogHealthSystem(context);
        system.Execute();
    }

     这样我们就可以在自己想要执行System的时候调用对应System的excute就可以执行对应的系统。

3.更多的System系统

1.反应式System

     如果你不想通过执行某个函数来调用一个系统,而是希望当实体的组件内的值变化时调用一个系统,我们可以使用一种反应式的System实现:

using Entitas;
using System.Collections.Generic;

public sealed class LogHealthSystem : ReactiveSystem<GameEntity>
{
    public LogHealthSystem(Contexts contexts) : base(contexts.game) { }
    protected override void Execute(List<GameEntity> entities)
    {
        foreach (var e in entities)
        {
            UnityEngine.Debug.Log(e.health.value);
        }
    }

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

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

     和刚才的System差不多,不过这里是通过GetTrigger获取你想要关心的组件。遗憾的是,这种反应式的组件还是没法做到自行反应,我们还是需要在update函数中执行调用函数才能达到我们想要的效果:

public class GameController : MonoBehaviour
{
    LogHealthSystem system;
    // Start is called before the first frame update
    void Start()
    {
        var context = Contexts.sharedInstance;
        var e = context.game.CreateEntity();
        e.AddHealth(100);
        system = new LogHealthSystem(context);
        
    }
    private void Update()
    {
        system.Execute();
    }

}
2.初始化式System

     这个就不用说太多废话了,就是可以用来初始化或者初始化某些实体的System:

using Entitas;

public sealed class CreatePlayerSystem : IInitializeSystem
{
    readonly Contexts _contexts;
    public CreatePlayerSystem(Contexts contexts) {
        _contexts = contexts;
    }
    public void Initialize()
    {
        var e = _contexts.game.CreateEntity();
        e.AddHealth(100);

    }
}

     调用起来和其他的System一样,在GameController里实例化一个系统,然后调用初始化函数:

public class GameController : MonoBehaviour
{
    LogHealthSystem system;
    CreatePlayerSystem createPlayerSystem;
    // Start is called before the first frame update
    void Start()
    {
        var context = Contexts.sharedInstance;
        system = new LogHealthSystem(context);
        createPlayerSystem = new CreatePlayerSystem(context);
        createPlayerSystem.Initialize();

    }
    private void Update()
    {
        system.Execute();
    }

}

3.System的根节点

     说白了就是一个所有System的集合:

public sealed class SystemRoot : Feature
{
    public SystemRoot(Contexts contexts) {
        Add(new CreatePlayerSystem(contexts));
        Add(new LogHealthSystem(contexts));
    }
}

     但是直接调用这个集合的初始化或者执行函数,其中的所有系统都可以执行对应函数,所以还是很方便的:

public class GameController : MonoBehaviour
{
    SystemRoot _systems;
    void Start()
    {
        _systems = new SystemRoot(Contexts.sharedInstance);
        _systems.Initialize();

    }
    private void Update()
    {
        _systems.Execute();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351