| || |
For simplicity's sake there is only one ship in my coursework submission; a square with one gun in each corner. You can touch and drag one of the gun types onto a quadrant of the ship and assign that weapon type, giving complete customisation to the player. The left analog stick aims the reticle and the ship will begin firing automatically in that direction (and moving in the opposite). You can shoot in the safety of the weapon selection screen, letting you try out the different weapons and combinations and see how the ship controls using them.
There are three weapon types in the game. The machine gun has a very high rate of fire which gives good motion control, thereby making the ship quite agile but at the cost of firepower (machine gun bullets are pretty weak individually). Cannons are the opposite of machine guns: very low rate of fire with each projectile throwing the ship considerably, but huge damage output (there were plans to make them deal splash damage to nearby enemies too). The rocket launcher is somewhere between the two with a medium rate of fire and damage output, the upside being that the rockets will track towards the nearest enemy. These weapons were all designed to play on the trade-off between damage output and mobility. There was also a fourth weapon type which was cut due to time constraints: the laser. The laser was planned to deal massive amounts of damage per second but without moving the ship at all. If they wanted to a player could choose to have four lasers on their ship, making them completely immobile but also nigh unreachable by the game's enemies.
Speaking of enemies, there are four types in the game. Seekers are small, quick kamikaze units that head straight for the player. The Oblivious will continually move in a certain direction, regardless of their surroundings. Mine Layers are big, slow bullet sponges which periodically lay stationary Mines (the fourth enemy type) in the world. In the screenshots Seekers are purple, Oblivious are green, Mine Layers are brown and Mines are red.
Killing an enemy (other than a Mine) will increase the score multiplier. Taking damage resets the multiplier back to one. The goal is just to get the highest score possible before you inevitably die and blow up, which makes keeping the multiplier alive key to being good at the game.
Below you can see a simplified UML class diagram representing the overall structure of the program and relationships between the classes. Note that only the most important members and functions for each class were included, for simplicity's sake. The classes belonging to the abfw namespace are from the basic framework one of my lecturers created which we all worked from.
How Stuff Works
The only objects which can possibly collide in the game are the ship and the enemies, and the ship’s projectiles and the enemies. As there is only one instance of the ship it is dealt with separately. The SpacialPartitioner class also defines the Cell structure which has a list of pointers to enemies and a list of pointers to projectiles (as shown in the UML diagram). Storing pointers reduces the memory overhead of the data structure, while lists were chosen as the container due to the relatively quick and inexpensive addition and removal of elements. The main data structure in the SpacialPartitioner class is a 2D array of type Cell spanning the game world.
The size of the game world in this application is 2880 pixels by 1632 pixels. This world is then divided into cells of size 32 pixels by 32. Since the largest objects in the game are the MineLayer instances at 20 pixels by 20, objects can span no more than two cells in either direction and the collision detection is therefore fairly straightforward. Decreasing the size of the cells would increase the amount of overhead associated with maintaining the data structure, but would result in more efficient collision detection thanks to smaller possible collision spaces. Increasing the size of the cells would decrease the overhead of maintaining the data structure and result in less empty cells, but would make the collision detection code work harder as there would be more objects to check across a larger area. The cell size of 32 pixels by 32 was chosen as it divides evenly into the world size and is a good compromise between the efficiency of maintaining the data structure and the efficiency of the collision detection.
Whenever a projectile or enemy is created it is added to the data structure and when they die they are removed. With every iteration of the game loop, after all of the objects' velocities and positions are updated, the SpacialPartitioner class is updated. It iterates through every element in every cell, checking the position to ensure that they are still in the correct cell (and moving them if they’re not). When all of the instances are in the correct cells, the collision detection routines can be run. The CheckProjectileEnemyCollisions function iterates through each enemy of each cell, checking for collisions with each of the projectiles in that cell and the surrounding ones. The CheckPlayerEnemyCollisions function first works out which cell the ship would reside in, then checks for collisions with each of the enemies in that cell and the surrounding ones. Something similar happens in the GetNearestEnemy function used by the Rocket class to acquire a target. As there is only ever one ship and a handful of rockets at a time, it didn't make sense to have empty containers for them in so many cells.
- Size (in pixels) - particles are squares of side-length size.
- Life (in frames) - particles are given an initial amount of health and lose one per frame. When a particle’s health reaches zero it is removed.
- Speed and angle of emission.
- Red, green, blue and alpha colour components.
- Emission delay (in frames) - the amount of time which must pass between new emissions.
- The maximum number of simultaneous live particles (when set to zero there is no limit).
- The total number of particles to emit (when set to zero it just keeps going).
- A boolean value representing whether to emit all of the particles at once (such as for an explosion) or whether to emit them over time using the emission delay (such as for a flame).
All of the values are validated to ensure that the minimum value of a property is less than or equal to the maximum value. The colour components are validated to be between 0 and 255 inclusive. There are also some hard-coded minimums such as a particle’s side-length being at least 1 pixel long. The speed and angle of emission of a particle can be positive or negative values.
Rather than run actual line-of-sight checks, the ActivateGuns function utilises the fact that the ship is a perfect square with one gun in each corner. In the diagram the ship is the black square, the reticle is the green square, activated guns are orange and deactivated guns are red. The blue lines show the straight-line distance between each gun and the reticle, while the purple lines show which direction the guns would fire. As can be seen, the guns which are the shortest straight-line distance to the reticle are the ones with line-of-sight. If the reticle is directly above, below or to the sides of the ship then only the two closest guns have line-of-sight. But if the reticle is in a corner of the ship then the three closest guns have line-of-sight. So the use of the reticle is twofold: it's relative position against the ship determines which guns are active, whilst the relative angle determines which direction those active guns fire.
To determine which of the edges of object 1 (in blue) is closest to object 2 (in green) it uses another of the PhysEng2D class’ functions: Angle. This function will return the angle between a line drawn from the centre of object 1 to the centre of object 2 and the positive horizontal, labelled α in the diagram. The value of α will lie in one of the four quadrants A, B, C or D relative to object 1 and this determines which side object 2 is closest to. In the example diagram the value of α would lie in quadrant A and so object 2 must be closest to the right-hand side of object 1 so could be moved appropriately. Note that this idea only applies to objects which are perfectly square.