6 Advanced Techniques
In the two preceding sections, it was mentioned that you will
very often want a mouse or menu selection to act as if the user had
typed a command. You might want to have buttons on the UI for common
verbs, such as "look", "take", etc. Now, it's not usually what you
want to just have the button respond to a click_event
by
calling <<look>>
;, because this doesn't count
as a turn; the turn counter isn't moved ahead, timers and daemons
aren't run, etc. So, if you want to force a specific command to be
executed, GWindows provides a mechanism for manipulating the command
line.
Let's consider the button window from our example UI. Suppose we want to change its behavior so that if a click occurs in the lower half of the window, it acts as if the player had typed 'take'.
GImageWin -> -> -> -> -> -> -> buttons
with split 47,
split_dir winmethod_Below,
image BUTTON_PIC,
click_event [ x y;
if (y > (self.height/2)
StreamWord("TAKE ");
],
has on;
Now, when the player clicks, the word "TAKE" will appear in
his input. StreamWord
takes a printable as its argument,
and inserts the text into the current command. You should take note
that if the player has already typed "FOOBAR", after clicking,
his input will read "FOOBARTAKE ". Though this may not be what
you want, imagine the trouble in implementing a list of clickable
nouns if StreamWord
erased current player input. You
should also notice that we put a space at the end of the string we
wish to "stream". Though this isn't required, since 'take' is a verb,
we can be pretty sure that the player is going to type another word
after it, so it's convenient to have the space already there.
Another thing to note is that Glk (at least, the implementations I
have tried; it appears to be required by the spec, but the author is
unsure) will redraw the entire input line whenever
StreamWord
modifies it. So if the user clicks our button
repeatedly...
>TAKE
TAKE TAKE
TAKE TAKE TAKE
And so on. The easiest way to avoid this problem is to use a separate input window. Then, the player will see the final
"TAKE TAKE TAKE " text, but not its intermediate incarnations. (Preventing the player from clicking out such a silly sentence as "TAKE TAKE TAKE " is a different matter altogether, and is much harder to solve in a way that won't anger the rare player who actually intends to take an object called the "take take". Good interface design is beyond the scope of this document, and would fill several books). If you want the typed command to also appear in the main window, GWindows can do this, via the GW_INPUT_ECHO
configuration constant.
Sometimes, you don't simply want to stick a word into the player's input, but replace it altogether. Suppose we wanted that button to execute the "look" command when the user clicked on the top half...
GImageWin -> -> -> -> -> -> -> buttons
with split 47,
split_dir winmethod_Below,
image BUTTON_PIC,
click_event [ x y;
if (y > (self.height/2)
StreamWord("TAKE ");
else cmd_override="LOOK";
],
has on;
If cmd_override
is set to a printable within an event handler, the game will act as if the player had typed the given command and pressed enter. Anything the player has typed himself is simply thrown away.
Unlike StreamWord
, command overrides are not
automatically printed back to the screen. As this code stands, the
user will see the output of the "look" command (a room description
will appear in the main window), but the actual word "LOOK"
will not appear. GWindows can print the command for you, and does if
you use the GW_ECHO_OVERRIDE
configuration
constant. GWindows also does this if you're using a separate command
window with input echoing enabled.
6.3 Multiple User Interfaces
So far, we have assumed that the game will have a single user interface which is used throughout the game. This may not be what you want, for several reasons. Several commercial games take the step of changing the interface design for critical sections of the game. For example, while in conversation, the user interface may switch to a "conversation mode", or while using some skill or piece of equipment, a specialized interface is shown. Perhaps more likely, you may wish to offer the user a choice of several interfaces. Our example so far is highly graphical. Users who cannot (or do not wish to) use graphical interfaces would be quite pleased if you allowed them to switch to a more traditional interface. If your interface is really exotic, you might want to provide three different versions: the "full" interface, a "reduced" version, which could, for example, offer menus and an input window, but not graphics, and a "traditional" version.
GWindows allows you to switch between interfaces at (more or less) any time by simply resetting the configuration variables, and restarting the system.
Let's say we want to let the player opt to play our game in a traditional motif. First, we design a traditional interface:
WindowPair text_root;
TextBuffer -> text_mainwin;
GStatusWin -> text_status;
Now, we add a verb to trigger the transformation...
[ tradmodesub;
Active_UI=text_root;
Main_GWindow=text_mainwin;
RestartGWindows();
"[Traditional mode selected]";
];
[ graphmodesub;
Active_UI=root;
Main_GWindow=mainwin;
RestartGWindows();
"[Graphical mode selected]";
];
verb meta 'mode' * 'traditional' -> tradmode
* 'graphical' -> graphmode;
We also want to make a slight change to our
InitGWindows
function:
[ InitGWindows;
if (Active_UI==0)
{
Active_UI=root;
Main_GWindow=mainwin;
}
];
This way, when the player restores his game,
the display comes up in the last mode he used (More than that --
InitGWindows
is called every time GWindows initializes,
so if you left the check out, the user interface would be immediately
reset to the graphical version).
We can even go a step further, and decide that if the user doesn't have graphics available to him, we'll switch him over automagically.
[ InitGWindows;
if (Active_UI==0)
{
Active_UI=root;
Main_GWindow=mainwin;
}
if (Active_UI==root &&
~~(GW_Abilities & GWIN_GWOK))
{
Active_UI=text_root;
Main_GWindow=text_mainwin;
}
];
Now, the user can switch modes by typing "mode traditional" or "mode graphical". GWindows will make sure that it only actually allows graphical mode if it's going to work.
6.4 Dynamic Layouts
What we've just described is a way to outright change the entire interface while the game is running. This is all well and good, but some times, you'd like to change the interface that is already there, without starting fresh.
Glk allows you to open a new window at any time. GWindows, however, does not. Your entire window layout must be defined ahead of time. The main reason for this is that GWindows has to know how to rebuild the interface after the game restarts or restores. Suppose you open a new window, then undo the move that caused the window to open. Should the window be there? Obviously not. Now, GWindows will handle one very simple case of this; when the quote box is used, the library splits a new quote window from the main window, which goes away again after the next move. However, the standard library contains a lot of special case code to make this work. If your case was any more complicated, you'd need to write all of that special case code yourself. So GWindows decides not to deal with the matter at all, by insisting that your windows are all created at the same time (Actually, it is possible to create new windows on the fly by adding new GWindow objects to the tree, but so much work is involved that this is hardly ever what you want.).
Nonetheless, GWindows does make it possible to make some alterations to the window structure while the game is running. You can't easily open or close windows, but you can resize them. The GWindows technique for defining a dynamic layout is this:
1. In your window layout, declare all the windows you will ever use. Windows that shouldn't be visible when the game starts should have a split size of 0. Glk specifies that windows with size 0 are invisible.
2. When the time comes to make the window visible, use
glk_window_set_arrangement
to resize the window, and
change the GWindow's split.
Let's consider the "popup menu". As it stands, this menu doesn't really "pop up" at all; it always takes up a portion of the main window. First, we change the definition of the popup window to reflect its initial state:
TextGrid -> -> -> -> -> -> -> popupmenu
with split 0,
split_dir winmethod_Right;
Now, we'll write functions to make the window appear and disappear:
[ ShowPopup;
glk_window_set_arrangement(parent(popupmenu).winid, winmethod_Right|winmethod_Proportional,
27, popupmenu.winid);
popupmenu.split=27;
];
[ HidePopup;
glk_window_set_arrangement(parent(popupmenu).winid, winmethod_Right|winmethod_Proportional,
0, popupmenu.winid);
popupmenu.split=0;
];
Now, whenever ShowPopup
or
HidePopup
are called, the window will be resized as
appropriate. What do the arguments to
glk_window_set_arrangement
mean?
- parent(popupmenu).winid
- This is the window you're
fiddling with. When you make the popup menu visible, you're really
telling Glk to change the distribution of space within its parent
window.
- winmethod_Right|winmethod_Proportional
- This just
tells Glk how to size the window
- 27
/0
- This is the new size of the
window
- popupmenu.winid
- Glk calls this the "key window" of
the split. What it amounts to is saying "This is the window whose new
size I'm giving".Now, the first argument in this case is the parent of the window you want to resize. But remember, some windows don't size themselves. If you wanted to pop up an entire Window Pair, you'd resize the parent of that window pair, but the "key window" would not be the window pair you were resizing, but rather the window which sizes that window pair.
Because this is a little complicated, GWindows provides a widget
for it. GPopupWin
is a window which switches between an
"inactive" size (usually zero) and an "active" size (usually
non-zero). Using a GPopupWin
:
TextGrid -> -> -> -> -> -> -> popupmenu
class GPopupWin,
with asplit 27,
split_dir winmethod_Right;
[ ShowPopup;
popupmenu.activate();
];
[ HidePopup;
popupmenu.deactivate();
];
6.4a The Validation Thing
There is one little pesky niggle with dynamic layouts, and this is the reason why it's better to use a GPopupWin if possible. Whenever the game restarts, restores, or undoes, GWindows goes through a complex procedure to attempt to salvage the window layout. Suppose we pop a window up, and save the game. Some time later, we fire the game up again, and restore. At the time we performed the restore, Glk had the window popped-down, but the game now thinks the window is popped up.
The last step of salvaging the user interface is called "Validation and Finalizing",
and during this step, each window is asked to verify that it has the dimentions it wants.
Every GWindow
and WindowPair
may provide a method, validate
,
which does this task. For a popup window, this is a simple matter of resizing the window
to its active, or inactive size, as determined by the current settings. GPopupWin
provides a validate method for doing just this.
Almost all the window-resizing behavior you're liable to need can be performed using a GPopupWin, but if you choose to do any window rearrangement yourself, you'll need to provide a validate method for ensuring the window has the right dimensions. If you can't be sure of how to rearrange the window, the validate method may simply return a non-zero value, to indicate that the window cannot be validated. Any layout containing such a window will not be salvaged; instead, whenever a restore, restart, or undo occurs, GWindows will simply discard the existing screen windows (and any scrollback with them), and start over.
6.4b Window Persistance
Most Glk implementations offer a scrollback facility, which allows players to review text which has scrolled off of the screen. How long the scrollback contents exist may depend on many things, but in general, scrollback will not persist after a window has been closed.
Consider a case where you close your normal game UI to open, for example, a chapter title page, then return to your old UI. In this case, the old Glk window corresponding to your main output has been closed and then opened anew. This is likely to irradicate any scrollback, which may be undesirable.
As of version .9B, GWindows has the facility to preserve one window from
the previous screen layout when changing UIs. The class GPersistantWin
represents a textbuffer window which should persist from UI to UI. If you switch between
two UIs, both having a GPersistantWin
, then the same Glk window will be
used for both. There are, however, several serious constraints:
GPersistantWin
must be the first GWindow allocated. This will generally
correspond to the first window (excluding WindowPair
s) in the source of the UI.
If the GPersistantWin
is not the first window, it will not be able to "inherit" the Glk window
from the previous UI. That said, a GPersistantWin
which is not the first window allocated
may still pass on its Glk window to the next UI. Thus, if User Interface A has a GPersistantWin
as its first window,
but User Interface B has it as a later window, the window will be preserved when switching from B to A, but not from A to B.
GPersistantWin
in a user interface. Trying to insert more will
result in a broken UI.
GPersistantWin
persists only between consecutive UIs. If you
switch to a UI without a persistant window, or with a persistant window which is
not the first window, then switch back, the persistant window will be lost. If you want to switch to a different UI
with no suitable window, you can "wrap" that UI in a persistant-window-preserving wrapper: add another window pair above the
top level with a GPersistantWin
as the elder child. Then set the size of your first window to 100 (Your first window
previously did not specify a size). This will result in the persistant window surviving the UI transition
as an invisible size-0 window.
GPersistantWin
specifies that the window is cleared whenever the screen initializes.
This is to guarantee that the window will be blank when the UI finishes initializing, as is true for other windows.
If you replace the init
method, you can change this behavior, in which case, remnants of the previous
contents of the screen may remain visible. (Whether or not you desire this effect is up to you. You might want to disable
the screen-clear when switching between, say, the graphical and text-only versions of the UI at the
user's request, but clear the screen when switching to or from a title page.)
In light of these restrictions, you may find persistant windows too much trouble to use if you have a large number of different screen UIs. While the choice is yours, many users appreciate an uninterupted scrollback. Window persistance is not related to validation; the persistance mechanism is employed only if Validation is not going to happen.
6.5 Style Hints
Glk provides a mechanism for specifying the appearances of the various text styles. "Hints" are given prior to creating each window, which give various metrics for how a style should look. The interpreter is, of course, free to translate these metrics into something that "makes sense" for the particular display, or to disregard them altogether, in favor of the user's preferred text appearances.
Any GWindow can specify an array of style hints which will be used for that window. Each hint is encoded in the array as a triple, which names the style, the hint, and the value of the hint. A full list of hints is available in the Glk specification.
Suppose that we want the popup menu in our sample game to display its normal text in italics, and it wishes for the first user-defined style to appear in reverse colors:
TextGrid -> -> -> -> -> -> -> popupmenu
class GPopupWin,
with asplit 27,
split_dir winmethod_Right,
stylehints
style_Normal stylehint_Oblique 1
style_User1 stylehint_ReverseColor 1;
6.6 The GConsole
GWindows provides a debugging feature called the GConsole
.
Include "gconsole" between gwindefs
and gwindows
to
install the console. When the GConsole is installed, a window at the bottom of
the screen will display informative messages from GWindows whenever any action is taken:
[GWindows]: GConsole activated. [GWindows]: Initializing the user interface. [GWindows]: Opening pair window "(trad_ui)". [GWindows]: Preparing to open window "(t_mainwin)". [GWindows]: Opening new window from window #58. Split method is 34; window type 3; split size 75. [GWindows]: Window "(t_mainwin)" open. Window ID is 59. [GWindows]: Preparing to open window "(t_statuswin)". [GWindows]: Setting stylehint 9 for style 0 to value 1 on window "(t_statuswin)". [GWindows]: Opening new window from window #59. Split method is 18; window type 4; split size 1. [GWindows]: Window "(t_statuswin)" open. Window ID is 61. [GWindows]: Pair window "(trad_ui)" opened. [GPopupWin]: Deactivating window (splash_quote).
These messages can be used to trace faults in the windowing system. Optionally, you can define GCONSOLE_PAUSE to some non-zero value to force the console to wait for keyboard input between each message -- though this can interfere with keystroke requests from other windows.
You can also print messages to the GConsole yourself. GConsole.write(x)
will write any printable to the console. If you want to print something more complex,
GConsole.penon();
redirects all printing to the GConsole until GConsole.penoff()
is called. You can safely leave GConsole commands in your code even when not using the GConsole,
but it is preferable to enclose GConsole commands in #ifdef USE_GCONSOLE;
blocks.