Unity Tutorial Simple Entity View and Movement - OneYoungMean/Entitas-CSharp-OYM GitHub Wiki

介绍

本教程将向您展示如何使用Entitas表示游戏状态(作为组件)以及如何使用Unity功能(通过系统)呈现该游戏状态。您还将了解如何将Unity用户输入传递到其他系统可以响应的组件并执行相关的游戏逻辑。最后,您将实现一个非常简单的AI系统,允许通过鼠标让实体执行移动命令。

完整的项目可以在这里.找到。

条件

如果你对entitas还一无所知,我们建议您先去看看 Hello World教程。

首先,我们需要为安装和配置最新版Entitas的2D安装新的空项目。如果您不知道如何执行此操作,请查 this guide对entitas进行安装配置。

Step 1 - 新建文件夹(可选)

我们建议您新建一个文件夹来存储接下来的内容,这将增进你对于entitas的理解。 新建一个叫做 "Game Code" 的文件夹,在里面新建两个文件夹,分别取名为"Components" and "Systems". 这是用来储存接下来新建的系统与新建的组件的地方。

Step 2 - 组件

为了替代组件在空间中的位置信息,我们需要新建一个脚本 PositionComponent (我们只会用到2D,所以使用vector2就足够了).我们同样需要一个实体来表示我们的角度,所以我们还要添加一个 float DirectionComponent变量。

Components.cs

using Entitas;
using Entitas.CodeGeneration.Attributes;
using UnityEngine;

[Game]
public class PositionComponent : IComponent
{
    public Vector2 value;
}

[Game]
public class DirectionComponent : IComponent
{
    public float value;
}

我们也希望将实体渲染到屏幕上。我们需要用到unity的SpriteRenderer,但是我们同样需要用到GameObject来存储我们的SpriteRenderer.我们需要两个组件, 一个ViewComponent负责gameobject和一个SpriteComponent来渲染那些存储了sprite的精灵。

Components.cs (续)

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

[Game]
public class SpriteComponent : IComponent
{
    public string name;
}

我们需要移动一些实体,所以我们可以创造做一个标签组件("movers")来表明这些实体是需要移动的,我们同样需要一个存储移动组件的目标位置的组件,和一个告诉我们移动成功的组件。

Components.cs (续)

[Game]
public class MoverComponent : IComponent
{
}

[Game]
public class MoveComponent : IComponent
{
    public Vector2 target;
}

[Game]
public class MoveCompleteComponent : IComponent
{
}

最后我们有一个来自Input context的组件。我们预期来自使用者鼠标的讯息,所以我们需要创建一个组件来存储鼠标的位置。我们同样需要区分鼠标左键与鼠标右键,鼠标是弹起还是按下。一个正常的鼠标一般只有一个鼠标左键,所以我们可以使用**[Unique]**标签。

补充:注意这个unique的标签,这意味着这个实体在这里就已经出现且可以被取出了,这个实体是独立且唯一的,取出这个实体可以直接使用,而不需要定义或者创造。 虽然我没有通过null方法确认它,但是在属性下面确认了我的想法,我还需要详细思考下。

Components.cs (contd)

[Input, Unique]
public class LeftMouseComponent : IComponent
{
}

[Input, Unique]
public class RightMouseComponent : IComponent
{
}

[Input]
public class MouseDownComponent : IComponent
{
    public Vector2 position;
}

[Input]
public class MousePositionComponent : IComponent
{
    public Vector2 position;
}

[Input]
public class MouseUpComponent : IComponent
{
    public Vector2 position;
}

你可以用一个文件存储你所有的组件定义来维持项目的简洁有序。在这个项目当中这个文件叫做componenet.cs。好现在我们返回untiy并且点击Generate来在你的组件中添加支持。现在我们可以开始使用这些组件制作我们的系统。

Step 3 - View Systems(视图系统)

我们需要维持玩家与游戏之间的通行, 我们需要通过一系列的ReactiveSystems来弥合代码层与视觉渲染层之间的间隙。 我们需要通过unity的 SpriteRenderer 类来渲染我们的组件. 这要求我们生成 GameObjects 来储存它,我们来看到下面这个系统:

AddViewSystem

AddViewSystem系统用来识别具有“SpriteComponent”但没有关联** GameObject 的实体。因此,我们仅仅对包含SpriteComponent且不包含ViewComponent的实体做出反应。构建系统时,我们还将创建一个父物体 GameObject **来保存所有子视图。当我们创建一个GameObject时,我们设置它的父节点,然后我们使用Entitas的“EntityLink”功能,在GameObject和它所属的实体之间创建一个链接。

值得一提的是:如果在运行游戏时打开Unity层次结构,您将看到此链接的效果 - GameObject的检查器窗格将在检查器中显示它所代表的实体及其所有组件。

下面是关于这个系统的构建:

AddViewSystem.cs

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

public class AddViewSystem : ReactiveSystem<GameEntity>
{
    readonly Transform _viewContainer = new GameObject("Game Views").transform;
    readonly GameContext _context;

    public AddViewSystem(Contexts contexts) : base(contexts.game)
    {
        _context = contexts.game;
    }

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

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasSprite && !entity.hasView;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            GameObject go = new GameObject("Game View");
            go.transform.SetParent(_viewContainer, false);
            e.AddView(go);
            go.Link(e, _context);
        }
    }
}

sprite渲染系统

随着GameObjects的到位,我们现在可以来解决sprites了,这个系统需要对添加了SpriteComponent的实体进行反应,就像前面做的一样,只有在这个时候我们过滤器才过滤那些ViewComponent包含实体。如果实体拥有ViewComponent,我们就知道它同样包含一个 GameObject ,我们可以修改或者访问它。

RenderSpriteSystem.cs

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

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

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

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasSprite && entity.hasView;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            GameObject go = e.view.gameObject;
            SpriteRenderer sr = go.GetComponent<SpriteRenderer>();
            if (sr == null) sr = go.AddComponent<SpriteRenderer>();
            sr.sprite = Resources.Load<Sprite>(e.sprite.name);
        }
    }
}

RenderPositionSystem(渲染位置系统)

接下来我们希望确认 GameObject 的位置与PositionComponent的数值相同。要做到这些我们需要创建一个对PositionComponent进行反应的系统。我们需要在过滤器中检查实体同样包含ViewComponent,我们需要访问它的Gameobject来移动它。

RenderPositionSystem.cs

using System.Collections.Generic;
using Entitas;

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

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

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasPosition && entity.hasView;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            e.view.gameObject.transform.position = e.position.value;
        }
    }
}

RenderDirectionSystem(方向渲染系统)

最后我们希望旋转我们的GameObject来反应实体DirectionComponent的值。在这个案例中我们需要收集 DirectionComponent并过滤出符合entity.hasView的部分,采用Excute()来在每一帧将它的角度转换成四元数 是一个简单旋转它的方法,我们可以将它运用在修改GameOject的Transforms上。

RenderDirectionSystem.cs

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

public class RenderDirectionSystem : ReactiveSystem<GameEntity>
{
    readonly GameContext _context;

    public RenderDirectionSystem(Contexts contexts) : base(contexts.game)
    {
        _context = contexts.game;
    }

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

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasDirection && entity.hasView;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            float ang = e.direction.value;
            e.view.gameObject.transform.rotation = Quaternion.AngleAxis(ang - 90, Vector3.forward);
        }
    }
}

ViewSystems (Feature)(feature层的视图系统)

我们现在需要把所有的系统放入Feature组织当中.这会给我们的System更好的工作debug环境在inspertor中,并且简化我们的Gamecontroll。

ViewSystems.cs

using Entitas;

public class ViewSystems : Feature
{
    public ViewSystems(Contexts contexts) : base("View Systems")
    {
        Add(new AddViewSystem(contexts));
        Add(new RenderSpriteSystem(contexts));
        Add(new RenderPositionSystem(contexts));
        Add(new RenderDirectionSystem(contexts));
    }
}

Step 4 - 移动系统

我们现在写一个简单的系统来获取AI entitas来自动移动到提供的位置。我们期望实体将会面向移动目标,然后以恒定速度移动。当到达目标时,他将会被停止,移动组件将会被移除。

我们需要用Excute系统在每帧之内都尝试实现它。所以我们需要准备一个时刻更新的GameEntities列表,我们需要取出这些entity当中包含MoveComponent 的,使用Group functionality是个不错的注意。我们也需要设置在构造函数中,然后在系统中保留只读的组供以后使用,我们可以通过使用 group.GetEntities()的方法获取一个组内的所有实体的list。

Execute()的方法将会运行每一个带有PositionComponentMoveComponent的entity并通过一个修复目标方向的值调整他们的位置。如果实体在目标的范围内,移动就算是完成了。我们使用MoveCompleteComponent作为一个标签来判断实体是否到达目标,以便将其与其他原因(改变目的地或者取消移动)而过早的改变的实体区分开来。我们也需要修改实体的方向来让他朝向目标。所以我也需要计算朝向目标应该旋转的角度在系统当中。

我们同需要清理所有的MoveCompleteComponent在我们的clean系统当中(即当所有的excute执行完成之后),清理部分确保MoveCompleteComponent仅标记在一帧内完成移动的实体,而不是那些在很久以前完成并且正在等待新移动命令的实体。(呼,好难写)

运动系统

MoveSystem.cs

using Entitas;
using UnityEngine;

public class MoveSystem : IExecuteSystem, ICleanupSystem
{
    readonly IGroup<GameEntity> _moves;
    readonly IGroup<GameEntity> _moveCompletes;
    const float _speed = 4f;

    public MoveSystem(Contexts contexts)
    {
        _moves = contexts.game.GetGroup(GameMatcher.Move);
        _moveCompletes = contexts.game.GetGroup(GameMatcher.MoveComplete);
    }

    public void Execute()
    {
        foreach (GameEntity e in _moves.GetEntities())
        {
            Vector2 dir = e.move.target - e.position.value;
            Vector2 newPosition = e.position.value + dir.normalized * _speed * Time.deltaTime;
            e.ReplacePosition(newPosition);

            float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            e.ReplaceDirection(angle);

            float dist = dir.magnitude;
            if (dist <= 0.5f)
            {
                e.RemoveMove();
                e.isMoveComplete = true;
            }
        }
    }

    public void Cleanup()
    {
        foreach (GameEntity e in _moveCompletes.GetEntities())
        {
            e.isMoveComplete = false;
        }
    }
}

MovementSystems (feature)(feature层的运动系统)

保存上述系统的功能称为“MovementSystems”:

MovementSystems.cs (feature)

public class MovementSystems : Feature
{
    public MovementSystems(Contexts contexts) : base("Movement Systems")
    {
        Add(new MoveSystem(contexts));
    }
}

Step 5 - Input Systems

好了,我们的目标是允许使用者用右键创造一个AI代理并用左键让它移动。我们将介绍一种方法,以一种灵活的方式将鼠标输入从unity输入,允许多个系统以不同方式与鼠标输入交互的方法。unity提供了三种不同的鼠标按键(i.e. GetMouseButtonDown(), GetMouseButtonUp() and GetMouseButton()),对于这些属性,我们有MouseDownComponent, MouseUpComponentMousePositionComponent的事件可以调用。我们的目标是输入数据从unity中到我们的component(经过翻译的考证,这个地方只能写componenet 不能写组件),所以我们可以在entitas的系统中使用它。

我们也需要定义两个 unique独特属性的标签组件,分别用于鼠标组件和鼠标右键。当他们被标记成独特属性,我们可以直接从上下文当中取出它。只需要调用_inputContext.isLeftMouse = true,我们可以为鼠标左键创建一个唯一的实体。只需要像其他组件那样,我们可以添加或者移除上面的组件。这些实体已经被unique标记,所以我们可以通过_inputcontext.leftMouseEntity_inputcontext.rightMouseEntity访问。然后,这两个实体都可以携带三个鼠标组件中的每按下,弹起和鼠标位置。

EmitInputSystem(发射输入系统)

这是一个每一帧询问 Input类的Execute系统,并在其按下相应的按钮时,替换独特的鼠标左键和右键实体上的组件。我们需要使用Initialize来设置两个独特的实体并使用Execute设置它们的componet。

EmitInputSystem.cs

using Entitas;
using UnityEngine;

public class EmitInputSystem : IInitializeSystem, IExecuteSystem
{
    readonly InputContext _context;
    private InputEntity _leftMouseEntity;
    private InputEntity _rightMouseEntity;

    public EmitInputSystem(Contexts contexts)
    {
        _context = contexts.input;
    }

    public void Initialize()
    {
        // initialize the unique entities that will hold the mouse button data
        _context.isLeftMouse = true;
        _leftMouseEntity = _context.leftMouseEntity;

        _context.isRightMouse = true;
        _rightMouseEntity = _context.rightMouseEntity;
    }

    public void Execute()
    {
        // mouse position
        Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);

        // left mouse button
        if (Input.GetMouseButtonDown(0))
            _leftMouseEntity.ReplaceMouseDown(mousePosition);
        
        if (Input.GetMouseButton(0))
            _leftMouseEntity.ReplaceMousePosition(mousePosition);
        
        if (Input.GetMouseButtonUp(0))
            _leftMouseEntity.ReplaceMouseUp(mousePosition);
        

        // right mouse button
        if (Input.GetMouseButtonDown(1))
            _rightMouseEntity.ReplaceMouseDown(mousePosition);
        
        if (Input.GetMouseButton(1))
            _rightMouseEntity.ReplaceMousePosition(mousePosition);
        
        if (Input.GetMouseButtonUp(1))
            _rightMouseEntity.ReplaceMouseUp(mousePosition);
        
    }
}

CreateMoverSystem(创建mover系统)

我们需要一个"movers"来完成我们的移动行为。这些将是带有“Mover”标志的component,PositionComponentDirectionComponent的实体,并将在屏幕上显示SpriteComponent。这个sprite在完整的项目中叫做BEE,随意使用你的sprite替换他吧。

这个系统将会对鼠标右键做出反应。在这里我们希望收集器收集RightMouseComponent 且包含 MouseDownComponent的实体。请不要忘记,这些、当用户按下鼠标右键时,这些设置在EmitInputSystem中就会被获取。

CreateMoverSystem.cs

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

public class CreateMoverSystem : ReactiveSystem<InputEntity>
{
    readonly GameContext _gameContext;
    public CreateMoverSystem(Contexts contexts) : base(contexts.input)
    {
        _gameContext = contexts.game;
    }

    protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)
    {
        return context.CreateCollector(InputMatcher.AllOf(InputMatcher.RightMouse, InputMatcher.MouseDown));
    }

    protected override bool Filter(InputEntity entity)
    {
        return entity.hasMouseDown;
    }

    protected override void Execute(List<InputEntity> entities)
    {
        foreach (InputEntity e in entities)
        {
            GameEntity mover = _gameContext.CreateEntity();
            mover.isMover = true;
            mover.AddPosition(e.mouseDown.position);
            mover.AddDirection(Random.Range(0,360));
            mover.AddSprite("Bee");
        }
    }
}

CommandMoveSystem

我们同时需要分配移动任务给我们的mover。要实现它我们需要对左键按下做出反应,就像我们对右键做的那样。在encute()我们将选中那些尚未执行移动的mover的组,我们可以通过GetGroup(GameMatcher.AllOf(GameMatcher.Mover).NoneOf(GameMatcher.Move))来获取它。这是所有实体中那些包含"Mover"且不包含MoveComponent的实体。

CommandMoveSystem.cs

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

public class CommandMoveSystem : ReactiveSystem<InputEntity>
{
    readonly GameContext _gameContext;
    readonly IGroup<GameEntity> _movers;

    public CommandMoveSystem(Contexts contexts) : base(contexts.input)
    {
        _movers = contexts.game.GetGroup(GameMatcher.AllOf(GameMatcher.Mover).NoneOf(GameMatcher.Move));
    }

    protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)
    {
        return context.CreateCollector(InputMatcher.AllOf(InputMatcher.LeftMouse, InputMatcher.MouseDown));
    }

    protected override bool Filter(InputEntity entity)
    {
        return entity.hasMouseDown;
    }

    protected override void Execute(List<InputEntity> entities)
    {
        foreach (InputEntity e in entities)
        {
            GameEntity[] movers = _movers.GetEntities();
            if (movers.Length <= 0) return;
            movers[Random.Range(0, movers.Length)].ReplaceMove(e.mouseDown.position);
        }
    }
}

InputSystems (feature)

在这个脚本当中我们需要在feature层当中添加上面两个系统,来让他们一起工作:

InputSystems.cs

using Entitas;

public class InputSystems : Feature
{
    public InputSystems(Contexts contexts) : base("Input Systems")
    {
        Add(new EmitInputSystem(contexts));
        Add(new CreateMoverSystem(contexts));
        Add(new CommandMoveSystem(contexts));
    }         
}

Step 6 - Game Controller

现在我们需要创建游戏控制器来初始化和激活游戏。 GameController的概念现在应该是您熟悉的,如果你遗漏掉了它,可以去 Hello World 看看 。保存此脚本后,在Unity heirarchy中创建一个空的游戏对象并将此脚本附加上去。

您可能需要调整sprite导入设置和摄像机设置,以使精灵看起来像您希望它们在屏幕上显示的方式。默认情况下,代码从Assets / Resources文件夹加载“Bee”sprite。完成的示例项目将相机的正交尺寸设置为10,背景设置为纯灰色。你的sprite也应垂直朝上,以确保正确渲染方向。

GameController.cs

using Entitas;
using UnityEngine;

public class GameController : MonoBehaviour
{
    private Systems _systems;
    private Contexts _contexts;

    void Start()
    {
        _contexts = Contexts.sharedInstance;
        _systems = CreateSystems(_contexts);
        _systems.Initialize();
    }

    void Update()
    {
        _systems.Execute();
        _systems.Cleanup();
    }

    private static Systems CreateSystems(Contexts contexts)
    {
        return new Feature("Systems")
            .Add(new InputSystems(contexts))
            .Add(new MovementSystems(contexts))
            .Add(new ViewSystems(contexts));
    }
}

Step 7 - 运行游戏

从Unity编辑器保存,编译和运行游戏。右键单击屏幕应创建在您单击的屏幕上显示sprite的对象。左键单击应该将它们移动到您单击的屏幕上的位置。注意他们的方向更新,以使他们朝向目标点。当它们到达目标位置时,它们将停止移动并再次可用于移动分配。

⚠️ **GitHub.com Fallback** ⚠️