Lua Scripting: CloneOut
Scripting Highlights
- Visual Level Creation Function: Scripted a level generation algorithm that greatly simplifies level creation through a visual framework
- Ball Function with English: Scripted ball interactions with realistic "English" (bouncing angles), giving players fine control over ball motion
- General Game Functions: Added music, custom player attributes (speed, lives, paddle size), progressive difficulty, and a scoring system
Visual Level Creation Function
With any game system I create, I try to keep it modular, scalable, and easy to use/modify. For this project, rather than randomly generating levels mathematically, I built a level constructor that allows for a more visual level building style. Each level consists of a series of rows of numbers, each number representing a block. This makes it easy to create visually interesting levels, as shown above.
-- Level 1
function FlagLevel()
gamePlayer.level = gamePlayer.level + 1
gameBlocks = {}
gameBlocks.count = 0
local row1 = { 4, 3, 3, 4, 3, 3, 4 }
local row2 = { 3, 4, 3, 4, 3, 4, 3 }
local row3 = { 3, 3, 4, 4, 4, 3, 3 }
GenLevelRow( row1, 0, 7 )
GenLevelRow( row2, gameMaterials[ "block3" ].h, 3 )
GenLevelRow( row3, 2 * gameMaterials[ "block3" ].h, 3 )
GenLevelRow( row2, 3 * gameMaterials[ "block3" ].h, 3 )
GenLevelRow( row1, 4 * gameMaterials[ "block3" ].h, 3 )
end
-- Level 2
function GreekLevel()
gamePlayer.level = gamePlayer.level + 1
gameBall.maxSpeed = 30
gameBlocks = {}
gameBlocks.count = 0
local row1 = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 }
local row2 = { 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 3 }
local row3 = { 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3 }
local row4 = { 3, 0, 3, 3, 3, 0, 3, 3, 3, 0, 3, 3, 3, 0, 3 }
local row5 = { 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 }
GenLevelRow( row1, 0, 9 )
GenLevelRow( row2, gameMaterials[ "block3" ].h, 9 )
GenLevelRow( row3, 2 * gameMaterials[ "block3" ].h, 5 )
GenLevelRow( row3, 3 * gameMaterials[ "block3" ].h, 5 )
GenLevelRow( row4, 4 * gameMaterials[ "block3" ].h, 5 )
GenLevelRow( row5, 5 * gameMaterials[ "block3" ].h, 5 )
GenLevelRow( row1, 6 * gameMaterials[ "block3" ].h, 5 )
end
-- Level 3
function PacManLevel()
gamePlayer.level = gamePlayer.level + 1
gameBlocks = {}
gameBlocks.count = 0
local row1 = { 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0 }
local row2 = { 1, 1, 1, 1, 0, 0, 0, 0, 0, 4, 4, 4, 0 }
local row3 = { 1, 1, 1, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4 }
local row4 = { 1, 1, 0, 0, 3, 0, 3, 0, 4, 0, 4, 0, 4 }
local row5 = { 1, 1, 1, 1, 0, 0, 0, 0, 4, 4, 4, 4, 4 }
local row6 = { 0, 1, 1, 0, 0, 0, 0, 0, 4, 0, 4, 0, 4 }
GenLevelRow( row1, 0, 5 )
GenLevelRow( row2, gameMaterials[ "block3" ].h, 5 )
GenLevelRow( row3, 2 * gameMaterials[ "block3" ].h, 7 )
GenLevelRow( row4, 3 * gameMaterials[ "block3" ].h, 7 )
GenLevelRow( row3, 4 * gameMaterials[ "block3" ].h, 7 )
GenLevelRow( row5, 5 * gameMaterials[ "block3" ].h, 7 )
GenLevelRow( row6, 6 * gameMaterials[ "block3" ].h, 7 )
end
-- Level 4
function WootLevel()
gamePlayer.level = gamePlayer.level + 1
gameBlocks = {}
gameBlocks.count = 0
local row1 = { 4, 0, 0, 0, 4, 2, 2, 2, 1, 1, 1, 3, 3, 3 }
local row2 = { 4, 0, 0, 0, 4, 2, 0, 2, 1, 0, 1, 0, 3, 0 }
local row3 = { 4, 0, 4, 0, 4, 2, 0, 2, 1, 0, 1, 0, 3, 0 }
local row4 = { 4, 4, 4, 4, 4, 2, 0, 2, 1, 0, 1, 0, 3, 0 }
local row5 = { 4, 4, 0, 4, 4, 2, 2, 2, 1, 1, 1, 0, 3, 0 }
local row6 = { 4, 0, 0, 0, 4, 2, 2, 2, 1, 1, 1, 0, 3, 0 }
GenLevelRow( row1, 0, 9 )
GenLevelRow( row1, gameMaterials[ "block3" ].h, 9 )
GenLevelRow( row2, 2 * gameMaterials[ "block3" ].h, 9 )
GenLevelRow( row3, 3 * gameMaterials[ "block3" ].h, 9 )
GenLevelRow( row4, 4 * gameMaterials[ "block3" ].h, 9 )
GenLevelRow( row5, 5 * gameMaterials[ "block3" ].h, 9 )
GenLevelRow( row6, 6 * gameMaterials[ "block3" ].h, 9 )
end
-- Level 5
function SmileyLevel()
gamePlayer.level = gamePlayer.level + 1
gameBlocks = {}
gameBlocks.count = 0
local row1 = { 0, 2, 0, 0, 0, 2, 0 }
local row2 = { 0 }
local row3 = { 2, 0, 0, 0, 0, 0, 2 }
local row4 = { 0, 2, 0, 0, 0, 2, 0 }
local row5 = { 0, 0, 2, 2, 2, 0, 0 }
GenLevelRow( row1, 0, 7 )
GenLevelRow( row2, gameMaterials[ "block3" ].h, 33 )
GenLevelRow( row2, 2 * gameMaterials[ "block3" ].h, 33 )
GenLevelRow( row3, 3 * gameMaterials[ "block3" ].h, 33 )
GenLevelRow( row4, 4 * gameMaterials[ "block3" ].h, 33 )
GenLevelRow( row5, 5 * gameMaterials[ "block3" ].h, 33 )
end
-- Level generation based on block layout table
function GenLevelRow( srcTable, srcHeight, srcScore)
local tmpMat = ""
local tmpBlock = {}
local rowLen = 0
local tmpPos = vec2( 0, 0 )
local tmpScore = 0
for iii = 1, table.getn(srcTable) do
--get material
if srcTable[iii] == 4 then
tmpMat = "block4" -- red
elseif srcTable[iii] == 3 then
tmpMat = "block3" -- blue
elseif srcTable[iii] == 2 then
tmpMat = "block2" -- green
elseif srcTable[iii] == 1 then
tmpMat = "block1" -- orange
else
tmpMat = nil
end
rowLen = table.getn(srcTable)
tmpPos = vec2( ( 0.5 * ( GAME_WIDTH - ( rowLen * gameMaterials[ "block3" ].w ) ) ) + ( gameMaterials[ "block3" ].w * (iii - 1) ) , ((GAME_HEIGHT * 0.15) + srcHeight ))
tmpScore = srcScore
if ( tmpMat ~= nil ) then
--create block from temp variables
tmpBlock = Block( { material = gameMaterials[ tmpMat ], position = tmpPos, score = tmpScore } )
--add the blocks to the game
table.insert(gameBlocks, tmpBlock)
gameState:AddActor( tmpBlock )
gameBlocks.count = gameBlocks.count + 1
end
end
end
Ball Function with English
When scripting the ball physics, I wanted to give the player as much control as possible. To that end, I added "English" to the ball, allowing the player to determine the angle of deflection by where the ball hit the paddle. Hitting the ball with the center of the paddle shoots it straight up, while hitting it on a corner deflects it to the side, giving the player fine tuned control. All other collisions maintain a constant angle of deflection, making the ball's movements more predictable.
function BallUpdate( self )
--Don't run this if there is no paddle
if (gameState.actors[ gamePlayer.actorIndex ] == nil) then
return
end
--Get new position
self.newposition = self.position + self.velocity
--Collided with side?
if ( self.newposition.x <= 0 ) or (self.newposition.x >= (GAME_WIDTH - self.material.w ) ) then
self.velocity.x = 0 - self.velocity.x
Mix_PlayChannel( -1, gameSounds.assets[ 2 ], 0 ) --add sound
end
--Collided with top?
if ( self.newposition.y <= 0 ) then
self.velocity.y = 0 - self.velocity.y
Mix_PlayChannel( -1, gameSounds.assets[ 2 ], 0 ) --add sound
end
--Collided with bottom?
if ( self.newposition.y >= ( GAME_HEIGHT - self.material.h ) ) then
Mix_PlayChannel( -1, gameSounds.assets[ 1 ], 0 ) --add sound
SDL_Delay( 1500 )
--reduce lives
gamePlayer.lives = gamePlayer.lives - 1
if ( gamePlayer.lives < 0 ) then
gameState.active = false
else
self.newposition = vec2( GAME_WIDTH / 2, GAME_HEIGHT / 2 )
SDL_Delay( 500 )
self.velocity = vec2( math.random( 2, 4 ), math.random( 2, 4 ) )
end
end
--Collided with paddle?
local paddle = gameState.actors[ gamePlayer.actorIndex ]
if ( self.newposition.x >= paddle.position.x ) and
( self.newposition.x <= paddle.position.x + paddle.material.w ) and
( self.newposition.y >= paddle.position.y - self.material.h ) and
( self.newposition.y <= paddle.position.y + paddle.material.h )
then
-- X velocity with English
local paddleMiddle = paddle.position.x + ( paddle.material.w / 2 )
local ballMiddle = self.newposition.x + ( self.material.w / 2 )
self.velocity.x = ( ( ballMiddle - paddleMiddle ) / paddle.material.w ) * self.maxSpeed
-- Y velocity
self.velocity.y = math.abs(self.velocity.x) - self.maxSpeed
Mix_PlayChannel( -1, gameSounds.assets[ 2 ], 0 ) --add sound
end
--Update ball position
self.position = self.newposition
--Update global ball references
gameBall.velocity = self.velocity
gameBall.position = self.position
end
--Base parameters for Ball if unassigned
function Ball( template )
--Inherit base from Actor
local p = Actor( template )
--Set defaults for Player
p.active = true
p.material = nil
p.maxSpeed = 0
p.newposition = vec2( 0, 0 )
p.position = vec2( 0, 0 )
p.render = BallRender
p.type = "ball"
p.update = BallUpdate
p.velocity = vec2( 0, 0 )
--Set any custom parameters
for k,v in gameBall do
p[ k ] = v
end
return p
end
General Game Functions
Some of the general polish features I added to CloneOut include:
- Automatic game scaling for playing in different windows
- Randomly selected music composed of five separate tracks
- Player lives, scoring system with end-level bonuses
- Increasing speed per level to create a smooth difficulty curve
function ScaleAssets()
for k,v in gameMaterials do
--Scale by gameBlocks.scaleX, gameBlocks.scaleY factors
x = gameState.scaleX / ( gameMaterials[ k ].w - 1 )
y = gameState.scaleY / gameMaterials[ k ].h
if ( k == "ball" ) then
x = ( GAME_WIDTH * 0.002 )
y = x
end
if ( k == "paddle" ) then
x = ( GAME_WIDTH * 0.0035 )
y = ( GAME_HEIGHT * 0.002 )
end
gameMaterials[ k ] = SDL_zoomSurface( gameMaterials[ k ], x, y, 1 )
end
end
--Initialize the Audio stream
function InitAudio()
if ( Mix_OpenAudio( 44100, AUDIO_S16SYS, 2, 2048 ) < 0 ) then
print( "Failed to initialize audio device: ", SDL_GetError() )
Mix_CloseAudio()
os.exit(2)
end
end
--Load a sound, return sound reference
function LoadSound( soundName, isMusic )
--Sound already exists in cache? If yes, return that reference
-- otherwise proceed with loading new
if ( isMusic == true ) then
if ( gameMusic.assets[ soundName ] ) then
return soundName
end
local snd = Mix_LoadMUS( GAME_PATH .. soundName)
if ( snd == nil ) then
print( "Could not load music " .. soundName .. ": " .. SDL_GetError() )
return nil
end
gameMusic.volume = 100
table.insert( gameMusic.assets, snd )
else
if ( gameSounds.assets[ soundName ] ) then
return soundName
end
local snd = Mix_LoadWAV( GAME_PATH .. soundName )
if ( snd == nil ) then
print( "Could not load sound " .. soundName .. ": " .. SDL_GetError() )
return nil
end
gameSounds.volume = 100
table.insert( gameSounds.assets, snd )
end
return snd
end
--Function that plays a song (this is rerun every time a song ends)
function playMusic()
if ( table.getn( gameMusic.assets ) <= 1 ) then
i = 1
else
repeat
i = math.random(1, table.getn( gameMusic.assets ) )
until i ~= lastSongPlayed
end
Mix_PlayMusic(gameMusic.assets[i], 0)
lastSongPlayed = i
end
--Game update once per frame
function RunGameLoop()
gameText.score = "Level " .. gamePlayer.level .. " Score " .. gamePlayer.score .. " Lives " .. gamePlayer.lives --.. "Count " .. gameBlocks.count -- HUD at bottom of screen
gameState.actors[ gamePlayer.scoreIndex ].output = gameText.score
--Ensure we always have an immediate connection to the player object, instead of having to find it every time
if ( gamePlayer.actorIndex == nil ) then
--Somehow we lost the player actor, so get it again
for index = 1, table.getn( gameState.actors ) do
for k,v in gameState.actors[ index ] do
if ( ( k == "type" ) and ( v == "player" ) ) then
gamePlayer.actorIndex = index
break
end
end
end
end
--Render Frame
RenderFrame()
--If not already playing, play music
Mix_HookMusicFinished(playMusic)
--new level
if ( gameBlocks.count <= 0 ) then
-- Update ball position, velocity, and max speed
for index = 1, table.getn( gameState.actors ) do
if ( gameState.actors[ index ].type == "ball" ) then
gameState.actors[ index ].position = vec2( GAME_WIDTH / 2, GAME_HEIGHT / 2 )
gameState.actors[ index ].newposition = vec2( GAME_WIDTH / 2, GAME_HEIGHT / 2 )
gameState.actors[ index ].velocity = vec2( math.random( 2, 4 ), math.random( 2, 4 ) )
gameState.actors[ index ].maxSpeed = gameState.actors[ index ].maxSpeed + 3
end
end
-- Update global ref to ball position and velocity
gameBall.newposition = vec2( GAME_WIDTH / 2, GAME_HEIGHT / 2 )
gameBall.position = vec2( GAME_WIDTH / 2, GAME_HEIGHT / 2 )
gameBall.velocity = vec2( math.random( 2, 4 ), math.random( 2, 4 ) )
gamePlayer.score = gamePlayer.score + ( 10 * gamePlayer.lives * gamePlayer.level )
gamePlayer.lives = gamePlayer.lives + 1
-- Choose next level
if ( gamePlayer.level == 1 ) then
GreekLevel()
elseif ( gamePlayer.level == 2 ) then
PacManLevel()
elseif ( gamePlayer.level == 3 ) then
WootLevel()
elseif ( gamePlayer.level == 4 ) then
SmileyLevel()
else
--game over
gameBlocks.count = 1
--print victory text
gameText.score = "YOU WON!!! Final Score " .. gamePlayer.score --.. " Final Lives " .. gamePlayer.lives
gameState.actors[ gamePlayer.scoreIndex ].output = gameText.score
UpdateActors()
RenderFrame()
SDL_Delay( 7000 )
gameState.active = false
end
UpdateActors()
RenderFrame()
SDL_Delay( 2000 )
end
end
--Sets up the game state, loads assets etc.
--Allows game to be restarted without recreating video window etc
function InitGame()
--Load assets: materials then sounds
LoadMaterial( "ball" )
LoadMaterial( "block1" )
LoadMaterial( "block2" )
LoadMaterial( "block3" )
LoadMaterial( "block4" )
LoadMaterial( "paddle" )
LoadSound( "empirehouse.wav", true )
LoadSound( "high_tech_techno.wav", true )
LoadSound( "HouseofGenius.wav", true )
LoadSound( "MrHyde.wav", true )
LoadSound( "triphop.wav", true )
LoadSound( "Buzz01.wav", false )
LoadSound( "Click01.wav", false )
--Scale Assets
ScaleAssets()
--Define player custom parameters
gamePlayer = {
actorIndex = nil,
level = 0,
lives = 3,
material = gameMaterials[ "paddle" ],
moveStepX = 10,
moveStepY = 0,
name = "Player 1",
newposition = vec2( ( GAME_WIDTH / 2 ) - ( gameMaterials[ "paddle" ].w / 2 ), ( GAME_HEIGHT ) - ( gameMaterials[ "paddle" ].h * 2 ) - ( gameFont[ 1 ].charHeight * 3 ) ),
score = 0,
scoreIndex = nil,
}
--Define ball custom parameters
gameBall = {
actorIndex = nil,
velocity = vec2( math.random( 2, 4 ), math.random( 2, 4 ) ),
position = vec2( GAME_WIDTH / 2, GAME_HEIGHT / 2 ),
material = gameMaterials[ "ball" ],
maxSpeed = 8,
}
--Define location for score text
gameText = {
output = "Score Error",
position = vec2( 0, ( GAME_HEIGHT - ( gameFont[ 1 ].charHeight * 2 ) ) ), -- OLD POSITION: vec2( ( 0.5 * GAME_WIDTH ) - ( 0.5 * string.len(gameText.score ) * gameFont[ 1 ].charWidth ), GAME_HEIGHT - ( gameFont[ 1 ].charHeight * 2 ) ),
visible = true,
}
--Add actors to the game
gameState:AddActor( Ball() )
gameState:AddActor( Player() )
gameState:AddActor( Text() )
--Begin First Level
--TestLevel()
FlagLevel() -- level 1
--GreekLevel() -- level 2
--PacManLevel() -- level 3
--WootLevel() -- level 4
--SmileyLevel() -- level 5
--Set the game to active
gameState.beginTime = SDL_GetTicks()
gameState.lastUpdateFPS = gameState.beginTime
gameState.lastUpdateMovement = gameState.lastUpdateFPS
gameState.active = true
--Start music if available
playMusic()
--Start the engine update loop
EngineLoop()
end




