Entity Component System Question

I'm trying to work out an ECS and am having a bit of difficulty figuring out how I can access the components I have attached to an entity.
Currently the code looks like this:
Entity.h
1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include "Component.h"

class Entity {
	int guid;
	Component* components[INDEXMAX];

public:
	void addComponent(ComponentIndex i, Component* c);
	void print();
};


Entity.cpp
1
2
3
void Entity::addComponent(ComponentIndex i, Component* c) {
	components[i] = c;
}


Component.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum ComponentIndex {
	HEALTH,
	INDEXMAX
};

class Health_t : public Component {
	int maxHp;
	int curHp;

public:
	Health_t(int max) {
		maxHp = max;
		curHp = max;
	}
};


I'm not sure how I could access the information in a component (obviously need getters setters etc) without including all of that in the base component class... which could get very unwieldy after awhile.

Is that the only option or am I missing something obvious? thanks guys
(obviously need getters setters etc)
It's a trend to use getters/setters, however this isn't quite right. The trend started right at the beginning with Microsoft VBXs'. Objects are not records, and they shouldn't be thought of as such. It limits, in your imagination, how they might be combined.

In code:
1
2
3
4
5
6
7
8
class Entity {
	int guid;
	Component* components[INDEXMAX];

public:
	void addComponent(ComponentIndex i, Component* c);
	void print();
};
It doesn't make sense for addComponent to take an index. How can you possibly know where it ought to go? If you need an index, maybe is should return the insertion point.

For example, what does this mean?
1
2
3
// same ComponentIndex
e->addComponent(2, new Timer(READ_TIMEOUT, 75));
e->addComponent(2, new TransactionCompleteNotify());


There's also a technical matter. What values do these values take?
1
2
3
4
enum ComponentIndex {
	HEALTH,
	INDEXMAX
};
And in that contect, what does this mean?
1
2
3
class Entity {
	int guid;
	Component* components[INDEXMAX];


Start again. Define your interfaces (for Entity and Component), then decide how those classes might implement those interfaces. When you begin with the implementation, you constrain your thinking from the start.
2 ways:

1
2
3
4
5
6
7
8
9
10
11
12
#include "Component.h"

class Entity {
	int guid;
	Component* components[INDEXMAX];

public:
	void addComponent(ComponentIndex i, Component* c);
         Component * getComponent(ComponentIndex i);
         int getComponentSomething(ComponentIndex i) {return components[i]->getsomething()};
	void print();
};


...
value = x.getComponent(3)->getsomething();
or
value = getComponentSomething(i);

you could inherit, but that isnt necessary, the pointers lead the way here.

you can totally abstract it with some more work... you can overload [] operator to return the dereferenced component pointer as a component& for example.

Last edited on
The way that this works is that the entity is supposed to just be a holder for components, components just PODs (I changed them to structs), and systems interact with the entities components.

The enum ComponentIndex is used to provide a consistent location ref inside the array. Thus if you wanted to add a Timer component you would also have a Timer value in the ComponentIndex. Basically they're used as magic numbers.

I got this working with static_cast
so it looks like
 
static_cast<Health_t*>(e.getComponent(HEALTH))->curHp


Are there any other ways to do this? Or at least a way to make this less ugly.
Last edited on
INDEXMAX has value 1. So the array is 1 element big, but the language just uses pointer arithmetic to determine the offset from the index. There is no range check. Almost certainly, the element is out of bounds.
Maybe something like this?
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
# include <iostream>
# include <map>
# include <type_traits>
# include <memory>

struct Counter { 
  static int c; 
};

int Counter::c = 0;

template <typename T>
struct ClassID: Counter {
    static int id() noexcept { 
        static int c = Counter::c++;
        return c;
    }
};

template <typename T>
int ClassID_v = ClassID<std::decay_t<T>>::id();


struct Component {};

struct Entity { 
    std::map<int, std::unique_ptr<Component>> components;
    template <typename Component>
    Component& get() { 
        return static_cast<Component&>(*components[ClassID_v<Component>]); 
    }
    
    template <typename Component>
    bool has_component() { return components.count(ClassID_v<Component>); }
    
    template <typename Component, typename... Args>
    Entity& add_component(Args&&... args) {
        components.emplace(ClassID_v<Component>,
                           std::make_unique<Component>(std::forward<Args>(args)...));
        return *this;
    }
};

struct Health: Component {
    Health() = default;
    Health(int current, int max)
        : current_health(current)
        , max_health(max) {}
    int current_health = 100, max_health = 100; 
};

struct Name: Component {
    Name() = default;
    Name(std::string n)
        : name(n) {}
    std::string name = "no name";
};

int main() {
    Entity player;
    player.add_component<Health>(150, 200)
          .add_component<Name>("John Smith");
          
    std::cout << "My name is " << player.get<Name>().name << '\n'; 
    Health const& hp = player.get<Health>();
    std::cout << "\tHP: " << hp.current_health << '/' << hp.max_health << '\n'; 
}


Live demo:
http://coliru.stacked-crooked.com/a/b3973d70ba7fa677

Related:
https://en.wikipedia.org/wiki/Decorator_pattern
Last edited on
As long as all other enum values are placed before INDEXMAX, it should never be a problem (as INDEXMAX will never be used as an index value aside from the initialization of the array) unless there is something I'm missing.
Enum tags will be 1 for 1 with components, with INDEXMAX at the bottom for array init.

----

That's a cool way to handle it, I'll have to read up on templates so I can fully understand it lol. But it seems like static_cast is the way to handle this, just a question of the best way to implement it.
Topic archived. No new replies allowed.