January 09, 2005

The Ramble Chronicles: Key Sequences

OK, this has got to the be the geekiest entry in this series to date, as well as the most specific, as it involves an obscure point of Tk programming that will be of interest to almost no one; on the other hand, there are some reflections on player controls that might be of interest to a broader audience.

Although Ramble has a GUI interface, it's a game played entirely with the keyboard. (I've seen tile-based games with mouse interfaces, and I've never liked them. ) This shouldn't be surprising; game console controllers got by for years with a handful of buttons and direction pad (basically four arrow keys combined into a single button); the newer consoles add analog joysticks, but for a tile-based game with discrete movement they don't really buy you anything. So using keyboard commands makes sense.

In your typical tile-based console game (Final Fantasy I, say, or Pokemon in its various flavors), your character is always facing one of four directions, which I'll call N, S, E, and W. If you wish to interact with a non-player character, or a switch, or some piece of machinery, you walk up to be and press a button. This is a nice, simple interface, and it works great for console games because you only need one button for all the different kinds of interaction you can do. On the other hand, it places some constraints on the game:

  • Any feature or creature in the game can only support one kind of interaction, because there's only one way to activate it (unless you have it pop-up a menu, which works but is obtrusive).
  • You can't face the diagonal directions (NE, NW, SE, and SW).
  • You need at least four separate graphics for the player and each kind of creature the player can talk to.

It's certainly possible to build a rich game under these constraints, but for several reasons I don't want to do so. First, I'd like to have features and creatures that have multiple interactions. Even now, features support at least two interactions, "examine" and "walkon" and therefore need at least two ways to be activated. Second, in the games I'm speaking of, combat doesn't take place in the same environment your character walks around in; instead you go to a separate combat mode. In Ramble, the monsters walk around just like you do, and the ability to attack or move along the diagonals can be really important. Third, life's too short for me to create all of those graphics.

So Ramble goes with a scheme where your character isn't facing any particular direction but can move and interact with creatures and features in any of the eight directions, N, S, E, W, NE, SE, NW, and SW. This has two implications: I need eight direction keys, instead of just four, and I'm going to need to use key-sequences for certain player actions. By key-sequence I mean two or more keys pressed in sequence; to examine the contents of an adjacent tile, for example, you press the "x" key, followed by a direction key. To talk to a creature in an adjacent tile, you press the "t" key, followed by a direction key. And so on.

As I say, I need eight direction keys. Two different sets suggest themselves; the first is that old standby, the numeric keypad:

I first wrote code to use a numeric keypad in this way back when I was learning to program back in the late 1970's (good grief--I've been programming for over twenty-five years!). This is decidedly retro, but if you play Angband then these are the keys you use to move your character around.

Unless of course they aren't, which leads me to the next possibility: the "Rogue" key set:

Back when the Unix operating system was first developed, many computer terminals didn't have arrow keys; consequently, many full-screen programs used the H, J, K, and L keys (or sometimes ^H, ^K, ^K, and ^L) as shown as direction keys. The vi text editor, in use daily by millions of programmers, still uses this scheme. The original Rogue game, the ancestor of Angband, extended this to the diagonals as shown. It's a remarkably efficient layout; you use the four fingers of the right hand for the four cardinal directions; the index finger picks up the diagonals. It takes a bit of getting used to, but it has one major advantage--it lets you play Angband on a keyboard that doesn't have a numeric keypad. My primary computer has been a laptop for many years now, and practically speaking laptops don't have a numeric keypad. As a result, this layout has become second nature.

I'm nerdier than 78 percent of the population according to an on-line quiz I took, did I tell you?

Anyway, Ramble will support both of these key sets: the Rogue keys for me and the numeric keypad for my kids.

OK, now on to the Tcl/Tk part of this essay. Tk allows you to define "key bindings"--in essence, you specify the keystroke and a Tcl command to execute, and when the key is pressed, Tk executes the command. Here are two of the key bindings for the numeric keypad:

bind . <Key-4> {game moveplayer w}
bind . <Key-6> {game moveplayer e}

In short, if the player presses 4, the player's character is moved west. Easy enough. But what about talking to another character where you need to press "t" and then indicate a direction? Tk handles this quite nicely:

bind . <key-t>        {game talk_intro}
bind . <Key-t><Key-4> {game talk w}
bind . <Key-t><Key-6> {game talk e}

When the player types "t" followed by "4", the player's character will talk to the creature to the west, if any. The first binding on "t" by itself logs a message that says to press a direction key to indicate the direction in which you want to talk.

So far so good. Now, our player can acquire a crossbow; once he has the crossbow, he can fire missiles using the "f" command and a direction. We can implement that in the same way:

bind . <key-f>        {game fire_intro}
bind . <Key-f><Key-4> {game fire w}
bind . <Key-f><Key-6> {game fire e}

This is exactly like the example above, except that it's more violent. But it presents a problem: what do we if the player hasn't found a crossbow yet? Obviously we tell him that he doesn't have a crossbow--but it seems silly to tell him to enter a direction key to fire a missile, and then tell him he has no crossbow. He shouldn't have been prompted to enter a direction key to begin with.

The naive solution is for the "fire_intro" routine to check whether the player has a crossbow, and either prompt the player for a direction or tell him he doesn't have one. Again, so far so good. The problem is, Tk is still waiting for the other shoe to drop. Suppose the player has no crossbow, and presses "f". He's told he has no crossbow. "Oh, dear," he says, "I'd better run." So he presses the "6" key to walk away from the monster--and the game calls the "fire" routine, and he fires a missile even though he has no crossbow.

Where I come from, we call that a bug.

OK, so the "fire" routine checks for a crossbow as well, and returns silently if the player has none. That's still not good enough, because then the player presses "6" to walk away from the monster, and nothing happens. Tk has called "fire" rather than "moveplayer", and so the player doesn't move. So what do we do now?

We could modify "fire" so that if the player has no crossbow, the player moves instead--which is such a dirty solution I'm ashamed to even have mentioned it. The right thing to do is somehow break the sequence: to do something so that Tk forgets that "f" was pressed, and is no longer looking for the direction key that follows the "f". Then when the player types "6" it's seen as a movement key, not as a direction in which to fire a missile.

There are two ways to do that that I've come up with, and the second is the obscure point of Tk programming I mentioned up top. The hard way is to add the "fire" bindings only when the user gets the crossbow, and remove them if he loses it. But there's an easier way. Tk's event command can generate mouse and keyboard events, just as though they came from the user. All I need to do is have the "fire_intro" routine generate a bogus (but harmless) keystroke, thus breaking the key sequence. The space character has no particular meaning to Ramble and has no special keybinding, so I'll use that. Here's the magic code:

event generate . <Key-space>

And that does the trick!

Posted by Will Duquette at January 9, 2005 03:41 PM