Still with me? Now might be a good time for a caffeine boost. There's a lot of math in the following paragraphs.
A chain object is one of those "out of the loop" objects that is not
displayed and cannot be deleted. Chain objects do not have physical coordinates; rather, they form a sort of hub that regulates a large area of the secondary
grid. Secondary grid information always references a chain object, which in turn
points to a "master" object (usually the object that generated the chain object).
If a grid square referenced by dispptr[][] equals zero, it means no chain information exists at that square. If the value in dispptr[][] is nonzero, this value points to a chain object. A chain object contains the following values:
char ctype -- 0 if additive, 1 if substitutive char pal -- palette change for additive chains char anim -- an additive offset for the sprite or matrix position int master -- a pointer to the object that generated the chain int sprite -- base sprite for additive chains, base matrix position for substitutive chains int xoff, yoff -- tweak offsets (in x- and y-direction) for entire chain
If ctype is zero, the square will function as follows:
|
Additive chains are simple and fast. Because the flip flags are just defined as bits 14 and 15, either dispoff[][] or sprite can control the flip flags.
Of course, what is displayed is purely visual. Chain object-based display does not carry over into the NIB-OOL collision messaging system. To check for collisions, the contents of dispptr[], dispoff[], and master must be examined and interpreted accordingly.
If ctype is one, the square will function as follows:
|
These steps may look easy at first, but they are not. The only
way to explain this properly is by showing how the variables represent the drawn
square visually.
The low-order byte of dispoff[][] is one of
the following values: 0, 16, 32, 48, or 64. This offset focuses on one portion
of the 16x5-sized y-line of the substitution matrix. This offset is then augmented
by anim (usually from 0 to 15) to obtain the final starting position. The value
of anim generally starts at 16 and decreases gradually to 0. The result? The
"window" into the matrix appears to be moving backwards, but the user (seeing the
window but not the matrix) thinks the image is moving forwards!
Note that even if the [anim+dispoff (low)] offset pushes the physical boundaries of the rightmost pixels (past 16x5), there will be a wrapping effect to the first blank square.
Good explanation? Well, not quite. We're not done yet. The "window" into the matrix, if displayed as-is, only shows a left-to-right image. The window must be transformed using one of twenty "curve types." Having no other name for them, I'll call the transforming functions "wheel transforms." A wheel transform is a mathematical function that takes in a coordinate pair and returns a coordinate pair, processing the input coordinate pair as a magnitude and direction (even though it resembles rectangular coordinates in the "window").
The result of a wheel transform is to make an image (e.g. snake) appear as if it is bending and winding its way around corners or into/out of holes. There are twelve 2-D wheel transforms (for winding around corners) and eight 3-D wheel transforms (for emerging from/diving into holes).
Four of the curve types are not
used. For programmatic purposes, it's easier to leave these blank.
The number 1 represents an
"invalid" or "miscellaneous" direction. If diving, the destination direction
is 1. If emerging, the origin direction is 1. Because cartesian directions
are one of the four numbers 0, 64, 128, and 192, they cannot be confused with
an invalid direction.
What, you may ask, goes on during a transformation? On the surface,
it looks like some fancy trigonometry is taking place. This is only half true.
Trigonometry was used to generate the transformation tables, but no trigonometric
functions actually need to be called when drawing a transformed sprite! For each
curve type, there is a 16x14 short integer array defined. It is this array that
is used to formulate the basis for the drawn sprite. Instead of individual
pixel data, the array is composed of references to pixels in the window of the substitution matrix. A single pixel is drawn according to the following
input/output steps:
Lost? Keep up the caffeine flow. One should note that the matrices themselves are not updated for palette changes during drawing. This step does not appear in the above list. So how do you get snakes of so many different colors? The answer is that when a section of the substitution matrix is loaded, the sprite is palette changed during the load process. The pal variable serves no purpose in the chain object when ctype = 1. This results in memory-intensive computation at the time of loading a section into a matrix, but it cuts down the drawing computation significantly.
How does one generate the 16x14 integer arrays that transform the sprites? Okay, I'll be very blunt. If you failed trigonometry, GO AWAY. Otherwise, read on.
I have spelled out here the exact trigonometric transforms.
If you can't figure them out, that's your problem.
Curve type | Wheel transform |
---|---|
0 | tx = x; ty = y; |
1 | tx = y * sin(x * M_PI_2 / 15); ty = y * cos(x * M_PI_2 / 15); |
2 | Not used |
3 | tx = 15 - (15 - y) * cos(x * M_PI_2 / 15); ty = 15 - (15 - y) * sin(x * M_PI_2 / 15); |
4 | tx = 15 - (15 - y) * cos(x * M_PI_2 / 15); ty = 15 - (15 - y) * sin(x * M_PI_2 / 15); |
5 | tx = y; ty = 15 - x; |
6 | tx = y * cos(x * M_PI_2 / 15); ty = 15 - y * sin(x * M_PI_2 / 15); |
7 | Not used |
8 | Not used |
9 | tx = 15 - (15 - y) * sin(x * M_PI_2 / 15); ty = (15 - y) * cos(x * M_PI_2 / 15); |
10 | tx = 15 - x; ty = 15 - y; |
11 | tx = 15 - y * sin(x * M_PI_2 / 15); ty = 15 - y * cos(x * M_PI_2 / 15); |
12 | tx = 15 - y * cos(x * M_PI_2 / 15); ty = y * sin(x * M_PI_2 / 15); |
13 | Not used |
14 | tx = (15 - y) * cos(x * M_PI_2 / 15); ty = (15 - y) * sin(x * M_PI_2 / 15); |
15 | tx = 15 - y; ty = x; |
16 | tx = 15 - 12 * cos(x * M_PI_2 / 15); ty = 8 + (y - 8) * (15 + x) / (15 * 2); |
17 | tx = 8 + (y - 8) * (15 + x) / (15 * 2); ty = 12 * cos(x * M_PI_2 / 15); |
18 | tx = 12 * cos(x * M_PI_2 / 15); ty = 8 - (y - 8) * (15 + x) / (15 * 2); |
19 | tx = 8 - (y - 8) * (15 + x) / (15 * 2); ty = 16 - 12 * cos(x * M_PI_2 / 15); |
20 | tx = 12 * sin(x * M_PI_2 / 15); ty = 8 + (y - 8) * (1 - x / (15 * 2)); |
21 | tx = 8 + (y - 8) * (1 - x / (15 * 2)); ty = 16 - 12 * sin(x * M_PI_2 / 15); |
22 | tx = 16 - 12 * sin(x * M_PI_2 / 15); ty = 8 + (y - 8) * (x / (15 * 2) - 1); |
23 | tx = 8 + (y - 8) * (x / (15 * 2) - 1); ty = 12 * sin(x * M_PI_2 / 15); |
What the heck does it all mean? I'll try to explain in lay terms (ha ha). There are two basic patterns: the 2-D wheel and the 3-D wheel. A 2-D wheel treats the x-coordinate of the substitution matrix window as a radian measure and the y-coordinate of the matrix window as a magnitude measure. This is roughly a conversion from rectangular to polar coordinates. Naturally, the proportions are distorted at some points, but not significantly, since a snake is supposed to be flexible.
The general form for a 2-D wheel is:
tx or ty = y * trig[(x / maxvaluex) * (pi / 2)]
The 3-D wheel is harder to picture, but fairly straightforward.
Instead of converting both tx and ty from polar representations of x and y, a
3-D wheel uses y as a linear magnitude from the x-axis (assuming the x-axis runs
down the center of the sprite). The value of x is a radian measure that is
multiplied by a fixed magnitude, which normally is less than the full width of
the square. The general form for a 3-D wheel is:
lateral = fixedmag * trig[(x / maxvaluex) * (pi / 2)] axial = yatxaxis + (y - yatxaxis) * [x / (maxvaluex * 2)]
All this aggravating math is performed in the program MATRXGEN.EXE. This program does a few other cosmetic things to the transforms, too. The y-coordinates need to be converted from square (16x16) to almost-square (16x14). Since leaving out two y-lines mangles the appearance of a few curve types, the program also employs a selective removal process to take out less significant y-lines (i.e. avoid removing lines that show the snake's facial expressions).
If I get a 640x480 version going, MATRXGEN.EXE will, unfortunately, need quite a bit of additional work. Then again, having a 32x32 sprite size eliminates the need for cosmetic changes, as no y-lines are removed.
That's all, folks. Compared to the mathematics that are necessary in
ray-tracing applications, this may not seem like much. But the logic is the cream
of the successful design in Nibbler: it looks really nice, and the user won't
need to learn the quirks of the game to be able to play it. More importantly,
substitution logic can be used to make more powerful transformations in 2-D or
3-D environments, clearing the way for even more enhanced designs in future
applications.