Justifying a Code Rewrite

I have a sketchy, working version of AdventureKit:

An animated gif showing AdventureKit in action. In it, we see a square from Lecce, Puglia. To the left, a man in a trench coat walks towards a bunny in a box in the middle of the square. As he approaches, the bunny shies away from him and the man says 'He seems a little afraid of me.'

Since I announced that I was working on it last year, it has grown considerably.

After taking a hard look at what I’ve done so far, I realised that there are some fundamental problems with it. It has become more complicated and the interactions between components have gotten messy.

It also has a few key things missing that would be tricky to add at this stage:

Generic Node Components

As I got further into game development, I realised the importance of having a generic node component that acted as a container for all other node types when working with ECS.

Right now AdventureKit has a corresponding specialised component for each node type (SpriteComponent for a SKSpriteNode, LabelComponent for an SKLabelNode, etc.) and each of these components has to implement the update(deltaTime:) method to handle position, rotation, and scaling of their specific nodes.

There are alternative solutions to this duplication that I could shoehorn in now, but none of them are as elegant as rewriting the framework to use a generic node component.

Cross-Platform Support

SpriteKit is a cross-platform framework but AdventureKit is not.

While the input methods are obviously different on iOS and macOS, I’ve found that they can be standardised into a set of generic interactions which the rest of the engine can then respond to.

A left-click on macOS and a tap on iOS can become a primary interaction, for example. Clicking and dragging with the mouse on macOS or panning a finger across the screen on iOS can be standardised into a pan interaction (with start, moved, and end states).

The engine can then be built without considerations of what platform it’s on. I would never have to think about platform-specific code again, and this is something I definitely want AdventureKit to have.

With that, I could then have…

A Clear Interaction System

My current interaction system is anything but.

AdventureKit uses a system that is designed around the concept of verbs. Every entity has a series of verbs that they enact when certain interactions happen. For example, a single tap on the background triggers the moveTo verb causing the player to walk to the tap location.

Here’s what I currently have that makes this work:

  • An InteractionComponent, which defines which of these verbs are available for each entity.
  • A PlayerControlComponent, which implements all of the gestures (and they are all currently limited to iOS gestures).
  • An ObjectiveComponent that implements the verbs themselves and keeps track of how long they’ve been running and whether they can be interrupted.

The PlayerControlComponent currently has to be aware of certain verbs. If a player is talking and a user taps the background (triggering a moveTo event) then this component will see the player is talking and prevent it from moving.

However, the ObjectiveComponent also has to be aware of verbs as it actually handles the implementation of the verbs. So the PlayerControlComponent will prevent the player from moving if they are talking and the ObjectiveComponent handles updating the DialogueComponent. This means that dialogue functionality is spread across three different components.

On top of that, both the ObjectiveComponent and PlayerControlComponent have corresponding systems that are called every frame to figure out between them what is happening.

It’s very messy and confusing and, since first drafting AdventureKit, I have come up with a much simpler solution.

My Case for Rewriting AdventureKit

Rewrites can be huge time sinks, are almost never worth it, and need to be heavily justified.

Clean code never survives contact with the real work. The more it gets used, the more edge cases need to be accounted for, and the messier it gets.

A younger version of me may not know as much as current me, but he was still not stupid (well, not all of the time). Things I did in the past were done for a reason and it’s often much more beneficial to slowly and carefully refactor what I already have rather than throw it out.

My justification for rewriting is that the AdventureKit rewrite will not be totally from scratch.

Much of my game development learning over the past year has been done using a different framework that I created specifically so I could experiment wildly without breaking what I had.

These experiments have now borne fruit, and the adventure game elements of this new framework are more robust than AdventureKit itself and fix many of its shortcomings.

There is also lot of good stuff in AdventureKit that I can simply copy over. The pathfinding and movement systems work really well and are isolated enough that I can basically copy them across to the new version and they’ll continue to work as they’ve always done. Same goes for the actual DialogueComponent itself.

Building on this new framework of basic components, AdventureKit will be more robust and extendable, with cross-platform support built in from day one.

However, the biggest change that I’m most excited about is that there is a much simpler interaction model:

let backgroundEntity = "Background"
let entity = AKEntity(named: backgroundEntity)
let instructionComponent = InstructionReceiverComponent()

// Define some interactions. See how easy they are to read! 
let moveInstruction = Instruction(backgroundEntity, wants: .entityWithID("Player"), to: .moveTo, .currentPoint)
let labelInstruction = Instruction(backgroundEntity, wants: .allExcludingItself, to: .showLabelAt, .currentPoint)
let animateInstruction = Instruction(backgroundEntity, wants: .itself, to: .runAnimation, .named("explodeBackground"))

// Define which interaction triggers each instruction
instructionComponent.add([moveInstruction], toInteraction: .primary)
instructionComponent.add([animateInstruction], toInteraction: .secondary)
instructionComponent.add([labelInstruction], toInteraction: .pan, withState: .moved)

Pseudo-English sentences for directing entities using a single instruction component! (I’ll be writing more about this in future posts.)

Generally, a code rewrite is a bad idea but sometimes something comes along that makes it absolutely worth it. I think this is one of those times.

Get all of my latest posts direct to your inbox! Pop in your email address here: