|ECE 476 Final Project
1.1 Project Summary
A two-player spaceship dueling game featuring multiple unique ships, sophisticated physics-gaming engine, and a zooming battlefield camera.1. Introduction
Our project is a space dogfighting video game where two players attempt to destroy each other using a variety of ships and weapons. The battlefield is a 128x100 pixel area on a TV, and a small planet resides in the middle, exerting gravitational forces on the ships and inviting them to certain death. The input devices are Sega Genesis controllers, which were chosen because of their signal simplicity and sleek look (which makes Nintendo/SNES controllers look dorky in comparison).
We initially had some trouble thinking of an interesting project, and discarded several options that would have taken too much time or money. Since we are both avid game players, we finally settled on the idea of making a video game, since this would be something we would enjoy working on for 4 weeks. Next, we had to agree on which type of game to make; I proposed a 2-D dogfighting game because it is a time-honored but dying genre, as nearly all dogfighting games now are 3-D and aim for realism and complexity rather than straightforward fun. Thus, we aimed to create a game that is simple to pick up and learn, yet entertaining and challenging to master.
For our inspiration, we drew upon a variety of old classics in the genre; Joran has had significant experience in playing the mutiplayer online shooter Subspace, and I have recently introduced him to one of my all-time favorites in the genre, Star Control II. Both games featured the kind of top-down shooting action we were looking for, although SCII's melee mode was closer to what we envisioned our final project to be. In addition, we saw that last year's ECE 476 projects featured a 3-player ship dueling game; however, its emphasis of that game was mainly on the impressive greyscale graphics scheme, and the actual gameplay elements were very simple. We felt that we could take a step in the other direction, and create a game with deeper gameplay, but less impressive graphics. Our goals were to implement several unique playable ships that had different handling characteristics and weapons, a planet that exerted gravitational forces upon the playing field, and a nifty zoom feature seen in SCII which would zoom in on the dueling ships if they were in close proximity. The end result was StarDuel.
The game consists of a state machine with 3 states: Title, Menu, and Game. The Title state simply shows a splash screen featuring our game name and logo, and prompts a user to press a button. The Menu state is where the players select their ship type, and the Game state is where all the action takes place. Below is a flowchart of the game setup logic:
In the Game state, time is either spent updating the graphics display or taking in user input and calculating what to display next. We borrowed some of the display framework code from Lab 4, and modified it for our use. The processor time is largely used to output the TV display lines 30-231, and afterwards we are free to use to remaining time for computations. Here, we would execute 7 major tasks, each possibly split into several minor tasks. The tasks were:
1. Poll for player inputs
Below is a flowchart of the gameplay logic.
Due to computational time constraints, we were forced to use an alternating scheme where all of Player 1's tasks would be executed in a frame, then Player 2's tasks would be executed in the next frame, and so on. The gameplay is still quite smooth though, and the difference from executing everything once per frame is not noticeable.
In the early design process, we considered several Atmel chips for the project. From our experiences with ATMega32 in Lab 4, we noticed the small amount of SRAM available. As a result, we spent some time studying other chip possibilities, such as ATMega162, which supports external memory. However, from discussion with various members of the course staff, we found out that external memory is not supported on STK500. We would have to switch to STK200, along with changing of clock crystals, etc. In summary, it would have taken at least a week just to get this hardware working. In the end, we decide to forgo the external memory support, and use the week for more software development. We saw the limitation of the SRAM as a challenge and motivation for better and more efficent coding.
On the software aspect, based on our past experiences, we decided to break up the code, so that the code would actually be maintainable. Breaking up the code into modules will also allow concurrent development by both game creators, and reduce the time spent to merge code together. Furthermore, we also decided to try to program as much in C, and only program in assembly for video and time critical sections. This decision was based on the fact that C code is easier to read and easier to debug. We rather spend more time implementing features, instead of taking on the job of a compiler as well.
2.4 Relevant Standards and Copyrights
We implemented three types of ships, 3 primary weapons, and 3 secondary weapons/specialities. Each player's ship is represented by a structure containing many variables, and at upon entering the gameplay state, we initialize most of the variables based on what ship type was chosen. Variables include ship x/y position, velocity, direction, ship type, weapon fire delays, and life remaining. In addition the the single variables, we also maintain arrays containing information about the ship's bullets (at most 4 onscreen at a time for each ship) and explosions caused by these bullets. The index (0-3) represents the bullet, and the arrays contain information such as x/y position, direction, time left till self detonation, whether it is active or not, etc. The explosions have a similar setup within the structure as well.
Aside from these constantly changing variables, we maintain flash tables that store static information about the various ship and bullet characteristics such as turning rate, damage, max life, max velocities, collision radius, etc. Simply by knowing the ship or weapon type, a function could quickly look up the value of a certain characteristic from these tables. Of particular note is that the x and y accelerations for the ships (corresponding to each direction) have also been hardcoded into flash, since we didn't want to do any intensive sine/cosine calculations. Bullet x/y velocities have been hardcoded similarly (we viewed them as massless, and thus didn't account for their acceleration).
The bitmaps for all objects are stored in flash. This includes the planet, explosion animations, and ships/bullets. Our ships and bullets only can face in 8 directions (cardinal and sub-cardinal), so we created bitmaps for each direction.
We created separate functions named acquireCommandsP1() and acquireCommandsP2() to poll for inputs from each player. The input devices we used were Sega Genesis controllers, which we hooked up to the interface ports of the STK-500. Each button corresponds to a certain pin, and they are active-low. In the acquireCommands() functions, we examine the activity of each pin, and take appropriate action depending on which button was pressed. The up button set the acceleration flag (which indicates to the updateShip() function that the ship's characteristic acceleration is to be added to the velocity), the left button increments the direction (a number ranging from 0 to 255 representing the angle from north), and the right button decrements the direction variable. If the B button (weapon 1 fire) is pressed, we first check if the ship has less than 4 active bullets at the moment, and if so we parse through the bullet activity array to find the index of an inactive bullet, make it active, and initialize the corresponding elements in the position/direction/weapon type/lifetime arrays based on the bullet type. A press of button C does something similar, except this is for the ship's secondary weapon. Secondary weapons may or may not be projectiles; for two of the ships, they are actually special abilities that don't fire anything (afterburner and self-repair), so a check for these types of "weapons" is done first, and the corresponding action is taken (acceleration doubled temporarily or life incremented by fixed amount).
We created a function updateShips() that operates on a ship structure, and updates its position based on current variable values. This function is basically our physics engine, and incorporates the effect of the planetary gravity field into ship movement. We first update the x/y velocity by augmenting them by the corresponding x/y acceleration (hardcoded in flash) as well as the corresponding x/y gravitational force at the ship's position (also hardcoded in flash). Of note is that each ship is also limited by a unique maximum velocity in the x and y directions, and this condition is enforced after taking the acceleration and gravity into account.
We determined the values of the acceleration for each angle outside of the program by first deciding on a fixed acceleration value, and then multiplying this by the cosine or sine of the current angle (which can only be a multiple of 0, 45, or 90 degrees). We truncated the decimal portion, since our accelerations were represented by signed integers.
The gravity field was also generated outside of the program.
Since the field is quite large (encompassing a 40x40 area around the
planet), we had to use a C script gravity.c to
generate the flash code for it. We determined the x and y forces at
a particular position by subtracting its x and y coordinates with the
planet's coordinates to find the distances relative to the planet.
Then, we use the distance formula:
Note that gravitational force falls off by the square of the distance, which is why we didn't bother taking a square root to find the actual distance. Finally, we determine the correct signs of the forces by noting that it should be the opposite of the distance's sign, i.e. if you are a positive x-distance away from the planet, the force should be pulling you back towards the planet (negative force).
Next, we determine how many pixels were moved in the x and y directions. Here we note that the velocity is not in terms of pixels per time unit, since that degree of granularity is too coarse for such a small playing field (128x100). Thus, we used an idea that last year's Grayscale Ships implemented: fractional distance. We can consider each pixel as being a 256x256 area, and thus each ship has a whole pixel position as well as a fractional pixel position. Velocities can thus be in terms of fractional distance per time, allowing us a finer degree of granularity. We first find how many whole pixels were moved, and then augment/decrement the fractional position by the fractional velocity and determine is this caused an additional unit of pixel movement (based on whether a boundary was crossed). The way this was done is somewhat confusing, and a look at the physics.c code may help clarify things.
There are many bullet-related arrays of length 4 (the max number of bullets a ship can have out at a time) within the ship structure, and one of these, bulletActive, tells us whether a certain bullet has expired (either collided or died out after a specified amount of time) and is free to be replaced by a new bullet. During each player's turn, we update the values in the bullet x/y position, lifetime, and type by first parsing through the activity array and checking to see if a certain index is active. If so, we use a modified version of our updateShip() code to determine the new position, given that bullets have no acceleration and fixed velocities specified in the flash tables. Then we decrement the entry's lifetime remaining in the appropriate array, and if the lifetime becomes 0, we set it's activity flag low. Of note is before we calculate the new positions, we first check if the bullet type is one of the homing projectiles. If so, then we enter a special homing() routine that will alter the direction of the projectile, which may result in a different position than a normal straight-line projectile. The homing() routine makes the bullet increment/decrement its direction variable depending on the bullet's relative position from the opponent, and the bullet's current direction. The way this is implemented can be better understood with a look at our physics code, which we have commented heavily.
These three tasks are interrelated, and are thus executed together within a function hitOpponent() that takes in the current player's ship and the opponent ship as arguments during a player's turn. We first defined a function distanceSQ, which takes in two sets of x-y coordinates and determines the distance between them. Next, we detect if the current ship has collided with the planet if the distance squared between their positions is less than the ship's radius plus the planet radius, squared. If so, the ship's life goes to zero. Next, we parse through the current ship's bullet arrays and find active bullets. If these bullets' positions are within the radius of the opponent's ship, we decrement the opponent's life by the bullet's damage value (found within the flash tables), and place an explosion at that position. Explosions are handled similar to bullets - in fact, they are highly correlated since each bullet can produce a corresponding explosion. Like bullets, explosions have arrays detailing their active state, lifetime, and x/y position.
Finally, we determine if the game is over if either ship's life is zero. In that case, we zoom onto the dead ship (which should be exploding), and declare the winner of the round. Then, we move back to the menu state in preparation for another round.
After all computations are completed and values updated, we can determine what to draw on the screen.
As an essential and fun aspect of gameplay, our graphics engine has a built-in
zooming system, which automatically provides close-up perspectives during battle
sequences. The zoom feature enlarges both the x and y directions by a factor of 2x,
thus, magnifies a quadrant of our battlefield. The zoom is triggered when
the ships close enough to each other, such that both ships can comfortably
fit within a bounding quadrant box. Upon detection of this proximity, we determine
to coordinates of the zoom quadrant/box while also ensuring that the box does not
fall off the edge of the battlefield. The actual zooming is performed by a
separate rastor output assembly code. Similar to the normal video output
code, the zoom code determines the indices of the
The most important role of the graphics engine is the capability to
draw and redraw ships without any flickering or video artifacts. Each
ship is a 7x7 bitmap, and requires pixel accuracy in its position
(for compatibility with physics engine). As a result, given the center
coordinates and orientation of a ship, the drawing code will have to erase
the previously drawn ship image, and replace it with an updated version of
the ship. The pixel accuracy requirement further complicates matters by
preventing us to align the render of our bitmaps to every 8 pixels
Similar to displaying ships, drawing of bullets and explosions require very similar steps. Bullet and explosion coordinates are provided by the physics engine, from which we have to erase the old graphics, and remap the new bitmaps to the screen buffer. The only new complexity is the rendering of explosions on top of ships. Since we only have black-and-white, drawing both the ship and an explosion together would result a big blob of indistinguishable white pixels. In order to allow people to visually distinguish and explosion apart from a ship, we actually perform explosion and bullet rendering on separate frames, such that for one frame, you see the explosion, while on the other, you see the ship. This technique allows the game player to visually distinguish these separate, overlapping components.
Since our project was a video game, we didn't have much hardware. We used two Genesis controllers as input devices, which feature DB9 connections. We bought some DB9 connectors at the Clark Hall stockroom, soldered wires onto the connectors, and attached them to the breadboard to make easy-to-use plug-in ports for our controllers.
Our other main piece of hardware was our video DAC, which we had previously used in the Digital Oscilloscope lab for ECE 476. A schematic of this and the controller hookups can be found in Appendix B.
We achieve all primary and most of our secondary objectives of this project, which was to create a two-player spaceship shooting game that features multiple unique ships, a sophisticated physics engine, and a scaling battlefield camera. We have both ships moving fluidly in reponse to user commands, three unqiue ship models with a variety of weapons, advanced physics engine which incorporates acceleration, velocity, and ship collisions, scaling battle field camera. Among our secondary goals which were accomplished were the planet which exerted a gravity field, as well as ending explosion sequence for death scenes. There were no flickering or artifacts issues, as the video timings were always accurate, since we divided up the drawing, physical simulations and collision detection sequences into separate frame. Our video game can be played on any TV which supports composite video input.
We achieved most of our goals through careful planning and design of the game. From the code architecture to the various physical equations used for simulation, our planning-before-coding saved us a lot of time, both in development and debugging. We decided to use Genesis controllers after sampling several options from old gaming systems, and were very statisfied with our choice. On the software aspect, our sepearation of functional units into separate modules allowed for concurrent development of both the physics engine and display code. Our early definition of the interface functions was a primary reason why our project was completed on time. We reached nearly the capacity of our AMega32, both in SRAM, flash and data capacity, which was a limiting factor to additional features. Given more development time, we may have been able to optimize our code further, and perhaps convert more sections of it into assembly for higher performance. Several out-of-the-world objectives were removed due to other limitations. For example, our dream of having moving asteroids would have cluttered up the screen and taken away some gameplaying enjoyment. The planet is a big death machine already, considering its small size.
Additional Features that we would have liked to include in the game:
In this project,we abide by the IEEE Code of Ethics:
Appendix A: Code Files:
Appendix B: Schematics
Figure 1. Video DAC
Figure 3. Global Setup
Appendix C: Parts and Costs
Our major hardware components included:
Appendix D: Labor Division
Allen wrote and debugged the physics engine, collision detection and weapons system, as well as created all the bitmaps for ships, bullets and explosions. Joran worked on developing the backend display functions, and the menu selection system. Both of us took part in the design of the code architecture, and other high-level decisions.
Ideas from: The Ur-Quan Masters - Star Control II Video Game
Technical Reference from: TV Oscilloscope Basis Code from ECE 476 SP 2004 Lab 4 - by Prof. B. Land
Technical Reference from: ATMega32 Full Data Sheet
Techical Reference from: Cantneroid from SP 2003 by C. Moschner & M. Cantlon