Techno-Plaza
Site Navigation [ News | Our Software | Calculators | Programming | Assembly | Downloads | Links | Cool Graphs | Feedback ]
 Main
   Site News

   Our Software

   Legal Information

   Credits

 Calculators
   Information

   C Programming

     Introduction to C

     Keyboard Input

     Graphics Intro

       Part I

       Part II

     Slider Puzzle 1

     Functions

     Pointers

     Dynamic Memory

     Slider Puzzle 2

     Structures

     Bit Manipulation

     Advanced Pointers

     File I/O

     Graduate Review

   Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

C Programming Lessons

TIGCC Programming Lessons

Lesson 3 - An Introduction to Graphics

Step 4 - An Introduction to Sprites

Sprites are the heart of efficient graphics manipulation. Sprites are small segments of display information designed to be placed, erased, and replaced easily and quickly.

The AMS has little direct need for sprites. Sprites are used mainly by people who need to change information on the screen in very specific, manageable, changeable, and advanced methods. Sprites are used to control characters in games, complex background display, and other forms of complex screen manipulation. Most everything you see in games or other programs which have complex display patterns which change frequently are based on sprites. Because the system does not do need complex display routines, it has little built in support for sprite manipulation. It does manipulate Bitmaps, but bitmaps are (usually) much larger than sprites, and the system displays them very slowly. However, thanks to TIGCC, we have extended sprite functions which are very fast and powerful for sprites no wider than 32 pixels (and it is very rare that you would need such a large sprite).

There are two kinds of sprites, masked sprites and unmasked sprites. Unmasked sprites are very easy to use, but masked sprites are much more powerful. However, since this is only an introduction to graphics, we will be using unmasked sprites. Masked sprites will be covered in a future lesson. If you plan to write complex games, masked sprites are almost the only way to go. Simple games however can use unmasked sprites and the difference isn't noticeable. Nibbles 68k used unmasked sprites and it is fine.

Step 5a - An Example Using Unmasked Sprites

Start TIGCC and create a new project. Create a new C Source file named sprites. Edit the file to look like this:

sprites.c


#include <tigcclib.h>

// define the global constants
#define SPRITE_HEIGHT   8
#define LEFT        0
#define RIGHT       1

void _main(void) {
    // declare the sprite positions
    int x1 = 20, y1 = 25, direction1 = RIGHT;
    int x2 = 40, y2 = 50, direction2 = LEFT;
    int x3 = 80, y3 = 75, direction3 = RIGHT;

    // keycode variable
    int key = 0;

    // 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};

    // clear the screen
    ClrScr();

    // print the control information strings
    DrawStr(0, 0, "Press 1, 2, or 3", A_NORMAL);
    DrawStr(0, 10, "to move the sprites!", A_NORMAL);

    // 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);

    // wait until the user presses ESC
    while ((key = ngetchx()) != KEY_ESC) {
        // if the user pressed 1
        if (key == '1') {
            // remove the 8-bit sprite
            Sprite8(x1, y1, SPRITE_HEIGHT, sprite1, LCD_MEM, SPRT_XOR);

            // 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;
                }
            }

            // redraw the sprite at the new position
            Sprite8(x1, y1, SPRITE_HEIGHT, sprite1, LCD_MEM, SPRT_XOR);

        // 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);

        // 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);
        }
    }
}

This is the most complicated program we have ever done here. Make sure you understand all the information contained in the previous lessons. You will need it here more than ever. We won't stop to explain every little line, but will instead be working with chunks of code, as you should have the more basic features down by now. If not, it's time to revisit lessons 1 and 2.

Build the project and send it to TiEmu. When you run it, you will see something similar to this.

TI-89 AMS 2.05 sprites.89z TI-92+ AMS 2.05 sprites.9xz

Step 5b - Program Analysis

// 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.

Step 5c - Program Conclusions

Well, that's the entire program. I know it looks complicated when you take it as a whole, and it's 3 times larger than any program we've done so far, but you can see how most of the code is very similar, and we only use a few basic concepts. The best advice I can give you if you don't quite get it is to play around with the code. Make some changes and see what happens.

The program is not very advanced, but does use some interesting new concepts, which introduce a whole slew of other concepts to test the ones we are learning now.

Most of these concepts are basic to C, so if you are really interested in learning all the nuances, you might want to consider picking up a C book from your local bookstore. It is a very powerful language, and has many nuances, but once you learn them, it becomes very easy to program almost anything.

Step 6 - Lesson Conclusions

This lesson introduced the foundation of graphics on the TI-89/92+/V200. Some simple lines introduced some concepts about screen dimensions, and sprites introduced the basic means of creating and working with moveable graphics. From here, we are at the turning point. We can make many simple programs, and even small games using the techniques we have learned here. But this is only the beginning. It is up to you to turn these techniques into something powerful by utilizing them in your own programs.

Just remember, thanks to TIGCC, and Zeljko Juric's TIGCC library, there is NOTHING we cannot program. Keep reading, experimenting, and putting things together. If you make something cool, send it to me. Maybe I'll make a lesson out of it. :-)


Lesson 3: An Introduction to Graphics
Questions or Comments? Feel free to contact us.


 

Copyright © 1998-2007 Techno-Plaza
All Rights Reserved Unless Otherwise Noted

Get Firefox!    Valid HTML 4.01!    Made with jEdit