Bossing Around
I finally got around to implementing the ‘Command’ design pattern for my little sokomand prototype. And it works quite beautifully. I am immensely grateful for Robert Nystroms explanation of the whole ordeal in his Game Programming Patterns book, which he even released for free on the web. I realize this coming from someone still wrangling with understanding many design patterns and their uses himself, this might not be the most reliable opinion on the matter but if you have any interest in writing up games and still want to get a deeper understanding of some of the common patterns arising there, this book is such an amazingly written trove of knowledge. It typically begins by presenting a problem, nudges you to say ‘hey, if this and that were different, we could make that way more flexible’ and then shows you how to do this and that. I love it. (though I am a filthy reader of the web version - whenever I next have some spare cash, this thing will be sitting in my bookshelf pronto.)
I carefully implemented the Pattern, following the book’s chapter step by step at first - thankfully so, because it exposed some errors of thinking on my part (who gets passed which information when?). It started very simply - one Command class, which was just an interface:
interface Command {
fun execute(entity: Entity)
}
One actual Command:
class Move(val dir: Direction): Command {
override fun execute(entity: Entity) {
entity.move(dir)
}
}
an input handler which would create the commands:
fun onKeyUp(keycode: Int) {
when(keycode) {
RIGHT -> level.executeCommand(Move(RIGHT))
LEFT -> level.executeCommand(Move(LEFT))
DOWN -> level.executeCommand(Move(DOWN))
UP -> level.executeCommand(Move(UP))
}
}
and finally, a function on the actual level itself, which would call the command on an entity (only the player for now)
fun executeCommand(command: Command) {
command.execute(player)
}
Abstractions
The Command does not exactly work like the first example in the chapter because
it carries state itself. See that small val: Direction
at the top of its
declaration? That is us passing in a state into the Command itself. In our case,
it can only be one of 4 different variables (UP
, DOWN
, LEFT
, RIGHT
)
since that are the only valid directions for moves in the game. One of the
advantages of commands not carrying state, but ONLY wrapping around an execute
function is that you can instantiate the Command class once and forever pass
that around. This would kind of still work here, just instantiate it four times,
once for each DIRECTION
possible, but if we could move for example in 360°
that would obviously go out the window. So think about whether you want your
state to be abstract but reusable - no state - or flexible but specific. As an
example in this case, we could have just created 4 different commands, one for
each direction, and it would not have carried any state and could be reused as
often as we wanted. Now, obviously this is a very simple example of the whole
thing since we only carry 4 different states. But I hope it illustrates the
difference between abstract and specific commands, and which you want to use.
(Think for example, if we could move the player to any position, what would we
have to pass in then? Would it still be a similar situation?)
And that is all the code I had to implement to get it to work exactly like before, when the input function itself found the player and said ‘Player: Move!’. Just, now it is exceedingly easy to remap a different command to the buttons we have or remap the button that controls a specific command. All we have to do is change our input handling function:
fun onKeyUp(keycode: Int) {
when(keycode) {
W -> level.executeCommand( TooHeavyToJump() )
A -> level.executeCommand(Move(LEFT))
S -> level.executeCommand(Move(DOWN))
D -> level.executeCommand(Move(UP))
}
}
Here, I changed both the keys we use to do stuff, and the stuff we can do. This
is the exact level of abstraction Commands grant us. By creating an ‘envelope’
which has a standard form, and wrapping it around our actual function call, both
sides now know how to handle not just specific function calls but any call we
pass in, any we can think of really. As long as the envelope conforms to its
dimensions (in our case: it has an execute function - which gets contracted with
the Command
interface), both sides will always know that it is one of our
envelopes and pass it along accordingly.
To implement the actual Undo functionality, I had to change it slightly yet again. But I will get to that later.