Gameplay events II

22 May 2015

To avoid writing the most boring blog post of the decade, I’m going to condense this update into no more than a few short paragraphs.

Gameplay events

Last time I talked about signaling global state-changes using gameplay events and handle them inside the K14Game class. At that time I was still debating how far I should take this concept, and whether I should also use gameplay events to signal entity-local state changes. Eventually I went all-out and added gameplay events for basically everything, from applying thrust, to shooting, rotating the player ship, everything. My reasoning was that if every state change, either global or local, was signaled using gameplay events, I could completely do away with the user input simulation for action replays. Instead of storing only user inputs (tilt, thrust, shooting, tractor beam) and exactly reproducing gameplay as if someone was playing the game, I would just record every state change instead. Replaying would just be a matter of going through the same state changes in order. The advantage of this approach is that replays would be resilient to changes in e.g. the control scheme, tweaks to gameplay constants, changes to enemy behavior logic, etc. All in all, it seemed like a great idea.

To make a long story short: after I added gameplay events for everything, including strictly entity-local state changes such as setting custom entity properties, I found out that serializing the resulting stream of events to an action replay would produce ridiculously large action replays. The culprits were state changes such as ‘player rotation changed’, ‘thrust applied’ and ‘fuel changed’. While playing, there would be seconds-long stretches of gameplay where these events fired on every update, and at an update frequency of 30Hz, a typical action replay for 1 minute of gameplay would already have over 2000 events, and run over 250KB of data.

I did try to come up with smart strategies to try to limit the number and size of gameplay events and to apply some kind of ‘compression’ to them, e.g. in the form of only registering events like ‘start rotate at speed’ and ‘end rotate’ instead of ‘set rotation to x degrees’. In the end, I concluded that every size optimization would either negate the advantage of storing gameplay events instead of input events, or require an ungodly amount of event parameters to be able to encode each and every gameplay constant with a gameplay event. So I reverted everything, and went the pragmatic way: only global state changes will be signaled and handled using gameplay events, and reproducibility of action replays will be purely based on input simulation. This means any change that affects input handling or game mechanics will invalidate the replay, but I’ll take that for granted.

I’ve defined the following set of gameplay events, all of which are handled from inside the K14Game step method. The constants should speak for themselves as to what the gameplay events signal. Everything outside of these events is handled by the entities themselves.

/** Enumeration type for gameplay events */
typedef NS_ENUM(int, K14GameplayEventType)
{
  K14GameplayEventTypeEntityHit,
  K14GameplayEventTypePlayerHit,
  K14GameplayEventTypeFuelPodDepleted,
  K14GameplayEventTypeProjectileLaunched,
  K14GameplayEventTypeOrbLifted,
  K14GameplayEventTypeEntityTractored,
  K14GameplayEventTypePlayerFuelChanged,
  K14GameplayEventTypeReactorSelfDestruct,
};

Next steps

The following thing I’ll be working on will probably be scriptable levels, which will involve defining a minimal C-like wrapper interface around K14Planet that can be exposed to Lua scripts.

Development scoreboard

I spent about 5 hours implementing gameplay events for about everything that happens in the game, then another hour to revert almost everything except the events used for global game state changes. This brings the total development time to ~175 hours. SLOC count increased by only 11 lines, mostly because I cropped a few lines of dead code. SLOC count is now at 2467.