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.
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);
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.)
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).
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 character | Description |
---|---|
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. |
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.
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.