Tutorial 3: Gamepad Controls and Sprite Manipulation

From Uzebox Wiki
Jump to navigation Jump to search
  • NOT COMPLETED
  • UPDATED:
    • 2014-02-10: Clean-up of code. Addition of collision code. NEED TO ADD GRAPHICS!
    • 2014-01-23: Covering how to map and move sprites, arrays, game input logic.
    • 2014-01-16: Initial creation


This tutorial will cover interpreting input from a gamepad and movement of graphics on the screen. This is where our game becomes more of a game than just a picture on the screen.

Fix Your Map

You should have noticed the white border on the right and bottom of the screen. White is the first tile in your tilesheet and that is used as the default tile to draw when the screen is cleared. The white is there because the map is not large enough to completely fill in the screen. We will need to modify the image and convert it again. We will also need to modify the convert.xml (gconvert).

First, let's fix the convert.xml file. look for this line:

<map var-name="screen1" left="0" top="5" width="28" height="26"/>

It should look like this instead:

<map var-name="screen1" left="0" top="6" width="30" height="28"/>

We changed the dimensions of the map and changed the 'top' part because we will be adding a row.

Now we need to adjust the image. You will need to add another 16x16 row of black squares and add another 16x16 column of width. Here is how I do it:

  1. Open your image in MS Paint.
  2. Add another 16 pixels of height and width. Press CTRL+E and add 16 to both the width and height.
    1. Now you have extra white space.
  3. Now, directly below the sprite tiles move the stuff on the bottom down into the new white space.
  4. The new row created will be white. Make it black.
  5. Now, copy exactly 16 pixels of width and the full height of the map. Paste into the white space on the right.
  6. Your image should be 240x272 pixels. Save the image and close MS Paint.
  7. Open up The GIMP and drag the file in. It should open right away.
    1. You will need to apply the Uzebox color palette again.
    2. Click Image > Mode > Indexed.
    3. Choose "Use custom palette"
    4. Uncheck the checkbox for "Remove unused colors from colormap".
    5. and then click "Convert".
    6. Now click File>Overwrite untitled.png.
    7. Close the GIMP and choose "Discard Changes". Close the GIMP.

Putting Sprites on the Screen

MapSprite() and MoveSprite()

  • Sprites in Video Mode 3 are 8x8 pixel tiles that can move freely and overlap other tiles. Often a single 8x8 sprite is not large enough for our hero so we combine them into 'MegaSprites'. We then move those connected sprites together.
  • A sprite with 4 tiles in it would be a 2x2 megasprite and will take up 4 sprites. Here is how you would make a megasprite of Link facing upward:
    • 'MapSprite2(0, link_up2, 0);'. The first value is the first sprite in the megasprite. The second value is the sprite map name. The third value can either be 0 or 1. If set to '1' then the sprite will flip on the X axis. Why is this useful? Often in many games, sprite movement left and right are identical but flipped. Instead of storing the values twice, just store them once and flip them when you want to face the other direction. In our case, only storing a left facing Link would save us 4 tiles. Remember, you can only have up to 255 tiles in a game and each tile takes up bytes of flash. Be efficient!
      • NOTE: In most Zelda games, Link is left-handed. When the megasprite gets its map flipped on the X axis, Link will be right-handed for that frame. It's not a big deal but look for this in other games. It happens often and the savings are worth it. Link is just one character. This technique would save you 4 tiles on every other 2x2 megasprite such as bad guys. It adds up.
  • Okay, now you have created your sprite map but it doesn't appear on the screen yet. You use MoveSprite() for that.
    • MoveSprite(1, g_HeroPositionX, g_HeroPositionY, 2, 2);
      • First argument is the first sprite of the megasprite, followed by the new X and Y values for that sprite. The last two values indicate the dimensions of the megasprite.
  • Okay! Those are the basics. Let's continue.

Arrays and Named Constants

  • An array is a like a variable that holds many values. Each value in an array has an index. That index is used to grab the value that you want out of the array. Arrays can be very useful because of that indexing. Let's say that you had your character moving upwards. You would need to map the upward movement image and adjust it's position on the screen. If you were moving down then you would map the downward movement image and adjust the position. It would be beneficial if we could just tell the Uzebox what direction we are facing and then it can decide which image to map and draw. Try something like this:
const char * directionframes[8] = {
 link_up1, link_up2,
 link_down1, link_down2,
 link_left1, link_left2,
 link_right1, link_right2
}

There are two movement frames for each direction. If you wanted to move up then you would pass this to MapSprite2: directionframes[0]. That would pull in the first index of the array. To move a step forward you would use directionframes[1]. Each step you would alternate the 0 and the 1.

However, 0 and 1 aren't very descriptive. You should create a named constant for the values of 0 and 1. Put this above the array you just created. Example:

#define dirLINKUP    0 // Index for this direction in directionframes[]
#define dirLINKDOWN  2 // 
#define dirLINKLEFT  4 // 
#define dirLINKRIGHT 6 //

Now, you can can use directionframes[dirLINKUP] and directionframes[dirLINKUP+1] for the upward movement. This makes more sense, right? Arrays really come in handy. The alternative would be to use different code for each direction.

NOTE: Named constants are handy because they can make the code more human-readable. There are 2 ways of creating a named constant.

  • You can use the pre-compiler function '#define constantname value' or 'const constantname = value;'. With the #define you do not need to set a datatype such as char or int. It actually just replaces all instances of 'constantname' with the value. This is great because this will NOT require any additional RAM. However, the drawback is that it will replace ANYTHING of the same name. So, make sure that your constant names are unique enough.
  • Using the 'const' keyword has a similar effect but it requires a datatype and actually uses RAM. Since these are constants and there will be no need to change them, I suggest using #define. Just remember to watch that you use unique names that aren't used anywhere else.

Walking frames

  • As said before, there are 4 directions that Link can move and there are two different frames that you need to alternate through to make it look like movement. I call them 'steps'. That step will have a value of 0 or 1. Create another variable to hold that value. Use: 'char linkstep = 0;' Now, each step call will look like this: 'directionframes[dirLINKUP+linkstep]'. Still, this requires you to specify the direction. Let's store the direction we are facing also. Use: 'char linkdir = dirLINKUP;'. Now we can use 'directionframes[linkdir+linkstep]' to get the proper frame. We will change the values of linkdir and linkstep when the user presses a key on the gamepad

Moving Link

Add this your variable list: ' unsigned char g_HeroPositionX = 120;// Holds Link's X position. unsigned char g_HeroPositionY = 128;// Holds Link's Y position. ' The game will need to keep track of where our hero is. We will be using it to adjust his current X and Y position based on what directional button was pressed on the gamepad.

Animated Character Movement

Changing directions

  • Let's control what the game does by using the gamepad. First, I would like to go over some basic concepts. If you already understand basic binary and bitwise operations then feel free to quickly just scan over this. But the rest of you need to read this.

Basic Binary and Bitwise Math

Binary numbers and bitwise math are not skills that are often clearly understood. However, they can be very valuable and in fact, necessary in programming.

Representing numbers differently

When you request the current state of the gamepad controller, a value is returned. If you were to display this value on the screen you may see a decimal representation of the number. For example, if you are holding buttons B and Y then you would get a response of 3. However, 3 can be represented in binary as 00000011. Both binary 00000011 and decimal 3 have the same value. They are just represented differently. In this case, the number 3 is not very helpful. However, the value B00000011 is. Binary has columns that are multiples of two from the previous column to it's right. This is similar to decimal where it would be a multiple of 10. Decimal has ten values, 0-9. Binary only has two values and they are 0-1. You can think of 0 and 1 as ON and OFF, TRUE and FALSE, YES and NO, and so on. How many buttons were you holding in the example? Two buttons, right, B and Y? Each of those 8 1's and 0'0 represent a different button. There are 11 buttons on a SNES gamepad and each button has a value.

* #define BTN_SR     2048  // In binary: 0000100000000000, In HEX: 0x0800
* #define BTN_SL     1024  // In binary: 0000010000000000, In HEX: 0x0400
* #define BTN_X      512   // In binary: 0000001000000000, In HEX: 0x0200
* #define BTN_A      256   // In binary: 0000000100000000, In HEX: 0x0100
* #define BTN_RIGHT  128   // In binary: 0000000010000000, In HEX: 0x0080
* #define BTN_LEFT   64    // In binary: 0000000001000000, In HEX: 0x0040
* #define BTN_DOWN   32    // In binary: 0000000000100000, In HEX: 0x0020
* #define BTN_UP     16    // In binary: 0000000000010000, In HEX: 0x0010
* #define BTN_START  8     // In binary: 0000000000001000, In HEX: 0x0008
* #define BTN_SELECT 4     // In binary: 0000000000000100, In HEX: 0x0004
* #define BTN_Y      2     // In binary: 0000000000000010, In HEX: 0x0002
* #define BTN_B      1     // In binary: 0000000000000001, In HEX: 0x0001
* #define NOBUTTONS  0     // In binary: 0000000000000000, In HEX: 0x0000

The code above defines a named value for each controller button. So, BTN_Y + BTN_B = 3 (or B00000011.) Make since? If a button is pressed then there will be a 1 in the proper column. Multiple buttons get multiple 1's. Can you see why decimal doesn't help when looking at these values? In the tutorial for Logic Gates below, representing each number as binary can really help.

The concepts that are about to be covered make more sense in binary than in hexadecimal or decimal. Bitwise math often uses Logic Gates.

Introducing: Truth Tables

  • Logic Gates have “Truth Tables.” The Truth Table is used in order to determine, based on its input(s), the output of a particular type of Logic Gate. Most common Logic Gates have 2 inputs (“A” and “B”) and 1 output. So, you can check the truth table to see what the output would be based on the two inputs.
  • Notice that there are 3 main types of gates. They are the AND, OR, and NOT gates. Sometimes it is easier to understand gates when they are presented mathematically.
  • More info can be found here [[1]]

AND Gates

  • For instance, notice that the AND gate is “X = A * B”? This is multiplication. We know that both inputs must be =1 to get a 1 out. So, if both inputs are =1 then the equation is 1 * 1 = 1. If either of the inputs was a 0 then the resulting equation would have to equal 0 because any number times zero equals zero.

OR Gates

  • Let’s also look at the OR gate. The OR gate requires that at least one of its inputs is a “1” (“ON”) in order to get a “1” (“ON”) out. Notice that the equation for the OR gate is “X = A + B”? This is addition. So, even if one of the inputs was =1, we would have a 1 out. For an OR gate, either input or both inputs need to be =1 to get a 1 out.

NOT Gates

  • Next is the NOT gate. The NOT gate is often referred to as an inverter and takes only one input. The output will be the opposite state of the input. So, if you apply a “0” on the input then you will have a “1” on the output. If you have a “1” on the input then you get a zero on the output. Do you notice the over-score above the equation for NOT? That means that you invert the result of that side of the equation. If it were a “1” then the result would be “0”.

XOR Gates

  • Slightly different than the OR Gate is the Exclusive OR (XOR) gate. In the XOR Gate, you can only get a “1” on the output if you only have a single “1” on the inputs. If both of the inputs are a “1” or both inputs are a “0” then the XOR Gate would output a “0”. This separates the XOR from the OR because in an OR gate it doesn’t matter how many “1’s” you have just so long as you have at least one of the inputs being a “1.” In an XOR Gate, only one of your inputs can have a “1” to get a “1” on the output side.
  • If you take a value and XOR it with a 1 then you will flip it to it's opposite state.
    • Also, the XOR Logic Gate has applications for encryption. A simple type of encryption is the Symmetric Stream Cipher which takes the text to encrypt as well as the encryption key as separate inputs into a XOR Gate. The resulting output is encrypted. If you then took that encrypted text and the encryption key and applied them to two separate inputs of an XOR Gate again the result is the original unencrypted text. That’s pretty neat!

NAND and NOR Gates

  • Last are the NAND and NOR gates. They work just like their AND gate and OR gate counterparts except the result will be inverted. Normally, a AND gate will have a 1 on the output if both inputs are true. A NOT AND gate will simply invert that output. This works the same way with a NOR gate. The output is simply inverted. Do you notice the over-score above the equations for NOR and NAND? This means that you invert the result of that side of the equation. If it were a “1” then the result would be “0”.

Bitwise Math

Logic gates can be used to check the state or switch the state of specific bits in a byte. In this case, you would have the original value and the second value would be the mask that you apply to it. For example, if you wanted to check the state of a specific bit in a byte you could do this: 01110111 & 00010000. If you wanted to check the whole byte you would check the LSB (Least Significant Bit) of both bytes. You have a 1 and a 0. With 1 and 0 you would get a 0 out because AND requires both inputs to be a 1 to get a one out. You would keep checking and you would see that only one of the passes gives a 1 out. Your value would be 00010000. Any value above 0 is considered to be a true so you can be sure that the specific bit was in fact a 1. You could do this to check the state of the gamepad. That's what we are about to do next.

I suggest that you read Controller_Event_Handling. It covers the basics and is part of my original source material. This tutorial will take some knowledge gained from that above article and elaborate on it.

How to read from the gamepad?

This is a quick example of how to read the gamepad.

int buttons = ReadJoypad(0);  // ReadJoypad is a built-in function. Reads gamepad 0.
// Do something if player presses "UP"
if(buttons & BTN_UP){ // BTN_UP is 0000000000010000 in binary (or 16 in decimal.)
 // Move up?
}

If you get a 1 back then the player was pressing the UP button when you polled the gamepad. They could have been pressing multiple buttons but this code only checked for one of them.

The following code can do multiple checks. Each button can give different effects. The comments indicate what key that button is mapped to on the emulator (just as a convenience.) Do you see the direction checking parts at the end? It uses the code we used above for mapping the correct sprite image and step. If you press one of those buttons, movelink(linkdir, movespeed); is called. The linkdir and movespeed is set when you press the directional button. The movelink function can move link around as you might expect.

joy = ReadJoypad(0); // Get the latest input from the gamepad.
 
if(joy & BTN_A){
 // a key
 }
 
 if(joy & BTN_B){
 // s key
 } 
 
 if(joy & BTN_Y){
 // z key
 }
 
 if(joy & BTN_X){
 // x key
 }
 
 if(joy & BTN_SL){
 // left shift
 }
 
 if(joy & BTN_SR){
 // right shift
 }
 
 if(joy & BTN_SELECT){
 }
 
 if(joy & BTN_START){
 }
 
 if(joy & BTN_UP){
  if(linkdir != dirLINKUP){MapSprite2(1, link_up1, 0);linkstep=0;linkdir=dirLINKUP; }
  movelink(linkdir, movespeed);
 }
 
 else if(joy & BTN_DOWN){
  if(linkdir != dirLINKDOWN){MapSprite2(1, link_down1, 0);linkstep=0;linkdir=dirLINKDOWN;}
  movelink(linkdir, movespeed);
 }
 
 else if(joy & BTN_LEFT){
  if(linkdir != dirLINKLEFT){MapSprite2(1, link_left1, 0);linkstep=0;linkdir=dirLINKLEFT;}
  movelink(linkdir, movespeed);
 }
 
 else if(joy & BTN_RIGHT){
  if(linkdir != dirLINKRIGHT) {MapSprite2(1, link_right1, 0);linkstep=0;linkdir=dirLINKRIGHT; }
  movelink(linkdir, movespeed);
 }
}

Basic Sprite Movement

Let's cover what the movelink() function will do. For this we will just move Link around. We will cover collisions next. Movelink calls nexttiles() and IsSolid(). We will cover them in the collision section of this guide.

First things first, the top part of the function gets some values for four arrays. These arrays will contain the tile index for the tiles immediately in front of the sprite on all for directions. We will cover that in a bit.

Next, we check what direction movelink was passed and act upon those directions accordingly. We will set a variable called g_Link.linkcurrentlymoving to 1 if the next tiles in that direction are NOT considered solid (non-traversal) tiles. Again, we will cover this in a bit.

The next section covers the sprite movement. A for() loop is set up to run 8 times. Inside, the loop checks if it has been long enough before changing the sprite to its alternate step (remember 2 step images per direction exist.) The last for if() statements check the direction and change the YPOS or XPOS of the g_Link struct according to the direction. The last two lines will wait for one Vsync and then finally move the sprite and draw it to the screen.

You'll see link walk 8 pixels in the chosen direction. I've chosen 8 because that will keep Link aligned on at least one axis (X or Y) which will minimize the need for ram tiles for Link other than when walking. It also looks smooth. Lastly, we leave the function with our last line of code setting linkcurrentlymoving to 0 .

void movelink(char direction){
// Get the tiles for the surrounding area.
// Check for screen boundaries. 
// Change the map screen if the screen boundary has been reached.
// Check for solid tiles in the future position of the moving sprites.
// Change the movement state.
// If movement is still allowed then animate the walking frames and move Link.
// Reset moving flag.

	unsigned char nexttilesUP[2] = {nexttiles(colcheckUP,0), nexttiles(colcheckUP,1)};
	unsigned char nexttilesDOWN[2]={nexttiles(colcheckDOWN,0), nexttiles(colcheckDOWN,1)};
	unsigned char nexttilesLEFT[2]={nexttiles(colcheckLEFT,0), nexttiles(colcheckLEFT,1)};
	unsigned char nexttilesRIGHT[2]={nexttiles(colcheckRIGHT,0), nexttiles(colcheckRIGHT,1)};	

// Change map on screen boundary, check for solid tiles.
	if(direction == dirLINKUP)   {
		if(g_Link.YPOSp > Uscreenboundry){
			if ( (IsSolid(nexttilesUP[0], 0)== false)
			&&   (IsSolid(nexttilesUP[1], 1)== false)){
				g_Link.linkcurrentlymoving = 1;
			}
			else {g_Link.linkcurrentlymoving = 0;}
		}
		else{
			// Check if there is a map on what would be the next screen.
			if(currentmapY<Ymapscreens-1){ // True is there is.
				prevmapY = currentmapY; // Store previous value.
				currentmapY++;	// Increment the map value
				g_Link.YPOSp = Dscreenboundry;	// Put Link at the correct boundry.
				loadmap(overworldmaps[currentmapX][currentmapY], 7); // Load the new map.
			} 
		}
	} 

	else if(direction == dirLINKDOWN) { 
		if(g_Link.YPOSp < Dscreenboundry){
			if ( IsSolid(nexttilesDOWN[0],0 )== false
			&&   IsSolid(nexttilesDOWN[1],1 )== false){
				g_Link.linkcurrentlymoving = 1;
			} 
			else{g_Link.linkcurrentlymoving = 0;}
		}
		else{
			if(currentmapY>0){
				prevmapY = currentmapY;
				currentmapY--;
				g_Link.YPOSp = Uscreenboundry;
				loadmap(overworldmaps[currentmapX][currentmapY], 7);
			}
		}
	}	
	 
	else if(direction == dirLINKLEFT) { 
		if(g_Link.XPOSp > Lscreenboundry){
			if ( IsSolid( nexttilesLEFT[0],0 )== false
			&&   IsSolid( nexttilesLEFT[1],1 )== false){
				g_Link.linkcurrentlymoving = 1;
			}
			else {g_Link.linkcurrentlymoving = 0;}
		}				
		else{
			if(currentmapX>0){
				prevmapX = currentmapX;
				currentmapX--;
				g_Link.XPOSp = Rscreenboundry;
				loadmap(overworldmaps[currentmapX][currentmapY], 7);
			} 
		}
	}
		
	else if(direction == dirLINKRIGHT){ 
		if(g_Link.XPOSp < Rscreenboundry){
			if ( IsSolid( nexttilesRIGHT[0],0 )== false
			&&   IsSolid( nexttilesRIGHT[1],1 )== false){
				g_Link.linkcurrentlymoving = 1;
			} 
			else {g_Link.linkcurrentlymoving = 0;}
		}
		else{
			if(currentmapX < Xmapscreens-1){
				prevmapX = currentmapX;
				currentmapX++;
				g_Link.XPOSp = Lscreenboundry;
				loadmap(overworldmaps[currentmapX][currentmapY], 7);
			} 
		}	
	}

	// Move and animate Link if required.
		for(unsigned char stepcount =0; stepcount<8;stepcount++){
			if (g_Link.framedelay > g_Link.framelat+1){
				if (g_Link.linkstep==1){
					MapSprite2(0, linkdframes[g_Link.linkdir+0], 0); 
					g_Link.linkstep=0; 
				} 
				else{
					MapSprite2(0, linkdframes[g_Link.linkdir+1], 0); 
					g_Link.linkstep=1; 
				}
				g_Link.framedelay=0;
			}	
			else { g_Link.framedelay++; }
			if(g_Link.linkdir == dirLINKUP)   { g_Link.YPOSp--; }
			if(g_Link.linkdir == dirLINKDOWN) { g_Link.YPOSp++; }
			if(g_Link.linkdir == dirLINKLEFT) { g_Link.XPOSp--; }
			if(g_Link.linkdir == dirLINKRIGHT){ g_Link.XPOSp++; }
			WaitVsync(1);
			MoveSprite(0, g_Link.XPOSp, g_Link.YPOSp, 2, 2);
		}
		g_Link.linkcurrentlymoving = 0;
	}
}

Collisions

If you used this code (and removed the usage of getnexttiles and IsSolid, Link would walk through walls. Whats the point of a wall if you can just walk over it? To the Uzebox, the walls are just graphics. It knows nothing of common physics unless you teach it. You have to instruct the Uzebox to prevent Link from walking into the wall graphics. Certain parts of the terrain you should be able to walk through. For example, the black door, the tan road. If the tile that we try to walk Link into is a solid tile (not one of the aforementioned tiles) then we need to instruct the Uzebox to deny that movement. A few more functions are needed as well as a list of tiles that Link can walk through.

This code should go at the top of your code. The colcheck values are used by getnexttile() to check what direction you are trying to move into. Please note that the tile ids may be different for your game. This is just an example. You can use mydisplaytilesheet() function to find out what you should put in the array.

#define colcheckUP 0
#define colcheckDOWN 1
#define colcheckLEFT 2
#define colcheckRIGHT 3

#define walkabletilesSIZE 4
const unsigned char walkabletiles[] ={ 
18, 	// Tan road
32, 	// Black tile (door)
36, 37, // Bridge
};

This function will display all the individual tiles of your game and show you their tile number. This should be a super-handy function. If there are more tiles in your tileset than the screen can display then you can press the BTN_A on the game pad to switch to the next page. You can have this function called by you pressing a button. Personally, I have a quick title page that displays the name of the game, some credits and such. You have to press BTN_START to continue to the game. I suggest a loop that waits for both buttons. Start the game if you press start and run the displaytilesheet() function if you press A.

void displaytilesheet(){
// Very useful function. Returns all tiles and tile numbers, displaying them onscreen.
	unsigned char x = 0;
	unsigned char y = 0;
	unsigned char rowcount = 0;
	
	// Print each value on a new row. Change columns every 25 rows.
	for(int thetiles = 0; thetiles<OVERWORLD_SIZE; thetiles++){
		SetTile(x,y,thetiles);
		Print(x+1, y, PSTR("")); PrintByte (x+3+0, y, thetiles, true);
		y+=1;
		rowcount++;
		if(rowcount==25){ x+=5;y=0;	rowcount=0;	}
		
		// Check if there is more than one page.
		if(thetiles==124){
			Print(16, 28,  PSTR("Press 'A' for next page"));
			while(ReadJoypad(0) != BTN_A){} 
			x=0;y=0; rowcount=0;
		}
	}
}

Remember the top of movelink() where we filled in 4 arrays? Those arrays get filled with this function. This function hides away some of the mess of figuring out which tile ids you need. I would like to explain a part of it though. There are up to 240 X pixels on the screen. However, tiles are not pixel positioned. They are all aligned on 8 pixel axis's. If you divide by 8 (ignoring the remainder) you'll get the x tile and y tile that you are on. The '>>3' part is a bit-shift to the right by 3 bits. It is easier on the microcontroller to do division in this manor as normal division can slow you down. You can also multiply by other powers of 2 by bit-shifting to the left. This is briefly mentioned here [2]. It is important to understand that although you are moving a 2x2 sprite that any checks against it will only check the position of it's first sprite. Nexttiles() will check all tiles that border your 2x2 megasprite. The end result of nexttiles() is that it returns the tile id.

unsigned char nexttiles(unsigned char direction, unsigned char whichtile){
	// This function returns the tile number for the requested tiles based on direction.
	// The second argument determines which of the 2 tiles will be returned. 
	// *** Might be able to pass the array as an argument instead of 'whichtile'.
	// *** http://www.cplusplus.com/forum/beginner/56820/
	
	unsigned char whattile = 0;

	if(direction == colcheckUP){
		if		(whichtile==0){whattile = GetTile(((g_Link.XPOSp>>3)+0), ((g_Link.YPOSp>>3)-1));}
		else if	(whichtile==1){whattile = GetTile(((g_Link.XPOSp>>3)+1)), ((g_Link.YPOSp>>3)-1));}
	}
	
	else if(direction == colcheckDOWN){
		if		(whichtile==0){whattile = GetTile(((g_Link.XPOSp>>3)+0), ((g_Link.YPOSp>>3)+2));}
		else if	(whichtile==1){whattile = GetTile(((g_Link.XPOSp>>3)+1), ((g_Link.YPOSp>>3)+2));}
	}
	
	else if(direction == colcheckLEFT){
		if		(whichtile==0){whattile = GetTile(((g_Link.XPOSp>>3)-1), ((g_Link.YPOSp>>3)+0));}
		else if	(whichtile==1){whattile = GetTile(((g_Link.XPOSp>>3)-1), ((g_Link.YPOSp>>3)+1));}
	}
	
	else if(direction == colcheckRIGHT){
		if		(whichtile==0){whattile = GetTile(((g_Link.XPOSp>>3)+2), ((g_Link.YPOSp>>3)+0));}
		else if	(whichtile==1){whattile = GetTile(((g_Link.XPOSp>>3)+2), ((g_Link.YPOSp>>3)+1));}
	}
	
	return whattile;
}


The IsSolid() function checks the tiles immediately ahead of our 2x2 megasprite. Since there are two tiles on a side we need two checks. We pass in the tile id (which we received from nexttiles() )and then if this is the 1st or second check. The function simply checks to see if the passed tile id is in our list of allowed walkable tile ids.

bool IsSolid(unsigned char t, unsigned char firstorsecond){
	// Checks the tile number of the passed tile against a list of non-solid tiles.
	for(unsigned char thiscounter=0;thiscounter<walkabletilesSIZE;thiscounter++){
		if(t == walkabletiles[thiscounter]) { return false; } 
	}
	return true;
}

One more look now at how we invoke all these functions. We use IsSolid and pass it the values of the nexttilesUP array (which had it's values filled in at the top of this function.) We need a false so that we can be sure that the value passed is NOT a solid. We check that it is one of the walkable tiles defined in the walkabletiles array.

if(direction == dirLINKUP)   {
	if(g_Link.YPOSp > Uscreenboundry){
		if ( (IsSolid(nexttilesUP[0], 0)== false)
		&&   (IsSolid(nexttilesUP[1], 1)== false)){
			g_Link.linkcurrentlymoving = 1;
		}
		else {g_Link.linkcurrentlymoving = 0;}
	}