Command Pattern ++
Introduction
If you don’t already know, Void Mining is a game about writing scripts for mining bots. You’ll also, naturally, have a console to interact with certain bots or use certain commands and test them out.
Requirements
Here is a list of things that I need in order to both make the commands amenable to the future scripting language additions:
- I must make it easy to add more commands
- I must make each command that modifies the world state to run at a specified, dynamic tick rate
- I must make each command that modifies the meta state (e.g., camera commands, print commands) to run ASAP
- Some commands must be able to run together, e.g., two moves in one tick, or a move and a print in one tick
- I would like to keep the overall coupling of the system relatively low.
- note: this entails more work than you’d think. more on this below.
Keeping Coupling Low
Let’s say that we had a massive switch statement with all possible command words (e.g., echo, move, select, follow, etc.) that then handled the parsing and logic and all function calls for that function. The naive implementation would be to have a “mega file” that contains the required classes and data for each command. For example, echo needs to access the output data for the output text field, the move command needs to access the DataTilemap, the select command needs to access the list of all bots, and the follow command also needs the list of all bots, along with the camera. This is far too much responsibility for a single class. The next logical step would be to separate each command handling function into its own command handler where it can manage its own required data. There are a few problems with this. The first is that there is now no longer an order to execution. If I type a move command and then immediately type an echo command, they will both run, presumably, on the same tick. This is a relatively complex fix, and let’s observe another problem. This does not easily allow for use by a future scripting language. It does not expose an API that we can use to queue actions or events or what have you.
The solution that I’ve implemented treats all concerns equally, but adds a little bit of complexity overhead. In short, I package commands into, well, objects of type Interface Command, with a single method called Execute(). Then, anybody can make one of these from the exposed factories, where the factory will automatically plop it into a commandQueue. The CommandInvoker will, every frame and every tick, take a command from the instantCommandQueue and the tickCommandQueue and executes it with Execute(). The command invoker has no idea what the command is. In fact, the command is created, and then its internal details are never touched nor modified nor read externally ever again.
Let’s get into it.
A Custom Tick System
This is necessary if you want to have something happen every specified number of ticks. I think FixedUpdate() can do a similar thing, but I would prefer to have my own class in which I can easily tune and pause the tickrate whenever I want.
This is pretty straightforward. Create a singleton (yuck, I know) that emits a tick event every set number of seconds. It will have a static int tick attribute and a static int GetTick() method. If you want, you can follow this tutorial from CodeMonkey on YouTube.
This singleton tick system will allow us to follow the same tick anywhere we are, allowing us to synchronize systems.
A Command, Without Console
Let’s first start by defining some commands before talking about how to instantiate them from the console.
public interface ICommand
{
void Execute();
}
Simple definition. Definitely constrains the ways in which we can do things, but you’ll see that it actually leaves a lot of room for custom functionality.
Simple Commands
Let’s start with a simple command: Printing to the console.
public class EchoCommand : ICommand {
private readonly string msg;
public EchoCommand(string msg) {
this.msg = msg;
}
public void Execute() {
Debug.Log(this.msg)
}
}
How about making a move command?
public class MoveCommand : ICommand {
private readonly Transform receiver;
private Vector3 newPos;
public MoveCommand(Transform receiver, Vector3 newPos) {
this.receiver = receiver;
this.newPos = newPos;
}
public void Execute() {
receiver.position = newPos;
}
}
Command Queue
This one is pretty straightforward. You know what a queue is, right? First in, first out? Right…
…
Well, that’s it, really. Make it a scriptable object, probably.
public class CommandQueue : ScriptableObject {
private readonly Queue<ICommand> queue = new Queue();
public void Enqueue(ICommand command);
public bool TryDequeue(out ICommand command);
public int Count;
public void Clear();
}
A couple things to note here:
- Note how we’re not doing events here. We have a shared instance of a
CommandQueueacross everything that uses it. - Note the TryDequeue. It returns
true/falsebased on whether theQueuehas an element that it’s able to dequeue. If it does have an element, great. If it doesn’t, theoutvar is undefined (although it’s typicallynull, you shouldn’t depend on that)
Command Invoker
Now that we have a CommandQueue, we need a CommandInvoker. Note that we need to make this a MonoBehaviour because it needs to have access to Update()
public class CommandInvoker : MonoBehaviour
{
[SerializeField] private CommandQueue tickQueue;
[SerializeField] private CommandQueue instantQueue;
private void OnEnable() => /* register your OnTick() function with your time tick system's event */
private void OnDisable() => /* deregister the OnTick() function!! */
private void Update() => InvokeCommand(instantQueue);
private void OnTick(int tick) => InvokeCommand(tickQueue);
public void InvokeCommand(CommandQueue queue)
{
// safety checks ommitted
ICommand command;
if (queue.TryDequeue(out command))
{
command.Execute();
return;
}
}
}
Great. Now, when we enqueue an ICommand instance to either of the command queues, it will be executed eventually by the CommandInvoker. Note that this is limited to one command per frame for the instantQueue. You may come to find that this is not enough. In this case, I think you could either use events to resolve this (i.e., whenever a command is added to the queue, immediately execute it) or you could just loop over ever command in the queue upon every frame. Or, sometimes you could use a…
Composite Command
You may be wondering to yourself, “O Moon, if I wanted to run multiple commands in a single tick/frame, how would I do so?” and I would say, “O Ye, underestimate me.”
Let’s say in our move command, in the same tick that the move is executed, we want to print whether it was successful or not. We can’t currently do this, especially not with the tick queue. We’re limited to chugging along at one command per tick. Or, what if we wanted to move multiple bots in a single tick, especially when a ton of scripts are running simultaneously? Oh god… Oh, wait. I’ve already figured out a solution.
The composite pattern applied to the command allows us to run multiple commands in a single command. Again, in a simple composite pattern, you have two or more classes extending an interface that has a single method. All of the classes except one are just normal implementations of the interface. The special implementation contains a list containing elements of the same type as the interface, and this special class’ method executes the methods of the classes in its list.
So, in the context of my ICommand, it would look like so:
public class BundledCommand : ICommand
{
private ICommand[] commands;
public BundledCommand(ICommand[] commands) {
this.commands = commands;
}
public void Execute() {
foreach (ICommand command in commands) {
command.Execute();
}
}
}
Example
Now, if we want to move to (0, 0, 0) then print “moved to origin!”, we can:
ICommand moveCommand = new MoveCommand(new Vector3(0, 0, 0));
ICommand echoCommand = new EchoCommand("moved to origin!");
ICommand bundledCommand = new BundledCommand(new ICommand[] {moveCommand, echoCommand});
// ...
tickQueue.Enqueue(bundledCommand)
// ...
// eventually, our command invoker will grab the bundled command from the queue
// the command invoker will execute the bundled command, which will in turn
// execute the move command followed by the echo command, all in a single tick.
Ciao
That’s “Hello” in Italian. Or “Goodbye.” Actually, I should’ve started by saying it was “Goodbye.”