Description:
This project required me to use LUA to re-create the traditional arcade game, Asteroids. Using the SDL Engine as the graphics supplement, this entire program was written in a month using nothing but LUA. The main game design / mechanics of the original, such as non-stop momentum, have been kept intact, while I took the liberty of creating new distinctive, and more colorful art assets.
Design Goals:
Design Goals:
- Re-create Asteroids using the SDL Engine within Lua
- Add a new twist to Asteroids, in this case, different art assets
- Implement physics attributes, such as momentum
- Implement multiple character "NPCs" using constructors
- Implement player character, with multiple abilities, such as fire bullets, rotation, momentum and omni-directional movement
constructor usage to mimic classes
The below example is a portion of my code for the player "template" constructor, that is used to mimic the functionality of classes in Object Oriented Programming. The template below is setup so that it accepts values from the main.lua file via table insertions.
-- This is a constructor for setting up the player function Player( template ) -- Creates a local table and fill its default values with those from Actor local p = Actor(template) -- Assigns default values for player properties p.type = "player" p.material = nil p.material2 = nil p.actorIndex = nil p.score = 0 p.name = "DefaultName" -- Position variables p.position = p.position or nil p.newposition = p.newposition or nil -- Rotation variables p.rotateAngle = p.rotateAngle or math.pi p.rotation = p.rotation or 0 p.rotationChange = p.rotationChange or 0 -- Thrust and speed variables p.maxSpeed = p.maxSpeed or vec2(0,0) p.velocity = p.velocity or vec2(0,0) p.thrust = p.thrustStep or vec2(0,0) -- Shooting variables p.projSpeed = p.projSpeed or vec2(5,5) p.activeProjNum = 0 -- Attaching a function to a string in order to be called by itself p.playerThrust = PlayerThrust p.playerFire = PlayerFire -- Render variables p.render = PlayerRender p.update = PlayerUpdate -- Replace any default properties with those in gamePlayer for k,v in gamePlayer do p[ k ] = v end return p end
As shown below, a table is created in the main.lua file, and values are populated into the table. These values go directly to the constructor within the player.lua file, and is used to create the player Actor. These values can hold values from any attribute ranging from name, maxSpeed, velocity, etc...
-- Create the gamePlayer table and populate it gamePlayer = { score = 0, name = "player", -- Set max speed maxSpeed = vec2(2.5,2.5), -- Set initial ship speed velocity = vec2(0.5,0.5), -- Multiplies to the speed of the ship / player thrust = vec2(0,0), -- Rotation values rotation = 0, rotateAngle = 2, rotationChange = 0, -- Firing projectile values shootSpeed = vec2(2,2), -- Set the materials for the player material = gameMaterials["ship"], material2 = gameMaterials["ship2"], material3 = gameMaterials["shipfire"], material4 = gameMaterials["shipmovefire"], actorIndex = nil, scoreIndex = nil, position = nil, -- Sets the starting position to the bottom 12th of the screen and centered left and right newposition = vec2((GAME_WIDTH / 2) - (gameMaterials["ship"].w / 2), GAME_HEIGHT - GAME_HEIGHT/2) }
velocity & Rotation
Recreating the velocity and rotation of the space-ship proved to the be most challenging aspect of this game, as it was required to have a pseudo physics nature, in which inertia still remained. The position was important too, due to the nature of needing use it to determine when the ship collided with asteroids, or needed to shoot lasers.
Below is an example of what I used to update the material (i.e. Ship sprite), while determining its self rotation, position and its "hit-box".
Below is an example of what I used to update the material (i.e. Ship sprite), while determining its self rotation, position and its "hit-box".
-- Update the material to show the rotation and makes sure it's within a certain range self.rotation = math.mod((self.rotation + self.rotationChange), 360) -- Constant update of the player speed, direction and acceleration self.newposition.x = self.newposition.x + self.thrust.x self.newposition.y = self.newposition.y + self.thrust.y -- Wrapping of player material when moving from left to right screen edge if(self.newposition.x <= (self.material.w * -1)) then self.newposition.x = GAME_WIDTH end -- Wrapping of player material when moving from top to bottom screen edge if(self.newposition.y <= (self.material.h * -1)) then self.newposition.y = GAME_HEIGHT end -- Wrapping of player material when moving from right to left screen edge if(self.newposition.x >= (GAME_WIDTH + self.material.w)) then self.newposition.x = self.material.w * -0.9 end -- Wrapping of player material when moving from bottom to top screen edge if(self.newposition.y >= (GAME_HEIGHT + self.material.h)) then self.newposition.y = self.material.h * -0.9 end
Thrust was next up in the cycle of creating the space-ship. There were two major aspects to the ships movement:
Thrust:
Thrust was the actual movement speed of the ship, from start-up to final speed. The ship needed to be able to start at a speed of "0", and increase to a max speed, in this case, "2.5".
Thrust:
Thrust was the actual movement speed of the ship, from start-up to final speed. The ship needed to be able to start at a speed of "0", and increase to a max speed, in this case, "2.5".
-- The player thrust function for determining and adjusting thrust / velocity speed function PlayerThrust( self ) -- Sets a thrust speed to the rotation and velocity of the below numbers self.thrust.x = self.thrust.x - (math.sin(math.rad(self.rotation)) * self.velocity.x) self.thrust.y = self.thrust.y - (math.cos(math.rad(self.rotation)) * self.velocity.y) -- Caps the thrust speed to a certain maxSpeed for the x-axis if( self.thrust.x > self.maxSpeed.x ) then self.thrust.x = self.maxSpeed.x elseif( self.thrust.x < (0 - self.maxSpeed.x) ) then self.thrust.x = (0 - self.maxSpeed.x) end -- Caps the thrust speed to a certain maxSpeed for the y-axis if( self.thrust.y > self.maxSpeed.y ) then self.thrust.y = self.maxSpeed.y elseif( self.thrust.y < (0 - self.maxSpeed.y) ) then self.thrust.y = (0 - self.maxSpeed.y) end end
Intertia Movement:
Lastly, a famous aspect of Asteroid involved the shipping continuing movement, until an opposite force was acted upon it. This allowed the ship to slide, until places positioning themselves in the opposite direction, used thrust, in order to slow down.
Lastly, a famous aspect of Asteroid involved the shipping continuing movement, until an opposite force was acted upon it. This allowed the ship to slide, until places positioning themselves in the opposite direction, used thrust, in order to slow down.
-- Calculates the position and then adds an incremental speed to the player movement gameState.actors[gamePlayer.actorIndex]:playerThrust()
weapons and collision
The final aspect of this game required players to be able to destroy enemies with lasers, and have the collision to detect it. That aspect was achieved by creating a playerfire function, with a gameLaser table that created the projectile "laser". Within this function, the projectile speed, position, and lifespan was determined.
-- The player firing function for shooting lasers function PlayerFire( self ) -- local variables for firing projectile local lnprojDirection = vec2(0,0) local lnstartPos = vec2(0,0) local projectileLife = 0 -- Calculates the projectile speed lnprojDirection.x = 0 - (math.sin(math.rad(self.rotation)) * self.projSpeed.x) lnprojDirection.y = 0 - (math.cos(math.rad(self.rotation)) * self.projSpeed.y) -- Spawns the projectile at the tip of the player ship lnstartPos.x = self.position.x + (self.material.h / 2) - (math.sin(math.rad(self.rotation)) * self.material.h / 2) lnstartPos.y = self.position.y + (self.material.h / 2) - (math.cos(math.rad(self.rotation)) * self.material.h / 2) -- sets a value that captures how long the projectile life span should be projectileLife = SDL_GetTicks() + 1800 -- Creates a gameLaser table for the laser projectile gameState:AddActor( Laser( { newposition = lnstartPos, position = lnstartPos, lnprojDirection = lnprojDirection, projectileLife = projectileLife, material = gameMaterials["laser"], active = true } ) ) -- Sets how many active projectiles are allowed on screen if( self.activeProjNum <= 3 ) then self.activeProjNum = self.activeProjNum + 1 end end
And example of collision code, used to determine the various positions in which the ship + aliens could collide, causing the players to lose a life, or end the game.
-- COLLISION FOR PLAYER + ASTEROID CODE HERE -- Define helpful local variables local playerTop = vec2((self.newposition.x + (self.material.w / 2)), self.newposition.y) local playerBottom = vec2((self.newposition.x + (self.material.w / 2)), self.newposition.y + self.material.h) local playerRight = vec2(self.newposition.x + self.material.w, (self.newposition.y + (self.material.h / 2))) local playerLeft = vec2(self.newposition.x, (self.newposition.y + (self.material.h / 2))) -- Step through each active actor for actorIndex = 1, table.getn(gameState.actors) do -- Checks to make sure this actor is an active block if((gameState.actors[actorIndex].type == "asteroid" or gameState.actors[actorIndex].type == "asteroid_medium" or gameState.actors[actorIndex].type == "asteroid_small") and gameState.actors[actorIndex].active == true) then -- Check for left collision if((playerLeft.x >= (gameState.actors[actorIndex].position.x)) and (playerLeft.x <= (gameState.actors[actorIndex].position.x)) and (playerLeft.y >= (gameState.actors[actorIndex].position.y)) and (playerLeft.y <= (gameState.actors[actorIndex].position.y))) then -- Play collision sound if ( Mix_Playing(7) == 0 ) then Mix_PlayChannel(7, gameSounds.assets[7], 0) end -- Instant player death and game over screen upon death gbisGameOver = true end
custom sprites
Inspiration for the custom sprites came in the form of the Metroids, from Super Metroid, along with Sci-Fi space fighters, such as Galaga, and Ikaruga.