Game Programming

ECS Concepts - Part 3: Components

by Cory on September 19, 2017 9:14 PM (Edited September 20, 2017 2:21 AM)

Uh, I’m just realizing that there isn’t much to say about Component and it’s implementation so this post will be on the short side. Because of the implementation of the Entity class, as you can read in the previous post, there isn’t much required for components. All that you need to do to create your own component is to inherit from the Component base class. This solution is relatively non-intrusive. The base class is super simple, and basically just contains a string identifier for development/debug purposes.

class Component {
public:
explicit Component(const std::string& id = “Component”) : id_(id) {}
virtual ~Component() {}

// this is a copy constructor
Component(const Component& other) : id_(other.id_) {}

// this is a copy assignment operator (uses the copy-swap idiom)
Component& operator=(const Component& other) {
Component tmp(other);
this->swap(tmp);
return *this;
}

// this is a swap implementation
void swap(Component& other) {
std::swap(this->id_, other.id_);
}

// id setter and getter
const std::string& id() const {
return this->id_;
}

void id(std::string id) {
this->id_ = id;
}

// serialize interface
virtual std::string serialize(Serializer& s) {
throw std::runtime_error("Need to implement custom serialize()");
}

virtual void deserialize(Serializer& s, Scene& scene, std::string& d) {
throw std::runtime_error("Need to implement custom deserialize()");
}

private:
std::string id_;
};

You’ll also notice that there are extra things in there like a copy constructor, copy assignment, swap, and serialization methods. The copy and swap stuff is standard C++ practice. For any object that you are planning on copying or passing by value or whatever, you should write your own copy stuff. They call this the Rule of Three (or Five for C++11 and newer... it’s a deep rabbit hole). This really only matters if you have a non-default destructor and/or you have things that need to be deep copied or handled specially like pointer members.

My destructor is empty, but I had to explicitly declare it virtual. I’m not sure if that counts as a custom destructor, but anyway the copy stuff will come in handy later when I implement “object pooling” and I’ll need to copy and move components every which where.

The serialization methods are part of my own code (I wrote about that earlier). They are there to help with serializing components. Normally, I would have declared these functions pure virtual to force the person inheriting from this class to implement the bodies of these functions, but I can’t do the copy-and-swap thing if the Component class is abstract. So to get around this, I implemented the serialization functions with empty stubs that throw run time exceptions when they are called. I was wondering if it is possible to put in static assertion checks or something to make it fail during compilation? A (brief) search on the internet didn’t turn up anything that I could make work like I had in mind. Ah well, run time exceptions are good enough.

Space Component (A Simple Component)

Looking at the base class code probably doesn’t really explain much. Let’s look at a relatively simple Component type that I made, the Space class.

The Space component, together with a SpatialSystem, is supposed to be an ECS integrated scene graph replacement. Each Space component must be attached to an entity. Each Space contains the Handle for all of it’s child entities as well as it’s parent entity. (The root of the scene graph does not have a parent so it should have an invalid Handle.) A Space also contains the transform and other render information that is applied to all child entities. Think “space” as in the linear algebra term where each space represents a dimensional plane that you can transform things in. Here’s the important part: Space contains just data about interconnections and transforms.

class Space : public Component {
public:
// children management interface
Handle add(unsigned int idx) const;
void add(Handle child, int idx = -1);

void remove(Handle child);
void remove(unsigned int idx);

Handle get(unsigned int idx) const;

unsigned int num_children() const;

// serialization interface
virtual std::string serialize(Serializer& s);
virtual void deserialize(Serializer& s, Scene& scene, std::string& d);

sf::RenderStates states; // public member
Handle parent_; // another public member

private:
std::vector<Handle> children_;
};

Notice how there’s no code for making sure that the structure of the scene graph is coherent. All the higher level stuff is done in the system. The system is the place where the scene graph can be queried and changes the scene graph can be made during normal game operation. Basically, the SpatialSystem listens for EntityAdded or EntityRemoved messages that can be sent from other systems or the game world itself and makes the necessary modifications to the provided Space structures.

Ideally, you want the Component to be as simple as possible. I didn’t eliminate all class methods or private members because sometimes it’s just easier to encapsulate very component specific stuff instead of just making it a Plain Old Data (POD) type. For instance, the serialization methods obviously have to be implemented, but I’ve also hidden the details for the children array behind getters and setters for the user’s convenience.

Ok so, actually in the real code I hid all the members behind getters and setters. This is just a stylistic choice and some people think it’s better because you can go and modify the code after the fact in case you need to do some special operations on every access or modification. I changed it here in the example to show that it’s fine to public members or encapsulated stuff or a mixture.

TileMap (A Slightly More Complicated Component)

For a second example, I present the TileMap component. This, along with the MapSystem is what I’m hoping to use to create tiled map backgrounds and stuff. Each TileMap represents one background. For instance, in Pokemon, you could use one of these TileMap components to store the positions and textures of each tile. The MapSystem would be used to query the TileMap and render out the tiles to the screen. Like this:

A test tile map. It has 4 pokemon sprites and a green rectangle in it.

I’m planning it so that you can have multiple entities with tile map components for different map layers. Y’know, like having trees in the foreground or mountains in the far distance?

class TileMap : public Component {
public:
using TileType = boost::variant<Circle, Rectangle, Sprite, Text, VertexList>;

explicit TileMap(const std::string& id = "TileMap Component");
TileMap(const TileMap& other);
virtual ~TileMap();

TileMap& operator=(const TileMap& other);
void swap(TileMap& other);

TileType* get(const sf::Vector2f& pos);
void set(TileType tile);

std::vector<TileType*> find(const sf::FloatRect& search_area);

// serialize interface
virtual std::string serialize(Serializer& s);
virtual void deserialize(Serializer& s, Scene& scene, std::string& d);

private:
std::vector<TileType> tiles_;

sf::Vector2f get_position_from_tile(TileType& tile);
sf::FloatRect get_global_bounds_from_tile(TileType& tile);
};

I only say it’s more complicated because I decided to make this more of a “traditional” OOP class. I don’t have any public members. I wanted to encapsulate the tile array management because right now I just wrote it using an std::vector so I could have something quick and easy to experiment. I’m sure I’ll probably run into performance issues with larger sized maps so at that point I’ll probably replace the implementation with some sort of quad tree or k-d tree to improve lookup. I don’t know. Maybe. Just spit-balling.

God I hope that was useful. The next bunch of posts are going to be about different aspects of the System class and it’s implementations. That’s where most of the meat is and the System class is waaaaaay more complicated than the other two. I’m still working it out so things might change on the way, but hopefully things will make more sense when the systems are fully explained?



This Thought is part of Game Programming

Game programming general topic. I eventually hope to split this into separate ideas exclusively about specific games that I make.

back to the

top