A two-player spaceship dueling game featuring multiple unique ships, sophisticated physics-gaming engine, and a zooming battlefield camera.
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.
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.
The display that we used was a black and white television, which follows the RS170 and NTSC video standards. RS170 specifies the voltage levels required to generate white, black, and sync signals as well as synchronization pulses in the horizontal and vertical direction. It specifies three different voltage levels (sync, black, and white) along with two sync pulses for horizontal and vertical alignment. The NTSC standard specifies that we need 525 lines per frame, 30 frames per second, with odd lines rendered separately from the even ones (interlacing). Instead of interlacing, we just repeated lines twice as done in lab, which cuts down on processing time but halves the potential resolution.
We based the design of our game on various classical spaceship fighter games, including Star Control II which has been publicly release under the GNU license. We give full credit to the Pam Levins and his programming team, as well as all contributors in the 3DO and sourceforge versions of the game.
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.
The details of the structure and variable organization can be found within our code, which we have commented heavily. In particular, game.c, flash.c, and ships.h should be referred to.
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.
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 screen buffer array from which to load, based on lineCount as well as an xOffset defined by the left edge/coordinate of the bounding box. To achieve the 2x zoom factor, each bit in the line is outputted twice, and each line of our array is outputted four times (compared to the normal two). The resulting effect is a dynamic and flexible zoom feature, which allows us to easily switch between the two modes without much computation, artifacts or flickering.
Given ship center coordinates, orientation and type, compute the top left corner for the ship mapping.
Erase the old ship image from screen buffer.
Acquire the 7x7 ship bitmap correspoinding to the given orientation and ship type line-by-line from flash.
For each line, based on the 8-bit alignment offset, rotate the 7-bit line element.
If the ship map spans two elements of the screen buffer, we map the appropriate parts of the 7-pixel image to the corresponding element.
Repeat the above for each of the 7 lines of the ship image.
The ship display code is based on Prof Land's ECE 476 TV Oscilliscope code. Further details of the display ships components can be viewed by studying the drawShip, eraseShip and renderShip functions in display.c.
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.
Powerups for ships and weapons.
Overall, we were very satisfied and proud of our work in this production. We feel that the original planning from early on really made development of this project fun and rewarding.
"...to accept responsibility in making engineering decisions consistent with the safety, health and welfare of the public..." We accounted for repetitive stress injuries arising from prolonged button pressing, and epileptic seizures arising from onscreen flashes at a certain frequency. As a result, we used a gaming device (Genesis Controller) that has been tested by millions of game players around the world. Our project did not have any flashing, so that latter issue is not a concern.
"...to reject bribery in all its forms..."We did not expect and have not received any form of monetary donations or gifts from anyone in the development of this game.
"...to maintain and improve our technical competence ..." Both of us learned a lot about the C, assembly programming for microcontrollers. We learned how to handle and debug (very important) timing problems that arose during the course of development.
"...to treat fairly all persons regardless of such factors as race, religion, gender, disability, age, or national origin..." This game does not discriminate against any persons in the development of this project. Both Allen and Joran accepted the input of all people in developing this game, as well as freely gave our input and ideas to fellow colleagues on their projects.
"...to avoid injuring others, their property, reputation, or employment by false or malicious action..." During the development of this project, we were careful in maintaining safety precautions in and around our lab bench. We made sure systems (ie. TV) were off when not in use, that extraneous wires were either removed, or tucked away, so that our game is safe for us, and others.
"...to be honest and realistic in stating claims or estimates based on available data..." We attest to the accuracy and completeness of all information provided on this website and during the demo.
The overall cost of our project is $13.90.
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.