In the last post about Component-based entity systems I talked about the way you can structure your project if you are using CBES. Now it’s time to start diving into the guts of our implementation to see how it works behind the scenes.
I’ll start with some of the work horses of our framework, namely the Component, Entity and Blueprint managers.
Storing components
All data in CBES is stored in components. Each component belongs to an entity. The component managers do the mapping between the entity identifiers (simple integers) and the components. Each component manager takes care about one specific component type. So in the end a component manager is not much more than a fancy dictionary:
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
namespace Slash.ECS.Components { using System; using System.Collections; using System.Collections.Generic; /// <summary> /// Maps entity ids to specific game components. By contract this manager /// should be responsible for mapping components of a single type, only. /// This way, entity ids can be mapped to different components, one of each /// type. /// </summary> public sealed class ComponentManager { #region Fields /// <summary> /// Components attached to game entities. /// </summary> private readonly Dictionary<int, IEntityComponent> components; #endregion #region Constructors and Destructors /// <summary> /// Constructs a new component manager without any initial components. /// </summary> public ComponentManager() { this.components = new Dictionary<int, IEntityComponent>(); } #endregion #region Public Methods and Operators /// <summary> /// Attaches the passed component to the entity with the specified id. /// Note that this manager does not check whether the specified id is valid. /// </summary> /// <param name="entityId"> /// Id of the entity to attach the component to. /// </param> /// <param name="component"> /// Component to attach. /// </param> /// <exception cref="ArgumentNullException"> /// Passed component is null. /// </exception> /// <exception cref="InvalidOperationException"> /// There is already a component of the same type attached. /// </exception> public void AddComponent(int entityId, IEntityComponent component) { if (component == null) { throw new ArgumentNullException("component"); } if (this.components.ContainsKey(entityId)) { throw new InvalidOperationException( "There is already a component of type " + component.GetType() + " attached to entity with id " + entityId + "."); } this.components.Add(entityId, component); } /// <summary> /// Returns an iterator over all components of this manager. /// </summary> /// <returns>Components of this manager.</returns> public IEnumerable Components() { return this.components.Values; } /// <summary> /// Returns an iterator over all entities having components of this manager attached. /// </summary> /// <returns>Entities having components of this manager attached.</returns> public IEnumerable<int> Entities() { return this.components.Keys; } /// <summary> /// Gets the component mapped to the entity with the specified id. /// Note that this manager does not check whether the specified id is valid. /// </summary> /// <param name="entityId"> /// Id of the entity to get the component of. /// </param> /// <returns> /// The component, if there is one attached to the entity, and null otherwise. /// </returns> public IEntityComponent GetComponent(int entityId) { IEntityComponent component; this.components.TryGetValue(entityId, out component); return component; } /// <summary> /// Removes the component mapped to the entity with the specified id. /// Note that this manager does not check whether the specified id is valid. /// </summary> /// <param name="entityId"> /// Id of the entity to remove the component from. /// </param> /// <returns> /// Whether a component has been removed, or not. /// </returns> public bool RemoveComponent(int entityId) { IEntityComponent component; return this.RemoveComponent(entityId, out component); } /// <summary> /// Removes the component mapped to the entity with the specified id. /// Note that this manager does not check whether the specified id is valid. /// </summary> /// <param name="entityId"> /// Id of the entity to remove the component from. /// </param> /// <param name="component">Removed component.</param> /// <returns> /// Whether a component has been removed, or not. /// </returns> public bool RemoveComponent(int entityId, out IEntityComponent component) { if (this.components.TryGetValue(entityId, out component)) { this.components.Remove(entityId); return true; } return false; } #endregion } } |
Interface for entity creation
While working with components, you won’t work with component managers directly. Instead the access is conducted through an Entity Manager which is unique in a game. To add/get/remove a component, there are according methods:
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
namespace Slash.ECS.Components { /// <summary> /// Creates and removes game entities. Holds references to all component /// managers, delegating all calls for adding or removing components. /// </summary> public class EntityManager { #region Public Methods and Operators /// <summary> /// Attaches the passed component to the entity with the specified id. /// </summary> /// <param name="entityId"> Id of the entity to attach the component to. </param> /// <param name="component"> Component to attach. </param> /// <exception cref="ArgumentOutOfRangeException">Entity id is negative.</exception> /// <exception cref="ArgumentOutOfRangeException">Entity id has not yet been assigned.</exception> /// <exception cref="ArgumentException">Entity with the specified id has already been removed.</exception> /// <exception cref="ArgumentNullException">Passed component is null.</exception> /// <exception cref="InvalidOperationException">There is already a component of the same type attached.</exception> public void AddComponent(int entityId, IEntityComponent component); /// <summary> /// Attaches a new component of the passed type to the entity with the specified id. /// </summary> /// <typeparam name="T">Type of the component to add.</typeparam> /// <param name="entityId">Id of the entity to attach the component to.</param> /// <returns>Attached component.</returns> public T AddComponent<T>(int entityId) where T : IEntityComponent, new(); /// <summary> /// Attaches the passed component to the entity with the specified id. /// </summary> /// <param name="entityId"> Id of the entity to attach the component to. </param> /// <param name="component"> Component to attach. </param> /// <param name="sendEvent">Indicates if an event should be send about the component adding.</param> /// <exception cref="ArgumentOutOfRangeException">Entity id is negative.</exception> /// <exception cref="ArgumentOutOfRangeException">Entity id has not yet been assigned.</exception> /// <exception cref="ArgumentException">Entity with the specified id has already been removed.</exception> /// <exception cref="ArgumentNullException">Passed component is null.</exception> /// <exception cref="InvalidOperationException">There is already a component of the same type attached.</exception> public void AddComponent(int entityId, IEntityComponent component, bool sendEvent); /// <summary> /// Gets a component of the passed type attached to the entity with the specified id. /// </summary> /// <param name="entityId"> Id of the entity to get the component of. </param> /// <param name="componentType"> Type of the component to get. </param> /// <returns> The component, if there is one of the specified type attached to the entity, and null otherwise. </returns> /// <exception cref="ArgumentOutOfRangeException">Entity id is negative.</exception> /// <exception cref="ArgumentOutOfRangeException">Entity id has not yet been assigned.</exception> /// <exception cref="ArgumentException">Entity with the specified id has already been removed.</exception> /// <exception cref="ArgumentNullException">Passed component type is null.</exception> public IEntityComponent GetComponent(int entityId, Type componentType); /// <summary> /// Gets a component of the passed type attached to the entity with the specified id. /// </summary> /// <param name="entityId"> Id of the entity to get the component of. </param> /// <typeparam name="T"> Type of the component to get. </typeparam> /// <returns> The component, if there is one of the specified type attached to the entity, and null otherwise. </returns> /// <exception cref="ArgumentOutOfRangeException">Entity id is negative.</exception> /// <exception cref="ArgumentOutOfRangeException">Entity id has not yet been assigned.</exception> /// <exception cref="ArgumentException">Entity with the specified id has already been removed.</exception> /// <exception cref="ArgumentNullException">Passed component type is null.</exception> /// <exception cref="ArgumentException">A component of the passed type has never been added before.</exception> public T GetComponent<T>(int entityId) where T : IEntityComponent; /// <summary> /// Removes a component of the passed type from the entity with the specified id. /// </summary> /// <param name="entityId"> Id of the entity to remove the component from. </param> /// <param name="componentType"> Type of the component to remove. </param> /// <returns> Whether a component has been removed, or not. </returns> /// <exception cref="ArgumentOutOfRangeException">Entity id is negative.</exception> /// <exception cref="ArgumentOutOfRangeException">Entity id has not yet been assigned.</exception> /// <exception cref="ArgumentException">Entity with the specified id has already been removed.</exception> /// <exception cref="ArgumentNullException">Passed component type is null.</exception> /// <exception cref="ArgumentException">A component of the passed type has never been added before.</exception> public bool RemoveComponent(int entityId, Type componentType); /// <summary> /// Tries to get a component of the passed type attached to the entity with the specified id. /// </summary> /// <param name="entityId">Id of the entity to get the component of.</param> /// <param name="componentType">Type of the component to get.</param> /// <param name="entityComponent">Retrieved entity component, or null, if no component could be found.</param> /// <returns> /// <c>true</c>, if a component could be found, and <c>false</c> otherwise. /// </returns> public bool TryGetComponent(int entityId, Type componentType, out IEntityComponent entityComponent); /// <summary> /// Tries to get a component of the passed type attached to the entity with the specified id. /// </summary> /// <typeparam name="T">Type of the component to get.</typeparam> /// <param name="entityId">Id of the entity to get the component of.</param> /// <param name="entityComponent">Retrieved entity component, or null, if no component could be found.</param> /// <returns> /// <c>true</c>, if a component could be found, and <c>false</c> otherwise. /// </returns> public bool TryGetComponent<T>(int entityId, out T entityComponent) where T : IEntityComponent; #endregion } } |
Furthermore the Entity Manager allows to easily create and initialize whole entities from a blueprint (see below) and a configuration of initial data:
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
namespace Slash.ECS.Components { /// <summary> /// Creates and removes game entities. Holds references to all component /// managers, delegating all calls for adding or removing components. /// </summary> public class EntityManager { /// <summary> /// Creates a new entity. /// </summary> /// <returns> Unique id of the new entity. </returns> public int CreateEntity(); /// <summary> /// Creates a new entity with the specified id. /// </summary> /// <param name="id">Id of the entity to create.</param> /// <returns>Unique id of the new entity.</returns> public int CreateEntity(int id); /// <summary> /// Creates a new entity, adding components matching the passed /// blueprint and initializing these with the data stored in the /// blueprint and the specified configuration. Configuration data /// is preferred over blueprint data. /// </summary> /// <param name="blueprint"> Blueprint describing the entity to create. </param> /// <param name="configuration"> Data for initializing the entity. </param> /// <param name="additionalComponents">Components to add to the entity, in addition to the ones specified by the blueprint.</param> /// <returns> Unique id of the new entity. </returns> public int CreateEntity(Blueprint blueprint, IAttributeTable configuration, List<Type> additionalComponents); /// <summary> /// Creates a new entity, adding components matching the passed /// blueprint. /// </summary> /// <param name="blueprint">Blueprint describing the entity to create.</param> /// <returns>Unique id of the new entity.</returns> public int CreateEntity(Blueprint blueprint); /// <summary> /// Creates a new entity, adding components of the blueprint with the specified id. /// </summary> /// <param name="blueprintId">Id of blueprint describing the entity to create.</param> /// <returns>Unique id of the new entity.</returns> public int CreateEntity(string blueprintId); /// <summary> /// Creates a new entity, adding components matching the passed /// blueprint and initializing these with the data stored in the /// blueprint and the specified configuration. Configuration data /// is preferred over blueprint data. /// </summary> /// <param name="blueprintId"> Id of blueprint describing the entity to create. </param> /// <param name="configuration"> Data for initializing the entity. </param> /// <returns> Unique id of the new entity. </returns> public int CreateEntity(string blueprintId, IAttributeTable configuration); /// <summary> /// Creates a new entity, adding components matching the passed /// blueprint and initializing these with the data stored in the /// blueprint and the specified configuration. Configuration data /// is preferred over blueprint data. /// </summary> /// <param name="blueprint"> Blueprint describing the entity to create. </param> /// <param name="configuration"> Data for initializing the entity. </param> /// <returns> Unique id of the new entity. </returns> public int CreateEntity(Blueprint blueprint, IAttributeTable configuration); /// <summary> /// Initializes the specified entity, adding components matching the /// passed blueprint and initializing these with the data stored in /// the blueprint and the specified configuration. Configuration /// data is preferred over blueprint data. /// </summary> /// <param name="entityId">Id of the entity to initialize.</param> /// <param name="blueprint"> Blueprint describing the entity to create. </param> /// <param name="configuration"> Data for initializing the entity. </param> /// <param name="additionalComponents">Components to add to the entity, in addition to the ones specified by the blueprint.</param> public void InitEntity( int entityId, Blueprint blueprint, IAttributeTable configuration, IEnumerable<Type> additionalComponents); /// <summary> /// Initializes the specified entity, adding the specified components. /// </summary> /// <param name="entityId">Id of the entity to initialize.</param> /// <param name="components">Initialized components to add to the entity.</param> public void InitEntity(int entityId, IEnumerable<IEntityComponent> components); /// <summary> /// <para> /// Issues the entity with the specified id for removal at the end of /// the current tick. /// </para> /// <para> /// If the entity is inactive, it is removed immediately and no /// further event is raised. /// </para> /// </summary> /// <param name="entityId"> Id of the entity to remove. </param> /// <exception cref="ArgumentOutOfRangeException">Entity id is negative.</exception> /// <exception cref="ArgumentOutOfRangeException">Entity id has not yet been assigned.</exception> /// <exception cref="ArgumentException">Entity with the specified id has already been removed.</exception> public void RemoveEntity(int entityId); #endregion } } |
Blueprints and how to store them
Blueprints are templates for entities. A blueprint contains a list of component types the entities will have and an attribute table with default values:
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 |
namespace Slash.ECS.Blueprints { /// <summary> /// Blueprint for creating an entity with a specific set of components /// and initial attribute values. /// </summary> [Serializable] public sealed class Blueprint : IXmlSerializable, IBinarySerializable { #region Public Properties /// <summary> /// Data for initializing the components of entities created with this /// blueprint. /// </summary> public IAttributeTable AttributeTable { get; set; } /// <summary> /// Collection of types of components to add to entities created with /// this blueprint. /// </summary> public List<Type> ComponentTypes { get; set; } /// <summary> /// Parent blueprint of this one. All components and attributes of the parent are also /// available for this one. Attributes can be overwritten though. /// </summary> public Blueprint Parent { get; set; } #endregion } } |
The main advantage of having blueprints is that they can be created from data, so entities doesn’t have to be created from code. A blueprint can be easily stored e.g. as Xml:
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 |
<Entry> <Id>laser</Id> <Blueprint> <AttributeTable> <Attribute keyType="System.String" valueType="System.String"> <Key>SpriteComponent.SpriteName</Key> <Value>Sprites/Projectiles/plasma</Value> </Attribute> <Attribute keyType="System.String" valueType="System.Single"> <Key>PhysicsComponent.MaxSpeed</Key> <Value>2000</Value> </Attribute> <Attribute keyType="System.String" valueType="System.Single"> <Key>PhysicsComponent.Speed</Key> <Value>2000</Value> </Attribute> <Attribute keyType="System.String" valueType="System.String"> <Key>SpawnSoundComponent.SpawnSound</Key> <Value>Sounds/Projectiles/Phoenix_AttackLaunch</Value> </Attribute> <Attribute keyType="System.String" valueType="System.String"> <Key>ExplosionEffectComponent.AudioClip</Key> <Value>Sounds/Projectiles/Phoenix_AttackImpact</Value> </Attribute> <Attribute keyType="System.String" valueType="System.Single"> <Key>DamageComponent.Damage</Key> <Value>10</Value> </Attribute> </AttributeTable> <ComponentTypes> <ComponentType>Game.Logic.Features.Movement.Components.TransformComponent</ComponentType> <ComponentType>Game.Logic.Features.Movement.Components.PhysicsComponent</ComponentType> <ComponentType>Game.Logic.Features.Damage.Components.DamageComponent</ComponentType> <ComponentType>Game.Logic.Features.Rendering.Components.SpriteComponent</ComponentType> <ComponentType>Game.Logic.Features.Audio.Components.SpawnSoundComponent</ComponentType> <ComponentType>Game.Logic.Features.Damage.Components.ExplosionEffectComponent</ComponentType> <ComponentType>Game.Logic.Features.Damage.Components.TargetComponent</ComponentType> <ComponentType>Game.Logic.Features.Firing.Components.LifeTimeComponent</ComponentType> <ComponentType>Game.Logic.Features.Targeting.Components.OwnerComponent</ComponentType> </ComponentTypes> </Blueprint> </Entry> |
To take care about all the blueprints of a game, there’s a Blueprint Manager which stores each blueprint under a unique string identifier. Like the component managers, the Blueprint Manager is not much more than a fancy dictionary, too. Beside the access to available blueprints, it also allows to read/write blueprints from/to Xml.
Furthermore a Blueprint Manager can be setup hierarchically to assemble the one manager which is used for a game from multiple specialized managers:
1 2 3 |
HierarchicalBlueprintManager hierarchicalBlueprintManager = new HierarchicalBlueprintManager(); hierarchicalBlueprintManager.AddChild(characterBlueprintManager); hierarchicalBlueprintManager.AddChild(weaponBlueprintManager); |
Creating an entity
With those managers on board, we have some easy ways to create the entities in our game. Let’s start with the most basic way, creating an entity and adding its components via code:
1 2 3 4 |
int entityId = entityManager.CreateEntity(); entityManager.AddComponent(entityId, createdEntityComponent); TestComponent testComponent = entityManager.AddComponent<TestComponent>(entityId); testComponent.DataValue = "xyz"; |
CreateEntity returns a new, unique id which can be used for an entity. Each AddComponent call creates the component of the specified type and connects it with the specified entity id. The created component is returned, so you can set its data.
If you have entities which are created more than once (e.g. for projectiles), you should create a blueprint once and use it to do the whole creation and initialization process for you:
1 2 3 4 5 6 7 |
Blueprint projectileBlueprint = new Blueprint(); projectileBlueprint.ComponentTypes = new List<Type> { typeof(TestComponent) }; projectileBlueprint.AttributeTable = new AttributeTable { { TestComponent.AttributeDataValue, "xyz" } }; ... int entityId = entityManager.CreateEntity(projectileBlueprint); |
You can pass a configuration as well to overwrite any default values the blueprint has defined (e.g. initial position/rotation). Furthermore the blueprint doesn’t have to be created in code, but received from the Blueprint Manager which might have loaded it from Xml:
1 2 |
var configuration = new AttributeTable() { { PositionComponent.AttributePosition, new Vector2F(13, 42) } }; int entityId = entityManager.CreateEntity(blueprintManager.GetBlueprint("laser"), configuration); |
Conclusion
As you can see, there’s not much magic behind the basic parts of our CBES implementation. But it makes up a pretty solid foundation to build upon and the game-specific parts can be completely moved to separate (component) classes and (Xml) data.
This way we didn’t have to make any bigger changes to the core of our framework for the last projects and see no need to change it for the upcoming ones, too. Maybe some refactoring and optimization, but the basic architecture should remain.
In the next part in two weeks, I’ll talk about the parts which use and modify the data of the components: the game systems. If you like to have a look at our full implementation, let me know and I’ll give you access to our repository to check it out in detail.
If you have any questions or suggestions for improvements, a comment would be great! Or maybe you have already written a CBES implementation on your own? I’m always looking for experiences from other programmers to learn from.
Pingback: CBES: Creating a component-based game – Coding with Style in Unity3D()