The 2024 Wheel Reinvention Jam just concluded. See the results.

Approaching entity systems

In the C++ variant of this, those entities all had vtables, yeah. Future variant is not worked out fully yet.

In general, you can't copy entities just by using a copy constructor. I don't like that way of doing things. There is an explicit procedure you call to clone an entity.
Thought I'd share an overview of what I got so far. Sounds like it shares some similar concepts with what mzaks posted.

So as I think I mentioned, I abandoned the notion of "components" and formulated them as "attributes" instead.

Perhaps it's simplest to start with the base for my system, the entity data.
Nothing fancy, just a big struct that points to all resources used by the entity system, including all the attribute data and any additional non-fixed size-per-instance data those attributes might be utilising.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// NOTE: u16 stands for unsigned short

// Entities are just indices
typedef u16 entity;

// Enums for attribute types
#define ATTRIBUTE_TRANSFORM		0
#define ATTRIBUTE_POSITION		1
#define ATTRIBUTE_VELOCITY		2
#define ATTRIBUTE_RENDER		3
#define ATTRIBUTE_LOGIC			4

#define ATTRIBUTES_TOTAL		5

struct entityData {
    u16 numEntities;
    u16 maxEntities;

    // Indices that are available for reuse
    u16 maxRemoved;
    u16 numRemoved;
    entity *entityRemoved;

    u16 maxCreateCalls;
    u16 numCreateCalls;
    entityCreateCall *listCreateCalls;

    u16 maxRemoveCalls;
    u16 numRemoveCalls;
    entityRemoveCall *listRemoveCalls;

    // From here follows attribute data
    u16 numTransform;
    u16 maxTransform;
    mat3 *transform;

    u16 numPosition;
    u16 maxPosition;
    vec2 *position;

    u16 numVelocity;
    u16 maxVelocity;
    vec2 *velocity;

    // Render
    u16 numRender;
    u16 maxRender;
    entityRender *render;

    // Basically memory arenas,
    // Allocates data wherever there's free space
    buffer *vertices;
    buffer *indices;

    // Logic
    u16 numLogic;
    u16 maxLogic;
    entityLogic *logic;
    buffer *logicNodes;

    // Entity and attribute index maps, I'll talk about them below
    // col: Attribute type    row: entity index    value: attribute index
    u16 *mapIndex;   
 
    // col: Attribute index   row: Attribute type   value: entity index
    u16 *mapEntity;

} entityData;


I keep track of which attributes an entity uses through two big matrices. I may turn these into hash maps in the future but currently I don't need a huge amount of entities.
The reason for there being two is that in some situations I need to fetch attribute indices by entity and the other way round.

The general workflow is to "create" an entity and then assign attributes to it.

1
2
3
4
5
6
7
8
9
    entity e = entityCreate();

    entityAssignTransform(e);
    entityAssignPosition(e);
    entityAssignVelocity(e);

    entityAssignRenderSprite(e, someSprite, someMaterial);

    entityAssignLogic(e, someLogicTemplate);


Attribute arrays are always packed so that they can be looped through in systems, so when an attribute gets removed (which is issued in the same fashion as assignments) the removed element is replaced with the last one. The matrices are updated to keep up with the change of indices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void entityAssignTransform(entity e) {
    u16 index = entityData.numTransform;
    entityData.numTransform++;

    entityRegisterAttribute(e, index, ATTRIBUTE_TRANSFORM);

    entityData.transform[index] = mat3Identity();
}

void entityRemoveTransform(entity e) {
    u16 removed = entityGetIndex(e, ATTRIBUTE_TRANSFORM);
    entityData.numTransform--;

    // Replace removed attribute with the last one in the matrices
    entityReplaceAttribute(e, removed, entityFromIndex(entityData.numTransform, ATTRIBUTE_TRANSFORM),    entityData.numTransform, ATTRIBUTE_TRANSFORM);

    // Copy data
    entityData.transform[removed] = entityData.transform[entityData.numTransform];
}


Regarding relationships and querying for attributes in systems.
If the program would ask for an attribute index the entity does not have it will simply get the value zero from the matrix. Therefore the 0th element in every attribute array is reserved as a dummy value and is never processed by systems. These could potentially be kept at neutral values that in cases of being used in calculations would not affect the final result.

In the case of any special behaviour it is driven by the logic system in which a given behaviour tree simply expects its owning entity to already have certain attributes. Whether this could become an issue remains to be seen. If an entity changes the properties of an other entity then that entity could replace it's behaviour to accommodate the changes.

The systems are just functions which main purpose is to process attributes.
They are the backbone of the game's update loop.

1
2
3
4
5
6
void systemRun(/*extra arguments it may need*/) {
    for (i = 1; i < entityData.numOfSomething; i++) {
        // Do things to attribute
        // Perhaps even fetch other attributes owned by its entity if required.
    }
}


I think I'll stop at that for this post. Next I could talk about the logic system if that would be of interest.

Edited by Andreas on

I was surprised to see this thread risen from the grave. I don't know if I would endorse anything I've written in this thread nowadays. Also I haven't really touched entity systems since then.

Regarding the logic system, it was basically a behavior tree.


Replying to Shastic (#26536)