D20 RPG – Component & System (ECS)

An Entity is nothing without its Components. It does nothing without its Systems.

Overview

In the last lesson we learned about the Entity Component System pattern overall, and implemented the first step in the pattern – the Entity. Now we will implement the remaining pieces so that we can attach various kinds of data, called Components, to our Entity via a System. Many systems will simply be responsible for basic CRUD operations (create, read, update and destroy) so we will also create reusable code for those purposes.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Create a new folder in Assets/Scripts named Component. Next create a new folder in Assets/Tests named Component. All of the scripts we create in this lesson will be placed in one of those two folders or pre-existing folders.

Entity Table System

The basic Component pattern I will use is to simply have a Dictionary that maps from an Entity to some kind of data. For example, a “Health” component could be a Dictionary that maps from an Entity to an int – where the Value is the amount of “hit points” that the Entity currently has. Optionally, you could also map to more complex data types such as a struct that records both the current hit points and also a max amount of hit points. The trick is figuring out what should be combined and what should be separate based on what features you will actually need.

Create a new C# script in Assets/Scripts/Component named EntityTableSystem. Copy the following:

public interface IEntityTableSystem<T>
{
    CoreDictionary<Entity, T> Table { get; }

    void Set(Entity entity, T value);
    T Get(Entity entity);
    bool TryGetValue(Entity entity, out T value);
    bool Has(Entity entity);
    void Remove(Entity entity);
}

We begin by defining the interface. Any Entity Table System will “have” a Table which is implemented as a CoreDictionary that maps from an Entity to something – that Value is the generic T part of the interface definition. I also provide some basic methods:

  • Set: for creating or updating data
  • Get: for reading data
  • TryGetValue: returns true if the Table has an entry for the given Entity, and the out parameter will hold the Table’s Value if available
  • Has: returns whether or not the Table has an entry for the given Entity
  • Remove: deletes an Entity and its associated data from the Table

Add the following:

public abstract class EntityTableSystem<T> : IEntityTableSystem<T>
{
    public abstract CoreDictionary<Entity, T> Table { get; }

    public virtual void Set(Entity entity, T value)
    {
        Table[entity] = value;
    }

    public virtual T Get(Entity entity)
    {
        T result;
        if (Table.TryGetValue(entity, out result))
            return result;
        return default(T);
    }

    public virtual bool TryGetValue(Entity entity, out T value)
    {
        return Table.TryGetValue(entity, out value);
    }

    public virtual bool Has(Entity entity)
    {
        return Table.ContainsKey(entity);
    }

    public virtual void Remove(Entity entity)
    {
        if (Table.ContainsKey(entity))
            Table.Remove(entity);
    }
}

Here we have provided an abstract class that implements the interface. The various methods like Set and Get are implemented using the Table so we won’t have to continually implement them on the concrete implementations of each new component system. However, the Table itself was left as an abstract property. Any concrete subclass should determine where the Table comes from. If the data it holds should be persisted, it may just be a wrapper of a field in the Data of the DataSystem. Otherwise it could be defined within the concrete class directly and used only in memory.

Name Component and System

Now let’s add a “real” component and system to manage it. We will have a component that gives a name to an Entity. There are any number of reasons you may wish to use this. It might be helpful for debugging purposes, it could be the name of an item that appears in a shop, or it could be the name of a hero – I’m sure you get the idea.

Create a new C# script in Assets/Scripts/Component named NameSystem. Copy the following:

public partial class Data
{
    public CoreDictionary<Entity, string> name = new CoreDictionary<Entity, string>();
}

I start by adding a partial implementation of Data, where I add a CoreDictionary that maps from an Entity to a string. I created the table here because I expect this component to be something that should be persisted.

Continue to add the next code snippets:

public interface INameSystem : IDependency<INameSystem>, IEntityTableSystem<string>
{

}

Here we define the interface for our new component system. The interface actually inherits from two different interfaces. It inherits from IDependency so that we can inject any conforming system we wish. It also inherits from IEntityTableSystem so that we have all of the basic CRUD operations and a reference to the Table that is managed by the system.

public class NameSystem : EntityTableSystem<string>, INameSystem
{
    public override CoreDictionary<Entity, string> Table => IDataSystem.Resolve().Data.name;
}

This class implements the INameSystem interface. Thanks to the EntityTableSystem base class which it also inherits from, NameSystem requires almost no additional code. All we needed was to specify where the Table comes from. In this case, it wraps the “name” field of our Data.

public partial struct Entity
{
    public string Name
    {
        get { return INameSystem.Resolve().Get(this); }
        set { INameSystem.Resolve().Set(this, value); }
    }
}

For anyone enjoying the more object-oriented like approach to their code, this partial definition of Entity exposes a Name property that wraps the relevant methods from the injected INameSystem. As an additional bonus, when debugging your IDE may automatically associate potentially helpful information for you. In the image below you can see that Visual Studio reports the Name of an Entity.

Unit Tests

Entity Table System Tests

Create a new C# test script in Assets/Tests/Component named EntityTableSystemTests. Copy the following:

using NUnit.Framework;

class TestEntityTableSystem : EntityTableSystem<int>
{
    public override CoreDictionary<Entity, int> Table { get { return _table; } }
    CoreDictionary<Entity, int> _table = new CoreDictionary<Entity, int>();
}

The EntityTableSystem is an abstract class, so we can’t instantiate one directly. We need a subclass to test on. We “could” test the features of EntityTableSystem simply by testing NameSystem, but this is not my preferred approach. It is possible that the requirements of any given system could change over time. With the NameSystem as an example, I could imagine needing to override the inherited methods in a scenario where we may want to prevent setting a name that is included in a list of bad words. Therefore, I will want to create a custom subclass that is just for testing, so I can be certain that all of the base class functionality is preserved. That is why I have added this TestEntityTableSystem – it is a concrete subclass and all it does is provide the necessary Table.

Copy the following:

public class EntityTableSystemTests
{
    TestEntityTableSystem sut;
    Entity entity = new Entity(1);
    int value = 123;

    [SetUp]
    public void SetUp()
    {
        sut = new TestEntityTableSystem();
    }

    [Test]
    public void Set_AddNewValue_Success()
    {
        sut.Set(entity, value);
        Assert.AreEqual(value, sut.Table[entity]);
    }

    [Test]
    public void Set_UpdateValue_Success()
    {
        sut.Table[entity] = 456;
        sut.Set(entity, value);
        Assert.AreEqual(value, sut.Table[entity]);
    }

    [Test]
    public void Get_HasValue_Success()
    {
        sut.Table[entity] = value;
        var result = sut.Get(entity);
        Assert.AreEqual(value, result);
    }

    [Test]
    public void Get_NoValue_ReturnsDefaultValue()
    {
        var result = sut.Get(entity);
        Assert.AreEqual(0, result);
    }

    [Test]
    public void TryGetValue_HasValue_ReturnsTrueAndValue()
    {
        sut.Table[entity] = value;
        int output;
        var result = sut.TryGetValue(entity, out output);
        Assert.AreEqual(value, output);
        Assert.True(result);
    }

    [Test]
    public void TryGetValue_NoValue_ReturnsFalseDefaultValue()
    {
        int output;
        var result = sut.TryGetValue(entity, out output);
        Assert.AreEqual(0, output);
        Assert.False(result);
    }

    [Test]
    public void Has_WithValue_ReturnsTrue()
    {
        sut.Table[entity] = value;
        var result = sut.Has(entity);
        Assert.True(result);
    }

    [Test]
    public void Has_NoValue_ReturnsFalse()
    {
        var result = sut.Has(entity);
        Assert.False(result);
    }

    [Test]
    public void Remove_Success()
    {
        sut.Table[entity] = value;
        sut.Remove(entity);
        Assert.False(sut.Table.ContainsKey(entity));
    }
}

I created a variety of tests for both the positive and negative cases of the various methods. For example, checking that Set can both add and update data, or that Has returns True or False when expected. Feel Free to head back to Unity and run the tests in the Test Runner. All the tests should pass!

Summary

In this lesson, we completed the basic implementation of the ECS pattern by implementing a Component and System. We made a generic abstract base class that could be subclassed and used for any type of Component that should be associated to an Entity via a Dictionary. Then we provided a real example by creating a “Name” Component and its associated System. All of the basic CRUD operations were given unit tests.

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

2 thoughts on “D20 RPG – Component & System (ECS)

    1. Great question. The Entity Table System can associate an Entity with anything – that could include a “Collection” of anything. In this particular implementation (letting the Unity Json serializer do the work), I would probably make a new class that had a List inside it. So for example I could have an “Inflictions” object that held a List of Entity where each Entity could have its own components indicating the attributes of the particular infliction.

Leave a Reply

Your email address will not be published. Required fields are marked *