Articles Archive
Articles Search
Director Wiki
 

Architecting a Console-Style RPG in Lingo: Part One

June 18, 2004
by CC Chamberlin

Introduction

When I decided to enter the role-playing game programming competition at RPGDX.NET, it was an experiment to see if Director is capable of creating a console-style RPG (role-playing game) with good playability. Most of the other programmers at that site use C or C++, boasting of high frame rates that are frankly difficult to achieve with Director. But with a little work and some creative solutions, I was able to produce a reasonable entry in the short time period allotted. Sacraments is the result.

It turns out that writing an RPG is a great way to flex your Lingo muscles, because it gives you an opportunity to really delve into some advanced topics, such as:

This article will discuss some of the things that I learned while building this game, give explanations of how the game is architected, and some of the solutions I came up with to solve certain problems. Because the game was built quickly over the course of a month, there are several things I would do differently if I were to try to reprogram it from scratch today. I'll note those areas as we go.

Overview

Before we begin, let's take a look at the road ahead. This article will be published in three parts. Here's the major sections of development that we'll be covering in each part:

The Interface Stack Model

When starting to write an RPG game, your first inclination might be to start on a map drawing engine, or to start building the combat engine, or maybe to work on how inventory will work. There's no problem with that - tinkering is always good - but at some point, before you begin in earnest, you should have some idea of how these different modules are going to work together to make a cohesive game.

For Sacraments, I used an "interface stack" to control the game. An "interface" (in this context) is simply a part of the game that can be responsible for dealing with the player. Usually, this means showing something to the player, or reacting to input from the player, or both, or, in rare occasions, neither. Each frame, all interfaces in the stack receive the "render" message from the bottom up, but only the topmost interface receives the "timepasses" message. When an interface receives the "render" message, it should draw itself to the stage, if necessary, but take no action. When it receives the "timepasses" message, it should respond to player input, and if appropriate, pop itself off the interface stack.

Interface objects can also be instructed to notify some other object of their "exit state" when they pop themselves off the stack. This is mainly so the interface that pushed it on there can get some information about what happened - such as the result of a dialog box.

Here's a detailed example of how it works:

Here's an illustration of the interface stack at this point:

Interface Stack Diagram

The entire game of Sacraments is built on this simple idea, from the opening screen, through the game itself, to the game over and final victory scenes. Once you have a mechanism like this in place, you've compartmentalized your development process into smaller pieces. You can build a dialog box engine, or a combat engine, or a mini-game, or whatever, as a standalone interface without worrying about how it hooks up to anything else. This method worked very well for this game style.

Custom Keyboard Control

Because we will have a lot of interface objects checking for the same keys in this game, it is possible to get a lot of crosstalk on the meaning of keys. For instance, suppose you press the space bar to talk to someone, but you also depress the space bar to dismiss the dialog box that comes up to show you what the person says. You cannot simply use keyPressed() in both cases, because the dialog would be immediately dismissed unless the player releases the space bar exactly between the first check and the second check.

You could wait until the space bar is not pressed, and then wait for it to be pressed again, but you wouldn't want to have to build that functionality into every single interface object.

Similarly, you can't rely on keyDown events, because those don't track continuous pressing very well, and there's a chance that keyDown events would go to the wrong interface object if the event occurs while objects are switching themselves out of the interface stack.

Sacraments uses a "KeyWatcher" object to address this problem. Basically, it maintains a list of keys that have abstract meanings, and then watches them continuously and responds to queries about whether they are valid key hits or not.

Abstract Key Meanings

First off, Sacraments uses a rather common trick of not just looking for literal keys, but instead looking for abstract key messages. That way, you can allow the user to remap the keyboard commands for the game without requiring a change in the codebase to support it. Sacraments has six abstract key classes: #up, #down, #left, #right, #action, and #menu. These start out with some default values, but they can be remapped onto nearly any key combination the user might want.

This is implemented very simply: a property list of key type instances, each of which is a property list. Here is the handler that initializes or resets the default key mapping:

on revertDefaultKeyMap me
  pKeys = [:]
  pKeys[#up] = [#action: #up, #key: "i", #alternate: 126, #state: #claimed ]
  pKeys[#down] = [#action: #down, #key: "k", #alternate: 125, #state: #claimed ]
  pKeys[#left] = [#action: #left, #key: "j", #alternate: 123, #state: #claimed ]
  pKeys[#right] = [#action: #right, #key: "l", #alternate: 124, #state: #claimed ]
  pKeys[#action] = [#action: #action, #key: " ", #state: #claimed ]
  pKeys[#menu] = [#action: #menu, #key: "m", #state: #claimed ]
end revertDefaultKeyMap

Note that each entry in pKeys basically contains metainformation about each key class. It is stored as a property list by action so that you can look up key entries by key class, but it also has the action in the property list so you can more easily discover the action if you're just looping through the list.

The #key property is the key (or key code) used with the keyPressed() command to see if that key is currently being pressed. The #alternate property is also used for this purpose, and is there so that the arrow keys always map onto the direction key classes.

The #state property tracks whether any interface has claimed the current "downness" of that key. In other words, if Interface A responded to the action key, you don't want Interface B to respond to the same "downness" of the action key. If Interface A can lay claim to the action key "downness" instance, Interface B doesn't need to respond to it.

The state property can have one of four states:

Each frame, the KeyWatcher checks all of its keys and updates the state property accordingly. If the current state is #empty and the key is pressed, it gets set to #unclaimed. If it is any other state and the key is not pressed, it gets set to #empty.

When an interface wants to check for a key, it calls checkKeyDiscrete() in the KeyWatcher with the abstract action it is interested in. If the state property is #unclaimed, it gets set to #claimed, and returns true. Otherwise, it returns false. In this manner, if another interface asks for a claimed key later, even though the key is still being pressed, it will come back as false. (The continuous motion version, checkKeyContinuous(), works the same way, only it returns true if the state is either #unclaimed or #continuous, and sets the state property to #continuous.)

I then wrote a movie script called checkKey() to tell the KeyWatcher object to poll for the keys, so that it would be easily available to all scripts.

With this in place, you can use code like this to check for key presses, without worrying that the keypress occured under the watch of some other interface:

if (checkKeyDiscrete( #action )) then
    -- Do something cool
  end if

Dialogs

Dialog boxes were implemented using the interface motif mentioned earlier - they draw themselves when they receive the "render" message, and they check user input when they receive the "timePasses" message.

It was a fairly straightforward exercise to write a simple message dialog box interface object. Essentially, all it did was generate the image for the dialog box, and draw it on the screen when the "render" message came in. On the "timePasses" message, it would just checkKey(#action), and if it came back true, it would pop itself off the stack.

Only slightly more complicated was the "select an item from a list of text items" dialog. The only real difference was that it had to rerender the dialog box image when the player presses #up and #down to show which item was currently highlighted, and then fire off its notifier when the player presses #action, to notify the other object what the final choice was.

Once you have a text-based selection dialog box in place, you can quickly make one which shows the images of inventory objects by changing the rendering mechanism, and with those under your belt, interfaces like the Outfitting, Inventory, and Advancement dialogs are not difficult.

Bitmap Fonts

The only real difficulty with the dialogs, in fact, was generating that image in the first place. Director's text engine is nice, but it does not produce reliable metrics on text. On different platforms, the text could come out looking radically different, especially if no appropriate fonts are installed on the end user's system.

So, I created a bitmap font renderer to create the text. Essentially, this involved making a bitmap that has all the characters I wanted to display in it, mapped onto fixed-height sections of a tall image, one character per line. It would read a text string passed to it, and, one by one, draw the images of the letters into a image buffer until the string was complete.

This was a two-pass process, because you had to figure out where all the letters go before you start copying, because you don't know ahead of time how large to make the image to copy into. Once you have figured out the rects for all the images, you can create the image buffer, and then step through them all doing the copying. (Using a monospace font vastly simplifies this process, because all the characters are exactly the same size.)

For more flexibility, I included some escape commands in the string that allow different effects. For instance, embedding a "\n" into a string places a carriage return at that location. You can change the color of the text by using "\c" followed by six hexadecimal digits in the standard RGB format. (This last bit was accomplished by simply changing the #bgcolor property when performing the copyPixels() command.)

(There are many resources out there on building a bitmap font renderer, so be sure to hunt around on the web for more information.)

Conclusion

So far, we've looked at the high-level architecture and seen how dialog boxes can be used within it. Using this system, we can have an arbitrary number and depth of interface contexts with which the player can interact, all using the same code protocol. Once you get to this point, creating the game is simply a matter of creating all the interface objects you need, and writing code to add and remove them from the interface stack.

In next week's article, we'll look at a much more complicated interface object: the map renderer. Although drawing the game world is much more complicated, we'll see that it fits neatly into the same architecture as a simple dialog box.

"C.C." Chamberlin returned to creating educational outreach multimedia for New Mexico State University after spending three years in Virginia creating Math and Science shockwave modules for ExploreLearning.com (while his wife worked on her doctorate at UVA). He has done work for the USDA, the FBI, USWEST, NSF, the Kellogg Foundation, and the Smithsonian, among others. During his non-programming hours, he enjoys writing, 3D graphics, eating hot chile, working on haunted house props for next Halloween, and spending time with his new baby boy.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.