要说学习新的东西,有什么能比得上一个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的基本思想,帮助大家了解这个框架。