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进行安装配置。
我们建议您新建一个文件夹来存储接下来的内容,这将增进你对于entitas的理解。 新建一个叫做 "Game Code" 的文件夹,在里面新建两个文件夹,分别取名为"Components" and "Systems". 这是用来储存接下来新建的系统与新建的组件的地方。
为了替代组件在空间中的位置信息,我们需要新建一个脚本 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来在你的组件中添加支持。现在我们可以开始使用这些组件制作我们的系统。
我们需要维持玩家与游戏之间的通行, 我们需要通过一系列的ReactiveSystems来弥合代码层与视觉渲染层之间的间隙。 我们需要通过unity的 SpriteRenderer 类来渲染我们的组件. 这要求我们生成 GameObjects 来储存它,我们来看到下面这个系统:
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);
}
}
}
随着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);
}
}
}
接下来我们希望确认 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;
}
}
}
最后我们希望旋转我们的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);
}
}
}
我们现在需要把所有的系统放入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));
}
}
我们现在写一个简单的系统来获取AI entitas来自动移动到提供的位置。我们期望实体将会面向移动目标,然后以恒定速度移动。当到达目标时,他将会被停止,移动组件将会被移除。
我们需要用Excute系统在每帧之内都尝试实现它。所以我们需要准备一个时刻更新的GameEntities列表,我们需要取出这些entity当中包含MoveComponent
的,使用Group functionality是个不错的注意。我们也需要设置在构造函数中,然后在系统中保留只读的组供以后使用,我们可以通过使用 group.GetEntities()
的方法获取一个组内的所有实体的list。
Execute()
的方法将会运行每一个带有PositionComponent
与 MoveComponent
的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”:
MovementSystems.cs (feature)
public class MovementSystems : Feature
{
public MovementSystems(Contexts contexts) : base("Movement Systems")
{
Add(new MoveSystem(contexts));
}
}
好了,我们的目标是允许使用者用右键创造一个AI代理并用左键让它移动。我们将介绍一种方法,以一种灵活的方式将鼠标输入从unity输入,允许多个系统以不同方式与鼠标输入交互的方法。unity提供了三种不同的鼠标按键(i.e. GetMouseButtonDown()
, GetMouseButtonUp()
and GetMouseButton()
),对于这些属性,我们有MouseDownComponent
, MouseUpComponent
和 MousePositionComponent
的事件可以调用。我们的目标是输入数据从unity中到我们的component(经过翻译的考证,这个地方只能写componenet 不能写组件),所以我们可以在entitas的系统中使用它。
我们也需要定义两个 unique独特属性的标签组件,分别用于鼠标组件和鼠标右键。当他们被标记成独特属性,我们可以直接从上下文当中取出它。只需要调用_inputContext.isLeftMouse = true
,我们可以为鼠标左键创建一个唯一的实体。只需要像其他组件那样,我们可以添加或者移除上面的组件。这些实体已经被unique标记,所以我们可以通过_inputcontext.leftMouseEntity
与 _inputcontext.rightMouseEntity
访问。然后,这两个实体都可以携带三个鼠标组件中的每按下,弹起和鼠标位置。
这是一个每一帧询问 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);
}
}
我们需要一个"movers"来完成我们的移动行为。这些将是带有“Mover”标志的component,PositionComponent
,DirectionComponent
的实体,并将在屏幕上显示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");
}
}
}
我们同时需要分配移动任务给我们的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);
}
}
}
在这个脚本当中我们需要在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));
}
}
现在我们需要创建游戏控制器来初始化和激活游戏。 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));
}
}
从Unity编辑器保存,编译和运行游戏。右键单击屏幕应创建在您单击的屏幕上显示sprite的对象。左键单击应该将它们移动到您单击的屏幕上的位置。注意他们的方向更新,以使他们朝向目标点。当它们到达目标位置时,它们将停止移动并再次可用于移动分配。