In the past, I have always written games/engines in full-on OOP, but I stumbled upon an architecture that purports to be widely used when it comes to games, which I'm sure you are all aware of, but I was not.
Entity/Component architecture is supposed to help with issues of having large hierarchies of inheritance when it comes to entities, like a character.
Following this article from Gamedev.net (https://www.gamedev.net/resources/_/technical/game-programming/implementing-component-entity-systems-r3382), I've recreated what I understand to be an Entity/Component architecture in JavaScript.
First of all, the article created a core World object that contains a list of all current entities in the game, and then weak maps (I'll explain this momentarily) of the components of each entity. In JavaScript, this looks like the following:
import Position from '../components/position';
import Size from '../components/size';
import Texture from '../components/texture';
const { Map, Set, WeakMap } = global;
export default class World {
options = new Map();
state = new Map();
entities = new Set();
components = new Map([
[ Position, new WeakMap() ],
[ Size, new WeakMap() ],
[ Texture, new WeakMap() ]
]);
}
As you can see, components is a Map of <ComponentClass, WeakMap>. What a WeakMap allows, is that if the key exists nowhere else in memory, the WeakMap will allow it to be garbage collected. The WeakMap is essentially <Entity, Component>, so if the entity is deleted from the world's entities Set, then it will automatically be garbage collected, preventing memory leaks.
The second part of this architecture is a Component. Components are strictly made up of data, and combined together as a group, make up an Entity. A simple example of one in JavaScript would be as follows:
export default class Position {
constructor ({ x, y, z = 0 }) {
this.x = x;
this.y = y;
this.z = z;
}
}
The third concept, of course, is an Entity. Now, an Entity isn't actually anything more than an identifier that components reference in order for services to work (I'll cover services next). Basically, Entity/Component architecture prefers composition, so I have a create method that takes configuration, and creates components. In JavaScript, this looks like:
import Position from '../components/position';
import Size from '../components/size';
import Texture from '../components/texture';
export default class Character {
static create (world, { position, size, texture }) {
const character = new Character();
world.entities.add(character);
world.components.get(Position).set(character, new Position(position));
world.components.get(Size).set(character, new Size(size));
world.components.get(Texture).set(character, new Texture(texture));
return character;
}
static delete (world, character) {
world.entities.delete(character);
}
}
You can see that Character is still a class, and is instantiated inside of the static create method, but this is simply to create a unique entry in memory that the services can correctly find all of the components of an entity.
The most complicated (it's not complicated) part of this architecture are the services. Services are just functions that you call that act upon entities. An example of a render services looks as follows:
import Position from '../components/position';
import Size from '../components/size';
import Texture from '../components/texture';
export default function render (world) {
const { components, entities, state } = world;
for (const entity of entities) {
const position = components.get(Position).get(entity);
const size = components.get(Size).get(entity);
const texture = components.get(Texture).get(entity);
if (!position || !size || !texture) {
continue;
}
state.get('drawContext').drawImage(texture.data,
texture.clip.x, texture.clip.y, texture.clip.width, texture.clip.height,
position.x, position.y, size.width, size.height
);
}
}
As you can see, for each entity, it checks if that entity has the required components, which in this case are Position, Size, and Texture. If all three exist, it uses the data from those components to draw the entity onto our canvas, using the drawContext.
To all who are familiar with this architecture and OOP; does this seem right to you? Have you used Entity/Component architecture before? Do you prefer OOP?