• Hey, guest user. Hope you're enjoying NeoGAF! Have you considered registering for an account? Come join us and add your take to the daily discourse.

Let's build a Sega Dreamcast game from scratch - Breakout

Kientin

Member
It's going to be slow for me but I'm going to try my best to do some of this on the weekends. Classes are really tough this semester. I'm really interested in this stuff however. Thanks so much for this, Krejiooc.
 

Krejlooc

Banned
Our code drew that little blue 10x10 square in the upper right hand corner of the screen. It did so because the first permutation of the upper part of the nested for-loop sets j to 0, and j is our marker for the vertical position of the drawing. Then, the lower part of the nested for loop sets i to 0 as well, and i is the marker for the horizontal position of the drawing. Thus, when run through our formula (i + (j * 320), we get (0 + (0 * 320)) = (0 + 0) = 0, which is the cell of the screen we draw to:

mknbkia.png


Next, the lower part of the nested for-loop iterates, increasing i to 1. This makes our formula (i + (j * 320)) = (1 + (0 * 320)) = (1 + 0) = 1, which is the cell we are drawing to next:

Nanzn2T.png


The lower part of the nested for-loop iterates yet again, increasing i to 2. Thus, our formula is (i + (j * 320)) = (2 + (0 * 320)) = (2 + 0) = 2, which is the cell we draw to:

rnyanxW.png


This continues on until the lower part of the nested for-loop reaches it's conditional, which is that i is no longer less than 10. In all, the lower portion of the loop will run a number of 10 times, moving the drawing cell over to the right each time:

VcH0VKp.png


With the lower portion of the nested for-loop completed, the upper portion can iterate again. That means j is increased to 1, and everything within the bounds of this lower portion of the for-loop is repeated. That means the lower portion of the for-loop, which runs 10 times, is also repeated. That means that, once again, i is set to 0. Thus, our formula works like this: (i + (j * 320)) = (0 + (1 * 320)) = (0 + 320) = 320. You see that this time, we draw to the 320th cell. This is because our screen is 320 pixels wide. The right-most portion of the first row of pixels ends at 319. Thus, we are drawing to the 320th cell, which is right below the 0th cell:

HkPAZaz.png


And our lower-for loop iterates again:

e0lKdrd.png


This zig-zagging pattern continues until the j loop iterates to the point where it's conditional is no longer met, i.e. j is no longer less than 10, meaning it will run a total of 10 times as well. That is how our square is drawn.

STEP 3: MOVING OUR DRAWING LOCATION

Right now, when we draw, we start at (0, 0) on the screen, which is cell number 0. This is the upper-left corner of the screen. The lower right hand corner of the screen would be (320, 240), which would be the 76,800th cell.


We can alter our drawing formula to change the starting position of the square we draw. In actuality, our formula should be:

Code:
vram_s[ (x + i) + ((y + j) * 320)] = PACK_PIXEL(0, 0, 255);

And, if you think about our output, which drew at (0,0), then our earlier formula is actually identical to that one. x and y, in our case, were both 0. We can define x and y as variables at the top of our main routine to control where we draw:

Code:
int main(void) {
 int quit = 0;
 int i = 0; int j = 0;
 int x = 2;
 int y = 3;

Compile and run this code, and you'll see this:

CCoQYpT.png


You see the little blue square has moved slightly. This is because, thanks to x and y in our formula, the drawing location changes. The lower part of the for-loop still begins with i set to 0, and j is still set to 0 in the upper part of the for-loop, but the formula now reads: (x + i + (y + j * 320)) = (2 + 0 + (3 + 0 * 320)) = (2 + (3 * 320)) = (2 + 960) = 962. We begin by drawing on the 962 cell of the screen:

EUpu3FQ.png


And from there, our previous zig-zag drawing pattern resumes:

zELbw3a.png


STEP 4: SPRITE DRAWING ROUTINE

Drawing a solid blue box isn't too useful itself. Ultimately, we'll want to draw complex images with many colors. A sprite is an array of data that contains information used to draw a 2D image to the screen. Ultimately, we want to draw sprites to the screen for our game.

For the purpose of this tutorial, Zeboyd Games have graciously donated the sprite sheet for their game Cthulu Saves the World for us to work with. These sprites are 16x16 pixels big. We'll begin by taking a look at a single sprite from their game:

7e8OSPn.png


This is part of a horizontal walking animation for the main character, Cthulu. If we count the number of colors, you'll see the sprite has 23 unique colors in it. To makes things simple for this tutorial, I have reduced the color count so that there are only 9 unique colors in the sprite:

muQft2d.png


Later in this tutorial, we'll learn how I reduced the color count, but for now that isn't important. Testing using the Colorcube Analysis tool in gimp confirms there are only 9 colors in the image:

jA3vfc1.png


If we use the Color Picker tool in Gimp:

mNiviDe.png


We can examine each color that makes up the image individually to see their Red, Green, and Blue values:

DmP9q4C.png


And we would ultimately find the colors being used in this image are:

Code:
color 0: 255, 0, 255
color 1: 4, 7, 4
color 2: 28, 46, 64
color 3: 147, 129, 105
color 4: 139, 168, 137
color 5: 213, 19, 12
color 6: 46, 210, 211
color 7: 47, 117, 19
color 8: 96, 27, 10

Let's begin by writing a function in our program that will change the color of the pixel we draw depending on which color we select.

Code:
//Function to plot a colored pixel on the screen, at (X,Y)
//takes an integer to determine which color to plot
int DrawPixel(int x, int y, int color)
{
	switch(color)
	{
		case 0: vram_s[ x + (y * 320)] = PACK_PIXEL(255, 0, 255);	
		return 1;
		break;
		case 1: vram_s[ x + (y * 320)] = PACK_PIXEL(4, 7, 4);	
		return 1;
		break;
		case 2: vram_s[ x + (y * 320)] = PACK_PIXEL(28, 46, 64);	
		return 1;
		break;
		case 3: vram_s[ x + (y * 320)] = PACK_PIXEL(147, 129, 105);	
		return 1;
		break;
		case 4: vram_s[ x + (y * 320)] = PACK_PIXEL(139, 168, 137);	
		return 1;
		break;
		case 5: vram_s[ x + (y * 320)] = PACK_PIXEL(213, 19, 12);	
		return 1;
		break;
		case 6: vram_s[ x + (y * 320)] = PACK_PIXEL(46, 210, 211);	
		return 1;
		break;
		case 7: vram_s[ x + (y * 320)] = PACK_PIXEL(47, 117, 19);	
		return 1;
		break;
		case 8: vram_s[ x + (y * 320)] = PACK_PIXEL(96, 27, 10);	
		return 1;
		break;
	}
	
	return 0;
}

This function, called DrawPixel, takes 3 integers – x, y, and color. x and y are ultimately the final resultant position where we will draw our pixel, and color is used to select which color we plot. We use a Switch Statement to select between all the possible values that color could be, from 0 to 8. What we have created, in essence, is a Palette, that takes a number 0-8, to draw different pixel colors.

We now need to map the image above to data that will tell our program to draw the image correctly. To do this, we mimic the way our program draws to the screen, reading the original image pixel by pixel in a zig-zagging pattern, and mapping data accordingly. Beginning with the first pixel:

VvbxbIF.png


We see that it matches color 0 in our palette. The first five pixels do, in fact. We don't encounter a different color until pixel six. That means our first 5 pieces of data should tell us to draw color 0:

EKYoftE.png


This new color we encounter matches color 1 on our palette, and continues for two pixels:

27DKrdO.png


And if we continue forward, eventually we will map our entire sprite in this fashion:

uInf2MD.png


Going back to our program, we can create a global array of integers to hold this data. After our preprocessor definitions, but before our DrawPixel function, declare the following data:

Code:
//A 16x16 array of 32-bit integers containing a single sprite frame
int SpriteData[] = { 
0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 3, 3, 4, 4, 4, 2, 0, 0, 0, 0, 0,
0, 0, 0, 1, 3, 3, 4, 4, 4, 4, 4, 2, 0, 0, 0, 0,
0, 0, 0, 1, 3, 4, 4, 4, 4, 4, 4, 2, 1, 0, 0, 0,
0, 0, 0, 2, 5, 3, 4, 4, 4, 4, 4, 1, 3, 1, 0, 0,
6, 0, 0, 2, 5, 5, 3, 4, 4, 4, 2, 0, 2, 4, 1, 0, 
6, 0, 1, 3, 4, 4, 3, 3, 4, 2, 1, 1, 3, 2, 3, 1, 
0, 6, 2, 3, 2, 4, 4, 2, 1, 1, 1, 2, 3, 2, 2, 4, 
0, 6, 3, 2, 3, 4, 2, 1, 7, 4, 4, 1, 1, 2, 3, 2,
0, 0, 2, 3, 2, 1, 1, 2, 2, 4, 4, 0, 0, 1, 3, 2,
0, 0, 2, 1, 1, 2, 2, 2, 3, 4, 3, 2, 0, 0, 1, 0,
0, 0, 4, 2, 2, 3, 4, 5, 4, 4, 2, 2, 0, 0, 0, 0,
0, 0, 4, 8, 5, 2, 2, 5, 3, 2, 5 ,8, 0, 0, 0, 0,
0, 0, 0, 1, 8, 8, 5, 8, 5, 5, 8, 8, 0, 0, 0, 0, 
0, 0, 0, 0, 2, 2, 3, 1, 3, 2, 1, 0, 0, 0, 0, 0, 
0, 0, 0, 1, 2, 4, 2, 4, 2, 2, 1, 0, 0, 0, 0, 0 };

Because this data is declared outside of a function, it's global in scope, meaning multiple functions can access it, and it won't be destroyed when a function exits. This data matches the pixel data from our original sprite. More importantly, SpriteData isn't just an integer, it's a pointer to an array of integers. This means that, in memory, we just declared 256 (16x16) integers in succession, each 32-bits long. SpriteData points to the location in memory where the first integer is located, and you access the integers afterwards by passing an offset in brackets. i.e. if you called SpriteData[1], then it returns the location of the integer located at SpriteData (the first integer declared) + 1 times 32-bits, which arrives at the location of the second integer in the array.

Now, let's write a routine that passes our DrawPixel function the appropriate data from SpriteData to draw the sprite:

Code:
//Draws a 16x16 sprite pattern with the top-corner location being (x,y)
//Passed a pointer to an array of data to draw from
int DrawSprite(int x, int y, int* Data)
{
	int i; 				//our down offset
	int j;				//our right offset
	int PixelCounter;		//which pixel to draw in Data array
	
	for(i = 0; i < 16; i++)
	{
		for(j = 0; j < 16; j++)
		{
			DrawPixel((x+j), (y+i), Data[PixelCounter]);
			PixelCounter++;	//incriment PixelCounter;
		}
	}
}

This matches the structure of our 10x10 nested for-loop drawing routine that we created in Main before, except this one goes 16x16 (the dimensions of the sprite we are drawing). This function takes an x and y coordinate that it passes to DrawPixel along with offsets i and j. It also takes a pointer to an integer called Data. Because arrays are pointers to data, this allows us to pass an entire array of data to our function.

New to this structure is integer called PixelCounter. This is a number we use as an offset to iterate through the Data array. Each time the nested for-loop completes, PixelCounter increases by 1, which makes us point to the next pixel in our array.

Finally, we need to change up our Main routine to call these new functions:

Code:
int main(void) {
 int quit = 0;
 int x = 2;
 int y = 3;
 
 //init kos
 pvr_init_defaults();
 
 //set our video mode
 vid_set_mode(DM_320x240, PM_RGB565);
 
 //Main Loop
 while(!quit) {
	 
	DrawSprite(x, y, SpriteData);
	
}

 return 0;
}

We still have our Main Loop, but we no longer have nested for-loops inside. Instead, each frame, we call DrawSprite and pass our x and y variables, along with the SpriteData array.

Build this program, and you should see the following:

E3lWdjh.png


Only problem is that the purple-pink color on the spritesheet (255, 0, 255) is obviously supposed to be transparent. We can make it so that our DrawPixel routine does nothing when it encounters this color with just a simple comment line:

Code:
//Function to plot a colored pixel on the screen, at (X,Y)
//takes an integer to determine which color to plot
int DrawPixel(int x, int y, int color)
{
	switch(color)
	{
		case 0: //vram_s[ x + (y * 320)] = PACK_PIXEL(255, 0, 255);	
		return 1;
		break;
		case 1: vram_s[ x + (y * 320)] = PACK_PIXEL(4, 7, 4);	
		return 1;
		break;
		case 2: vram_s[ x + (y * 320)] = PACK_PIXEL(28, 46, 64);	
		return 1;
		break;
		case 3: vram_s[ x + (y * 320)] = PACK_PIXEL(147, 129, 105);	
		return 1;
		break;
		case 4: vram_s[ x + (y * 320)] = PACK_PIXEL(139, 168, 137);	
		return 1;
		break;
		case 5: vram_s[ x + (y * 320)] = PACK_PIXEL(213, 19, 12);	
		return 1;
		break;
		case 6: vram_s[ x + (y * 320)] = PACK_PIXEL(46, 210, 211);	
		return 1;
		break;
		case 7: vram_s[ x + (y * 320)] = PACK_PIXEL(47, 117, 19);	
		return 1;
		break;
		case 8: vram_s[ x + (y * 320)] = PACK_PIXEL(96, 27, 10);	
		return 1;
		break;
	}
	
	return 0;
}

This way, when the DrawPixel command is passed 0 as the color, it will simply return without problem. Build the program again and you'll see the color is now transparent:

LCXcbfP.png
 

Krejlooc

Banned
STEP 5: ADDING MOTION

Now that we can draw a sprite to the screen, let's expand our ability to control it. To begin with, let's redefine our DrawSprite routine so that we can flip the sprite horizontally if we want:

Code:
//Draws a 16x16 sprite pattern with the top-corner location being (x,y)
//Passed a pointer to an array of data to draw from
int DrawSprite(int x, int y, int Direction, int* Data)
{
	int i; 				//our down offset
	int j;				//our right offset
	int PixelCounter;		//which pixel to draw in Data array
	
	//Draw normal
	if(Direction > 0)
	{
		for(i = 0; i < 16; i++)
		{
			for(j = 0; j < 16; j++)
			{
				DrawPixel((x+j), (y+i), Data[PixelCounter]);
				PixelCounter++;	//increment PixelCounter;
			}
		}
	} else	//flip the sprite
	{
		for(i = 0; i < 16; i++)
		{
			for(j = 15; j > (-1); j--)
			{
				DrawPixel((x+j), (y+i), Data[PixelCounter]);
				PixelCounter++;	//incriment PixelCounter;
			}
		}
	}
}

Our function now takes an additional integer, Direction. Before we get into our nested for-loops that draw our 16x16 sprite, we check Direction's value. If Direction is greater than 0, then we draw our sprite the same way we drew it before. Otherwise, if Direction is 0 or less, then we change our nested for-loops.

This time, when the lower for-loop begins, j is set to 15, the width of the sprite (0-15 = 16 indexes), and every iteration j decreases. At the same time, PixelCounter is still increasing linearly. So the first time we loop, we draw like this:

0p2ABGt.png


And as we go through the loop, PixelCounter increases, while j decreases:

2ozuDUh.png


Eventually, the sprite is drawn in a reverse Z pattern:

xBm3hNX.png


If we change our main loop like so:

Code:
//Main Loop
 while(!quit) {
	 
	DrawSprite(x, y, -1, SpriteData);
	
}

And build our program, we can see the sprite has flipped:

GRQFbkd.png


While we are at it, let's change our SpriteData type. Currently, SpriteData is a pointer of type integer consisting of 16x16 (256) elements. Each element is 32-bits big, meaning our array is 8,192 bits big, or 1024 bytes, or 1 KB. That's a lot of space for such a tiny picture. As we learned earlier, we can store 2 Hexadecimal digits in 1 byte, and since we only have 9 colors we're drawing with, 1 hexadecimal digit is big enough to represent 1 pixel. Thus, if we switched our type from 32-bit integer, to 8-bit byte, we could save a ton of space without losing any data.

Start by replacing the SpriteData array with the following:

Code:
//An 8x16 array of 8-bit unsigned integers containing a single sprite frame
uint8 SpriteData[] = { 
0x00, 0x00, 0x01, 0x12, 0x22, 0x00, 0x00, 0x00,
0x00, 0x00, 0x13, 0x34, 0x44, 0x20, 0x00, 0x00,
0x00, 0x01, 0x33, 0x44, 0x44, 0x42, 0x00, 0x00,
0x00, 0x01, 0x34, 0x44, 0x44, 0x42, 0x10, 0x00,
0x00, 0x02, 0x53, 0x44, 0x44, 0x41, 0x31, 0x00,
0x60, 0x02, 0x55, 0x34, 0x44, 0x20, 0x24, 0x10, 
0x60, 0x13, 0x44, 0x33, 0x42, 0x11, 0x32, 0x31, 
0x06, 0x23, 0x24, 0x42, 0x11, 0x12, 0x32, 0x24, 
0x06, 0x32, 0x34, 0x21, 0x74, 0x41, 0x12, 0x32,
0x00, 0x23, 0x21, 0x12, 0x24, 0x40, 0x01, 0x32,
0x00, 0x21, 0x12, 0x22, 0x34, 0x32, 0x00, 0x10,
0x00, 0x42, 0x23, 0x45, 0x44, 0x22, 0x00, 0x00,
0x00, 0x48, 0x52, 0x25, 0x32, 0x58, 0x00, 0x00,
0x00, 0x01, 0x88, 0x58, 0x55, 0x88, 0x00, 0x00, 
0x00, 0x00, 0x22, 0x31, 0x32, 0x10, 0x00, 0x00, 
0x00, 0x01, 0x24, 0x24, 0x22, 0x10, 0x00, 0x00 };

Recall that prefacing a digit with 0x or $ indicates that it is a hexadecimal digit. Though the data above looks nearly identical to the 32-bit integer data, it is much smaller. This is an array of 8x16 bytes, each 8-bits big, meaning the total array is only 128 bytes big, or 0.125 KB. That's the same exact data, only less than 1/10th the size!

If we are going to use this smaller data, we need to rewrite our DrawSprite routine to make sure it reads the high and low portion of the byte correctly using bit-masking and bit-shifting:

Code:
//Draws a 16x16 sprite pattern with the top-corner location being (x,y)
//Passed a pointer to an array of data to draw from
int DrawSprite(int x, int y, int Direction, uint8* Data)
{
	int i; 				//our down offset
	int j;				//our right offset
	int PixelCounter;		//which pixel to draw in Data array
	
	//Draw normal
	if(Direction > 0)
	{
		for(i = 0; i < 16; i++)
		{
			for(j = 0; j < 16; j+=2)
			{
				DrawPixel((x+j), (y+i),  (Data[PixelCounter] & 0xF0) >> 4);
				DrawPixel(x+j+1, (y+i), (Data[PixelCounter] & 0x0F));
				PixelCounter++;	//increment PixelCounter;
			}
		}
	} else	//flip the sprite
	{
		for(i = 0; i < 16; i++)
		{
			for(j = 15; j > (-1); j-=2)
			{
				DrawPixel((x+j), (y+i),  (Data[PixelCounter] & 0xF0) >> 4);
				DrawPixel(x+j-1, (y+i), (Data[PixelCounter] & 0x0F));
				PixelCounter++;	//increment PixelCounter;
			}
		}
	}
}

You can see now we call DrawPixel twice now, because each byte of data contains info for two pixels. First, we examine the high portion of the byte, then the low portion on the second DrawPixel call. Because we branch depending on direction, our change must be reflected twice in each branch.

Build this program to make sure it works. If everything is fine, you should arrive at a sprite that looks identical to our last build:

GRQFbkd.png


Now, let's change the Main Loop to make our sprite draw in a different position every frame:


Code:
 int x = 0;
 int y = 224;
 
 ...
 
 //Main Loop
 while(!quit) {
	x++;
	x =	(x%320);
	DrawSprite(x, y, 1, SpriteData);
	
}

You can see that the first thing done every time the Main Loop runs is that x is increased by 1. However, immediately afterwards, x is boundary checked by the width of the screen, 320, using modulus division. Modular arithmetic is a system where numbers &#8220;wrap around&#8221; after reaching a maximum, which is represented by the divisor. Since our divisor is 320, when x is greater than 320, it'll loop back around to 0. This means if we increased x to 321, then (x%320) would return 0. If we increased x to 322, then (x%320) would return 1. If we increased x to 323, (x%320) would return 2. And so forth.

Another way to think of modulus division is that it is essentially giving you the remainder for integer division. Recall that integers in C and C++ are whole numbers, no decimal places. That means that the integer division of 6/5 would return 1, because it cannot represent 1.2. However, modulus division 6%5 would return 1. 6/5 = 1, and 6%5 = remainder 1.

In the code above, we set x to equal the result of (x%320) which ensures that our x will infinitely wrap around the screen as we scroll. I have also set the y position to 224, so it will look like our sprite is running across the floor of our program. Build and run, and you'll see a strange error:

NyUdYpT.png


Our sprite moves, but it leaves a long trail behind him. This is because every frame we set certain pixels in the frame buffer to new colors, but never change the old position colors back to black. We are essentially never refreshing the screen, just drawing on top of the old one. To fix this, we'll need to draw some more pixels to hide our previous work.

Let's start by defining another 16x16 tile beside our sprite data:

Code:
//An 8x16 array of 8-bit unsigned integers containing a 16x16 blue tile
uint8 BGTile[] = {
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66
};

This tile is still 8-bits per 2 pixels, but each pixel is set to color 6 in our palette, which is a blueish color. If we were to draw this tile all over frame before each time we draw the sprite, we would effectively change the entire frame to sky blue each update.

Let's create a function to do just that:

Code:
//Draws a 16x16 sprite pattern with the top-corner location being (x,y)
//Passed a pointer to an array of data to draw from
int ClearScreen()
{
	int i; 				//our down offset
	int j;				//our right offset
	for(i = 0; i < 240; i+=16)
	{
		for(j = 0; j < 320; j+=16)
		{
			DrawSprite(j, i, 1, BGTile);
		}
	}
	
	return 1;
}

ClearScreen takes no parameters, and uses a nested for-loop that runs the entire width and height of the screen. However, instead of incrementing i and j by 1 each iteration, this time i and j jump by values of 16. This is because our background tile is 16x16 big, we don't need to draw it for every single pixel horizontally and vertically, only every 16th pixel horizontally and vertically. Instead of passing DrawSprite our SpriteData, we instead pass it the BGTile.

Let's also go ahead and define a second frame of animation for our sprite:

Code:
//An 8x16 array of 8-bit unsigned integers containing a single sprite frame
uint8 SpriteFrame1[] = { 
0x00, 0x00, 0x01, 0x12, 0x22, 0x00, 0x00, 0x00,
0x00, 0x00, 0x13, 0x34, 0x44, 0x20, 0x00, 0x00,
0x00, 0x01, 0x33, 0x44, 0x44, 0x42, 0x00, 0x00,
0x00, 0x01, 0x34, 0x44, 0x44, 0x42, 0x10, 0x00,
0x00, 0x02, 0x53, 0x44, 0x44, 0x41, 0x31, 0x00,
0x60, 0x02, 0x55, 0x34, 0x44, 0x20, 0x24, 0x10, 
0x60, 0x13, 0x44, 0x33, 0x42, 0x11, 0x32, 0x31, 
0x06, 0x23, 0x24, 0x42, 0x11, 0x12, 0x32, 0x24, 
0x06, 0x32, 0x34, 0x21, 0x74, 0x41, 0x12, 0x32,
0x00, 0x23, 0x21, 0x12, 0x24, 0x40, 0x01, 0x32,
0x00, 0x21, 0x12, 0x22, 0x34, 0x32, 0x00, 0x10,
0x00, 0x42, 0x23, 0x45, 0x44, 0x22, 0x00, 0x00,
0x00, 0x48, 0x52, 0x25, 0x32, 0x58, 0x00, 0x00,
0x00, 0x01, 0x88, 0x58, 0x55, 0x88, 0x00, 0x00, 
0x00, 0x00, 0x22, 0x31, 0x32, 0x10, 0x00, 0x00, 
0x00, 0x01, 0x24, 0x24, 0x22, 0x10, 0x00, 0x00 };

//An 8x16 array of 8-bit unsigned integers containing a single sprite frame
uint8 SpriteFrame2[] = {
0x00, 0x00, 0x01, 0x12, 0x22, 0x00, 0x00, 0x00,
0x00, 0x00, 0x13, 0x34, 0x44, 0x20, 0x00, 0x00,
0x00, 0x01, 0x33, 0x44, 0x44, 0x42, 0x00, 0x00,
0x00, 0x01, 0x34, 0x44, 0x44, 0x42, 0x10, 0x00,
0x00, 0x02, 0x53, 0x44, 0x44, 0x41, 0x31, 0x00,
0x00, 0x02, 0x55, 0x34, 0x44, 0x20, 0x21, 0x00,
0x00, 0x13, 0x44, 0x33, 0x42, 0x11, 0x32, 0x20,
0x00, 0x23, 0x24, 0x42, 0x11, 0x12, 0x32, 0x40,
0x60, 0x32, 0x34, 0x21, 0x44, 0x21, 0x12, 0x30,
0x06, 0x23, 0x21, 0x14, 0x44, 0x40, 0x11, 0x70,
0x00, 0x74, 0x54, 0x42, 0x22, 0x28, 0x00, 0x10,
0x00, 0x07, 0x52, 0x28, 0x82, 0x28, 0x00, 0x00,
0x00, 0x01, 0x88, 0x58, 0x57, 0x81, 0x00, 0x00,
0x00, 0x14, 0x24, 0x22, 0x21, 0x23, 0x10, 0x00,
0x00, 0x00, 0x32, 0x32, 0x12, 0x31, 0x00, 0x00
};

SpriteData has been changed to SpriteFrame1, and there is a new frame called SpriteFrame2. These hold different frames of animation to draw from.

Inside our Main Loop, let's make a few changes:

Code:
 //Main Loop
 while(!quit) 
 {
	 	 
	uint64 start = timer_us_gettime64(); 		//get the start time of this operation in micro seconds
	x++;										//increment x each update
	x = (x%320);								//wrap x around 320 using modulus division
	
	ClearScreen();								//clear the screen by drawing BGTile all over the frame
	
	if(x%2==0)									//if this is an even frame
	{
			DrawSprite(x, y, -1, SpriteFrame1);	//draw sprite 1
	} else										//else this is an odd frame
	{
			DrawSprite(x, y, -1, SpriteFrame2);	//draw sprite 2
	}
	uint64 end = timer_us_gettime64();			//get end time of this operation
	float fps = 1000000/((float)(end-start));	//calculate the FPS
	
	printf ("Time to draw screen:  %u us\n",(int)(end-start));		//output to console time it took to draw in microseconds
	printf ("FPS:  %f\n",fps);										//output to console fps
 }

The first major change we see is that at the beginning of the Main Loop, we allocate an integer called start. This isn't a normal 32-bit integer, it's an unsigned 64-bit integer. It's 64-bit long, instead of 32-bit long, so we can express greater numbers. You see that we set it to the result of a function called timer_us_gettime64(). KOS defines timer_us_gettime64. It returns the result of the Dreamcast's realtime clock in microseconds, as a 64-bit integer. This will let us know in microseconds the precise time the loop started.

Next, after binding x to the width of the screen using Modulo division, we call the function ClearScreen. This will draw a cyan pixel over every pixel on the screen, effectively removing our last drawn frame.

Next, we do a new conditional if branch that checks the result of x%2. The result of x%2 will tell us if x is either even or odd. If x%2 == 0, then x is even, otherwise x is odd. On even frames, we pass DrawSprite the array SpriteFrame1, and on odd frames, we pass DrawSprite the array SpriteFrame2.

Finally, once all this is done, we capture another microsecond into a uint64 called end. We then calculate the frames per second into a floating point number called fps, that is the result of (1000000/ end &#8211; start). This will yield the frames per second on average given the time it took to draw the frame. We end our main loop by using printf to output some text to the Dreamcast's console. The first time, we output &#8220;Time to draw screen&#8221; and then an unsigned number, followed by a new line return. In our printf function, we pass (end-start) as the unsigned number printed. Second, we print &#8220;FPS: &#8220; followed by a floating point number, then pass fps as our float to output.

Note that we can only see the Dreamcast's console output via emulation. On a real Dreamcast, we have no console to output to unless you are using a devkit.

Build this current program and you'll see the following:

jJEo0Ef.png


This is, realistically, just about the limits of what we can do with software rendering. If you look on the console output to the left, we aren't even hitting 60 fps with this demo. There are ways we could speed up our drawing &#8211; we could use a form of John Carmack's Adaptive Tile Refresh to limit the amount of pixels we are updating to the screen at once. But that would ultimately be putting lipstick on a pig. In truth, if you were to mess with the y position of the sprite we are drawing, you'd actually see the tear line on the screen as it draws. The mighty 128-bit* Dreamcast (*marketing fluff) is having trouble drawing a single tiny sprite to a blue screen?!

As I said before starting this chapter, this is intentionally the wrong way to approach Dreamcast programming. We need to hit the hardware in a specific way to make it fly. The Dreamcast is capable of way more than this. Ultimately, it's the slow bus and slow CPU speed of the Dreamcast that makes this style of software rendering unfeasible for realistic development. However, on a modern system, this type of rendering is totally viable. If you want to apply this style of drawing to a modern PC, you should have more than enough behind your CPU to create an entire 2D game this way.

Learning how to do things the wrong way winds up being beneficial because we learn the fundamentals of drawing to the screen. In the next lesson, we'll begin using the Dreamcast hardware to start really drawing to the screen with speed.
 

Krejlooc

Banned
Awesome.

This "wrong way" of doing things on DC is actually how things are done in PC, right?

Not necessarily. It's one way you can do things, but not how I would do things. SDL2 offers hardware acceleration for 2D drawing, so you don't have to use software rendering to do 2D drawing.
 

Krejlooc

Banned
I need a title theme and a game-over theme composed for this game. Anybody have any musical chops think they could string together a 20-30 second long title jingle, or a 20-30 second long game over jingle? I already have some music for the normal gameplay.
 
This is pretty interesting. I've been wondering what retro console would be the best target platform for a "permanent" 2D game binary...
 

Krejlooc

Banned
Download links for files and source codes have been added to the opening post. The lesson 3 examples include the source codes and built cdi images that can be burned or run in NullDC.
 
I'm not sure what you mean by a permanent 2D game binary?
There's probably a better term for it, but a target hardware platform (for 2D games specifically) with high likelihood of being faithfully emulated more or less forever, that's also supported by modern (2016) compilers.

vs say, a linux x86 binary that will be broken by the next Ubuntu LTS, and probably nonfunctional for the systems that run in 20 years.

(insert existential software dev thoughts here)
 

Krejlooc

Banned
There's probably a better term for it, but a target hardware platform (for 2D games specifically) with high likelihood of being faithfully emulated more or less forever, that's also supported by modern (2016) compilers.

vs say, a linux x86 binary that will be broken by the next Ubuntu LTS, and probably nonfunctional for the systems that run in 20 years.

(insert existential software dev thoughts here)

Well, for what it's worth, if you avoid using libraries that constantly evolve without regard for backwards compatibility, you don't really need to worry about application obsolescence. I have some SDL 1.x applications I wrote over a decade ago that I can still run, both in modern windows and in linux.

Also, while the Dreamcast is an non-evolving deployment platform, as a development platform it still is changing. Just 7 months ago, KOS underwent a pretty substantial change, which even reflected in needing this guide to be revised. It's feasible that old dreamcast source codes won't build today in some circumstances.
 

Krejlooc

Banned
The next chapter isn't done (and probably won't be done for at least a week or so) but in the mean time, I'd suggest reading this book: http://bizzley.com/

It's free. It's about the development history of the ZX Spectrum port of R-Type. Fascinating read if you want a peak into what the early days of Contract Game Development was like. Really interesting stuff.
 
Top Bottom