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