Yarn Command Attributes - Part 3

Posted On: 2020-02-24

By Mark

Last week's post explored the disadvantages of using Yarn Command Attributes, but didn't provide any specific guidance on an alternative. This week's post is intended to remedy that: in it, I'll explore designing a custom command handler, focusing on how I approached building my own. Like any of the posts in this series, today's post builds on concepts introduced in earlier posts, so if you haven't yet read them, please do so before continuing on.

Start with a Plan

Before diving into developing a custom command handler, it is useful to examine, at a high level, what the goal of the command handler is. In my case, the goal was to create a system that allowed me to loosely couple code without relying on Unity names. In order to accomplish this, I needed to solve two important problems:

  1. How does one connect Yarn Commands to code?
  2. How does the code know which object(s) to affect?

Delegating to Components

In general, Unity offers two ways to locate a given object: by its name or by its type. Names, as explained in part 2, are quite brittle, and likely to introduce bugs if code depends upon them. Types, on the other hand, are not only more resistant to breakage (the compiler will automatically verify a type exists) but also more versatile: interfaces can be used to group similar types together, so that all of them can be easily located at once. As such, I generally prefer to find objects by type or interface whenever I am looking to decouple code.

Making an Interface

When designing the custom command handler, I looked for ways to use types and interfaces to loosely couple the custom command handler to the individual commands themselves. To that end, I created a single interface (let's call it ICommand). Whenever I want to create a command, I create a component that inherits from that interface, and specify two pieces of information:

  1. The name of the command
  2. The behavior of the command
Together, those two pieces will be enough for the custom command handler to both identify and execute the relevant command.

Using the Interface

Once the interface exists, a command handler can be setup to use it. Essentially, the command handler has three key responsibilities:

  1. Locate all the available commands
  2. Filter the available commands list by the command name, to find the command(s) that should be run
  3. Run the behavior associated with the command(s), passing along any provided parameters
For my approach, I use GetComponentsInChildren<ICommand>to locate the commands, as this allows me to group all the commands together under one unified parent object. Once I have all eligible commands, I use a Linq query to filter the list just to the one command that matches the provided name*. Once I have just that one command, I can call the function associated with that command's behavior, passing in the command parameters (provided by Yarn Spinner).

Connecting the Command to Objects

One convenience about using Yarn Command Attributes is that the command handler is guaranteed to be attached to the correct object (since the it looks up the object by name.) The custom command handler has no such convenience; however, command parameters can still be used to indicate to the command which object(s) it should affect. For my project, I already make use of a number of system singletons (aka. Managers) that are smart enough that they can identify objects by either a unique name or ID*. As such, the bulk of the work of the command is just a matter of using the right manager - a command simply calls the correct method on the manager and passes along the necessary information as parameters. As a result, most of the actual work of a command is done well in advance: when a feature is first added to a manager it is implemented and thoroughly tested; it is only after the manager is verified that I implement and test the command to use it (which is much faster and requires far less testing.)

Conclusion

When all these parts run together, they create one single harmonious process. Yarn spinner passes any commands it encounters to the command handler, which looks up the correct component based on its name. Once identified, it runs that component's command behavior, which makes use of Managers to find the correct object and perform the correct action (such as explode a particular barrel.)

If you're considering using this approach (or something similar) yourself, then be sure to tune in next week, for the fourth (and final) part to this series. In it, I will explain several additional advantages that this approach provides, as well as the pitfalls that one should be aware of before committing to using it.