Blake Alexander - Designer - Scripter
  • Wildstar
    • Warplot Bosses >
      • Avatus
      • Stormtalon
      • Forgemaster Trogun
      • Spiritmother Selene
      • Murderbot
      • Mordechai Redmoon
      • Metal Maw
      • Dreadphage Ohmna
      • Kraggar the Earthrender
    • Quest Content >
      • Mechsuit Assault
      • Eldan Test Lab
      • Kel Voreth Underforge
      • Creepy Cave
      • Mayday
  • Levels
    • Jackson Square
    • Titan
    • Forbidden Palace
    • Warped Citadel
  • Other Games
    • Voodudes
    • Rooty Isles
    • Telesloth
  • Scripting
    • UnrealScript
    • Lua
    • C-Sharp
  • Resume and Contact

Lua Scripting: CloneOut

Picture
     
Picture

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
Picture
 

Visual Level Creation Function

Picture
     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
Picture
 

Ball Function with English

Picture
     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
Picture
 

General Game Functions

Picture
     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
Picture

Return to top of page

Picture
    Wildstar
     ~ Boss Design
     ~ Quest Content

Levels
~ Jackson Square
~ Titan
~ Forbidden Palace
~ Warped Citadel
Other Games
~ Voodudes
~ Rooty Isles
~ Telesloth
Scripting
~ UnrealScript
~ Lua
~ C#
Resume and Contact
Picture
Picture
Picture
© 2017 Blake Alexander  |  All Rights Reserved
Powered by Create your own unique website with customizable templates.