Unity3D Entitas Wooga的ECS Entity Component System入门学习1

版本:unity 2017.1  语言:C#

 

总起:

今天才知道2015年谷歌(是Wooga的)在Github上发布了一个名叫Entitas的ECS框架,它主要基于Unity和C#来进行开发。之前还看到暴雪公司在讲他们的《守望先锋》时就用到了类似的技术。

 

Github地址:https://github.com/sschmid/Entitas-CSharp/wiki。百度上一查也没人来介绍该技术,嘛,我就以一个初学者的眼光来介绍一下该框架的基本原理和用法。

 

所以什么是ECS框架呢?其实Unity本身的组件开发就是ECS框架。但对比着来说,Unity中写脚本,拖到每一个GameObject下,都是一个MonoBehaviour,一个脚本中既包含了数据又包含了方法;而Entitas的目标是让数据和方法分离,在GameObject下只添加数据,而方法由其他系统完成。

 

类似于Java、C#中的垃圾回收机制,如果有对象失去了所有引用,则在一个特定的时间点上,垃圾回收机制将所有的对象统一删除。类似的,Entitas下,GameObject的组件不能有任何方法,只保存数据,数据代表特性,拥有一样特性的执行逻辑(也就是方法)会在特定时间点上统一执行。

 

最终做到数据和方法之间的解耦。有几个非常明显的好处:

1.添加的功能可以逐个做单元测试;

2.代码共享方便;

3.GameObject下只有数据,没有任何方法或者说没有逻辑上的代码;

4.很容易查询到拥有相同类型数据的对象;

5.效率高,因为普通的MonoBehaviour包含方法,例如在创建100个游戏对象时同样会创建100个相同的方法,然而这显然是没有必要的,因为ECS系统可以用一个方法就代替这100个方法的操作。(创建100个相同方法这边被网友指出是不存在的,不管有多少个对象,方法只有一个,这边大家不要被我误导了。我这边想表达的是:Unity在调用Update时需要反射,所以调用100次会比较费。如果有不对、不准确的地方,还希望不吝指教)

 

下面来介绍一下框架具体实现的几个部分。

 

4种概念:

框架的实现上有四种概念,分别是:Entiy实体、Context环境、Group组、Collector收集器,在开始写程序之前必须摸清楚这些概念的职责。

 

我们先来看一张官方提供的图,对几个概念有个感性的认识:

Unity3D Entitas Wooga的ECS Entity Component System入门学习1_第1张图片


Entity就是只有数据的GameObject对象,所有的Entity都放在同一个Context中,每一个Entity拥有Component组件(就是Unity上的组件),Component中只有数据、没有方法,而Group是拥有相同Component的Entity集合,用于快速查找拥有特定属性的Entity。

 

Entity 实体

Entity就是一个数据的集合,再次强调一遍没有方法,你可以以组件形式(IComponent)添加、替代、删除这些数据,Entity会触发相应的事件以处理数据的变化。

 

Context 环境

Context就是创建销毁Entity的工厂,你可以使用它来过滤出你所感兴趣的Entity。

 

Group 组

    组在Context中可以进行快速过滤,它能不间断的更新以保持当前的组中的Entity是最新的。想象一下,如果你拥有上千的Entity,但只有两个Entity拥有PositionComponent(位置数据),那只要向Context询问特定的组就能立刻获取到所有符合的Entity。

 

组和组内过滤到了Entity会被缓存下来,所以即使多次调用GetGroup获取组的方法,对性能也不会造成多大的影响。

 

同时组拥有OnEntityAdded、OnEntityRemoved和OnEntityUpdated方法,来处理拥有相同类型数据Entity的行为。

 

Collector 收集器 

Collector提供了一种简单的方法来处理Group中Entity变化的反应。与组中直接添加方法的区别是,它能汇总处理Entity,并且很方便的实现特定情况下的处理方式。

 

下面通过一个简单的例子,来实际体验一下Entitas框架的使用。

 

一个例子:

首先是Entitas的包,Github上有,我把它传到CSDN上了:http://download.csdn.net/detail/u012632851/9903779。

 

解压压缩包之后,把里面的Entitas文件夹拖到Unity工程的Assets文件夹下,然后做一些配置。点开菜单栏的Tools -> Entitas -> Preferences...,打开如下窗口:

Unity3D Entitas Wooga的ECS Entity Component System入门学习1_第2张图片


Data Providers、Code Generators、Post Processors默认情况下是Nothing,点击选为Everything,而Post Processors只要不选择Console.WriteLine generated files的最后一项就好。

 

搞定之后,点击绿色的Generate按钮,就会生成默认的一些脚本文件。

 

创建一个Component

目标是Component打印一段信息。

// DebugMessageComponent.cs
// Entitas的命名空间,其他的可以删掉了,不会用到的
using Entitas;

// Game标签,说明这个Component是GameContext下的,框架中暂时就两种标签:Game、Input
// 标签是必须的,不然没法通过Context找到该Component
[Game]

// 继承于IComponent,说明该类是一个Component
public class DebugMessageComponent : IComponent
{
    // 属性数据
    public string message;
}

 

内容很简单,唯一要注意的是[Game]标签与继承IComponent类。每次写好Component记得按一下生成按钮(快捷键Ctrl +  Shift + G),之后我们可以注意到生成代码目录下的Game/Components目录中有了GameDebugMessageComponent。

 

点开来看看,仅仅只是GameEntity和GameMatcher的局部类用来对DebugMessageComponent进行获取,这就是为什么一定要有[Game]的原因,如果写了[Input]那对应的肯定就是InputEntity和InputMatcher的局部类。

 

编写处理Component的方法

没错,方法,在ECS框架中,处理每一个Component的是System,System本身是一个Collector的子类,拥有过滤Entity、并执行的作用。

// DebugMessageSystem.cs
using Entitas;
using UnityEngine;
using System.Collections.Generic;

// 继承ReactiveSystem,功能是只要Component的值一发生变化,其中的Execute就会执行
public class DebugMessageSystem : ReactiveSystem
{
    // 将环境中的game环境传入,GameEntity当然是放在game环境中的
    public DebugMessageSystem(Contexts contexts) : base(contexts.game){ }

    // 过滤获取指定Component的Entity,这里必须是拥有DebugMessageComponent的Entity才能被提取
    protected override ICollector GetTrigger(IContext context)
    {
        // 返回过滤器
        return context.CreateCollector(GameMatcher.DebugMessage);
    }

    // 最终检查,判断成功才能执行
    protected override bool Filter(GameEntity entity)
    {
        return entity.hasDebugMessage;
    }

    // 过滤到的Component统一执行操作
    protected override void Execute(List entities)
    {
        foreach(var e in entities)
        {
            Debug.Log(e.debugMessage.message);
        }
    }
}

一般一个Component都会对应一个ReactiveSystem,在值变换时进行操作。Component是属性、ReactiveSystem是方法,属性和方法都有了,接下来就要创建对象了。

 

创建一个初始化System

// HelloWorldSystem.cs
using Entitas;

// 继承于IInitializeSystem,作用是在程序启动时,执行一次Initialize方法
public class HelloWorldSystem : IInitializeSystem
{
    // game环境
    readonly GameContext _context;

    // 创建时传入game的环境
    public HelloWorldSystem(Contexts contexts)
    {
        _context = contexts.game;
    }

    // 初始化方法
    public void Initialize()
    {
        // 在game环境中创建一个Entity,并在Entity上添加一个DebugMessageComponent,内容是Hello World
        _context.CreateEntity().AddDebugMessage("Hello World!");
    }
}

上面的System单纯只是在game环境中添加一个拥有DebugMessageComponent的Entity而已。

 

好了Entity实体(或者说是对象)有了,接下来是将所有系统整合在一起。

 

将所有的系统添加入一个Feature中

Feature是Systems的子类,它拥有统一执行所有System方法的能力。例如我们上面写的Initialize、Execute,不管有多少个System只要将其加入到Systems中,直接调用Systems的方法就能在一些特定的条件下执行相应的方法。

 

这是Systems的能力,那为什么还要搞个Feature子类呢?主要是给程序员便利,它可以判断当前环境,如果是Unity编辑器模式下,就会在场景中生成一个System节点,可以查看当前创建的所有System以及其消耗的性能,并可以进行调试。当然出包之后并不需要该功能,Feature也会判断后将其舍去。

 

说了那么多,实际上代码很简单,就是将用到的所有System都放在里面:

// TutorialSystems.cs
public class TutorialSystems : Feature
{
    // 添加所有要用到的System,base里面是调试节点的名字
    public TutorialSystems(Contexts contexts) : base("Tutorial Systems")
    {
        Add(new HelloWorldSystem(contexts));
        Add(new DebugMessageSystem(contexts));
    }
}

用到的System一共两个,一个是处理DebugMessageComponent的DebugMessageSystem,另一个是添加一个拥有DebugMessageComponentEntity。

 

整个逻辑写完了,最后是把Entitas连接到Unity中。

 

 创建一个与Unity交互的MonoBehaviour

// GameController.cs
using UnityEngine;
using Entitas;

public class GameController : MonoBehaviour
{
    // 存放系统集的最高节点,当然它本身也是个系统集
    Systems _systems;

	void Start ()
    {
        // 获取当前的环境组Contexts,里面有game环境和input环境
        var contexts = Contexts.sharedInstance;

        // 创建系统集,将自定义的系统集添加进去
        _systems = new Feature("System")
            .Add(new TutorialSystems(contexts));

        // 初始化,会执行所有实现IInitialzeSystem的Initialize方法
        // 当然这边就会创建那个拥有DebugMessageComponent的Entity
        _systems.Initialize();
	}
	
	void Update ()
    {
        // 执行系统集中的所有Execute方法
        _systems.Execute();

        //  执行系统集中的所有Cleanup方法
        _systems.Cleanup();
	}
}

GameController添加到场景中,最终完成了我们的HelloWorld:



可能有些童鞋对上面的结果还有所疑惑,我们这边Execute方法每帧都会在Update中执行,HelloWorld咋只打印一次呢?

 

这就是负责执行操作ReactiveSystem的特性了,它只会在Component的属性发生变化时才会执行Execute。不信的话,在DontDestroyOnLoad下找到DebugMessage的组件,试着改变其中message的值。每改变一次,Execute就会执行一次。

 

总结:

一不小心更得有点长了,这个例子其实没有完,下篇补充一下。但是原理已经在上面了,就是将方法和属性分开,这个HelloWorld有点长吧?确实,但是越研究越有味道,你不需要哪个System,直接在TutorialSystems中注释掉,非常的方便。

 

有一句话说的是:现在慢是为了以后的快。一个框架学习起来就要耗费大量的精力,但是一个好的框架能让一件事情事半功倍,别人还在复制粘贴代码的时候,你利用一些技巧几行代码就搞定了问题,这个时候你就知道一个好的程序框架组织的重要性了。


你可能感兴趣的:(ECS)