Introduction
In a previous post, an idea was discussed on how to handle input events with our ECS architecture. Basically, the input events are fetched from somewhere and put into a separate Entity Database. There are two types of Systems for this separate database. One set of Systems generates such Entities and puts them into the database; another set of Systems filters events they are interested in, performs some processing and alters the primary Entity Database accordingly. However, it was left unmentioned where those input events come from and how to access them in Systems. This post addresses these questions and some other details which are critical to actual development.
Access Input Devices
In an object-oriented approach, it’s tempting to model devices such as keyboards and mice as classes like Keyboard
and Mouse
. The problem is this approach doesn’t fit well with every platform. Under Linux, device input can be accessed via reading device files. Therefore the OOP approach seems to have a good match. However, with Windows, the raw input is designed to work with applications instead of windows. This makes working with individual devices hard. Since the OOP approach forces us to match physical devices with device class instances, another problem arises which is that the devices can be plugged and removed at any time during the game. Who is supposed to manage the lifetime of the objects and how? We put in the Entity Database to have good replay support. But with those objects, all promises could be broken.
The things look easier if we could leverage the functionalities of the Entity Database. The Database provides filters over component types. If we could use component types to tag all the events, the Systems can use those tags to filter out only those events coming from devices they are interested in. For example, we can tag all keyboard events with ComponentEventKeyboard
and mouse events with ComponentEventMouse
. Likewise, we can use ComponentIndex<0>
and such tags to indicate from which specific device the event came from. If the System is not interested in that info, it can just ignore this tag and only use ComponentEventKeyboard
to filter the Event Entities.
This approach has a limitation, though. Because the component types must be specified at compile-time, only limited numbers of devices of the same type may be supported at the same time. Hopefully, this won’t be a serious obstruction in reality. For an application to support multiple devices, a feasible way could be inserting a templated System for each device index meant to be used. For example, if we were to support two joypads for multiplayer purpose we can just insert two Systems each bound to a different character. If any event coming from the second joypad was detected, the System just responses accordingly.
This way, the abstraction of devices as classes are completely eliminated, despite that the input manager implementation can make them on their own if it finds it useful for particular platforms. Without those unnecessary clutters, the Systems neither have to worry about device changes during the game.
Entities inside the Event Database
The correct processing of input events relies on an unspoken promise: the events come in by the order of time. Imagine what happens if an event for a key was released comes before an event for the key was pressed. The purpose of the Entity Database is to store data coherently so that accessing them results in good cache behavior. For that, we have the Archetype class that stores a reference to the last pages used to create an Entity from the Archetype so we have likely coherent Entities in one page. This indicates that when we insert Entities for different device events, they go to different pages so that good cache behavior could be maintained.
However, from the System’s perspective of view, the events should be accessed in the order of event timestamps despite they may come from different devices. For example, a GUI System is interested not only in the key presses but how the mouse pointer is moved into some region of interest, the key events are processed until that the pointer leaves the region handled by the GUI System. In this case, the System would like to process two kinds of events in one loop.
The bad news is: since the events are scattered on different pages, it is almost guaranteed that the events will be visited in a wrong time order even if the Component Filter covers both mouse and keyboard events.
To resolve this issue, let’s first review the design of the Entity Database.
The Entity ID is a 64-bit integer that will never be reused because we were never supposed to run out of numbers in it. At each time when an Entity Page is allocated, the ID counter is assigned to the page as its starting ID and the counter is then incremented by the page size, which is 32. The Entities are always created from an Archetype, which refers to the last page used to create from this very Archetype, or if such a page is not available, a new page will be allocated on-site. This guarantees that if we use the same Archetype to create Entities, the ID of the created Entity will only increase.
However, it is NOT guaranteed that the Entity IDs will be consecutive because there could be other pages created between the one being reused and the one being allocated when the old page is full, possibly A LOT. Therefore, if we were to store the input events in an Entity Database, the only thing that is guaranteed is that the Entity ID goes up with the insertion – nothing else.
A feasible solution is to somehow insert all the events with the same Archetype so that the ID goes up with the timestamps of the events. Obviously, despite the ugliness it creates in the codebase, it also hinders the cache coherent. So this is a big no-no.
We have made an assumption till now that the events come from the system in the order of timestamp and we put them into the database in the same order. Hopefully, this is always true for all platforms. As long as this premise is valid, no matter how the events are scattered in the database, we can actually always externally synchronize events from specific devices by their timestamps.
The trick is if we can have an iterator for just one device which guarantees that the events of that particular device could be visited in the order of time, we can just take an iterator of another device and merge these two as if we were merging two sorted arrays. To make this work, the iterator must also guarantee that the Entities are always visited in the incremental order of Entity IDs.
1 thought on “Bridging the Input Manager with Input Event Database”