笨木头  2017-09-01 20:54     Unity3D     阅读(14194)     评论(7)
转载请注明,原文地址: http://www.benmutou.com/archives/2431
文章来源:笨木头与游戏开发
转载请注明,原文地址:http://www.benmutou.com/archives/2431 文章来源:笨木头与游戏开发
要说学习新的东西,有什么能比得上一个HelloWorld呢?
一个HelloWorld还学不会,那就来两个。

1. Entity(实体)、Component(组件)、System(系统)间的关系

如果你觉得想尽快看到HelloWorld,那就直接从第2步开始,然后再回来看这一步吧。
这里简单介绍ECS三者之间的关系。
 
Entity,即实体,显然,它是一个实质的带有功能的对象,但它又不仅限于角色、怪物这些实体。
因为,在Entitas中,就连鼠标点击产生的事件也作为实体。
 
组件就是一些零散的功能点,如:坐标、武器、攻击力等。
实体就是这些组件的载体,把部分组件组合起来。


 
比如,我有以下的组件:图片、攻击力、武器。
这些是零散的东西,总得有个角色把它们组合起来吧?所以,这个角色就是实体。
而组件是千奇百怪的,比如:坐标。
在产生点击事件的时候,得有个实体把坐标给组合起来,那么,就有个“点击事件实体”的东西了。
 

至于System(系统),可以理解为Controller(控制器),用来控制实体逻辑的。
不理解没关系,看完这篇的HelloWorld大家就明白了。
 

2. 创建Component(组件)

好吧,我们开始。
Entitas的基本思想就是,把实体和它的属性完全分开,以组件的形式组合这些属性。
 
我们从创建组件开始一步步走向HelloWorld吧。
创建一个DebugMessageComponent.cs文件,代码目录结构就随意了,只是学习而已。
using Entitas;

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

注意两个地方,一个是给类加上一个[Game]特性,从目前我所理解的情况来看,这个Game的特性代表了这个组件是属于一个叫做GameEntity的实体。
这个组件叫做调试信息,拥有这个组件的实体就代码它拥有了调试信息这个属性。

3.自动生成Entity(实体)

Entitas会自动帮我们根据组件生成实体,我们是不需要在实体的创建上花费太多心思的。
 
现在,回到IDE,依次选择【Tools】-【Entitas】-【Generate】:


你会看到项目下多了一个GameDebugMessageComponent文件(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); } }

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; } } }
 

该文件里包含两个类:GameEntity和GameMather。
 
如之前所说,我们给组件赋予了[Game] 特性,所以它会帮我们生成GameEntity实体(的一部分)。
如果你仔细看的话,会发现,GameEntity是用partial定义的,也就是说,它是一个分部类。
 
什么是分部类?就是把一个类拆分为多个部分,在不同的地方定义。
并没有什么特别的,只是方便我们做不同的处理,比如在不同的文件里定义同一个类,这个文件定义一些属性,那个文件定义一些函数等待。
 
而Entitas会根据不同的组件生成多个实体的分部类,每个分部类里只定义一个组件的相关操作,这是Entitas的特点之一。
至于GameMatcher,是用来筛选实体的,这里暂时不多说。
 

4. 创建“控制器”——System(系统)

对于Entitas,组件只是定义一些属性,实体是自动生成的,而我们要做的事情大部分都在System里。
现在,我们来创建一个新的C#文件,命名为:DebugMessageSystem.cs
 
其内容如下:
using System.Collections.Generic;
using Entitas;
using UnityEngine;

public class DebugMessageSystem : ReactiveSystem<GameEntity> { public DebugMessageSystem(Contexts contexts) : base(contexts.game) { }

protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context) { // 该控制器只关心含有DebugMessage组件的实体 return context.CreateCollector(GameMatcher.DebugMessage); }

protected override bool Filter(GameEntity entity) { // 只有hasDebugMessage为true的实体才会触发下面的Execute函数 return entity.hasDebugMessage; }

protected override void Execute(List<GameEntity> entities) { // 满足GetTrigger和Filter的实体保存在entities列表里 foreach (var e in entities) { // 打印信息 Debug.Log(e.debugMessage.message); } } }
这个System比较关键,我分步解释一下:
  • a 通常情况下,我们的System类都要继承ReactiveSystem(还有其他System我们以后再说)
  • b ReactiveSystem是干什么的呢?可以粗暴地理解为:监测那些属性发生了改变的实体,然后对它们做一些厉害的事情
  • c GetTrigger函数的作用:一个System通常只对某些类型的实体感兴趣,比如我们的DebugMessageSystem,它只对那些拥有DebugMessage组件的实体感兴趣,GetTrigger函数就是用来筛选实体的。之前生成的实体里包含了一个GameMatcher类,它也是一个分部类,作用就是筛选实体类型。
  • d Filter函数:这也是一个筛选函数,为毛已经有了GetTrigger的情况下还需要Filter函数?因为,GetTrigger函数仅仅是筛选实体类型,但是并不是这个实体类型下的所有实体我都感兴趣,Filter函数就是在已经筛选了实体类型的情况下,再根据具体的需求进一步筛选。这里筛选的就是那些有打印信息的实体。
  • e Execute函数是重点,经过GetTrigger和Filter的重重筛选后,我们终于得到了自己感兴趣的实体。这些实体会通过Execute的参数传递进来,我们可以对这些实体做我们想做的事情(咳咳,注意道德底线)。比如这里我们就是把所有有打印信息的实体的信息打印出来。

5.Systems(系统组)

所以,我们可以开始HelloWorld了么?
不,还不行,我还想继续说(旁白:那你自己说个够吧,反正我已经听不下去了)
 
快了快了,大家坚持一下。
我们现在要来创建一个管理System的System,因为一个游戏开发过程中,不可能只有一个System的,为了方便管理,便有了【Feature】System的概念。
 
我们先来创建一个TutorialSystems.cs类,内容如下:
using Entitas;

public class TutorialSystems : Feature { public TutorialSystems(Contexts contexts) : base ("Tutorial Systems") { Add(new DebugMessageSystem(contexts)); //Add(new DebugMessageSystem2(contexts)); //Add(new DebugMessageSystem3(contexts)); //Add(new DebugMessageSystem4(contexts)); } }
 

这个类要继承Feature,它的内容很简单,就是在构造器里Add所有System进去,我们现在只有一个DebugMessageSystem,所以只需添加一个。
Feature就像一个管理System的管理器,有什么好处?等会大家就知道了。

6. 最后一步——让Entitas的东西和Unity关联

之前我们一直在做的事情基本上都和Unity无关,因此,想要让这组件、实体、系统运行起来,就必须把它们关联起来。
 
我们来创建一个GameController.cs文件,内容如下:
using Entitas;
using UnityEngine;

public class GameController : MonoBehaviour { Systems _systems;

void Start() { // 获取Entitas的上下文对象,类似一个单例管理器 var contexts = Contexts.sharedInstance; // 获取所需的System组 _systems = new Feature("Systems") .Add(new TutorialSystems(contexts));

// 初始化System _systems.Initialize();

}

void Update() { // 调用System的Execute函数,这里并不是每帧都执行Execute逻辑,因为Syetem里Execute会在实体满足一定条件的情况下才执行的(GetTrigger和Filter函数的作用) _systems.Execute(); } }

7. 运行?

所以,我们可以运行了?
差不多是吧,在Unity里创建一个GameObject,然后把GameController挂上去,然后就能运行了。
...
...
...
 
所以,大家看到HelloWorld了吗?(旁白:并没有!)
看不到就对了,这就是Entitas的神奇之处。(旁白:神奇你妹啊!我只是想看个HelloWorld,再不出现我就Alt+F4了啊!)
 
大家别急(靠,我自己都急了),虽然所有代码都正常运行了,但是别忘了,我们的DebugMessageSystem的Execute函数是什么情况下执行的?
拥有DebugMessage组件的实体(GetTrigger函数限制的),并且hasDebugMessage属性为true(Filter函数限制的)才会触发Execute函数。
 
我们现在没有任何GameEntity,是不可能触发Execute函数的。
什么情况下才有新的GameEntity出现,这个是由我们来决定的,这个问题就和“什么时候出现怪物”是一样的。
 
不过,既然是HelloWorld,我们可以自己添加一些测试实体。
方法就是,在GameController的Start函数里自己添加实体,代码如下:
    void Start()
    {
        // 获取Entitas的上下文对象,类似一个单例管理器
        var contexts = Contexts.sharedInstance;
        
        // 获取所需的System组
        _systems = new Feature("Systems")
            .Add(new TutorialSystems(contexts));

// 初始化System _systems.Initialize(); // 测试,添加一些测试实体 contexts.game.CreateEntity().AddDebugMessage("Hello World!"); }
获取game的上下文对象,调用CreateEntity函数即可创建实体。
创建完实体后呢,对,得给它添加DebugMessage组件(AddDebugMessage函数),添加组件的同时传递一个DebugMessage字符串。
 
于是,这个实体就同时满足了【拥有DebugMessage组件】、【hasDebugMessage属性为true】的条件。
再次运行游戏,在Console里,大家会看到姗姗来迟的HelloWorld...太感人了。


8. 唠叨一下

可能大家觉得,Entitas的HelloWorld也太难出现了吧,得写一堆东西。
确实,毕竟是框架,不是语言。
我们得按照框架的把代码搭起来才能做一些简单的事情,可能大家对于Entitas的认识还是很模糊,我已经把官方的HelloWorld教程精简了,可它还是很复杂。
 
下一篇我就和大家讲解Entitas的基本思想,帮助大家了解这个框架。
 
7 条评论
  • 1258 2018-07-28 10:58:59

    简单的说ECS就是带有c# Reactive的一套东西吧,现在看的还太少了,Reactive灵活的部分不知道怎么运用……和我之前自己写的小框架思路略微不同……
    0回复
  • demo 2018-04-12 21:04:37

    如何在别的地方修改呢 hello world
    0回复
    • 博主 笨木头 2018-04-13 11:51:59

      不好意思,Entitas我没接着研究了,就研究了helloworld,现在在研究GameFramework...
      0回复
  • fs 2018-04-02 10:52:10

    我选择自己写框架……
    0回复
    • 博主 笨木头 2018-04-03 08:44:36

      厉害:-O
      0回复
      • fs 2018-04-03 09:18:14

        就因为不够厉害才自己写啊 /捂脸,毕竟不用考虑通用性。 真心搞不定这些高端框架啊。。。
        0回复
        • 博主 笨木头 2018-04-03 11:48:35

          哈哈,我之前也是一直用自己封装的,感觉学习一下别人框架,还是受益匪浅...GF就是文档太缺,哭
          0回复
发表评论
粤ICP备16043700号

本博客基于 BlazorAnt Design Blazor 开发