-
But with all these new capabilities, how do I keep track of what is going on?
With regular Inform error checking, glk gestalt testing, glk class iteration,
and
Glulx's Inform's new built-in glk entry points.
The most important thing to remember about glk objects is that they are dynamic
-- created "on-the-fly" in memory and stored in memory.
-
Regular Inform Error Checking
- Since glk objects are dynamic, a lot of error checking is required.
A regular Inform object, such as a room, is all-text and platform-independent,
so it can be saved to a disk file. But a glk object -- let's say a graphic
window with an image in it -- is opened (window) and drawn (image) dynamically
by the glulxe interpreter as the player's platform allows. So it
may be
affected by: the player's operating system which may use an
interpreter that doesn't support graphic windows, or the size of the player's
monitor, etc. So, since glk
objects
are platform-dependent and managed in memory, they cannot be
saved to disk.
What is saved? The global variables that "point" to
each glk object. In Zarfian these are called references. For instance, to
create a graphic window, you
would need to include a global variable and a constant rock number for it.
Note that none of the following code in this section has been tested (although
it has been updated to correct an error someone spotted and another I spotted).
Constant GG_MYGRAPHWIN_ROCK 210;
Global gg_mygraphwin = 0;
Then, somewhere in your code, usually in Initialise, you would actually open
the window.
gg_mygraphwin = glk_window_open(gg_mainwin,
winmethod_Above+winmethod_Proportional,
30, wintype_Graphics, GG_MYGRAPHWIN_ROCK);
The variable, gg_mygraphwin, is what will be saved to disk. It will be
either:
0 = non-existent/closed or # (object reference number) = created/open.
Because Initialise is called before the first turn, to the player it looks like
the game started up with your window there. But actually it was created in
memory by the Glulxe interpreter and its glk library right after the game was
loaded.
A glk object's global variable needs to be checked to see that it is not zero
before you attempt to use
the object itself,
as it may not have been successfully opened (due to memory constraints, screen
size, etc.). It even needs to be
checked before subsequent use, because something may have happened in the
interim.
if (gg_mygraphwin==0) {
print "Ooops! We have a problem, graphic window
for image not available. Picture not shown.^";
return;
}
glk_image_draw(gg_mygraphwin, my_greatpic, 0, 0);
-
Glk Gestalt Testing
- The glk gestalt is really glk's
potential
configuration, as all interpreters may not implement all glk's capabilities.
So to keep your game from crashing, you need to check glk's gestalt to test if
the player's interpreter supports: graphic images, mouse
input, hyperlink selection, and sound playing. Also, some platforms may only
be able to play certain type of sounds.
The same function,
glk_gestalt,
is used to test for all these capabilities. The first argument determines which
glk capability is being tested.
result = glk_gestalt(gestalt_Sound, 0);
If the player's interpreter doesn't support a capability, you can include code
to: exit the game (graphic games, if graphic images aren't supported); use
if-elses to include/exclude that capability; or have a text response instead.
if (glk_gestalt(gestalt_Sound, 0) && gg_myschannel)
glk_schannel_play(gg_myschannel, babble);
else
print "You hear a babble of voices.^";
Regular Inform error checking can be combined with the gestalt function to make
sure everything is "on-line". It is not a bad idea to always call
the gestalt function prior to accessing a glk object. For instance,
the
player may restore his/her game on a different computer, another
program may allocate the sound card during game play, etc.
Every way glk_gestalt can be called is shown in the attached wrapper
reference.
-
Glk Class Iteration
- Each glk object is also a member of a class. A glk window is a member of the
glk window class, a glk sound channel is a member of the glk sound channel
class, etc. A glk iteration function has been provided so you can loop through
all the members of a glk class and affect each object. This not only saves
on coding, it becomes
necessary
when dealing with sound channels in
IdentifyGlkObject.
obj = glk_schannel_iterate(GLK_NULL, GLK_NULL);
while (obj)
{ ! do something with sound channel obj
obj = glk_schannel_iterate(obj, GLK_NULL);
}
The second word in the function name (as defined in infglk) varies depending on
which class it
is, such as glk_
window
_iterate. If the first argument is GLK_NULL,
glk_
class
_iterate returns the first object of that class.
Note:
Inform's NULL equates to -1, while the NULL used by glk is a c NULL,
which equates to 0. So John Catre has provided the constant GLK_NULL in
infglk.h.
-
Glulx Inform's Glk Entry Points
-
InitGlkWindow
-
{Window Objects}
InitGlkWindow
is called by the Glulx Inform library before
Initialise
to set up the default windows. If you include your own
InitGlkWindow,
it will be called instead. So, if desired, this is where you may change the
main text window's default text styles and colors. (Remember, style hints only
affect text buffer windows that are opened subsequently, just as setting
graphic windows' background colors only affects their subsequent
clears/resizes.)
Three default windows are defined in parserm.h and created by Initialise:
gg_mainwin
(main text window : text buffer),
gg_statuswin
(status line : text grid), and
gg_quotewin
(quote box : text buffer). If you include your own routine, you must provide a
value
for gg_mainwin, or the game will exit. But a status line isn't required, and
quote boxes are created/destroyed before/after each use. For details see
Zarf's "The Game Authors Guide to Glulx Inform".
-
IdentifyGlkObject
- If you create any of your own glk objects you *must* include
IdentifyGlkObject . After a restart/restore/undo, the Glulx Inform library
restores the state of the default windows (main text window, status line, quote
box) and the default streams & file references (save, script, command
record, script
file name), but calls IdentifyGlkObject for any glk objects it doesn't
recognize. Unrecognized glk objects are the ones created by you, the game
author. Since glk objects are created and stored dynamically in memory, it is
understandable that the Glulx library can't restore them. It can only recognize
the
ones it defined and created.
To simplify this explanation, let's resort to Zarfian "speak".
;-) A reference is the global variable that points to a glk object. When a glk
object is opened/created its reference gets filled in with a number that points
to that object's address in memory. (Again, that's not quite it, but close. For
those who are interested, it actually points to an entry in a hashing table.)
After restart/restore/undo, references can
be reloaded with previous values that may not match their objects' actual
states as they currently are in memory. For instance, gg_mygraphwin could
be restored with a number, indicating the window it points to is open, but
since game start that window has been closed. Since it was created and stored
in memory, the restore will not recreate the window, it will just restore the
variable that points to it.
Phases:
IdentifyGlkOject is called in three passes,
called phases: 0,1, 2.
Phase zero
should clear references to zero.
Phase
one
loops through all your glk objects in memory, identifying each by rock
number, and resets their references to match their objects'
current
states in memory (# = open/created, 0 =
closed/non-existent).
Phase two
allows you to correct those references so they match their
previous
states as recovered by restart / restore / undo. This can be accomplished
by: opening, closing, creating, destroying, arranging, redrawing, playing
so your glk objects correlate with their references.
Phase zero
and
one
deal only with (0) window, (1) stream, and (2) file reference objects.
Phase two
is for other glk objects, such as sound channels. (Sound
channels are not handled automatically by the Glulx Inform library.) So if you
haven't created any sound channels, you might ignore this phase. However,
if you have
changed
the state of any of your other glk objects since game start, phase two
shouldn't be skipped.
Example
: Fifty turns into the game, you open a text grid window
called
gg_mygridwin and fill it with a password, *******, for the player to figure
out. When he/she does, the window is removed. But when the player restores the
game he/she saved while still trying to solve the puzzle, the text grid window
is not on the screen. Why? When the game was restored, the global variable
gg_mygridwin was reloaded with a non-zero value indicating that the window it
points
to is open. But that window has been closed in memory prior to the player's
restore. It is now invalid, no longer pointing to a valid memory address.
Phase
zero would clear gg_mygridwin to zero. Phase one would check the glk window
referred to
by gg_mygridwin, discovers it doesn't exist, and reset gg_mygridwin to its
current state, 0. Phase two is where you would open the window so that it
matches
the restored game.
Constant GG_MYGRIDWIN_ROCK 210;
Global gg_mygridwin = 0;
DrawGridWin();
[ IdentifyGlkObject phase type ref rock
! clear out glk object variables
if (phase == 0)
{ gg_mygridwin = 0;
return; ! return so processing can continue
}
! reset variables to current values stored in memory
if (phase == 1) ! Glulx loops through your glk objects
{ switch(type)
{ 0 : ! it's a window
switch(rock)
{ GG_MYGRIDWIN_ROCK : gg_mygridwin = ref;
}
1 : ! it's a stream
! But I didn't create any.
2 : ! it's a file reference
! But I didn't create any.
}
return; ! return so processing can continue
}
! restore state of glk objects themselves
if (phase == 2)
{ DrawGridWin();
return; ! return so processing can continue
}
];
[ DrawGridWin;
! be sure to include this line
if (gg_mainwin == GLK_NULL) return;
! also called by other game code
if (((turns < 50) || got_password)) &&
(gg_mygridwin))
{ ! close open window
glk_window_close(gg_mygridwin, 0);
gg_mygridwin = 0;
}
else if (((turns >= 50) && (~~(got_password))) &&
(gg_mygridwin == 0))
! open window
gg_mygridwin = glk_window_open(gg_mainwin,
winmethod_Right+winmethod_Fixed,
15, wintype_TextGrid,
GG_MYGRIDWIN_ROCK);
];
Example:
In Initialise, you create a graphic window, gg_mygraphwin, and draw a graphic
image in it showing the first room. As the player moves around, the image
changes, displaying each room. But when the player restores the game, the
graphic image is not of the room he/she was in when the game was saved, because
he/she has moved on. Phase
zero and one would operate the same as the above. Phase two is where you would
change the graphic image to match the restored room.
Constant GG_MYGRAPHWIN_ROCK 210;
Global gg_mygraphwin = 0;
Global curroom_pic = 0;
DrawRooms();
[ IdentifyGlkObject phase type ref rock
! clear out glk object variables
if (phase == 0)
{ gg_mygraphwin = 0;
return; ! return so processing can continue
}
! reset variables to current values stored in memory
if (phase == 1) ! Glulx loops through your glk objects
{ switch(type)
{ 0 : ! it's a window
switch(rock)
{ GG_MYGRAPHWIN_ROCK : gg_mygraphwin = ref;
}
1 : ! it's a stream
! But I didn't create any.
2 : ! it's a file reference
! But I didn't create any.
}
return; ! return so processing can continue
}
! restore state of glk objects themselves
if (phase == 2)
{ DrawRooms();
return; ! return so processing can continue
}
];
[ DrawRooms;
! be sure to include this line
if (gg_mainwin == GLK_NULL) return;
! also called by other game code
! do graphics work?
if ((glk_gestalt(gestalt_Graphic, 0) && gg_mygraphwin)
{ ! is image information available?
if (~~(glk_get_image_info(curroom_pic, 0, 0))
{ print "^Picture of current room not shown, because
image could not be found.^";
return;
}
glk_image_draw(gg_mygraphwin, curroom_pic, 0, 0);
}
else
! graphic window not open or interpreter has changed
print "^Picture of current room not shown, although
your interpreter supposedly supports
graphics.^";
];
Example:
You create a sound channel in Initialise. When the player discovers
an old radio in the game, he/she can turn it on and listen to music. But while
it is
still playing, he/she undoes to before turning it on and can still hear the
music. Phase is
where you would turn the music off to match the game's undone state.
Constant GG_MYSCHANNEL_ROCK 510;
Global gg_myschannel = 0;
[ IdentifyGlkObject phase type ref rock id
! id is a local variable
! clear out glk object variables
if (phase == 0)
{ gg_myschannel = 0;
return; ! return so processing can continue
}
! reset variables to current values stored in memory
if (phase == 1) ! Glulx loops through your glk objects
{ switch(type)
{ 0 : ! it's a window
! But I didn't create any.
1 : ! it's a stream
! But I didn't create any.
2 : ! it's a file reference
! But I didn't create any.
}
return; ! return so processing can continue
}
! restore state of glk objects themselves
! since sound channels are not handled by library,
! have to do our own looping and value resetting here
if (phase == 2)
{ if (glk_gestalt(gestalt_Music, 0)
{ ! get the first sound channel
id = glk_schannel_iterate(GLK_NULL, gg_arguments);
while (id)
{ ! gg_arguments returns the rock number
! array is discussed in wrapper reference
switch(gg_arguments-->0)
{ GG_MYSCHANNEL_ROCK : gg_myschannel = id;
! resetting to current value stored in memory
}
id = glk_schannel_iterate(id, gg_arguments);
}
if (gg_myschannel)
{ if (radio_off)
! turn music off
glk_schannel_stop(gg_myschannel);
else
glk_schannel_play_ext(gg_myschannel,
loudmusic_snd, -1, 1);
! turn music on
! -1 = will play endlessly
! 1 = sound notification true, until the
! player turns it off -- notification
! is trapped by HandleGlkEvent
}
}
return; ! return so processing can continue
}
];
-
HandleGlkEvent
-
{Events}
Glulx Inform, like regular Inform, handles player line/character input
automatically. But it does this with two new routines:
KeyboardPrimitive,
and
KeyCharPrimitive.
Basically KeyboardPrimitive (or KeyCharPrimitive for Yes/No or menus) is
operated in a continuous loop, so that a line input request is always pending
(the blinking prompt). So when the player enters a line, a character, clicks
the mouse (if requested), or selects a hyperlink (if requested); whichever of
the two key routines is active calls HandleGlkEvent to respond. In other words:
they take priority, call HandleGlkEvent, and process any other input
events that may have superceded line or character input. Thus, essentially,
HandleGlkEvent is called after every event. It also handles any non-input
events that may have occurred while line/character input was pending.
If there are any player-entered commands, and player input is not aborted, they
are sent onto the Inform parser. If there are any author-generated commands,
and player input is aborted, they are sent onto the parser. If there are no
commands (usually because the events are not input events), nothing is sent
onto the parser. In this fashion, the loop continues.
If you use any of glk's extended input you *must*
include your own HandleGlkEvent
to deal with the consequences. When included, like InitGlkWindow, your routine
is called instead of the library one.
The extended events you may need to provide code for are: mouse input,
hyperlink selection, sound notification (sound finishes playing), glk
timer,
text buffer/ text grid / graphic and window resizes, and graphic window redraws.
-
Arguments:
(Passed to HandleGlkEvent)
Three arguments are passed to HandleGlkEvent by the Glulx Inform
library: ev, context, abortres. You may see other parameters listed, but,
although they follow certain naming conventions, they are actually variables
local to the routine and not passed arguments.
[ HandleGlkEvent ev context abortres newcmd cmdlen;
newcmd and cmdlen are local variables
-
Event Array
- The
first
argument passed is ev, the event array. It is defined in parserm.h as gg_events
and used by that name elsewhere, but when passed through HandleGlkEvent it is
called ev. This array isconsidered to have a "structure", as certain
elements of the array are used for specific things.
gg_event-->0 event type
gg_event-->1 window event is associated with
gg_event-->2 value 1 = depends on usage
gg_event-->3 value 2 = depends on usage
Event Type:
The event type is defined by constants in infglk.h. They have the suffix of
evtype_.
None - none
Timer - event repeated at fixed intervals
CharInput - keystroke input in a window
LineInput - full line of input in a window
MouseInput - mouse input in a window
Arrange - some windows sizes have changed
Redraw - graphic windows need redrawing
SoundNotify - sound finished playing
Hyperlink - selection of a hyperlink in a window
-
Line/Character Input
- You save yourself a lot of trouble if you just let Glulx Inform handle line
input just like regular Inform. Also,
KeyCharPrimitive
will probably provide all the single character input functionality you will
need.
-
Arrange
- Arrange signals when the player resizes the overall
application window (for the interpreters that support that), which, of course
will resize all the windows inside it. When text buffer windows are resized,
the text just flows into a shorter
lines. But when text grid and/or graphic windows -- which, except for the status
line, can only be author-created -- are resized they may have their screen
areas cut-off with information lost. So arrange lets you know when you might
need to rearrange (& resize) them.
-
Redraw
- Redraw is only for graphic windows. In addition to being resized, a graphic
window may be disrupted when the player fiddles with their monitor resolution
and/or color settings (or hits their
monitor), possibly removing the graphic image from the window or changing its
appearance. So redraw lets you know when graphic images may need to be redrawn
in graphic windows.
-
Sound Notification
- Notifies when a sound has finished playing.
Mouse Input / Hyperlink Selection
- Both are self-explanatory -- the player clicks an spot in window or on
hyperlink.
Associated Window:
All events, except for the glk timer and sound channels, are associated
with a specific window.
Values:
Value one and two
are only used with some input events. For mouse input, value one and two will
be the x, y coordinates in
the window where the mouse was clicked (character positioning for text grid;
pixel for graphic).
For hyperlink selection, value one will be the number associated with that
hyperlink. For sound notification, value one will be the sound channel's
rock id number and value two will be a non-zero value if sound notification is
desired (notify HandleGlkEvent when a sound finishes playing). And,
since windows are not relevant to sound channels, the window pointer
(ev-->1) will be GLK_NULL.
-
Context
- The
second
argument passed, context, tells whether the event took place while a line input
request (0) or a character input request (1) was pending. Since HandleGlkEvent
is called either during
KeyboardPrimitive
or
KeyCharPrimitive,
one or the other will apply. Usually, but not necessarily always, these
requests will be in the main text window. And usually it will be a line input
request since it is just about always pending (the blinking prompt). If you
want to abort player input, when context = 0 you might cancel a line input
request. When context = 1, you might cancel a character input request. Although
you will have to determine which window to cancel the request for.
-
Abortres
- The
third
argument passed is the input buffer. It can return a command as if the player
typed it in at the keyboard. If not used for that purpose it can be ignored.
newcmd = "command";
cmdlen = PrintAnyToArray(abortes+WORDSIZE,
INPUT_BUFFER_LEN-WORDSIZE,newcmd);
abortres-->0 = cmdlen;
The above puts a string into the input buffer so it can be sent to the Inform
parser as an author-generated command. (If this is done, HandleGlkEvent should
also return 2 to abort player input -- see Return Values below.) This also
demonstrates a nifty
little trick -- PrintAnyToArray will return the exact byte length of anything
(see Zarf's Game Author's Guide to Glulx Inform).
-
Return Value
- The return value from HandleGlkEvent can be return (nothing), rtrue
(1), or rfalse (0); which will not affect player input. Or 2, which will abort
player input so that a command in abortres can be treated as if the player
typed it
in at the keyboard. Or -1, which will allow player input to continue even
after pressing enter (line input) or entering a keystroke (character input).
However, -1 is not usually used.
-
Pending Input Requests
- Line, character, mouse, and hyperlink input can all be pending at the same
time, but usually not in the same window at the same time. A quick look at the
chart used
earlier shows why.
|
Window type
|
Input type supported
|
Not supported
|
|
text buffer
|
line input
character input
hyperlink selection
|
mouse input
|
|
text grid
|
line input
character input
mouse input
hyperlink selection
|
|
|
graphic
|
mouse input
|
line input
character input
hyperlink selection
|
Only text grid windows support all types of input. But there are other
limitations as well.
It is illegal (a BIG problem) to request line and character input in the same
window at the same time
(text buffer; text grid). Since
most interpreters can't prioritize mouse and hyperlink input requests in the
same window at the same time
(text grid; because they both use the mouse), that is also not recommended.
(Same time means the other input request is still pending, i.e. not been
completed yet. It is considered complete when responded to by HandleGlkEvent.
Concurrent requests in different windows are okay.)
-
Canceling Input Requests
-
Text cannot be written to a window when line or character input requests are
pending in that window.
So canceling whichever one is pending when HandleGlkEvent responds to
mouse or hyperlink input instead, is not a bad idea. If you
don't plan to print to the window in question,
canceling is not necessary, but it is still might not a bad idea.
Example:
Letting the player select a hyperlink to move in a direction.
glk_set_hyperlink(1);
print "north";
glk_set_hyperlink(0);
print " ";
glk_set_hyperlink(2)
print "south";
glk_set_hyperlink(0);
[ HandleGlkEvent ev, context, abortres newcmd cmdlen;
! newcmd, cmdlen are local variables
! ev-->0 = event type
! ev-->1 = window of event
! ev-->2 = number of link
switch(ev-->0)
{ evtype_Hyperlink :
! cancel input in main text window to be safe
if (context = 0)
glk_cancel_line_event(gg_mainwin);
else
! also requesting char input in text grid window
! so cancel char request in that window
glk_cancel_char_event(ev-->1);
! create command of direction to go
switch(ev-->2)
{ 1 : newcmd = "n";
2 : newcmd = "s";
}
! put command in input buffer
! PrintAnyToArray returns actual number of
! bytes in "n" or "s"
cmdlen = PrintAnyToArray(abortres+WORDSIZE,
INPUT_BUFFER_LEN-WORDSIZE, newcmd);
! put length of command in buffer
abortres-->0 = cmdlen;
! return 2 to abort any pending player input
! so this command will take effect instead
return 2;
}
];
Example:
- Letting the player use the mouse to pick up an object.
! First open a graphic window and draw a graphic in it.
[ HandleGlkEvent ev, context, abortres newcmd cmdlen;
! newcmd, cmdlen are local variables
! ev-->0 = event type
! ev-->1 = window of event
! ev-->2 = number of link
switch(ev-->0)
{ evtype_MouseInput :
! for double click
glk_request_mouse_event(ev-->1);
! area of object - x, y coordinates of pixel
if ((ev-->2 > ### && ev-->2 < ###) &&
(ev-->3 > ### && ev-->3 < ###)) &&
{ ! cancel input in main window to be safe
if (context = 0)
glk_cancel_line_event(gg_mainwin);
else
glk_cancel_char_event(gg_mainwin);
! create take command
newcmd = "take lantern";
! put command in input buffer
! PrintAnyToArray returns actual number of
! bytes in "take lantern"
cmdlen = PrintAnyToArray(abortres+WORDSIZE,
INPUT_BUFFER_LEN-WORDSIZE, newcmd);
! put length of command in buffer
abortres-->0 = cmdlen;
! return 2 to abort any pending player input
! so this command will take effect instead
! -- removing lantern from image would be
! handled DrawCave called in the lantern's
! after; by switching two images, one with
! lantern, one without
return 2;
}
! does not affect player input
evtype_Arrange:
DrawCave();
return;
evtype_Redraw:
DrawCave();
return;
}
];
-
This is getting complicated. What can I skip? What can't I skip?
Can't Skip
-
Arguments
- Glk functions have a set number of arguments. Unlike regular Inform
routines, if a argument is zero you may not drop it off the end. If a glk
function has five arguments, you must pass it five arguments (even if all but
the first are zero). So this also applies to the wrappers in infglk.h.
Update 8/3/00 -
infglk.h, version 0.6.1, allows trailing zero arguments to be dropped.
-
Gestalt Testing
- Unless you want your game to burp embarrassingly, you need to
test if the player's interpreter supports a specific glk capability (see
Gestalt Testing
above).
-
Glk Entry Points
- If you create any of your own glk objects you *must* include
IdentifyGlkObject. If you use any of glk's extended
input capabilities you *must* include your own HandleGlkEvent. If you insert any
graphic images you *must* include your own graphic image redraw routine. (see
Glk Entry Points
above)
Can Skip
-
I Don't Do Windows
- However, again, thankfully for us dunces, some window creation is automated
by Glulx Inform (see
InitGlkWindow
above). So you only have to worry about
glk's window functions if you want to create windows of your own.
-
A Whole Lot of Streaming Going On
- Ditto with streams. Since the bi-platform Glulx Inform library has been
rewritten to use glk streams
for normal file I/O (saving, restoring, scripting), you only need to use glk's
stream functions when you want to affect window streams and/or write
specialized disk files. Printing to a text window other than
the main one can easily be accomplished by: switching to the other
window [glk_set_window(gg_mywin)], printing, then switching
back [glk_set_window(gg_mainwin)] to the main text window to resume normal I/O.
Part One
Part Two
Part Four
Top of Page
Appendix One
(infglk Wrapper Reference)
Appendix Two
(Interpreter & Color Charts)
doeadeer3@aol.com
|