Yes, all right, I won't do the menu … I don't think you realise how long it takes to do the menu, but no, it doesn't matter, I'll hang the picture now. If the menus are late for lunch it doesn't matter, the guests can all come and look at the picture till they are ready, right?
— John Cleese and Connie Booth, Fawlty Towers
Sometimes one would like to provide a menu of text options, offered to the player as a list on screen which can be rummaged through with the cursor keys. For instance, the hints display in the “solid gold” edition of Infocom's ‘Zork I’ shows a list of “Invisiclues”: “Above Ground”, “The Cellar Area”, and so on. Moving a cursor to one of these options and pressing RETURN brings up a sub-menu of questions on the general topic chosen: for instance, “How do I cross the mountains?” Besides hints, many modern games use menu displays for instructions, background information, credits and release notes.
An optional library file called "Menus.h"
is provided to manage such menus. If you want its facilities then, where
you previously included Verblib
, now write:
Include "Verblib"; Include "Menus";
And this will make the features of Menus.h available. This section describes what these simple features are, and how they work, as an extended example of Z-machine programming.
The designer of this system began by noticing that menus and submenus and options fit together in a tree structure rather like the object tree:
Hints for ‘Zork I’ (menu)
→ Above Ground (submenu)
→ How do I cross the mountains? (option)
→ some text is revealed
→ The Cellar Area (submenu)
→ ...
The library file therefore defines two classes of object,
Menu
and Option
. The short name of a menu is
its title, while its children are the possible choices, which can be of
either class. (So you can have as many levels of submenu as needed.)
Since choosing an Option
is supposed to produce some text,
which is vaguely like examining objects, the description
property of
an Option
holds the information revealed. So, for instance:
Menu hints_menu "Hints for Zork I"; Menu -> "Above Ground"; Option -> -> "How do I cross the mountains?" with description "By ..."; Menu -> "The Cellar Area";
Note that such a structure can be rearranged in play just as the rest of the object tree can, which is convenient for “adaptive hints”, where the hints offered vary with the player's present travail.
How does this work? A menu or an option is chosen by
being sent the message select
. So the designer will launch
the menu, perhaps in response to the player having typed “hints”,
like so:
[ HintsSub; hints_menu.select(); ];
As the player browses through the menu, each menu sends
the select
message to the next one chosen, and so on. This already suggests
that menus and options are basically similar, and in fact that's right:
Menu
is actually a subclass of Option
, which
is the more basic idea of the two.
· · · · ·
The actual code of Menus.h is slightly different
from that given below, but only to fuss with dealing with early copies
of the rest of the library, and to handle multiple languages. It begins
with the class definition of Option
, as follows:
Class Option with select [; self.emblazon(1, 1, 1); @set_window 0; font on; style roman; new_line; new_line; if (self provides description) return self.description(); "[No text written for this option.]^"; ],
The option sends itself the message emblazon(1,1,1)
to clear the screen an put a bar of height 1 line at the top, containing
the title of the option centred. The other two 1s declare that this
is “page 1 of 1”: see below. Window 0 (the ordinary, lower
window) is then selected; text reverts to its usual state of being
roman-style and using a variable-pitched font. The screen is now empty
and ready for use, and the option expects to have a description
property which actually does any printing that's required. To get back
to the emblazoning:
emblazon [ bar_height page pages temp; screen_width = 0->33; ! Clear screen: @erase_window -1; @split_window bar_height; ! Black out top line in reverse video: @set_window 1; @set_cursor 1 1; style reverse; spaces(screen_width); if (standard_interpreter == 0) @set_cursor 1 1; else { ForUseByOptions-->0 = 128; @output_stream 3 ForUseByOptions; print (name) self; if (pages ~= 1) print " [", page, "/", pages, "]"; @output_stream -3; temp = (screen_width - ForUseByOptions-->0)/2; @set_cursor 1 temp; } print (name) self; if (pages ~= 1) print " [", page, "/", pages, "]"; return ForUseByOptions-->0; ];
That completes Option
. However, since
this code refers to a variable and an array, we had better write
definitions of them:
Global screen_width; Global screen_height; Array ForUseByOptions -> 129;
(The other global variable, screen_height
,
will be used later. The variables are global because they will be needed
by all of the menu objects.) The emblazon
code checks
to see if it's running on a standard interpreter. If so, it uses output
stream 3 into an array to measure the length of text like “The
Cellars [2/3]” in order to centre it on the top line. If not,
the text appears at the top left instead.
So much for Option
. The definition of
Menu
is, inevitably, longer. It inherits emblazon
from its superclass Option
, but overrides the definition
of select
with something more elaborate:
Class Menu class Option with select [ count j obj pkey line oldline top_line bottom_line page pages options top_option; screen_width = 0->33; screen_height = 0->32; if (screen_height == 0 or 255) screen_height = 18; screen_height = screen_height - 7;
The first task is to work out how much room the screen
has to display options. The width and height, in characters, are read
out of the story file's header area, where the interpreter has written
them. In case the interpreter is really poor, we guess at 18
if the height is claimed to be zero or 255; since this is a library
file and will be widely used, it errs on the side of extreme caution.
Finally, 7 is subtracted because seven of the screen lines are occupied
by the panel at the top and white space above and below the choices.
The upshot is that screen_height
is the actual maximum number
of options to be offered per page of the menu. Next: how many options are
available?
options = 0; objectloop (obj in self && obj ofclass Option) options++; if (options == 0) return 2;
(Note that a Menu
is also an Option
.)
We can now work out how many pages will be needed.
pages = options/screen_height; if (options%screen_height ~= 0) pages++; top_line = 6; page = 1; line = top_line;
top_line
is the highest screen line used
to display an option: line 6. The local variables page
and
line
show which line on which page the current selection
arrow points to, so we're starting at the top line of page 1.
.ReDisplay; top_option = (page - 1) * screen_height;
This is the option number currently selected, counting
from zero. We display the three-line black strip at the top of the
screen, using emblazon
to create the upper window:
self.emblazon(7 + count, page, pages); @set_cursor 2 1; spaces(screen_width); @set_cursor 2 2; print "N = next subject"; j = screen_width-12; @set_cursor 2 j; print "P = previous"; @set_cursor 3 1; spaces(screen_width); @set_cursor 3 2; print "RETURN = read subject"; j = screen_width-17; @set_cursor 3 j;
The last part of the black strip to print is the one offering Q to quit:
if (sender ofclass Option) print "Q = previous menu"; else print " Q = resume game"; style roman;
The point of this is that pressing Q only takes us
back to the previous menu if we're inside the hierarchy, i.e., if the
message select
was sent to this Menu
by another
Option
; whereas if not, Q takes us out of the menu altogether.
Next, we count through those options appearing on the current page
and print their names.
count = top_line; j = 0; objectloop (obj in self && obj ofclass Option) { if (j >= top_option && j < (top_option+screen_height)) { @set_cursor count 6; print (name) obj; count++; } j++; } bottom_line = count - 1;
Note that the name of the option begins on column 6
of each line. The player's current selection is shown with a cursor
>
appearing in column 4:
oldline = 0; for (::) { ! Move or create the > cursor: if (line ~= oldline) { if (oldline ~= 0) { @set_cursor oldline 4; print " "; } @set_cursor line 4; print ">"; } oldline = line;
Now we wait for a single key-press from the player:
@read_char 1 -> pkey; if (pkey == 'N' or 'n' or 130) { ! Cursor down: line++; if (line > bottom_line) { line = top_line; if (pages > 1) { if (page == pages) page = 1; else page++; jump ReDisplay; } } continue; }
130 is the ZSCII code for “cursor down key”.
Note that if the player tries to move the cursor off the bottom of the
list, and there's at least one more page, we jump right out of the loop
and back to ReDisplay
to start again from the top of the
next page. Handling the “previous” option is very similar,
and then:
if (pkey == 'Q' or 'q' or 27 or 131) break;
Thus pressing lower or upper case Q, escape (ZSCII 27) or cursor left (ZSCII 131) all have the same effect: to break out of the for loop. Otherwise, one can press RETURN or cursor right to select an option:
if (pkey == 10 or 13 or 132) { count = 0; objectloop (obj in self && obj ofclass Option) { if (count == top_option + line - top_line) break; count++; } switch (obj.select()) { 2: jump ReDisplay; 3: jump ExitMenu; } print "[Please press SPACE to continue.]^"; @read_char 1 -> pkey; jump ReDisplay; } }
(No modern interpreter should ever give 10 for the
key-code of RETURN, which is ZSCII 13. Once again, the library file
is erring on the side of extreme caution.) An option's select
routine can return three different values for different effects:
2 | Redisplay the menu page that selected me |
3 | Exit from that menu page |
anything else | Wait for SPACE, then redisplay that menu page |
Finally, the exit from the menu, either because the player typed Q, escape, etc., or because the selected option returned 3:
.ExitMenu; if (sender ofclass Option) return 2; font on; @set_cursor 1 1; @erase_window -1; @set_window 0; new_line; new_line; new_line; if (deadflag == 0) <<Look>>; return 2; ];
And that's it. If this menu was the highest-level one, it
needs to resume the game politely, by clearing the screen and performing
a Look
action. If not, then it needs only to return 2,
indicating “redisplay the menu page that selected me”:
that is, the menu one level above.
The only remaining code in "Menus.h" shows some of the flexibility of the above design, by defining a special type of option:
Class SwitchOption class Option with short_name [; print (object) self, " "; if (self has on) print "(on)"; else print "(off)"; rtrue; ], select [; if (self has on) give self ~on; else give self on; return 2; ];
Here is an example of SwitchOptions
in
use:
Menu settings "Game settings"; SwitchOption -> FullRoomD "full room descriptions" has on; SwitchOption -> WordyP "wordier prompts"; SwitchOption -> AllowSavedG "allow saved games" has on;
So each option has the attribute on
only if
currently set. In the menu, the option FullRoomD
is displayed
either as “full room descriptions (on)” or “full room
descriptions (off)”, and selecting it switches the state, like a
light switch. The rest of the code can then perform tests like so:
if (AllowSavedG hasnt on) "That spell is forbidden.";
· · · · ·
Appearance of the final menu on a screen 64 characters wide: | |
line 1 | Hints for Zork I [1/2] |
line 2 | N = next subject P = previous |
line 3 | RETURN = read subject Q = resume game |
line 4 | |
line 5 | |
line 6 | Above Ground |
line 7 | > The Cellar Area |
line 8 | The Maze |
line 9 | The Round Room Area |
•
REFERENCES
Because there was a crying need for good menus in the early days of
Inform, there are now numerous library extensions to support menus
and interfaces built from them. The original such was L. Ross Raszewski's
"domenu.h", which provides a core of basic routines.
"AltMenu.h" then uses these routines to emulate the
same menu structures coded up in this section. "Hints.h"
employs them for Invisiclues-style hints; "manual.h"
for browsing books and manuals; "converse.h" for
menu-based conversations with people, similar to those in graphical
adventure games. Or indeed to those in Adam Cadre's game ‘Photopia’,
and Adam has kindly extracted his menu-based conversational routines
into an example program called "phototalk.inf".
For branching menus, such as a tree of questions and answers, try
Chris Klimas's "branch.h". To put a menu of commands
at the status line of a typical game, try Adam Stark's "action.h".