Moving from Delegates to Closures

One of the advantages of blogging about my work is that it makes me think carefully about what I’m doing. After finishing my post on my instruction system, I started reading more about closures which led me to reconsider some of the decisions I’d made.

There are a few things that I really don’t like about using the delegate method as it currently stands:

Firstly, the required for loop and switch statement required at the start of the setupNext(instructions:for:) method.

This method requires me to remember how the system processes instructions. I need to know that the array of instructions coming in are designed to be started together.

Secondly, the code to perform the instruction is being written somewhere other than where the instruction is defined.

This can add friction to debugging—I have to check that the correct instruction is being added to a given entity in one place and then they would have to look somewhere completely different to see what it’s actually doing.

Thirdly, if I use multiple handler files to organise my code (e.g. for different groups of entities), then it can be confusing where the verb should be defined. The instruction is added to the entity that receives the interaction but the instruction system looks for the handler of the target entity.

If I’m targeting all entities or a random entity, then the same verb will need to be duplicated in all of these separate handler files.

Moving to a Closure-Based Syntax

I tried using closures instead and came up with the following:

Pros

  1. The action code is defined next to where the Instruction is defined, keeping everything in one place.
  2. The initialisation of the Instruction struct involves fewer parameters.
  3. As a closure, it can capture any additional information from its surrounding context that might be relevant to the action.
  4. The system can provide a lot more information to the closure when it’s called.
  5. Returning an InstructionState enum instead of a bool makes it clear when reading what state the instruction is left in at any given point.
  6. A Verb closure only has to be defined once (as opposed to the potential duplication when using multiple handler files). I could even use the factory pattern to vend the same verbs to different entities across different files in the game.

Cons

  1. Working with closures as variables isn’t always as straightforward as using a switch statement (warning: link contains cursing).
  2. Having the code to set up the action and the code deciding whether or not it has completed in the same place is messier than having them separated. I don’t love using timeElapsed == 0 as a way of figuring out if this is the first time the closure has been called.
  3. You can no longer have named parameters in a closure. This means that it isn’t as clear when defining the closure (although code completion can help somewhat)—here’s the Swift team’s reasoning behind this.
  4. I have to be more careful about capturing state. It is a lot easier to create retain cycles with closures or to inadvertently modify a reference before the closure has finished (especially as it’s potentially being called many times over a long period of time).
  5. There is a danger that a large number of long, complicated Verb closures could make the code for setting up instructions for a given entity much less readable. However, as each Verb closure could potentially be called once per frame, they really should be short blocks of code that do little more than set a bunch of properties on components and return quickly.

Moving to Closures

Much of coding, like much of design, is about managing trade-offs. In this case, I think the clarity that comes form having the action of the instruction defined as part of the instruction is a lot less confusing than using the delegate method.

It also encourages brevity within the closure, which is good as these closures will get called many times a second.

I’ve now moved my entire basic components library across to this system and everything works as it did previously with no perceivable loss of performance.

The best part is that, while doing this, I was able to delete a half dozen files and many lines of code. I’m choosing to read this as a sign that this is the better approach!

  • Share:

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