This post will give you another bunch of information how to implement a component-based entity system. We talked already about the way to setup a project in a clean way and the way data is stored. Now we’ll have a look where the logic of the game has its home.
On this note I’ll also explain the event-driven way of our framework. As a system is very self-contained, events are the way to communicate with other systems and the world outside a system in general.
Adding a event-driven architecture
An event consists of a unique identifier, the event type, and an arbitrary bunch of data, the event data. For the event type we use a simple enum value. Although you could use one big enum for your event types, every feature should have its own event enum instead. This way each feature stays separated from the others. There are some general events, like Initialized. So if each feature has its own enum you won’t run into naming conflicts as well.
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 |
namespace Slash.ECS.Events { using Slash.ECS.Systems; /// <summary> /// Type of an event that occurred within the entity system. /// </summary> [GameEventType] public enum FrameworkEvent { /// <summary> /// <para> /// A new entity has been created. /// </para> /// <para> /// Event data: <see cref="int" /> (Entity id). /// </para> /// </summary> EntityCreated, /// <summary> /// <para> /// An entity has been removed. /// </para> /// <para> /// Event data: <see cref="int" /> (Entity id). /// </para> /// </summary> EntityRemoved, /// <summary> /// <para> /// Entity components have been initialized. /// </para> /// <para> /// Event data: <see cref="int" /> (Entity id). /// </para> /// </summary> EntityInitialized, /// <summary> /// <para> /// The game starts. /// </para> /// <para> /// Event data: <c>null</c> /// </para> /// </summary> GameStarted, /// <summary> /// <para> /// The game has been paused. /// </para> /// <para> /// Event data: <c>null</c> /// </para> /// </summary> GamePaused, /// <summary> /// <para> /// The game has been resumed. /// </para> /// <para> /// Event data: <c>null</c> /// </para> /// </summary> GameResumed, /// <summary> /// <para> /// A new system has been added. /// </para> /// <para> /// Event data: <see cref="ISystem" /> /// </para> /// </summary> SystemAdded, /// <summary> /// <para> /// A new component has been added. /// </para> /// <para> /// Event data: <see cref="EntityComponentData" /> /// </para> /// </summary> ComponentAdded, /// <summary> /// <para> /// A component has been removed. /// </para> /// <para> /// Event data: <see cref="EntityComponentData" /> /// </para> /// </summary> ComponentRemoved, /// <summary> /// <para> /// A component was enabled. /// </para> /// <para> /// Event data: <see cref="EntityComponentData" /> /// </para> /// </summary> ComponentEnabled, /// <summary> /// <para> /// A component was disabled. /// </para> /// <para> /// Event data: <see cref="EntityComponentData" /> /// </para> /// </summary> ComponentDisabled, /// <summary> /// <para> /// A generic logging event. /// </para> /// <para> /// Event data: <see cref="string" /> (Log message). /// </para> /// </summary> Logging, /// <summary> /// <para> /// Action to submit a cheat code into the game. /// </para> /// <para> /// Event data: <see cref="string" /> (Cheat code). /// </para> /// </summary> Cheat, } } |
The event data is passed through the framework as an object variable. So it might be a simple integer value as well as whole custom class. The custom class doesn’t have to fulfill any specific requirements.
The receiver of an event has to cast the event data to the correct type. This is why we added the information about the type of the event data in the comment of each event type.
EventManager: Sending and receiving events
In the last posts I talked about two important managers already: The component manager to add, remove, initialize the components of the entities and the blueprint manager to hold the templates to create entities from. For the event handling we need another manager, fittingly called EventManager.
It contains four main methods for event handling:
- QueueEvent Queues a new event which is send to the listeners later
- RegisterListener Registeres a callback for a specific event type
- RemoveListener Removes a callback from a specific event type
- ProcessEvents Processes all queued events
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 |
/// <summary> /// Queues a new event of the specified type along with the passed /// event data. /// </summary> /// <param name="eventType"> Type of the event to queue. </param> /// <param name="eventData"> Data any listeners might be interested in. </param> public void QueueEvent(object eventType, object eventData) { this.QueueEvent(new GameEvent(eventType, eventData)); } /// <summary> /// Queues the passed event to be processed later. /// </summary> /// <param name="e"> Event to queue. </param> public void QueueEvent(GameEvent e) { this.newEvents.Add(e); } /// <summary> /// Registers the specified delegate for events of the specified type. /// </summary> /// <param name="eventType"> Type of the event the caller is interested in. </param> /// <param name="callback"> Delegate to invoke when specified type occurred. </param> /// <exception cref="ArgumentNullException">Specified delegate is null.</exception> /// <exception cref="ArgumentNullException">Specified event type is null.</exception> public void RegisterListener(object eventType, EventDelegate callback) { if (eventType == null) { throw new ArgumentNullException("eventType"); } if (callback == null) { throw new ArgumentNullException("callback"); } if (this.listeners.ContainsKey(eventType)) { this.listeners[eventType] += callback; } else { this.listeners[eventType] = callback; } } /// <summary> /// Unregisters the specified delegate for events of the specified type. /// </summary> /// <param name="eventType"> Type of the event the caller is no longer interested in. </param> /// <param name="callback"> Delegate to remove. </param> /// <exception cref="ArgumentNullException">Specified delegate is null.</exception> /// <exception cref="ArgumentNullException">Specified event type is null.</exception> public void RemoveListener(object eventType, EventDelegate callback) { if (eventType == null) { throw new ArgumentNullException("eventType"); } if (callback == null) { throw new ArgumentNullException("callback"); } if (this.listeners.ContainsKey(eventType)) { this.listeners[eventType] -= callback; } } /// <summary> /// Passes all queued events on to interested listeners and clears the /// event queue. /// </summary> /// <param name="dt">Time passed since the last tick, in seconds.</param> /// <returns>Number of processed events.</returns> public int ProcessEvents(float dt) { // If events are currently processed, we have to return as the events are // otherwise processed in the wrong order. if (this.isProcessing) { return 0; } this.isProcessing = true; int processedEvents = 0; // Process queues events. while (this.newEvents.Count > 0) { this.currentEvents.AddRange(this.newEvents); this.newEvents.Clear(); foreach (GameEvent e in this.currentEvents) { this.ProcessEvent(e); ++processedEvents; } this.currentEvents.Clear(); } this.isProcessing = false; return processedEvents; } |
You can think of the event manager as a generic event delegate which lets you listen for specific types of events. There is also a RegisterListener / RemoveListener method to listen for all events. This is mainly used for debugging issues, such as logging all processed events.
Actions vs. events
In our last projects we split up the events into two groups: The standard events which are send from the systems when something happened which might by of interest for another system or the presentation of the game. And the actions which are requests from outside to start an action in the system. The actions are most of the time initiated by the user via his input.
Although the division is a pure logical one (the framework itself doesn’t care if an event is an action or not), it makes it easier to see which information go in and out of a system. The naming of an event should be chosen according to its group. Events indicate that something happened, so we use the past form (HealthChanged, WeaponFired, SomethingHappend,…). Actions are requests/orders for a system, which is reflected by the imperative (FireWeapon, UseHealthPack, DoSomething,…).
One feature, one system
Now we have all parts together to create the logic for a specific game: We can create entities which hold data in their components. And we can send events around to trigger actions and inform other features and the presentation side of the game what happened.
A system uses this ingredients to implement the logic of a specific feature. You can think of a system as a small framework on its own. You’ll have fixed parts with well-defined functions that you have to implement depending on the feature:
- Init Initialization
- UpdateSystem Per frame update
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 |
namespace Slash.ECS.Systems { using Slash.Collections.AttributeTables; using Slash.ECS.Blueprints; using Slash.ECS.Components; using Slash.ECS.Events; using Slash.ECS.Inspector.Utils; using Slash.ECS.Logging; /// <summary> /// Base system class. /// </summary> public class GameSystem : ISystem { #region Public Properties /// <summary> /// Blueprint manager for this system. /// </summary> public IBlueprintManager BlueprintManager { get; set; } /// <summary> /// Entity manager for this system. /// </summary> public EntityManager EntityManager { get; set; } /// <summary> /// Event manager for this system. /// </summary> public EventManager EventManager { get; set; } /// <summary> /// Logger for logic events. /// </summary> public GameLogger Log { get; set; } #endregion #region Public Methods and Operators /// <summary> /// Initializes this system with the data stored in the specified /// attribute table. /// </summary> /// <param name="configuration">System configuration data.</param> public virtual void Init(IAttributeTable configuration) { // Initialize from configuration. InspectorUtils.InitFromAttributeTable(this.EntityManager, this, configuration); } /// <summary> /// Ticks this system. /// </summary> /// <param name="dt"> /// Time passed since the last tick, in seconds. /// </param> public virtual void UpdateSystem(float dt) { } #endregion } } |
In the Init method you have the chance to use the EventManager reference to register for events that are of interest for the feature you are implementing. This includes events from other features and actions that trigger some functionality, e.g. due to user input.
The UpdateSystem method gives you the possibility to do some frame-by-frame stuff. You should try to implement your features as event-driven as possible. But there are some features that are better off with a regular update, like movement, collision or some timer system.
Those two methods are all you need as a foundation to implement the logic of your feature. When you start a new one, you’ll have to think about the data that each entity needs for this feature. Than you will most likely add some actions (e.g. FireWeapon), use an existing event from another feature (e.g. DamageTaken) or use the regular update to trigger the functionality of the feature.
Adding a feature to the game
Implementing the system is very independent from the rest of the game. So independent that I got to the point a bunch of times where I implemented a feature, started the game and didn’t see any changes. The code compiled correctly and I was sure that there had to be at least some effect I should see.
What I just missed was to add the system to the game! It’s a really interesting thing that you have features in your game that you can just take out and the game will run fine, just without this one feature. This makes not only the entities component-based, but the whole game is component-based. The components are the systems in this case.
To add the system to the game a single line of game.AddSystem<TSystem>() or game.AddSystem(new TSystem()) is enough where TSystem is the type of the system. Its Init method will get called when the game starts and its UpdateSystem method each frame.
To make things even easier (and not forget to add it that often anymore), we added an attribute called GameSystem to our framework. If you flag your system class with it, the game will automatically add the system on game start via reflection.
Conclusion
Systems are the components of the game and implement its features. Due to an event-driven architecture, each system is very self-contained and independent from other systems. From within the system you have access to the EventManager to register for events and queue events. The EntityManager reference lets you create new entities or get the data of existing ones.
The system is the point of contact for your project. Here is where you add the game-specific code besides the game-specific components to hold the data which is used by the systems. It may take some time to become familiar with the way to split up the features. But due to the self-contained way of the systems, it’s easy to refactor a system, if you want or have to.
This concludes the main concepts of our component-based entity system framework already. One of the next posts will be a wrap-up to show how they come together in a game. If you feel already informed enough and you are interested in having a deeper look at our framework, get in touch and I can give you access to our github repository.
If you have questions or feedback, just submit a comment, I’m happy to answer all your questions and hear your opinion!
Pingback: CBES: Creating a component-based game - Coding with Style in Unity3D()