MultiReactiveSystem Tutorial - OneYoungMean/Entitas-CSharp-OYM GitHub Wiki

前言

Entitas版本0.42引入了一个要求很高的新功能 - 能够使用单个反应系统同时对来自多个环境的实体进行操作。这对于游戏的每个部分都需要的反应系统非常有用。例如,用于销毁实体的系统在每个上下文中都是有用的。查看系统通常也非常通用,并且在多个上下文中很有用。本教程将指导您完成第一个MultiReactiveSystem的实现过程。

第一步

创建一个新的untiy项目,安装Entitas库。使用Entitas偏好设置一个文件夹来存放生成的代码,并定义三个上下文(Game, Input, Ui)。点击generate可以生成基于Context类与feature类或者独立的Game, Input 与 Ui的上下文。如果你不知道如何设置,也不知道这意味着什么。请参考早期的教材 Hello World! Tutorial.

阅读这篇教程的前提是您知道如何创建GameController以实际运行您创建的系统。如果您对此不熟悉,请再看一下 Hello World! or Simple View and Movement 等教程

简单的多重删除系统

我们将介绍的第一个示例是DestroySystem,它可以在我们所有的三种系统中运行。第一步我们需要一个component定义在其他的上下文当中。这是一个简单的component标签,如果如果缺少Ui的相关上下文,则可以通过Entitas首选项添加Ui上下文。

Components.cs

using Entitas;
using UnityEngine;

[Game, Input, Ui]
public class DestroyedComponent : IComponent 
{
}

创建多重反应系统的第一步是定义一个接口,允许我们调用生成的方法,而无需知道实体来自哪个上下文。Entitas代码生成器将查找具有多个上下文标记的所有组件,并为每个组件生成一个接口。从上面的组件中,生成器将创建一个IDestroyedEntity接口。实现IDestroyedEntity的类必须具有.isDestroyed属性。

从这里我们可以定义一个界面,上面写着“我是一个实体,我可以拥有一个DestroyedComponent”多重反应系统在此接口上运行,而不是直接在GameEntity,UiEntity或InputEntity上运行。

一旦定义了这个接口,我们需要告诉编译器我们所有不同的,特定于上下文的实体都将实现这个接口。然后可以将它们安全地送入系统。

MultiDestroySystem.cs

using System.Collections.Generic;
using Entitas;
using Entitas.Unity;
using UnityEngine;

// IDestroyed: "I'm an Entity, I can have a DestroyedComponent"
public interface IDestroyableEntity : IEntity, IDestroyedEntity { }

// tell the compiler that our context-specific entities implement IDestroyed
public partial class GameEntity : IDestroyableEntity { }
public partial class InputEntity : IDestroyableEntity { }
public partial class UiEntity : IDestroyableEntity { }

// inherit from MultiReactiveSystem using the IDestroyed interface defined above
public class MultiDestroySystem : MultiReactiveSystem<IDestroyableEntity, Contexts>
{
    // base class takes in all contexts, not just one as in normal ReactiveSystems
    public MultiDestroySystem(Contexts contexts) : base(contexts)
    {
    }

    // return an ICollector[] with a collector from each context
    protected override ICollector[] GetTrigger(Contexts contexts)
    {
        return new ICollector[] {
            contexts.game.CreateCollector(GameMatcher.Destroyed),
            contexts.input.CreateCollector(InputMatcher.Destroyed),
            contexts.ui.CreateCollector(UiMatcher.Destroyed)
        };
    }

    protected override bool Filter(IDestroyableEntity entity)
    {
        return entity.isDestroyed;
    }

    protected override void Execute(List<IDestroyableEntity> entities)
    {
        foreach (var e in entities)
        {
            Debug.Log("Destroyed Entity from " + e.contextInfo.name + " context");
            e.Destroy();
        }
    }
}

现在运行你的游戏并在你的unity hierarchy中找到上下文观察者(在DontDestroyOnLoad下)。查找上下文,创建实体,然后将Destroyed组件添加到其中。观察控制台中的消息。在每个上下文中尝试这些操作,现在你有一个全方位控制的销毁系统来统治它们!

MultiDestroySystem with View(渲染中的多重系统)

实体破坏通常需要统一销毁View GameObject。为了展示这个用例,我们还将定义一个简单的'ViewComponent'。有关权利中的视图的更多信息,请参阅 Simple View and Movement Tutorial. 一旦 components.cs 保存,你就需要返回untiy,生成新的代码 (ctrl-shift-G).

Components.cs (接上)

[Game, Input, Ui]
public class ViewComponent : IComponent 
{
    public GameObject gameObject;
}

要对Views执行操作,我们需要IDestroyableEntity接口来实现IViewEntity以及IDestroyedEntityIView实体实现了属性hasViewview,以及我们用来操作它的“Add”,“Remove”和“Replace”方法。我们将其添加到接口定义中。

MultiDestroySystem.cs

using System.Collections.Generic;
using Entitas;
using Entitas.Unity;
using UnityEngine;

// IDestroyed: "I'm an Entity, I can have a DestroyedComponent AND I can have a ViewComponent"
public interface IDestroyableEntity : IEntity, IDestroyedEntity, IViewEntity { }

// tell the compiler that our context-specific entities implement IDestroyed
public partial class GameEntity : IDestroyableEntity { }
public partial class InputEntity : IDestroyableEntity { }
public partial class UiEntity : IDestroyableEntity { }

// inherit from MultiReactiveSystem using the IDestroyed interface defined above
public class MultiDestroySystem : MultiReactiveSystem<IDestroyableEntity , Contexts>
{
    public MultiDestroySystem(Contexts contexts) : base(contexts)
    {
    }

    protected override ICollector[] GetTrigger(Contexts contexts)
    {
        return new ICollector[] {
            contexts.game.CreateCollector(GameMatcher.Destroyed),
            contexts.input.CreateCollector(InputMatcher.Destroyed),
            contexts.ui.CreateCollector(UiMatcher.Destroyed)
        };
    }

    protected override bool Filter(IDestroyableEntity entity)
    {
        return entity.isDestroyed;
    }

    protected override void Execute(List<IDestroyableEntity> entities)
    {
        foreach (var e in entities)
        {
            // now we can access the ViewComponent and the DestroyedComponent
            if (e.hasView)
            {
                GameObject go = e.view.gameObject;
                go.Unlink();
                Object.Destroy(go);
            }
            Debug.Log("Destroyed Entity from " + e.contextInfo.name + " context");
            e.Destroy();
        }
    }
}

此示例显示如何编写接口以允许您从多个响应系统中访问多个组件。但是请注意,必须在相同的上下文中定义这些组件才能在多系统中使用它们。比方说我们仅在[Game]和[Ui]中定义了View组件,那么InputEntity将无法实现IViewEntity,因为它不会实现与视图组件关联的生成方法。

Performing context-specific actions in multi-reactive systems(在多反应系统中执行特定于上下文的操作)

有时我们必须在系统中使用特定的上下文 - 如果我们需要从中访问一个方法,或者将它的引用传递给另一个对象。例如,在向Unity中的实体添加ViewComponent时,通常通过调用gameObject.Link(entity,context)来使用EntityLink功能。在上面的示例中,我们没有方便地引用进入系统的每个实体的特定上下文,因此我们无法将其传递给link方法。然而,实体带有一个contextInfo字段,我们可以用它来查找它所属的上下文。

通过使用扩展方法,我们可以使用其名称,编写检索上下文引用的功能,我们可以在多反应系统中使用这种扩展来获得对每个实体的“IContext”的引用。请注意,IContext并没有实现每个单独上下文所做的所有方法,我们需要在下面的系统中调用其链接的方法。

这是一个静态类,它将上下文的字符串名称中的映射存储到其“IContext”引用中。字典被懒惰地填充(首次需要时?)。 Contexts类的扩展方法返回与传递的名称相关联的IContext

关于link()的补充:这段代码的作用就是把gameobject与entity内在的联系起来,如果你在sence中选中你的gameobject,这段代码会帮你在hierarchy中给出相对应的entity,这是Debug过程中必不可少的一部分。同时还有很多其他的内在联系,作者的建议是,如果你试图addview一个go,一定不要忘了给他一个link。

ContextExtentions.cs

using System.Collections.Generic;
using Entitas;

public static class ContextExtentions
{
    private static readonly Dictionary<string, IContext> _contextsLookup = new Dictionary<string, IContext>();

    public static IContext GetContextByName(this Contexts contexts, string name)
    {
        if (_contextsLookup.Count == 0) SetContextsDictionary(contexts);
        return _contextsLookup[name];
    }

    private static void SetContextsDictionary(Contexts contexts)
    {
        foreach (var context in contexts.allContexts)
        {
            _contextsLookup.Add(context.contextInfo.name, context);
        }
    }
}

在这个例子里我们是多个component,一个简单的标签用于表示我们希望添加一个视图到这个实体上。

Components.cs (contd.)

[Game, Input, Ui]
public class AssignViewComponent : IComponent
{
}

现在我们可以编写一个MultiReactiveSystem来为任何上下文中的实体添加视图,就像我们在 Simple View and Movement中做的那样,我们将创建父GameObjects来存储统一层次结构中的组织视图。我们可以使用字典为每个视图找到合适的父级。

MultiAddViewSystem.cs

using System.Collections.Generic;
using Entitas;
using Entitas.Unity;
using UnityEngine;

// IViewEntity: "I am an Entity, I can have an AssignViewComponent and a ViewComponent"
public interface IViewableEntity : IAssignViewEntity, IViewEntity, IEntity {}
public partial class GameEntity : IViewableEntity {}
public partial class InputEntity : IViewableEntity {}
public partial class UiEntity : IViewableEntity {}

public class MultiAddViewSystem: MultiReactiveSystem<IViewableEntity, Contexts>
{
    private readonly Transform _topViewContainer = new GameObject("Views").transform;
    private readonly Dictionary<string, Transform> _viewContainers = new Dictionary<string, Transform>();
    private readonly Contexts _contexts;

    public MultiAddViewSystem(Contexts contexts) : base(contexts)
    {
        _contexts = contexts;
        // create a view container for each context name
        foreach (var context in contexts.allContexts)
        {
            string contextName = context.contextInfo.name;
            Transform contextViewContainer = new GameObject(contextName + " Views").transform;
            contextViewContainer.SetParent(_topViewContainer);
            _viewContainers.Add(contextName, contextViewContainer);
        }
    }

    protected override ICollector[] GetTrigger(Contexts contexts)
    {
        return new ICollector[] {
            contexts.game.CreateCollector(GameMatcher.AssignView),
            contexts.input.CreateCollector(InputMatcher.AssignView),
            contexts.ui.CreateCollector(UiMatcher.AssignView)
        };
    }

    protected override bool Filter(IViewableEntity entity)
    {
        return entity.isAssignView && !entity.hasView;
    }

    protected override void Execute(List<IViewableEntity> entities)
    {
        foreach (var e in entities)
        {
            string contextName = e.contextInfo.name;
            GameObject go = new GameObject(contextName  + " View");
            go.transform.SetParent(_viewContainers[contextName]);
            e.AddView(go);
            go.Link(e, _contexts.GetContextByName(contextName));
            e.isAssignView = false;
        }
    }
}

这里我们使用上下文名称来访问两个字典,一个用于提供我们可以传递给实体链接方法的IContext的引用,另一个用于查找正确的视图容器以设置为我们的新视图GameObject的父级。

结论

多反应系统有助于消除对许多独立系统的需求,每个系统中的代码完全相同。如果没有它们,我们需要一个GameDestroySystem,一个UiDestroySystem和一个InputDestroySystem - 每个都可以运行完全相同的代码,但具有不同的特定于上下文的类型(GameMatcher, GameContext, GameEntity etc).MultiReactiveSystems可以帮助您 深化 你的代码库。