// define the global constants
#define SPRITE_HEIGHT 8
#define LEFT 0
#define RIGHT 1
I hope you noticed these lines at the top of the program before _main.
These are called #define preprocessor directives. The #define directives
tell the preprocessor that these strings actually mean something else,
so replace them in the source code with the value we have given you. In
effect, this means that every time you see the string SPRITE_HEIGHT in
the program, it actually gets replaced with the value 8. #define
directives serve two major purposes: first, oo make code more readable
(which is what we have done here). The second is to control how code is
handled. We will talk about these situations in a future lesson.
We are going to skip the integer declarations, because you should
already have a working understand of such simple things.
// declare the sprites (8,16,32) pixels wide x 8 pixels tall
unsigned char sprite1[] = {0xC3,0x3C,0xC3,0x3C,0xC3,0x3C,0xC3,0x3C};
unsigned short int sprite2[] = {0xFFFF,0xFFFF,0x0FF0,0xF00F,0x0FF0,0xF00F,0xFFFF,0xFFFF};
unsigned long int sprite3[] = {0xFFFFFFFF,0xFF0000FF,0xFF0000FF,0xFF0000FF,
0xFF0000FF,0xFF0000FF,0xFF0000FF,0xFFFFFFFF};
These declarations are our sprite declarations. They vary in size and
type based on their width. They also introduce several new concepts in
C, which means we need to break them down into components. Let us start
with sprite1[].
This variable declaration can be broken down into the following pieces:
'unsigned', 'char', 'sprite1', '[]', '=', and '{...}'. We already know
that unsigned means we can hold positive values twice as high as signed
variables, so we will skip that. The 'char' means we are defining a
variable of type 'char', which in C means a single byte (8 bits) of
storage space, as opposed to integers which are 16-bit, and long
integers which are 32-bit. 'sprite1' is of course the name of the
variable. The '[]' however is the interesting part. The [] is used to
signify an array. An array is a set of variables of the same type which
we can access with a single name, and position index. We will talk more
about arrays in the future. The '=' is of course the assignment
operator, which you should know.
The last point of interest is the {...} declaration which controls the
values of the array. To declare an array of a finite site (meaning only
so many variables will be stored in this array), and assign values to
such an array, we use braces {}, and separate the values with commas
','. The interesting part is the values themselves. They do not look
like characters or integers. In fact they are hexadecimal numbers. I
could spend a week talking about numeric bases, but I won't. I will just
give you what you need to know.
Each digit in hexadecimal (which we will now refer to as hex) can be in
the range from '0' to '9' and 'A' to 'F' with 'A' representing 10, and
'F' representing 15, which all the other numbers in between. The data we
are defining here is pixel data. To know what this is, we should look at
these values in binary instead. The sprite1[] array in binary looks like
this:
11000011 | 1100 0011 | or, | 11 11
00111100 | 0011 1100 | without | 1111
11000011 | 1100 0011 | the | 11 11
00111100 | 0011 1100 | zeros, | 1111
11000011 | 1100 0011 | it | 11 11
00111100 | 0011 1100 | becomes | 1111
11000011 | 1100 0011 | more | 11 11
00111100 | 0011 1100 | clear. | 1111
Does that last one look familiar? Look at the screenshot. You will see
this is exactly how the first sprite looks on the screen. This is how
sprites are created. We use rows of pixels one after another to
represent which pixels to turn on, and which to turn off. In the end,
although a little hard to visualize as raw data, we can see how the
sprites are put into source code for display as something visual.
But how did we get from these raw binary pixels to something in hex
format? Well, the simplest way by hand is to break the binary digits up
intro groups of fours. Each hex digit represents four binary digits.
This is why I broke the data up in the second column above. Now we can
convert based on a simple table:
Binary to Hex to Decimal Conversion Table
|
0000 = 0 = 0 |
0100 = 4 = 4 |
1000 = 8 = 8 |
1100 = C = 12 |
0001 = 1 = 1 |
0101 = 5 = 5 |
1001 = 9 = 9 |
1101 = D = 13 |
0010 = 2 = 2 |
0110 = 6 = 6 |
1010 = A = 10 |
1110 = E = 14 |
0011 = 3 = 3 |
0111 = 7 = 7 |
1011 = B = 11 |
1111 = F = 15 |
So, how do we create sprites? Simple. First we draw the picture in
binary. Then we split the digits up into groups of four. Now we convert
these numbers to hex. Now we can put the hex digits in C. We could enter
binary natively, as the TIGCC authors have added that functionality, but
its use is discouraged because it is not part of the C standard. So, you
can either convert to hex, or look up in the TIGCC docs how to enter
binary natively. So now we have pixel data. But C needs to know how to
distinguish hex digits from regular digits. Not all numbers will use the
A-F range, so we need something else. In C, we use the 0x prefix. In C,
0x#### means this is a hexadecimal number, where #### are the hex
digits.
Remember that we separate values in the array by using commas ','. So we
have 8 values in each of the arrays. In sprite definition language, this
means all of our sprites are 8 pixels tall. Their widths are 8 pixels
for the first sprite, 16 pixels for the second, and 32 for the third.
These are the 3 types of sprites we have representations for. However,
we can make the sprites as tall as we want (up to LCD_HEIGHT of course),
but few sprites need to be taller than 8 or 16 pixels. Because of the
limited screen area, they are usually much smaller.
Well, that took almost forever. If you understood all (or any) of that,
please feel free to continue with the analysis. If not, do what I do,
change something and see what happens. Make a theory about it and test
your theory. If it works consistently, it's probably how you do
something. I didn't understand everything Zeljko Juric said about
sprites in the TIGCC documentation, so I played around and got it
working. Now I'm telling you what I know. But remember, trial and error
is still a very good way of reaching understanding.
I will skip the ClrScr() and DrawStr() functions, as we have talked
about them in previous lessons. If you need more help with them, consult
one of the earlier lessons.
// draw the sprites
Sprite8(x1, y1, SPRITE_HEIGHT, sprite1, LCD_MEM, SPRT_XOR);
Sprite16(x2, y2, SPRITE_HEIGHT, sprite2, LCD_MEM, SPRT_XOR);
Sprite32(x3, y3, SPRITE_HEIGHT, sprite3, LCD_MEM, SPRT_XOR);
These next functions are the actual drawing functions for sprites. As
you can see, we have three different functions: Sprite8(), Sprite16(),
and Sprite32(). Their arguments are basically the same, but Sprite8()
takes an unsigned character array, Sprite16() takes an unsigned integer
array, and Sprite32() takes an unsigned long integer array for their
fourth argument.
The syntax for these functions is: SpriteXX(x position, y position,
sprite height, sprite array definition, screen memory pointer,
drawing technique), where XX is the bit width of the sprite (up to XX
pixels wide), the height is the size of the sprite array (the row
definitions), the screen memory pointer is always LCD_MEM (unless you
are mapping sprites to other planes, which is more advanced, so we'll
talk about it in the future), and the drawing technique is how to
combine the pixels of the sprite with the pixels that may already be on
the screen at that location. SPRT_XOR means to use exclusive or, which
is to say if either pixel is turned on, but not both, then the pixel is
on, and all other cases are turned off. So, if a pixel is on the sprite,
and a pixel is on the screen, it is turned off. If a pixel is on the
sprite, but not on the screen, we turn the pixel on. If it is already on
the screen, but not in the sprite, we leave it on. And if it is neither
on the screen, nor on the sprite, it is left off. XOR (exclusive or) can
better be explained using a bit table, like this:
XOR Table
|
|
BIT 2 OFF |
BIT 2 ON |
BIT 1 OFF |
OFF |
ON |
BIT 1 ON |
ON |
OFF |
This is getting a bit complicated, but try to remember this. The XOR of
something and itself is the inverse of something. So, we use XOR with
unmasked sprites because they are easy. To draw them, we XOR them with
the background. To erase them, we simply XOR them again. Not too hard,
eh? Let it sink in. Play around with it if you like. You can see the
effects better if you just play around with them and see what happens.
If it wasn't clear already, the sprite height is how many rows you have
defined in the array. This is done to provide looping for the draw
function. We have to know how many times to loop to draw all the rows of
the sprite, and if we specify too many, we will get an invalid array
index, which will cause big problems (like calculator crashing).
To reiterate, I know this is complicated, especially if you have never
been introduced to some of these concepts before. It's a lot to take in
all at once. My advice to you is to play around with the code. See what
happens when you change things. This is really the best way to learn,
because you can figure things out for yourself, and then explain them
better with what works for you. I'll tell you what I know, but I've been
doing this stuff for a long time, so it's easy for me to take something
for granted that you might not think is trivial. Just be patient with
the lessons, and give them time to sink in.
I'm going to skip the outer while loop, because it's exactly the same thing
we had in lesson 2, so it shouldn't be too difficult.
// if the user pressed 1
if (key == '1') {
Here is another new kind of statement. The if statement is one of the
conditional statements in C. Like the while loop and the for loop, it
executes the body (enclosed within the { } braces) only if the condition
inside it's parentheses is true. In this case, we want to see if the
value of the key the user pressed is equal (== means equal to, one =
means assignment. It's easy to get them confused, but they have
very different meanings.) to the value of the character 1. All
characters in C are actually integers, they are just drawn on the screen
differently. In addition, most of the common keys (like the numbers and
letters) have the same value on the TI-89/92+ that they have on the
computer. This means we do not have to use the integer value of the
number 1 (which is actually 49), but we can use the character 1 enclosed
in single quotes (this tells C we have a character, not an integer -- C
will do the conversion from char to int for us when it compares an
integer to a character). C will translate this internally to if (key ==
49) {. It's just one of the shortcuts we have thanks to C's simplicity.
// remove the 8-bit sprite
Sprite8(x1, y1, SPRITE_HEIGHT, sprite1, LCD_MEM, SPRT_XOR);
Remember what we said above about the XOR of something and itself being
the inverse? Well, this is how we erase the sprite. We simply call the
exact same SpriteXX() function with the same arguments using XOR.
Because we are now giving the same value, it will do the inverse of
drawing it, which is erasing it. Simple, no?
// alter the sprite's position
if (direction1 == LEFT) {
x1-=20;
if (x1 < 10) {
x1+=40;
direction1 = RIGHT;
}
} else {
x1+=20;
if (x1 > (LCD_WIDTH - 8)) {
x1-=40;
direction1 = LEFT;
}
}
Well, here is the biggest conditional statement we have encountered so
far. Don't worry, it breaks down very simply. Remember from the first
#define directives that LEFT and RIGHT actually have numeric values
which will be replaced before the compiler works with this. So, we want
to know if the direction of our first sprite is LEFT. The direction1
variable is used to keep the direction of the first sprite. For
simplicity, I made the example only go left and right, but of course it
can go anywhere on the screen in any direction.
If the direction is left, then we take 20 pixels away from the x1 (the x
position of the first sprite), because we are moving left. The two
special operators we are using now are the -= operator and the +=
operator. The -= operator means to subtract the rvalue from the lvalue
and assign the new value to the lvalue. The += is the exact opposite, we
add the rvalue to the lvalue and assign that value to the lvalue.
(Remember lvalues are variables, rvalues are values). This might create
problems if the sprite gets too far left (we don't want to draw it off
screen). So here we have another test to make sure the sprite is at
least 10 pixels from the edge. (I think the > and < operators
should be rather self-explanatory) If this is not so, then we add twice
the number of pixels we subtracted (this is because we are going to
start moving to the right, so we have to counteract the pixels we just
subtracted, and move it to the right the same number of pixels we do the
left).
The next big part is the else clause. If statements have several
clauses, the if clause (which is the body right after the { } of the if
condition, the else clause (which is optional), which comes after the
else keyword and the new body declaration { } braces, and sometimes, but
also optional, the else if clauses, which specify different conditions
to test for, if the first condition failed. There can be as many else if
clauses as you like, but we will discuss that later.
Right now, let's concentrate on that else clause. The else clause means,
if the if condition was not true, execute my body instead. It
also means if all the else if conditions were also not true, but we'll
discuss that later. So, if the direction was not LEFT, (then it
must be right, because we only have two directions), then we do the
opposite of the if clause.
The one difference in the else clause, is that instead of seeing if the
sprite will go too far to the left, we need to make sure it stays far
enough away from the right. We do this with the internal if clause,
testing to see if x1 is greater than the LCD_WIDTH (either 160 or 240,
depending upon if it's an 89 or a 92+/V200) minus the width of the
sprite (in this case, 8 pixels). So (LCD_WIDTH - 8) is the width of the
screen in pixels minus 8. So, if x1 were equal to (==) (LCD_WIDTH - 8),
then the sprite would be drawn to the very edge of the right end of the
screen.
// if the user pressed 2
} else if (key == '2') {
// remove the 16-bit sprite
Sprite16(x2, y2, SPRITE_HEIGHT, sprite2, LCD_MEM, SPRT_XOR);
// alter the sprite's position
if (direction2 == LEFT) {
x2-=15;
if (x2 < 10) {
x2+=30;
direction2 = RIGHT;
}
} else {
x2+=15;
if (x2 > (LCD_WIDTH - 16)) {
x2-=30;
direction2 = LEFT;
}
}
// redraw the sprite at the new position
Sprite16(x2, y2, SPRITE_HEIGHT, sprite2, LCD_MEM, SPRT_XOR);
Here is the else if clause we talked about. Basically, else if lets us
test for things other than the first if condition, but not every
condition other than the if condition, which is what else means. In this
case, if we did the first if test (which we already did), and it was
false, then we go to this test. Now we are testing if the key pressed
was 2, but not 1, which we already know it wasn't. So, this else if
clause will be executed if the key is 2, and we will never get to it if
it is one, or any other value.
The internals of this else if clause are almost identical to the first,
but we will examine the differences quickly. We use the Sprite16()
function instead of the Sprite8() function since we are working with the
16-bit sprite now. I also decided to have the larger sprites move slower
across the screen, so instead of moving it 20 pixels at a time, it moves
15 pixels at a time. You can see how the first sprite moves the fastest,
and the bottom sprites move more slowly.
The last change is the LCD_WIDTH - xx calculation, which takes into
effect this is a 16-bit sprite instead of an 8-bit sprite, so we need to
make sure it is at least 16 pixels away from the right edge.
// if the user pressed 3
} else if (key == '3') {
// remove the 32-bit sprite
Sprite32(x3, y3, SPRITE_HEIGHT, sprite3, LCD_MEM, SPRT_XOR);
// adjust the sprite's position
if (direction3 == LEFT) {
x3-=10;
if (x3 < 10) {
x3+=20;
direction3 = RIGHT;
}
} else {
x3+=10;
if (x3 > (LCD_WIDTH - 32)) {
x3-=20;
direction3 = LEFT;
}
}
// redraw the sprite at the new position
Sprite32(x3, y3, SPRITE_HEIGHT, sprite3, LCD_MEM, SPRT_XOR);
}
Here is our last else if clause, which tests for the 3 key. If the user
pressed 3, and not 1 or 2, we execute this body. It is very similar to
the first two bodies, but it uses the Sprite32() function instead of
Sprite16() or Sprite8(), since we are working with the 32-bit sprite
now. 32-bit sprites are gigantic, so you probably won't use them very
often, but I put an example for all of them in, so you could get a feel
for them.
The other differences are the LCD_WIDTH - xx calculation, which uses 32
since it's a 32-bit sprite and we need to keep at least 32 pixels away
from the edge. Finally, we move this sprite only by 10 pixels at a time,
instead of 20 or 15, so it moves the slowest.