4
\$\begingroup\$

Some backstory:

I decided it was time to start making a game. I've got a bachelor degree in computer sciences, but I didn't really get to code much during my studies.
I've started making a quick prototype with libgdx, checking how the library worked et al. Then at some point I decided I knew enough of the basics and that it was time to move on and start doing something more complex and flexible.

I've started wondering how organize the logic and the data, and so I found out about Entity-Component-System architecture. Basically my implementation is something like this:

  • Entity: just an id (UUID).
  • Component: data. It can be a PositionComponent (float x, y), VelocityComponent (float x, y), RenderableComponent (texture t), ...
  • System: Has an update() function that runs the logic every frame, and listens to message..

I've then discovered the EventManager, and have been struggling in implementing it propely.
Right now, it has an evergrowing enum to identify the different types of events. I can feel it is a bad thing since every class that listesns to / receives events need to include that, but I can't figure out another way to handle it.

Question:

What does this eventmanager really do? It doesn't send any message other than "hey, wake up, something you care about happened, can't tell you what tho, check it yourself".
Every type of event needs custom data to work with.

So the question is, how to append data (of different types) to an event message?

Practical example:

I would have liked to change the Entity-Component-System architecture a bit. Basically Systems may require more than a component to work with.

For example the MovementSystem will need both PositionComponent and VelocityComponent,
RenderSystem will need PositionComponent and TextureComponent.

So I wanted to create another object, the Node to hold all the components that a System requires, if the Entity has them.

To do so I need to keep a list of Nodes for every System, wich can be achived (I guess) with messages on ComponentAdded / Component events telling wich component has been added/removed and check if a node needs to be added/removed.

Conclusion:

I don't know if this architecture I tried to describe is actually any good, so I'm open for any complete refactoring suggestions.
My ultimate goal is to make a game, but I'm really interested in learning design patterns and techniques aswell, so I don't mind if I'm overengineering it.
There's no design behind the game yet, so for now I just want to do something 'general'.

It's my first post here, so sorry for my excessive usage of ThisThingIObviouslyDontKnowHowToUsePropely.

\$\endgroup\$
4
\$\begingroup\$

I often see people over-engineering event systems, my advice would be to keep it simple. For a simple game an enum suffices to identify events. For games with a large amount of events from different code modules and scripting functionality it will become a burden because it is not extendable (and increases compile time).

Imagine you added a script system and I want to write an AI using your game engine. I will want to add certain events, but won't be able to, because I can't add an enum from the scripting environment in hindsight. An easy way to solve this is to add a function like uint32_t eventType = registerEventType("PLAYER_LOOKS_AT_ACTOR") which will hash the string name of the event to an integer, which you can use instead of the enum. You may get hash collisions, so you will either need to use a linked list here (complex) or just tell the user to use a different event name (simpler, shouldn't happen often with a good hash anyway).

Your event will then look something like this:

typedef struct {
  uint32_t type;
  uint32_t time;
  void     *data;
} Event;

Where the type of data is known due to the event type. You can go fancy here with language features, but you will want to avoid run-time allocation of memory, because you will create a lot of events per frame usually. Basically you want a pre-allocated, large chunk of memory somewhere that you can use to store event data of various events in and just point to that. Professional game engines will even have their own malloc implementations that just point to pre-allocated memory. Also, when communicating such data between a scripting environment and the host environment, you will pretty much end up with a C-solution anyway.

A custom event would then be able to attach data with something like setEventData(Event*, void* data, size_t dataSize), which would find free memory in the pre-allocated chunk, mark it as used, copy the event data there and set the pointer in the event. The event system does not understand the contents of this data and it doesn't need to, it's just a chunk of memory. This makes it more modular and extendable.

Event newEvent;
newEvent.type = registerEventType("PLAYER_LOOKS_AT_ACTOR");

typedef struct {
  uint64_t actorUUID;
} PlayerLooksAtActorData;

PlayerLooksAtActorData playerLooksAtActorData;
playerLooksAtActorData.actorUUID = 0xdeadbabe; // some unique id of the actor being looked at, better to avoid pointers for this

if(setEventData(&newEvent, &playerLooksAtActorData, sizeof(playerLooksAtActorData)) {
  // on failure, not enough memory in pre-allocated chunk?
}

All components that want to listen to this event would now use the same hash function as the registerEventType function and need to know what the contents of the data look like (they should share the include for the struct or redeclare it):

void player_handleEvents(Event* event) {
  // static to only compute this once
  static uint32_t const playerLooksAtActor = getEventType("PLAYER_LOOKS_AT_ACTOR");

  switch(event->type) {
    case playerLooksAtActor: {
      PlayerLooksAtActorData* lookatData = (PlayerLooksAtActorData*) event->data;
      if(lookatData) {
        log("player looks at actor %08x.\n", lookatData->actorUUID);
      }
    }
    break;
  }
}

That said, there are obviously many ways to go about this. I'm just suggesting a simple but quite powerful system.

\$\endgroup\$
  • \$\begingroup\$ Thanks alot for your answer! The enum replacement solution sounds great, I'll have to study how to handle hashmap collisions. :) Also the void* thing is new to me, I've did I quick research and found that the Java equivalent should be creating an Object and cast it to the appropriate type, just like you do with the void*. I'll see if I can make it work. :D \$\endgroup\$ – quien Mar 7 '14 at 15:33
  • \$\begingroup\$ In the end it was quite trivial, I feel silly for not knowing the powers of casting to a subclass. ç_ç I'll now clean the code and work on that Node system I mentioned in the opening post. :D \$\endgroup\$ – quien Mar 7 '14 at 17:52
  • \$\begingroup\$ Good design is usually trivial, but rarely obvious. You may want to research some other solutions on the web (be warned, most will be bad). You'll see that some people go all enterprise-programming over this, introducing meta-compilers and complex design patterns they're aching to use because they never had the chance to. \$\endgroup\$ – bogglez Mar 7 '14 at 19:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.