Site hosted by Angelfire.com: Build your free website today!
Previous Page
Next Page
Table of Contents


Variable usage and scripting layout

As a language of unit operations, a programmer must give up a few niceties of general-purpose high-level languages. Granted, there are some object-oriented languages that can deliver very good performance while giving a lot user flexibility, but I mentioned in the first chapter of this documentation that I'm cheap. Go figure.

Standard usage

Assigning/modifying variables is fairly straightforward.

var1 = exp2;
var1 += exp2;
var1 *= exp2;
var1 <= exp2;

"var1" can be any assignable variable: a global variable (singular, array, or grid), a local variable, or a pointer to a local variable of another object. "exp2" can be anything that var1 is, or it can be a function or a constant. Some examples:

var1 = var2;
fruitleft[5] = var1;
grid1 (+4, +4) &= 2;
mag = cos(dir);

Coordinate pairs

You can see in the third line above that grid1 is not addressed in the standard C/C++ method of two-dimensional arrays. I have designed a special addressing method in this language that treats all coordinate pairs in a variety of ways.

Absolute addressing: A grid coordinate that is exactly at (x, y) is written out just like that: (x, y). In the case of vectors (such as the addvect commands), these pairs are treated as relative coordinates, not absolute coordinates. In all other instances, including commands that require coordinate pairs and indexing two-dimensional grids, (x, y) represents these exact coordinates.

Ground zero addressing: To refer to a grid coordinate that is positioned exactly at the current object's x and y coordinates, without any offset, just use a pipe symbol. The convention "place |, anobj" occurs frequently with animation and projectile firing.

Relative addressing: To refer to a grid coordinate that is offset from the current object's x and y coordinates, use the convention (+x, +y), or (-x, +y), etc. These (+/-) prefixes tell the compiler to treat the coordinates to be evaluated as (currentx + x, currenty + y) or (currentx - x, currenty + y), etc. A common usage of relative coordinates is (+0, +0), or the equivalent of (currentx, currenty).

Polar relative addressing: To refer to a grid coordinate that is offset from the current object's x and y coordinates, but using a polar vector, replace the parenthesis with brackets. The vector [32, dir] will evaluate as a number of different possible vectors. If dir points right, it's like adding 32 to currentx. If dir points diagonally left and down, it's like the coordinate (currentx + 32 * cos(5pi/4), currenty + 32 * sin(5pi/4)).

Some notes about the coordinate system. The x-axis increases from left to right, but the y-axis increases from top to bottom. This is because it's easier to map video memory with this method. The maximum visible coordinate space is (0, 0) to (80 * 16, 48 * 16). However, movement of objects off the edges of these visible boundaries is permitted. Because fixed-point, 4-byte numbers address this space, the lower 16 bits can be thought of as the fractional part of pixels for practical purposes.

The grid squares are nominally 16 x 16. This means that an absolute coordinate pair of (35, 20) refers to grid square (35 / 16, 20 / 16), or (2, 1). It may take some practice for the programmer to remember that this "automatic integer division" takes place. It is VERY convenient not to need to perform the conversion manually in the object-oriented language.

Okay, I said the grid squares are 16 x 16, but those with sharp eyes will notice that the actual size in 320x200x256 mode is actually 16 x 14. The coordinate system is, assuredly, completely square, but the aspect ratio of 320x200 mode is not. Modes like 320x240 and 640x480 have square pixels, but the 16 x 14 size is close to "looking" square. I have designed the language so that the programmer can ignore the video mode the game is run in. (Note: if I end up extending Nibbler to 640x480, no change in the object-oriented code is needed. The only difference is that the x-value and y-value may be shifted once to the left to yield a more "continuous" delta in the display of sprites.)

Values in functions

A function returns a value from one or more parameters. There really isn't much to say here about functions if one already knows how they work. Functions can take values of any type, just as assignment commands can take values of any type in the second operand. However, the command interpreter in NIB.EXE will choose to interpret some of the values as specifically a 1-byte integer, a 2-byte integer, or a 4-byte fixed-point.

Functions can be thought of as passing variables by reference. In almost every instance, variables are not changed when they are passed to a function, but there are a few exceptions (see the command and function reference).

Significant local variables

Obviously, assigning "n" number of variables to any object type won't mean much unless at least some variables signify something important. The following is a complete description of how each one operates.

char type: This is an "implicit variable." By this, I mean that all objects will have a "type" variable, whether or not the variable is declared in the local variable definitions. This value is assigned to the order in which the object appears in the compiled CON file. The first one (infofeed) has a value of 1, the second one (spawner) has a value of 2, etc. Objects can read this value off other objects to determine how to associate with them or respond to them. It is a good idea to NEVER write a different value to this variable.

char flags: Another "implicit variable." By default, all flags are set to zero or clear. If a flags declaration does appear, it will have the form of the "set/clear" convention as described on the previous page. Flags can be both read from and written to, and they hold important properties.

Flag characterDescription
B - Block This bit is used in conjunction with the "move" command to check for collisions of sprites. If a "move" command, initiating from either the current object or another object, results in an overlap of the two objects' sprites, each object will (potentially) be sent the "hit" message.
H - Horizontal sector overlap This is a bit set and cleared by NIB.EXE to optimize drawing. An object script should not touch it.
V - Vertical sector overlap This is also a bit set and cleared by NIB.EXE to optimize drawing. An object script should not touch it.
I - Idle The idle flag is set and cleared implicitly by certain commands of NIB-OOL. When clear, the object executes opcodes. When set, the object remains in the game, but does not execute opcodes. An "end" statement is the most frequent cause of the idle flag being set. It is not a good idea for an object script to change this value.
F - Follows This flag tells the object-oriented language to assign a pointer to the object that placed it. The only way this flag is set is if the object has a "follows" pointer variable declaration. The flag should not be manipulated by an object script.
M - Matrix This flag tells the command interpreter to assign a reference to a substitution matrix. This flag is only set if the object has a "matrix" variable declaration. The flag should not be manipulated by an object script.
P - Palette If set, the "pal" variable (if declared) will function as a color-lookup table index for the object's sprite. If not set, no changes in palette will take place when drawing the sprite, even if "pal" is not zero. A script can freely change this value.
S - Solid If set, the "pal" variable (if declared) will function as a uniform color to change the sprite to during the drawing process. If not set, the sprite is drawn normally. If both this flag and the palette flag are set, the solid flag has precedence. A script can freely change this value.


int sprite: This implicit variable is the most important. It is set to zero (NONE) if not declared; if declared, it can equal any sprite macro. Usually, only the transparent sprites are valid for objects. However, tile objects can make use of opaque sprites, and chain objects have very complicated usages of their sprite variables. In general, one assumes that an object's sprite is representative of a center-based image at the location on the screen derived from the object's x and y coordinates. (Note: The NONE sprite has nominal 16x16 coordinates. If an object is susceptible to sprite collision logic (block flag=set), it may still generate "hit" messages even though it displays nothing on the screen.)

Another important feature about the sprite variable--the variable actually consists of two parts, the macro definition and the flip flags. The first 14 bits of a sprite represent the value as assigned from the macros generated from GET.EXE. The two flip flags occupy the remaining two bits. Bit 14 is the horizontal flip flag and bit 15 is the vertical flip flag. One may bit-fiddle these values into a sprite after it is assigned, but there are also some commands that can make the process simpler (fliphoriz, turnru, etc.)

The variables type, flags, and sprite are the only implicit variables that objects have. The remainder of the variables are optional.

char z
long x, y

The three variables listed above must all be declared at once if they are to work properly. They must also appear in the exact order listed and must be the first two lines of unimplicit variables declared. Almost every object has this combination of coordinates: z, which is used in the calculation of drawing order, and x and y, which define the physical location of the object in the valid game space.

If an object has these coordinates, the following rules must be obeyed. The variable z may be given an initial value, but it must be altered by a zmove command during the game sequence if it needs to be changed. The variables x and y are given initial values based on the placement protocol. They should not be altered during the game sequence explicitly, but can be changed by a "move," "moveobj", or "moveoff" command. All three values, however, can be read from by any object.

If an object does not have these coordinates, the following rules must be obeyed. The object must be placed either superficially by NIB.EXE during the game sequence startup, or else it may be placed by using the "place" statement or function with an "out-of-the-loop" condition. The correct way to do this is to replace the coordinate pair with an exclamation point, for example "place !, statworker". This tells the command interpreter not to treat the object as movable or visible in the normal sense. As a consequence, the object cannot perform "move," "moveobj," "moveoff," or "zmove" commands. It also may not be deleted.

Some examples of "coordinateless" objects include the screen-scrolling calculator, the status bar interface, the infofeed buffers, the spawner object, and all chain objects. It is not necessary to display things at specific coordinates based on these individual objects, so they do not have coordinates.

char pal: The palette variable is assumed to exist if the palette or solid flags are set in the flags variable. If it doesn't exist, the value for this variable always evaluates to zero.

char dir: This variable is assumed to exist when certain functions are used. It is most frequently used to calculate sprite flip flags and determine movement vectors. If it doesn't exist, the value for this variable always evaluates to zero.

A direction in Nibbler ranges from 0-255 angle measurements. This is well short of the number needed in ray-casting engines and other specialized trigonometry, but it is functional for Nibbler. Zero means right, 64 means up, -128 (or 128) means left, and -64 (or 192) means down. Note that even though increasing angles turn counterclockwise, the coordinate system has increasing values of y pointing down. This is the standard in all trigonometry in Nibbler, including the values returned by the built-in sine function.

char tag: Tags from the level editor are deposited into this variable. The "taglink" function searches for other objects of equal tags. After all links have been performed, an object can do anything it wants to this value. A common usage for tags aside from the editor taglinks is a secondary means of recognizing object types or classifications.

int hobj: Whenever an object is sent a message, this variable, if it is declared, will be assigned a pointer to the sender. Object sprite collisions will cause this variable to be set, but explicit messages from one object to another will also do it. This variable is very useful to determine interactive physics between objects because the pointer can be used to trace back to type, tag, and coordinate information about the object that generated the message.

This completes the "special" local variable section. The variables listed above are part of the "genvariable" control block and are the only variables that can be referenced in another object by a generic pointer. For more information on pointers, read on.

About object pointers

Object pointers are used to access or change information in other objects. The usage of pointers in this language is not much different from C/C++.

A pointer to an object is just a 2-byte integer. There really isn't much more to say about pointers in terms of their declarations. Any 2-byte integer may function as a pointer. However, like pointers in other languages, a pointer in NIB-OOL must be assigned a meaningful value before it is used. A null pointer has the value of zero.

There are two types of pointers: generic pointers and specific pointers. Neither of these types is declared explicitly; whether or not a pointer is generic or specific depends on the context in which the pointer is used.

A generic pointer is assigned to an object by any number of means: a "place" function, a "taglink" function, a "target" function, as an initial value, or through direct assignment of another variable. It is another important feature of NIB-OOL that the objects referenced by pointers are non-relocatable. In other words, a pointer that is assigned to an object will stay valid as long as the object is alive, regardless of where it moves, what values it changes, or what other objects are created or destroyed.

Generic pointers can be used to send messages to other objects, but the most frequent usage of such pointers is to access data of other objects. Of course, generic pointers are "generic" in that no type information about the object referenced is known at the time of the data access. Therefore generic pointers are allowed to access a limited scope of variables that are common to many objects. The list mentioned above covers these variables and their functions.

ref = target standard, (targobj.type == worm);
ref.pal = PAL_BLUE;
ref.dir = UP;

Some things to realize. A generic pointer could possibly request information or try to change variables that don't exist in the referenced object's defined variable space. Changing the "pal" and "dir" local variables of the targeted object in the above example assumes both of these variables actually exist in the "object worm" header. If they do, fine, but if not, the operation will fail. No values to invalid dereferences will be written anywhere, and attempts to read an invalid dereference will result in a value of zero returned. In practice, it is not good form to rely on these safeguards, even though I have implemented them in the command interpreter.

A specific pointer can come about in several ways. Unlike a generic pointer, a specific pointer addresses the referenced object as an exact known type without any chance of invalid dereferences. The referenced object's entire set of local variables can be accessed and changed via a specific pointer.

There are two ways to create a specific pointer. One is by typecasting. A typecast is in the form of (*objtype)pointer.localvar. Yes, I know, it's not true to form like in C/C++, but it is used in only one way. Typecasts are a kind of "quick connect" to turn any generic pointer into a specific pointer on the fly.

(*worm)ref.velocity = V_WORM;

The other way to create a specific pointer is by a follows declaration. The declaration itself does not make it a specific pointer; an initial value needs to be assigned.

follows mymaster; /* General pointer */
follows myblast = blast; /* Specific pointer */

The first form tells the command interpreter, "Assign mymaster to the object that created me." There is no immediate way to determine what type of object created the newly generated object, so it is a generic pointer that has two bytes and functions like any integer. The second form, however, marks "myblast" to be treated as if it was typecasted to (*blast) whenever it is used. Some caution needs to be used when declaring the second form--obviously, only a single type of object can create the object with such a condition, or else the indexing of local variables might be of the wrong type and cause an error in the program.

A common type of pointer is one to a chain object. These objects function as visual display descriptors, and they must be updated and monitored carefully, since a single object might affect a screenful of data.

While powerful, pointers to objects have one critical drawback--they do not automatically "know" whether or not the object they reference has passed on. Objects that die with pointers pointing to them are discontinued from any linked list operation. The area they had occupied is converted to free space. I have implemented a safeguard that causes a pointer that tries to reference a free memory block to assign no value if dereferenced for writing and return zero if dereferenced for reading. The operation will also cause the pointer to be assigned a value of null. Once again, it is not a good idea to rely heavily on this safeguard, as free space may not stay free for a very long time. Some of the advanced targeting algorithms should immediately cease to follow an object if there is evidence that it has died or is about to die.

Substitution matrix definitions

The type matrix is used to declare a substiution matrix. Due to technical limitations, I allow for only eight matrices to be declared in any given level. While other local variable definitions "personalize" the variables to a given object, a matrix definition can be thought of as allocating a limited supply of additional memory.

matrix mymatrix;

The value of "mymatrix" is a 2-byte integer, although the contents are not necessarily meaningful to the programmer of the object script. The variable is assigned (much like a follows pointer) the moment the object has been generated, and it is a good idea not to change it. An object has the responsibility to assign a chain object the value in this variable, where it is interpreted meaningfully. Note that once declared, it is impossible to "undeclare" and deallocate a matrix.

It is quite easy to declare a substitution matrix, even if it is not so easy to maintain one. Each snake always possesses one matrix. A few other creatures possess one or more matrices as needed. For example, the squid boss has many independent tentacles, which require one matrix each. For more information about how substitution matrices are drawn, go to Advanced Graphics Display.



Previous Page
Next Page
Table of Contents
Back to Title Page