476
Francisco Woodland & Jeff Yuen
ECE 476, Cornell University
April 28, 2003


Code organization

The project code is mostly done in optimized C with some assembly mainly for video generation and other time-critical procedures like line decompression.

Video signal

The video signal is produced on the odd lines 30 and 222, using the Timer1 Compare A interrupt for the correct timing. On the even lines between 30 and 222, the assembly code in the Timer1 Compare A interrupt prepares a buffer for the next odd line to read out of. During these lines, the video output is kept at black level. The preparation involves determing what combination of images go on the next line and the process of fetching them from memory.

Input polling

To read data from the NES controller, there is a function called readNintendo(). This function polls the device by sending it Latch and Pulse signals, which tell the controller to send its state in a serial fashion, in sync with the pulses we send it. We space out these pulses using an assembly macro called buttondelay, which inserts 12 cycles worth of nops. By trial and error, we found that using delays lower than 12 cycles results in inconsistent data being read from the NES controllers. The controller is polled every frame, and the state of each controller is kept in button_input[0-3] (1 variable for each connected controller). In this variable, each bit corresponds to one button on the controller (active-low).

Game states

There are 3 states that the game goes through:

STATE_STARTUP:

The purpose of this screen is to allow the user to select the number of players (2-4). There is also a large graphic that is displayed at this screen, which shows the capabilities of our gray-scale display. Once player 1 presses start, the game will begin, and the game will progress to the SCOREBOARD state.

STATE_SCOREBOARD:

The game goes through this state to show the current score to the players before/after every round of play. This screen also has a large graphic which uses up a lot of the computation time. This state can only be left after all active players indicate that they are ready by pressing the START button on their controller. The next state is usually the PLAY state, unless one of the players just reached the limit of 5 wins to end the game, in which case the next state is the STARTUP state.

STATE_PLAY:

This state is where all of the work is done in the game, since this is where the action happens. There are many stages that this state goes through in a frame to run the game code, and these are all described below.


Erase old sprites

We want to erase the ship or bullet graphic before we move it or change it in some way, so this gives us a clean slate to work with.

Alter ship attributes

This is where we change the ships velocity or ships rotational orientation based on user input (which we poll every frame). Both of these operations are kind of tricky. The ship has a possible 16 different angles that it can be pointing at, and depending on the angle, the velocity X and Y components should change at different rates. The different angles are accomplished by having 16 different graphics of the ship, 1 facing each angle. The changing velocity accurately is quite complicated. First of all, we have a hardcoded sin/cos table so we can compute the X and Y components that a given thrust should add to the current velocity. Also, the velocity number is in 1/256ths of a pixel, since the velocity resolution at 1 pixel, 2 pixels, etc. is just not acceptable when dealing with weird angles. This stage is also responsible for animating the explosion graphic when a ship dies.

Move ships

This is where the ship's X and Y coordinates change based on their current velocities. One tricky part about this is that the ships have a (X,Y) coordinate, and then they have a (X,Y) fraction. As mentioned earlier, having the velocity quantized at only 1 pixel, 2 pixels, etc, is not good enough to look reasonable. This is where the fractions come in. They measure how close the ship is to an adjacent pixel. Each component goes from -128 to 127. For example, if the X fraction is 0 and the Y fraction is 127, this means the the actual logical position of the ship it right in the middle of the pixel horizontally, and right at the bottom of the pixel vertically.

Produce bullets

This stage is responisble for firing bullets if the user presses the A button. The bullets take on the velocity of the ship, plus an added thrust component based on the direction the ship is facing. Each bullet also has a lifetime, after which they just fizzle out. A ship can only fire one bullet at a time (with the small playing area we have, more than 1 bullet would be chaos).

Move bullets

Very similar to the move ships stage, this does the same thing for bullets.

Collision detection

This part was a little tricky because of the fact that the bullets and ships wraparound the screen. Basically, the idea of this stage is that if any enemy bullet is overlapping one of the ships, then that ship should explode.

Draw ships and bullets

This is where all of the ship graphics are applied to our canvas structure. These functions are very complicated, so we won't go into much detail here. The code is well-commented, and so this part of our program would be much easier to understand while looking at the code. Drawing the bullet is just turning on one pixel in the canvas, which is still kind of complicated, but much less complicated than handling the ship's 8x8 graphic.

Endgame detection

The last stage of the game code, it checks to see that there is more than one ship alive. If there isn't, then the game must be over, and it advances to the SCOREBOARD state, giving points to the winning player (or no points in case of a draw).

April 28, 2003 | Francisco Woodland & Jeff Yuen.
free to use for academic purposes.