3
\$\begingroup\$

The Plan
I am up to write write an own Trigger/Event System, an event engine or how you call it. I would like to achive something similar to Blizzard's most level editors, like Starcraft 1-2, Warcraft level editors.

They are all kind of event/trigger based, you can set actions, conditions which actually control the gameplay itself. What do you think, what would be the best architecture for this event/trigger system?

The Architecture
I thought it would look something like this:

  • EventManager - would handle the events, checking if the conditions meet for any event, and calls the event if so.

  • EventHolder - would hold the list of the events.

  • EventEntity - would have an ID, the actions' ID and the params for them.

Every entity, enemies, player and other stuff would have an ID in the world, which would help to get the entity itself and fire a trigger on it.

Obviously, there would be finit and pre-set BASIC actions, but that would be stored by the current project itself. The project could pass the EventSet (list of EventEntity) to the EventHolder. Thus this whole event/trigger system would be very flexible, and could compile it as a DLL for use in future projects.

Do you have any better solution for this? Or some pro tips that I could use? Would really appreciate any help! Thanks!

Edit:
This picture shows it quite well:
Wacraft 3 Map Editor's Trigger Editor

Edit2:
I have wrote a raw architecture of a possible "engine", but it is missing some MAIN points:

{Initializing}

//in order
[Game]--set event list--[EventManager]--requests list of EventEntity--[Game]--sends list of EventEntity--[EventManager]

{EventManager loop}

[EventManager]--checks if any of the events meet in an EventEntity
[EventManager]--if any meets--[EventManager]--checks if all of the conditions are meeting in the EventEntity
[EventManager]--if all meets--[EventManager]--fires actions of the EventEntity

{Architecture of the Models}

[EventEntity] contains
- [List of Event Models]
- [List of Condition Models]
- [List of Action Models]

[EventModel] contains
//quite similar to the ConditionModel, an event only should fire, aka check the conditions and
//fire the actions, if these special conditions, happenings, events are fired
//like a unit dies, player reaches a specific position, etc.

[ConditionModel] contains
//model must simulate AND, OR logic statements
//model must simulate IF statements

[ActionModel] contains
- [List of Targets]
- [List of Action IDs] //One action per Target, a specific Target can be listed more than once in the [List of Targets]
//something is missing

Trying to write the "system" as standalone as possible, it must not depend on any project's source. Thus I think the best would be to seperate the data from the architecture, could store the event, condition and action datas in another DLL unit, and could read from that.

\$\endgroup\$
3
+50
\$\begingroup\$

A trigger-event system is pretty fool-proof (as illustrated here). Trigger an event, and all objects listening to that event will react. It can get quite cumbersome though, managing event types, especially when you have similar events, but different targets. The alternative I'm proposing is a messaging solution similar to your example, with a few key differences:

Event {
    List ids;
    Action action;
    List args;
}

The ids represent all the entities that should receive the event (eg. player1, goblin3, world, HUD). The action would be an action from a preset list of possibilities (eg. damage_player, heal_goblin, reset_level). The args would be any list of arguments relating to the action (so for a heal action, it might be a number representing the amount).

Next, you have your EventManager:

EventManager {
    Queue events;
    Dictionary listeners; 

    addEvent(Event event);
    delegateEvents();

    addListener(String id, Listener listener);
}

The event manager holds two types of items: events, and listeners. Whenever an event occurs, it gets passed to the event manager (addEvent()). The event gets added to the queue of events. Then, at some point, (such as during the next update loop), you call delegateEvents(), which sends the list of events to the approproate listeners. Let's define a listener before moving on:

Listener {
    Queue events;
}

Every game object/entity, that is capable of receiving events, will have a listener attached to it. The EventManager will store a reference to this listener (added via the addListener() function), and will delegate events to these listeners, as illustrated above.

Once the events have been passed on to the listeners, the game objects/entities themselves will go through the events passed through to their listeners. The way in which they interpret the events will be specified for the game object itself. So if a heal (action) event is passed to the player (id), with an argument of 418 (arg), the player will update its own health with 418 additional hp. The player would then send an event to the renderer, and sound engine, that a heal of type foo was initialized at location bar, and they would then initialize the appropriate animation and sound.

Just to illustrate the difference between using triggers vs. messages, again using the healing example:

The first iteration of your game doesn't have a healing animation. You heal, it only updates a stat. Then, you want to polish it up, add some visual flare; with a trigger-event system, you need to register the renderer as a listener to the player heal event. In comparison, with the messaging solution, you need to tell the player to send a heal event to the renderer.

Two ways to achieve the same thing. I'd advocate the messaging solution, but that's mostly just personal preference.

\$\endgroup\$
  • \$\begingroup\$ This sounds really interesting: I built my own "trigger system". There are different TYPES of triggers, like camera trigger, sound trigger etc, and a list of parameters just as in your example. There are initializer events which the game runs immediately at the beggining of the level. So I actually seperated event types and instead of checking the events all the time, I pass the entities the IDs of the events to be called. So on death for example, the hero calls an event (which can be an event chain aswell). This may not be the best solution, but I found it really flexible. \$\endgroup\$ – Zhafur Feb 6 '14 at 14:54
2
\$\begingroup\$

To be honest, your system sounds like it will work just fine. There isn't going to be any best way to make something like this, but whatever works better for your tools and editor.

One suggestion I might make however is to use some sort of map, using either a string for the key or perhaps a hashmap. That way, a script or plugin could more easily extend the available events/triggers than having to compile a new DLL each time.

\$\endgroup\$
2
\$\begingroup\$

There might be a heavy lifting work on the hands of the EventManager if every frame god knows how many parameters must be accessed and checked, this is a passive solution.

I recommend using an active solution, a bit dirtier and harder to maintain but much more efficient potentially. Where each parts of the engine and game where parameters are subject to be watched, could trigger an event by calling a signal on the mutator method. This means, that you need boost::signals or libsignals, or C# with multicast events plugged to delegates.

This also allows dependency inversion and high decoupling, which is desirable. It is even better than classic DIP using interfaces. Philosophically there is not much difference between using a virtual call and a signal system though, the design is different, less contract therefore less clear big picture (harder for maintenance), but better decoupling, more flexibility.

Anyway, this is a low level cosnideration, but still, don't go too much by the rule "early optimize is root of all evil", I abhor this phrase, its just plain wrong and make people end up into UniformlySlowCode faster than lighting. Think well on the design phase so that impossible refactoring doesn't have to ensue later.

Then, using a true ECS (Entity Component System) will help the signal/slot design because of easier control of property mutation, so if you have an ECS you must have paved your way for smoothness.

In a classical hierarichal design, it still can be possible to get ECS-equivalent behavior if using a reflection system right from the start. Where mutators/accessors are generalized through a common interface, and access to members is made via global indices. This can be a serious overload, specially when sometimes the get/set is not trivial (big switch and each member has its way of being set/get...) but some engines DO work like that.

You may use JSON or something equivalent to describe your triggers and the list of parameters they convey. Then a visual scripting GUI can be presented to the designers so that events may show up as boxes on a graph, with sockets on the right side that can be plugged to entities (represented as boxes as well) using arrows, this will represent the signal to slot connection in terms of code. and one signal can be plugged to multiple slots. Then you can have computing nodes, like if you want the position of a hit event to get through a math function so that you can use a random seed for a spark emitter, or a timer to delay an event, then the nodes re-outputs the modified event to any plugged entity.

This description is obviously just saved as an event file for the data driven event engine, which can be isolated in a dll where features like event-file parsing can be done, and the whole event-file data model in memory can be stored, but necesarily, there will be procedures to code in upper libraries to connect their signals, which cannot be done by a DLL since bi-directional dependencies cannot exist. beware of your dependencies.

hope that helps getting a perspective

\$\endgroup\$

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.