Skip to content

Entitas Unity project to illustrate several simple concepts such as rendering and movement.

Notifications You must be signed in to change notification settings

FNGgames/Entitas-Simple-Movement-Unity-Example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NOTE:

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.

Introduction

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.

Prerequisite

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.

Step 1 - Setup Folders

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.

Step 2 - Components

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.

Step 3 - View 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:

AddViewSystem

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.

RenderSpriteSystem

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);
        }
    }
}

RenderPositionSystem

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;
        }
    }
}

RenderDirectionSystem

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);
        }
    }
}

ViewSystems (Feature)

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));
    }
}

Step 4 - Movement Systems

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 MoveCompleteComponents 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.

MovementSystem

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)

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));
    }
}

Step 5 - Input Systems

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.

EmitInputSystem

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);
        
    }
}

CreateMoverSystem

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");
        }
    }
}

CommandMoveSystem

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);
        }
    }
}

InputSystems (feature)

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));
    }         
}

Step 6 - Game Controller

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));
    }
}

Step 7 - Run the game

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.

About

Entitas Unity project to illustrate several simple concepts such as rendering and movement.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages