Some API changes have been made since this tutorial was written. Most notably, the return type of GetTrigger methods has switched to use the interface ICollector<GameEntity>
instead of the base class Collector<GameEntity>
and the entity-link method no longer has a Contexts
argument. The code in the repo is no longer maintained but the readme will stay up to date helping you quickly fix errors when you download the project.
This tutorial you will show how to represent game state in Entitas (as components) and how to render that game state using Unity functionality (via systems). You'll also see how to pass Unity user-input into components that other systems can react to and carry out related game logic. Finally you'll implement a very simple AI system that allows entities to carry out movement commands issued by mouse clicks.
The finished tutorial project can be found here.
If you are brand new to Entitas, you should make sure to go over the Hello World tutorial before you attempt this one.
To start we will need to have a new empty Unity project setup for 2D with the latest version of Entitas installed and configured. If you don't know how to do this then please check out [[this guide|Using-Entitas-with-Unity]] for detailed step-by-step instructions on how to do that before continuing.
Create a new folder called "Game Code" and inside it create two additional folders called "Components" and "Systems". This is where we will store our created components and systems respectively.
To represent entity position in space we'll need a PositionComponent
(we're in 2D so we'll use a Vector2 to store the position). We're also going to represent the entity's direction as a degree value, so we'll need a 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;
}
We will also want to render our entities to screen. We'll do this with Unity's SpriteRenderer, but we will also need a Unity GameObject to hold the SpriteRenderer. We'll need two more components, a ViewComponent
for the GameObject and a SpriteComponent
which will store the name of the sprite we want to display.
Components.cs (contd)
[Game]
public class ViewComponent : IComponent
{
public GameObject gameObject;
}
[Game]
public class SpriteComponent : IComponent
{
public string name;
}
We're going to move some of our entities, so we'll create a flag component to indicate entities that can move ("movers"). We'll also need a component to hold the movement target location and another flag to indicate that the movement has completed successfully.
Components.cs (contd)
[Game]
public class MoverComponent : IComponent
{
}
[Game]
public class MoveComponent : IComponent
{
public Vector2 target;
}
[Game]
public class MoveCompleteComponent : IComponent
{
}
Finally we have the components from the Input context. We are expecting user input from the mouse, so we'll create components to store the mouse position that we will read from Unity's Input class. We want to distinguish between mouse down, mouse up, and mouse pressed (i.e. neither up nor down). We'll also want to distinguish the left from the right mouse buttons. There is only one left mouse button, so we can make use of the Unique attribute.
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;
}
You can save all of these Component definitions in a single file to keep the project simple and organized. In the finished project it is called Components.cs
. Return to Unity and allow the code to compile. When compiled, hit Generate to generate the supporting files for your components. Now we can begin to use those components in our systems.
We need to communicate the game state to the player. We will do this through a series of ReactiveSystems that serve to bridge the gap between the underlying state and the visual representation in Unity. SpriteComponents provide us a link to a particular asset to render to the screen. We will render it using Unity's SpriteRenderer class. This requires that we also generate GameObjects to hold the SpriteRenderers. This brings us to our first two systems:
The purpose of this system is to identify entities that have a SpriteComponent
but have not yet been given an associated GameObject. We therefore react on the addition of a SpriteComponent
and filter for !ViewComponent
. When the system is constructed, we will also create a parent GameObject to hold all of the child views. When we create a GameObject we set its parent then we use Entitas' EntityLink
functionality to create a link between the GameObject and the entity that it belongs to. You'll see the effect of this linking if you open up your Unity hierarchy while running the game - the GameObject's inspector pane will show the entity it represents and all of its components right there in the inspector.
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);
}
}
}
Note: Older versions of this tutorial called go.Link
with _contexts
as the second argument (i.e. go.Link(e, _contexts)
). This is no longer necessary, nor possible.
With the GameObjects in place, we can handle the sprites. This system reacts to the SpriteComponent
being added, just as the above one does, only this time we filter for only those entities that have already had a ViewComponent
added. If the entity has a ViewComponent
we know it also has a GameObject which we can access and add or replace it's SpriteRenderer.
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);
}
}
}
Next we want to make sure the position of the GameObject is the same as the value of PositionComponent
. To do this we create a system that reacts to PositionComponent
. We check in the filter that the entity also has a ViewComponent
, since we will need to access its GameObject to move it.
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;
}
}
}
Finally we want to rotate the GameObject to reflect the value of the DirectionComponent
of an entity. In this case we react to DirectionComponent
and filter for entity.hasView
. The code within the execute block is a simple method of converting degree angles to Quaternion rotations which can be applied to Unity GameObject 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);
}
}
}
We will now put all of these systems inside a Feature for organization. This will give use better visual debugging of the systems in the inspector, and simplify our GameController.
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));
}
}
We will now write a simple system to get AI entities to move to supplied target locations automatically. The desired behaviour is that the entity will turn to face the supplied movement target and then move towards it at a constant speed. When it reaches the target it should stop, and it's movement order should be removed.
We will achieve this with an Execute system that runs every frame. We can keep an up to date list of all the GameEntities that have a MoveComponent
using the Group functionality. We'll set this up in the constructor then keep a read-only reference to the group in the system for later use. We can get the list of entities by calling group.GetEntities()
.
The Execute()
method will take every entity with a PositionComponent
and a MoveComponent
and adjust it's position by a fixed amount in the direction of its move target. If the entity is within range of the target, the move is considered complete. We use the MoveCompleteComponent
as a flag to show that the movement was completed by actually reaching a target, to distinguish it from entities that have had their MoveComponent
removed for other reasons (like to change target or simply to cancel the movement prematurely). We should also change the direction of the entity such that it faces its target. We therefore calculate the angle to the target in this system too.
We will also clean up all the MoveCompleteComponent
s during the cleanup phase of the system (which runs after every system has finished its execute phase). The cleanup part ensures that MoveCompleteComponent
only flags entities that have completed their movement within one frame and not ones who finished a long time ago and who are waiting for new movement commands.
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;
}
}
}
The feature that holds the above system will be called "MovementSystems":
MovementSystems.cs (feature)
public class MovementSystems : Feature
{
public MovementSystems(Contexts contexts) : base("Movement Systems")
{
Add(new MoveSystem(contexts));
}
}
Our goal is to allow the user to create AI agents with a right mouse click and command them to move using the left mouse click. We're going to introduce a way to push mouse input from unity into Entitas in a flexible way that allows multiple systems to interact with the mouse input in different ways. Unity provides three distinct mouse button states for each button (i.e. GetMouseButtonDown()
, GetMouseButtonUp()
and GetMouseButton()
). We have defined components for each of these events MouseDownComponent
, MouseUpComponent
, and MousePositionComponent
. Our goal is to push data from Unity to our components so we can use them with Entitas systems.
We have also defined two unique flag components, one for left mouse button and one for right mouse button. Since they are marked as unique we can access them directly from the context. By calling _inputContext.isLeftMouse = true
we can create a unique entity for the left mouse button. Just like any other entity, we can add or remove other component to them. Because they are unique we can access these entities using _inputcontext.leftMouseEntity
and _inputcontext.rightMouseEntity
. Both of these entities can then carry one of each of the three mouse components, up, down and position.
This is an execute system which polls Unity's Input
class each frame and replaces components on the unique left and right mouse button entities when the corresponding buttons are pressed. We will use the Initialize phase of the system to set up the two unique entities and the Execute phase to set their components.
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);
}
}
We'll need some "movers" to carry out the movement. These will be entities that carry the "Mover" flag component, a PositionComponent
, a DirectionComponent
and will be displayed on screen with a SpriteComponent
. The sprite in the complete project is called "Bee". Feel free to replace this with a sprite of your own as you follow along.
This system will react to the right mouse button being clicked. For this we want the collector to match all of RightMouseComponent
and MouseDownComponent
. Remember, these get set in the EmitInputSystem
when the user presses the right mouse button down.
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");
}
}
}
We also need to be able to assign movement orders to our movers. To do this we'll react on left mouse button presses just as we reacted to right mouse button presses above. On execute we'll search the group of Movers who don't already have a movement order. To configure a group in this way we use GetGroup(GameMatcher.AllOf(GameMatcher.Mover).NoneOf(GameMatcher.Move))
. That is all of the entities that are flagged as "Mover" that do not also have a MoveComponent
attached.
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);
}
}
}
The feature that holds the above two systems will be called "InputSystems":
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));
}
}
Now we need to create the game controller to initialise and activate the game. The concept of the GameController should be familiar to you by now, but if not please re-visit Hello World to get a description. Once this script has been saved, create an empty game object in your unity heirarchy and attach this script to it.
You may need to adjust your sprite import settings, and camera settings to get the sprites to look the way you want them to look on screen. By default, the code loads "Bee" sprite from the Assets/Resources
folder. The finished example project sets the camera's orthographic size to 10 and the background to solid grey. Your sprite should also face vertically up so as to make sure the direction is properly rendered.
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));
}
}
Save, compile and run your game from the Unity editor. Right-clicking on the screen should create objects displaying your sprite at the position on the screen which you clicked. Left clicking should send them moving towards the position on the screen which you clicked. Notice their direction updating to aim them towards their target point. When they reach the target position they will stop moving and again become available for movement assignments.