Lincoln Li - Game Designer
  • Shipped Products
  • Resume
  • Work Samples
    • Vicious Circle - Characters (password)
    • Vicious Circle - Sandbox (password)
    • Vicious Circle - Progression (password)
    • Untitled - Mission Systems (password)
    • H2A Sandbox / Combat
    • H2A Game Modes
    • Doom MP Spawn System
    • MWR Improved Destruction for AC130
    • MWR Audio Scripting
    • Hobby Project - Slacky's Command
  • About Me
    • Design Philosophy
Picture
DEVELOPMENT INFO
Game: Clonesteroids
Genre: 2D Side Scroller
Engine: Lua / SDL Engine
Development Time: 4 weeks
Game Mode: Single Player Game
RESPONSIBILITIES
  • Recreated Asteroids in Lua
  • Integrated SDL Engine in Lua for visuals
  • Created custom art assets
  • Controls, Physics, and Gameplay
Download Source Files


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:

  • 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".
	-- 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".
-- 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.
	-- 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.

Copyright 2021 | Lincoln Li

  • Shipped Products
  • Resume
  • Work Samples
    • Vicious Circle - Characters (password)
    • Vicious Circle - Sandbox (password)
    • Vicious Circle - Progression (password)
    • Untitled - Mission Systems (password)
    • H2A Sandbox / Combat
    • H2A Game Modes
    • Doom MP Spawn System
    • MWR Improved Destruction for AC130
    • MWR Audio Scripting
    • Hobby Project - Slacky's Command
  • About Me
    • Design Philosophy