Overview

Professor Cadbury and the Candy Castle is a 2D platforming game that is all about sweet, sweet speed.

Dash through the halls of Cadbury’s castle as quickly as you can, while eating delicious candy to increase your speed using the sugar rush. However, eating too much candy will cause Professor Cadbury to get sick, so watch that sweet tooth!

Statistics

  • Project:
    • Type: Game
    • Developed: Oct 2013 - Dec 2013 (2 Months)
    • Engine: GuildEd
    • Platforms:
      • Windows
  • Team:
    • Name: Team Yarnball
    • Developers: 6
      • 1 Artist
      • 3 Designers
      • 1 Producer
      • 1 Programmer

Solo Programmer

  • Developed core gameplay code for 2D platformer.
  • Wrote lua scripts for all UI, including HUD and menus.
  • Flexibly implemented unexpected features that were needed mid-milestone.
  • Level End Menu

    Motivation

    At the end of each level, our team wanted to give the player a score in order to reinforce that speed and collection were important. We decided that the best way to implement this idea was to have a separate “end level” that would present the score.

    Design

    My focus in the design of this class was flexibility. This class was created midway through the project, and I knew we were going to have to make changes to this menu over time.

    There are four main elements to the score menu: the title bar, the score table, a star rating, and a short poem based on that rating. Each of these four elements, as well as each element inside the table, can have its font, font size, position, and icons changed independently. We didn’t end up using some of this flexibility in the final product, bug having it available was helpful.

    Code

    End-of-Level Menu

        
            
    --JS: File load and execution priority
    priority = -1000
    
    dofile( self, APP_PATH .. "scripts/include/AnimationBase.lua")
    dofile( self, APP_PATH .. "scripts/include/Time.lua")
    
    -------- UI Variables --------
    --General
    displayStartX   = 230
    displayStartY   = 140
    backgroundStr   = "x.lua"
    
    --Title
    titleFontString = "arial"
    titleFontSize   = 24
    titleOffsetX    = 400
    titleOffsetY    = 140
    
    --Table
    tableFontString = "arial"
    tableFontSize   = 12
    tableOffsetX    = 150
    tableOffsetY    = 180
    tableSpacingX1  = 40
    tableSpacingX2  = 100
    tableSpacingX3  = 80
    tableSpacingX4  = 80
    tableSpacingY   = 40
    tableRowIcoStr1 = "x.lua"
    tableRowIcoStr2 = "x.lua"
    tableRowIcoStr3 = "x.lua"
    
    --Total Score
    totalFontString = "arial"
    totalFontSize   = 30
    totalOffsetX    = 175
    totalOffsetY    = 300
    
    --Ratings
    ratingFontStr   = "arial"
    ratingFontSize  = 14
    rateTextOffsetX = 500
    rateTextOffsetY = 300
    rateAnimOffsetX = 550
    rateAnimOffsetY = 200
    rateYGapOffset  = 20
    ratingAnimStr1  = "x.lua"
    ratingAnimStr2  = "x.lua"
    ratingAnimStr3  = "x.lua"
    
    -------- In-game Variables --------
    --General
    displayPosition = nil
    backgroundAnim  = nil
    
    --Title
    titleTextString = "Title:"
    titleFontObject = nil
    titlePosition   = nil
    
    --Table
    tableFontObject = nil
    tablePosition   = nil
    tableRow1Icon   = nil
    tableRow2Icon   = nil
    tableRow3Icon   = nil
    
    --Total Score
    totalTextString = "Final Score: "
    totalFontObject = nil
    totalPosition   = nil
    
    --Ratings
    ratingFontObj   = nil
    ratingAnimPos   = nil
    ratingTextPos   = nil
    ratingAnim1     = nil
    ratingAnim2     = nil
    ratingAnim3     = nil
    ratingText11    = "Rating Haiku:"
    ratingText12    = "You seem new at this"
    ratingText13    = "You made too many mistakes"
    ratingText14    = "Play level again"
    ratingText21    = "Rating Haiku:"
    ratingText22    = "You played this quite well"
    ratingText23    = "It was a skillful display"
    ratingText24    = "There is more to learn"
    ratingText31    = "Rating Haiku:"
    ratingText32    = "Your play was the best"
    ratingText33    = "Your actions are without peer"
    ratingText34    = "Your title: master"
    scoreForRating2 = 20
    scoreForRating3 = 35
    
    --Row Enablers
    showTitle       = true
    showTableHeads  = true
    showPlayerCoins = true
    showTargetTime  = true
    showPlayerTime  = true
    showTotalScore  = true
    showScoreRating = true
    
    --Multipliers
    coinMultiplier  = 1
    timeMultiplier  = 1
    
    --Score Components
    playerCoins     = 0
    playerCoinScore = 0
    playerTime      = 0
    targetTime      = 60
    playerTimeScore = 0
    totalScore      = 0
    
    --Distance
    displayDistance = 20
    
    --Animation
    animationAngle  = 0
    animationTime   = 0
    
    scoreSaveName   = "score"
    
    ui = {
        --General
        uiHeadingMain   = { order =  1,  type = "boolean", label = "GENERAL SETTINGS:",             default = false                             },
        displayStartX   = { order =  2,  type = "number",  label = "Display Top Left Corner X:",    default = 230                               },
        displayStartY   = { order =  3,  type = "number",  label = "Display Top Left Corner Y:",    default = 140                               },
        backgroundStr   = { order =  4,  type = "anim",    label = "Background Image:",             default = "x.lua"                           },
        displayDistance = { order =  5,  type = "number",  label = "Rendering Distance Threshold:", default = 20                                },
        scoreSaveName   = { order =  6,  type = "string",  label = "Save Session Variable:",        default = "score"                           },
    
        --Title
        uiHeadingTitle  = { order = 10,  type = "boolean", label = "TITLE SETTINGS:",               default = false                             },
        titleTextString = { order = 11,  type = "string",  label = "Title Text:",                   default = "Title:"                          },
        titleFontString = { order = 12,  type = "string",  label = "Title Font:",                   default = "arial"                           },
        titleFontSize   = { order = 13,  type = "number",  label = "Title Font Size:",              default = 20                                },
        titleOffsetX    = { order = 14,  type = "number",  label = "Title X Offset:",               default = 400                               },
        titleOffsetY    = { order = 15,  type = "number",  label = "Title Y Offset:",               default = 140                               },
    
        --Table
        uiHeadingTable  = { order = 20,  type = "boolean", label = "TABLE SETTINGS:",               default = false                             },
        tableFontString = { order = 21,  type = "string",  label = "Table Font:",                   default = "arial"                           },
        tableFontSize   = { order = 22,  type = "number",  label = "Table Font Size:",              default = 12                                },
        tableOffsetX    = { order = 23,  type = "number",  label = "Table X Offset:",               default = 150                               },
        tableOffsetY    = { order = 24,  type = "number",  label = "Table Y Offset:",               default = 180                               },
        tableSpacingX1  = { order = 25,  type = "number",  label = "Table X Spacing(Icon Column):", default = 40                                },
        tableSpacingX2  = { order = 26,  type = "number",  label = "Table X Spacing(2nd Column):",  default = 100                               },
        tableSpacingX3  = { order = 27,  type = "number",  label = "Table X Spacing(3rd Column):",  default = 80                                },
        tableSpacingX4  = { order = 28,  type = "number",  label = "Table X Spacing(4th Column):",  default = 80                                },
        tableSpacingY   = { order = 29,  type = "number",  label = "Table Y Spacing:",              default = 40                                },
        tableRowIcoStr1 = { order = 30,  type = "anim",    label = "Table Coin Animation:",         default = "x.lua"                           },
        tableRowIcoStr2 = { order = 31,  type = "anim",    label = "Table Target Time Animation:",  default = "x.lua"                           },
        tableRowIcoStr3 = { order = 32,  type = "anim",    label = "Table Player Time Animation:",  default = "x.lua"                           },
    
        --Data  
        uiHeadingData   = { order = 35,  type = "boolean", label = "DATA SETTINGS:",                default = false                             },
        coinMultiplier  = { order = 36,  type = "number",  label = "Score Multiplier (Coins):",     default = 1                                 },
        timeMultiplier  = { order = 37,  type = "number",  label = "Score Multiplier (Time):",      default = 1                                 },
        targetTime      = { order = 38,  type = "number",  label = "Target Time (Seconds):",        default = 60                                },
    
        --Total
        totalTextString = { order = 40,  type = "string",  label = "Total Score Prefix Text:",      default = "Final Score: "                   },
        totalFontString = { order = 41,  type = "string",  label = "Total Score Font:",             default = "arial"                           },
        totalFontSize   = { order = 42,  type = "number",  label = "Total Score Font Size:",        default = 30                                },
        totalOffsetX    = { order = 43,  type = "number",  label = "Total Score X Offset:",         default = 175                               },
        totalOffsetY    = { order = 44,  type = "number",  label = "Total Score Y Offset:",         default = 300                               },
    
        --Ratings
        uiHeadingRate   = { order = 50,  type = "boolean", label = "RATINGS (Low to High):",        default = false                             },
        ratingFontStr   = { order = 51,  type = "string",  label = "Rating Font:",                  default = "arial"                           },
        ratingFontSize  = { order = 52,  type = "number",  label = "Rating Font Size:",             default = 14                                },
        rateTextOffsetX = { order = 53,  type = "number",  label = "Rating Text X Offset:",         default = 500                               },
        rateTextOffsetY = { order = 54,  type = "number",  label = "Rating Text Y Offset:",         default = 300                               },
        rateAnimOffsetX = { order = 55,  type = "number",  label = "Rating Anim X Offset:",         default = 550                               },
        rateAnimOffsetY = { order = 56,  type = "number",  label = "Rating Anim Y Offset:",         default = 200                               },
        rateYGapOffset  = { order = 57,  type = "number",  label = "Rating Y Gap Offset:",          default = 20                                },
    
        ratingAnimStr1  = { order = 61,  type = "anim",    label = "Low Rating Animation:",         default = "x.lua"                           },
        ratingText11    = { order = 62,  type = "string",  label = "Low Rating Text 1:",            default = "Rating Haiku:"                   },
        ratingText12    = { order = 63,  type = "string",  label = "Low Rating Text 2:",            default = "You seem new at this"            },
        ratingText13    = { order = 64,  type = "string",  label = "Low Rating Text 3:",            default = "You made too many mistakes"      },
        ratingText14    = { order = 65,  type = "string",  label = "Low Rating Text 4:",            default = "Play level again"                },
    
        scoreForRating2 = { order = 71,  type = "number",  label = "Medium Score Threshold:",       default = 20                                },
        ratingAnimStr2  = { order = 72,  type = "anim",    label = "Medium Rating Animation:",      default = "x.lua"                           },
        ratingText21    = { order = 73,  type = "string",  label = "Medium Rating Text 1:",         default = "Rating Haiku:"                   },
        ratingText22    = { order = 74,  type = "string",  label = "Medium Rating Text 2:",         default = "You played this quite well"      },
        ratingText23    = { order = 75,  type = "string",  label = "Medium Rating Text 3:",         default = "It was a skillful display"       },
        ratingText24    = { order = 76,  type = "string",  label = "Medium Rating Text 4:",         default = "There is more to learn"          },
    
        scoreForRating3 = { order = 81,  type = "number",  label = "Best Rating Threshold:",        default = 35                                },
        ratingAnimStr3  = { order = 82,  type = "anim",    label = "Best Rating Animation:",        default = "x.lua"                           },
        ratingText31    = { order = 83,  type = "string",  label = "Best Rating Text 1:",           default = "Rating Haiku:"                   },
        ratingText32    = { order = 84,  type = "string",  label = "Best Rating Text 2:",           default = "Your play was the best"          },
        ratingText33    = { order = 85,  type = "string",  label = "Best Rating Text 3:",           default = "Your actions are without peer"   },
        ratingText34    = { order = 86,  type = "string",  label = "Best Rating Text 4:",           default = "Your title: master"              },
    
        --Row Enabling
        uiHeadingEnable = { order = 90,  type = "boolean", label = "ROW DISABLE SETTINGS:",         default = false                             },
        showTitle       = { order = 91,  type = "boolean", label = "Show Title?",                   default = true                              },
        showTableHeads  = { order = 92,  type = "boolean", label = "Show Table Headings?",          default = true                              },
        showPlayerCoins = { order = 93,  type = "boolean", label = "Show Player Coins?",            default = true                              },
        showTargetTime  = { order = 94,  type = "boolean", label = "Show Target Time?",             default = true                              },
        showPlayerTime  = { order = 95,  type = "boolean", label = "Show Player's Time?",           default = true                              },
        showTotalScore  = { order = 96,  type = "boolean", label = "Show Total Score?",             default = true                              },
        showScoreRating = { order = 97,  type = "boolean", label = "Show Rating for Player?",       default = true                              }
    }
    
    function removeCollision()
        if ( rigidBody ~= nil ) then
            g.physics:remove( rigidBody )
        end
        rigidBody = g.physics:createBox( x, y, RigidBody_STATIC, iconW/pixelsPerUnit, iconH/pixelsPerUnit, 0 )
        rigidBody:setCollisionCategory( CollisionCategory_NONE )
        rigidBody:setCollisionMask( CollisionMask_NONE )
    end
    
    function init()
    
        removeCollision()
    
        --Turn off Base icon
        iconVisible = false
    
        updateAlways = false
    
        --Initialize animations first ( we need their sizes during position creation )
        backgroundAnim  = initializeAnimationOrReturnNil( backgroundStr )
        ratingAnim1     = initializeAnimationOrReturnNil( ratingAnimStr1 )
        ratingAnim2     = initializeAnimationOrReturnNil( ratingAnimStr2 )
        ratingAnim3     = initializeAnimationOrReturnNil( ratingAnimStr3 )
        tableRow1Icon   = initializeAnimationOrReturnNil( tableRowIcoStr1 )
        tableRow2Icon   = initializeAnimationOrReturnNil( tableRowIcoStr2 )
        tableRow3Icon   = initializeAnimationOrReturnNil( tableRowIcoStr3 )
    
        --Now figure out our display positions
        displayPosition = { x = displayStartX,                   y = displayStartY                   }
        titlePosition   = { x = displayStartX + titleOffsetX,    y = displayStartY + titleOffsetY    }
        tablePosition   = { x = displayStartX + tableOffsetX,    y = displayStartY + tableOffsetY    }
        totalPosition   = { x = displayStartX + totalOffsetX,    y = displayStartY + totalOffsetY    }
        ratingAnimPos   = { x = displayStartX + rateAnimOffsetX, y = displayStartY + rateAnimOffsetY }
        ratingTextPos   = { x = displayStartX + rateTextOffsetX, y = displayStartY + rateTextOffsetY }
    
        --Finish it up by initializing the fonts
        titleFontObject = Font( titleFontString, titleFontSize )
        tableFontObject = Font( tableFontString, tableFontSize )
        totalFontObject = Font( totalFontString, totalFontSize )
        ratingFontObj   = Font( ratingFontStr,   ratingFontSize )
    
    end
    
    function getDistanceToPlayer()
        playerLocation  = { x = g.player.rigidBody:posX(),  y = g.player.rigidBody:posY() }
        ourLocation     = { x = rigidBody:posX(),           y = rigidBody:posY() }
    
        return ( ( ourLocation.x - playerLocation.x ) * ( ourLocation.x - playerLocation.x ) ) + ( ( ourLocation.y - playerLocation.y ) * ( ourLocation.y - playerLocation.y ) )
    end
    
    function update( dt )
    
        if ( getDistanceToPlayer() < ( displayDistance * displayDistance ) ) then
            g.player.timerRunning = false
        else
            g.player.timerRunning = true
        end
    
        if ( showPlayerCoins ) then
            playerCoins = g.player.score
            playerCoinScore = playerCoins * coinMultiplier
        end
    
        if ( showPlayerTime ) then
            playerTime  = g.player.timeInLevel
            playerTimeScore = g.math.floor( ( targetTime - playerTime ) * timeMultiplier )
        end
    
        if ( showTotalScore ) then
            totalScore = playerCoinScore + playerTimeScore
            SetSessionVariable( scoreSaveName, totalScore )
        end
    
        animationTime = animationTime + dt
    
    end
    
    function render( dt )
    
        if ( getDistanceToPlayer() > ( displayDistance * displayDistance ) ) then
            return
        end
    
        if ( backgroundAnim ~= nil ) then
            backgroundAnim:draw( animationTime, displayPosition.x, displayPosition.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
        end
    
        if ( showTitle ) then
            titleFontObject:draw( titlePosition.x, titlePosition.y, titleTextString )
        end
    
        rowPositionY = tablePosition.y
        if ( showTableHeads ) then
            renderTableRow( tableFontObject, tablePosition.x, rowPositionY, nil, "", "Collected", " Multiplier", "Score" )
        end
    
        if ( showPlayerCoins ) then
            rowPositionY = rowPositionY + tableSpacingY
            renderTableRow( tableFontObject, tablePosition.x, rowPositionY, tableRow1Icon, "Coins:", g.tostring( playerCoins ), g.tostring( coinMultiplier ), g.tostring( playerCoinScore ) )
        end
    
        if ( showTargetTime ) then
            rowPositionY = rowPositionY + tableSpacingY
            renderTableRow( tableFontObject, tablePosition.x, rowPositionY, tableRow2Icon, "Target Time:", rawTimeToString( targetTime ), " ", "" )
        end
    
        if ( showPlayerTime ) then
            rowPositionY = rowPositionY + tableSpacingY
            renderTableRow( tableFontObject, tablePosition.x, rowPositionY, tableRow3Icon, "Time:", rawTimeToString( targetTime - playerTime ), g.tostring( timeMultiplier ), g.tostring( playerTimeScore ) )
        end
    
        if ( showTotalScore ) then
            totalFontObject:draw( totalPosition.x, totalPosition.y, totalTextString .. g.tostring( totalScore ) )
        end
    
        if ( showScoreRating ) then
            
            if ( totalScore < scoreForRating2 ) then
    
                if ( ratingAnim1 ~= nil ) then
                    ratingAnim1:draw( animationTime, ratingAnimPos.x, ratingAnimPos.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
                end
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y,                           ratingText11 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + rateYGapOffset,          ratingText12 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 2 *  rateYGapOffset ), ratingText13 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 3 *  rateYGapOffset ), ratingText14 )
    
            elseif ( totalScore < scoreForRating3 ) then
    
                if ( ratingAnim2 ~= nil ) then
                    ratingAnim2:draw( animationTime, ratingAnimPos.x, ratingAnimPos.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
                end
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y,                           ratingText21 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + rateYGapOffset,          ratingText22 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 2 *  rateYGapOffset ), ratingText23 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 3 *  rateYGapOffset ), ratingText24 )
    
            else
    
                if ( ratingAnim3 ~= nil ) then
                    ratingAnim3:draw( animationTime, ratingAnimPos.x, ratingAnimPos.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
                end
    
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y,                           ratingText31 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + rateYGapOffset,          ratingText32 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 2 *  rateYGapOffset ), ratingText33 )
                ratingFontObj:draw( ratingTextPos.x, ratingTextPos.y + ( 3 *  rateYGapOffset ), ratingText34 )
    
            end
    
        end
    
    end
    
    function renderTableRow( fontObject, rowStartX, rowStartY, leftIconAnimation, leadingString, rawScoreString, multiplierString, multipliedScoreString )
        
        drawPositionX = rowStartX
        if ( leftIconAnimation ~= nil ) then
            leftIconAnimation:draw( animationTime, drawPositionX, rowStartY, animationAngle, Image_COORDS_SCREEN )
        end
    
        drawPositionX = rowStartX + tableSpacingX1
        fontObject:draw( drawPositionX, rowStartY - 10, leadingString )
    
        drawPositionX = drawPositionX + tableSpacingX2
        fontObject:draw( drawPositionX, rowStartY - 10, rawScoreString )
    
        drawPositionX = drawPositionX + tableSpacingX3
        --If the first character is a space, don't draw the multiplier. Instead, draw the string without the space.
        if ( multiplierString:sub( 1, 1 ) ~= " ") then
            fontObject:draw( drawPositionX, rowStartY - 10, "x" .. multiplierString )
        else
            fontObject:draw( drawPositionX, rowStartY - 10, multiplierString:sub( 2 ) )
        end
    
        drawPositionX = drawPositionX + tableSpacingX4
        fontObject:draw( drawPositionX, rowStartY - 10, multipliedScoreString )
    end
    
        
    

  • In-Game HUD

    Motivation

    One of the less developed features in GuildEd was the UI system. As a result, even though our game needed a minimal UI, I needed to write a custom UI class for our game.

    Design

    As with the end-of-level menu, my main focus in the writing of this class was flexibility. Each UI Element is able to be set to a specific corner of the screen, given an independent offset, linked to a specific piece of game data, rendered as a number or bar, and given a specific icon. This flexibility allowed us to quickly set up our HUD and modify it when necessary.

    Code

    In-Game HUD

        
            
    --JS: File load and execution priority
    priority = -850
    
    dofile( self, APP_PATH .. "scripts/include/Time.lua")
    
    --=================== Constants ===================--
    --HUD Types
    local HUD_NONE      = 0
    local HUD_ICON      = 1
    local HUD_TEXT      = 2
    local HUD_BAR       = 4
    
    --Item Types
    local LINKED_WITH_NADA   = 0
    local LINKED_WITH_HEALTH = 1
    local LINKED_WITH_SUGAR  = 2
    local LINKED_WITH_TIMER  = 4
    local LINKED_WITH_SCORE  = 8
    
    --Location Types
    local LOCATION_CENTER       = 0
    local LOCATION_NORTH        = 1
    local LOCATION_SOUTH        = 2
    local LOCATION_WEST         = 4
    local LOCATION_NORTH_WEST   = 5
    local LOCATION_SOUTH_WEST   = 6
    local LOCATION_EAST         = 8
    local LOCATION_NORTH_EAST   = 9
    local LOCATION_SOUTH_EAST   = 10
    
    
    
    
    --=================== UI Variables ===================--
    local uiHeadingGeneral  = false
    local uiHeadingLabel    = false
    local uiHeadingData     = false
    displayPosString        = "CENTER"
    
    --Label Variables
    hudLabelTypeString  = "NONE"
    labelOffsetXMinus   = false
    labelOffsetX        = 0
    labelOffsetYMinus   = false
    labelOffsetY        = 0
    labelFontTypeString = "arial"
    labelFontSize       = 12
    labelAnimString     = "x.lua"
    
    --Data Variables
    hudDataTypeString   = "TEXT"
    linkedWithString    = "NOTHING"
    dataOffsetXMinus    = false
    dataOffsetX         = 0
    dataOffsetYMinus    = false
    dataOffsetY         = 0
    dataFontSize        = 12
    dataFontTypeString  = "arial"
    --dataAnimString        = "x.lua" --this could be useful for animated bars
    timeUpImageStr      = "x.lua"
    timeUpPosX          = 0
    timeUpPosY          = 0
    
    --=================== In-Game Variables ===================--
    --General HUD Variables
    hudPosition     = nil
    
    --Label Variables
    hudLabelHasType = HUD_NONE
    labelPosition   = nil
    labelTextString = ""
    labelFontObject = nil
    labelAnimation  = nil
    
    --Animation Variables
    hudDataHasType      = HUD_TEXT
    hudLinkedWith       = LINKED_WITH_NADA
    dataPosition        = nil
    dataFontObject      = nil
    dataBarHeight       = 20
    dataBarLength       = 100
    haveDataBarBacking  = true
    dataBarBackingSize  = 2
     --dataAnimation = nil --this could be useful for animated bars
    
     --count up or down
     timeTicksDown      = false
     countDownMaxTime   = 60
     showTimeUpImage    = false
     timeUpImage        = nil
     timeUpPosition     = nil
    
     --Window Width and Height
     windowWidth = 0
     windowHeight = 0
    
    --time used to animate icon sprites
    animationTime = 0
    
    ui = {
        --General variables
        uiHeadingGeneral    = { order = 1,  type = "boolean", label = "GENERAL SETTINGS:",              default = false },
        displayPosString    = { order = 2,  type = "list",    label = "Where is the HUD positioned?",   default = "WEST",    values = { "CENTER", "NORTHWEST", "NORTH", "NORTHEAST", "WEST", "EAST", "SOUTHWEST", "SOUTH", "SOUTHEAST" } }, 
    
        --Label variables
        uiHeadingLabel      = { order = 10, type = "boolean", label = "LABEL SETTINGS:",                default = false },
        hudLabelTypeString  = { order = 11, type = "list",    label = "What is the label HUD Type?",    default = "NONE",    values = { "NONE", "ICON", "TEXT" } },
        labelOffsetX        = { order = 12, type = "number",  label = "Label X-axis offset",            default = 0 },
        labelOffsetXMinus   = { order = 13, type = "boolean", label = "Is label X offset negative?",    default = false },
        labelOffsetY        = { order = 14, type = "number",  label = "Label Y-axis offset",            default = 0 },
        labelOffsetYMinus   = { order = 15, type = "boolean", label = "Is label Y offset negative?",    default = false },
    
        labelTextString     = { order = 21, type = "string",  label = "(Text Only) Label Text: ",       default = "Label: " },
        labelFontTypeString = { order = 22, type = "string",  label = "(Text Only) Label Font: ",       default = "arial" },
        labelFontSize       = { order = 23, type = "number",  label = "(Text Only) Label Font Size:",   default = 12 },
    
        labelAnimString     = { order = 31, type = "anim",    label = "(Icon Only) Icon Image: ",       default = "x.lua" },
    
        --Data variables
        uiHeadingData       = { order = 40, type = "boolean", label = "DATA SETTINGS:",                 default = false },
        hudDataTypeString   = { order = 41, type = "list",    label = "What is the data HUD Type?",     default = "TEXT",    values = { "NONE", "TEXT", "BAR"  } },
        linkedWithString    = { order = 42, type = "list",    label = "Link to what variable?",         default = "NOTHING", values = { "NOTHING", "HEALTH", "SCORE", "SUGAR", "TIMER" } },
        dataOffsetX         = { order = 43, type = "number",  label = "Data X-axis offset",             default = 0 },
        dataOffsetXMinus    = { order = 44, type = "boolean", label = "Is data X offset negative?",     default = false },
        dataOffsetY         = { order = 45, type = "number",  label = "Data Y-axis offset",             default = 0 },
        dataOffsetYMinus    = { order = 46, type = "boolean", label = "Is data Y Offset negative?",     default = false },
    
        dataFontTypeString  = { order = 51, type = "string",  label = "(Text Only) Data Font: ",        default = "arial" },
        dataFontSize        = { order = 52, type = "number",  label = "(Text Only) Data Font Size:",    default = 12 },
    
        dataBarHeight       = { order = 61, type = "number",  label = "(Bar Only) Bar Height:",         default = 20 },
        dataBarLength       = { order = 62, type = "number",  label = "(Bar Only) Bar Length:",         default = 100 },
        haveDataBarBacking  = { order = 63, type = "boolean", label = "(Bar Only) Have Bar Background?",default = true },
        dataBarBackingSize  = { order = 64, type = "number",  label = "(Bar Only) Bar Background Size:",default = 2 },
        timeTicksDown       = { order = 65, type = "boolean", label = "(Timer Only) Count Down?",       default = false },
        countDownMaxTime    = { order = 66, type = "number",  label = "If countdown, starting time?",   default = 60 },
        showTimeUpImage     = { order = 67, type = "boolean", label = "If countdown, show image at 0?", default = false },
        timeUpImageStr      = { order = 68, type = "anim",    label = "If countdown, time up image? ",  default = "x.lua" },
        timeUpPosX          = { order = 69, type = "number",  label = "Time up image X Position",       default = 0 },
        timeUpPosY          = { order = 70, type = "number",  label = "Time up image Y Position",       default = 0 },
    }
    
    function getHUDTypeFromString( hudTypeStr )
    
        if      ( hudTypeStr == "ICON" ) then
    
            return HUD_ICON
    
        elseif  ( hudTypeStr == "TEXT" ) then
    
            return HUD_TEXT
    
        elseif  ( hudTypeStr == "BAR" ) then
    
            return HUD_BAR
    
        else
    
            return HUD_NONE
    
        end
    end
    
    function getVariableLinkFromString( linkedWithStr )
    
        if      ( linkedWithStr == "HEALTH" ) then
    
            return LINKED_WITH_HEALTH
    
        elseif  ( linkedWithStr == "SUGAR" ) then
    
            return LINKED_WITH_SUGAR
    
        elseif  ( linkedWithStr == "TIMER" ) then
    
            return LINKED_WITH_TIMER
    
        elseif  ( linkedWithStr == "SCORE" ) then
    
            return LINKED_WITH_SCORE
    
        else 
    
            return LINKED_WITH_NADA
    
        end
    end
    
    function getHUDPositionOnScreen( screenPosStr )
    
        local hudPos = { x = 0, y = 0 }
    
        --Start with X position
        if     ( screenPosStr == "WEST" or screenPosStr == "NORTHWEST" or screenPosStr == "SOUTHWEST" ) then
    
            hudPos.x = 0
    
        elseif ( screenPosStr == "NORTH" or screenPosStr == "CENTER" or screenPosStr == "SOUTH" ) then
    
            hudPos.x = windowWidth * 0.5
    
        elseif ( screenPosStr == "EAST" or screenPosStr == "NORTHEAST" or screenPosStr == "SOUTHEAST" ) then
    
            hudPos.x = windowWidth - 150
    
        end
    
        --Then do Y position
        if     ( screenPosStr == "NORTH" or screenPosStr == "NORTHWEST" or screenPosStr == "NORTHEAST" ) then
    
            hudPos.y = 0
    
        elseif ( screenPosStr == "WEST" or screenPosStr == "CENTER" or screenPosStr == "EAST" ) then
    
            hudPos.y = windowHeight * 0.5
    
        elseif ( screenPosStr == "SOUTH" or screenPosStr == "SOUTHWEST" or screenPosStr == "SOUTHEAST" ) then
    
            hudPos.y = windowHeight
    
        end
    
        return hudPos
    
    end
    
    function getOffsetPositionFrom( startPosition, offsetX, minusXOffset, offsetY, minusYOffset )
    
        local offsetPos = { x = 0, y = 0 }
    
        if minusXOffset then
            offsetPos.x = startPosition.x - offsetX
        else
            offsetPos.x = startPosition.x + offsetX
        end
        
        if minusYOffset then
            offsetPos.y = startPosition.y - offsetY
        else
            offsetPos.y = startPosition.y + offsetY
        end
    
        return offsetPos
    end
    
    function removeCollision()
        rigidBody:setCollisionCategory( CollisionCategory_NONE )
        rigidBody:setCollisionMask( CollisionMask_NONE )
    end
    
    
    function init()
        removeCollision()
    
        --Turn off Base icon
        iconVisible = false
    
        --Object HUD is attached to may not always be on screen, so update always is necessary.
        updateAlways = true
    
        if ( g.session.gotWindowSize == nil ) then 
            -- this part is executed once PER GAME, not between different levels
            g.session.gotWindowSize = 0;
            g.session.windowWidth = width()
            g.session.windowHeight = height()
            windowWidth = width()
            windowHeight = height()
        else
            windowWidth = g.session.windowWidth
            windowHeight = g.session.windowHeight
        end
    
        --Set HUD Types
        hudLabelHasType = getHUDTypeFromString( hudLabelTypeString )
        hudDataHasType  = getHUDTypeFromString( hudDataTypeString )
        hudLinkedWith   = getVariableLinkFromString( linkedWithString )
    
        --Set the positions for the HUD
        hudPosition     = getHUDPositionOnScreen( displayPosString )
        labelPosition   = getOffsetPositionFrom( hudPosition, labelOffsetX, labelOffsetXMinus, labelOffsetY, labelOffsetYMinus )
        dataPosition    = getOffsetPositionFrom( hudPosition, dataOffsetX,  dataOffsetXMinus,  dataOffsetY,  dataOffsetYMinus  )
        timeUpPosition  = { x = timeUpPosX, y = timeUpPosY }
    
        --Setup icon
        labelAnimation  = ImageAnim( labelAnimString )
        timeUpImage     = ImageAnim( timeUpImageStr  )
    
        --Setup fonts for text
        --labelFontSize   = g.math.clamp( labelFontSize, 1.0, 100.0 ) -- clamping is causing some issue, not sure what
        if ( labelFontObject == nil ) then
            labelFontObject = Font( labelFontTypeString, labelFontSize )
        end
        --dataFontSize    = g.math.clamp( dataFontSize, 1, 100 )
        if ( dataFontObject == nil ) then
            dataFontObject  = Font( dataFontTypeString, dataFontSize )
        end
    
        if timeTicksDown then
            g.maxLevelTime = countDownMaxTime
        end
    end
    
    function update( dt )
        --Update animation time so that icons will animate properly
        getHUDPositionOnScreen()
        animationTime = animationTime + dt
    end
    
    function render( dt )
        local animationAngle = 0
        if ( showTimeUpImage and g.player.timeInLevel > g.maxLevelTime ) then
            timeUpImage:draw( animationTime, timeUpPosition.x, timeUpPosition.y, animationAngle, Image_COORDS_SCREEN_TOPLEFT )
        end
        --Render UI object label
        if ( hudLabelHasType == HUD_TEXT ) then
    
            drawLine( 0, 0, 0, 0, 1, 1, 1 )
            if ( labelFontObject ~= nil ) then
                labelFontObject:draw( labelPosition.x, labelPosition.y, labelTextString )
            end
    
        elseif ( hudLabelHasType == HUD_ICON ) then
    
            if ( labelAnimation ~= nil ) then
                labelAnimation:draw( animationTime, labelPosition.x, labelPosition.y, animationAngle, Image_COORDS_SCREEN )
            end
    
        end
    
        --Render UI object data
        drawLine( 0, 0, 0, 0, 1, 1, 1 )
        if ( hudDataHasType == HUD_TEXT ) then
    
            local dataText = ""
    
            if     ( hudLinkedWith == LINKED_WITH_HEALTH ) then
    
                dataText = g.tostring( g.player.health.current )
    
            elseif ( hudLinkedWith == LINKED_WITH_SUGAR ) then
    
                dataText = g.tostring( g.math.floor( g.player.maxSpeed.x ) )
    
            elseif ( hudLinkedWith == LINKED_WITH_TIMER ) then
    
                currentTime = 0
    
                if timeTicksDown then
                    currentTime = g.maxLevelTime - g.player.timeInLevel 
                else
                    currentTime = g.player.timeInLevel  
                end
    
                if currentTime < 0 then currentTime = 0 end
                dataText = rawTimeToString( currentTime )
    
            elseif ( hudLinkedWith == LINKED_WITH_SCORE ) then
    
                dataText = g.tostring( g.player.score )
    
            end
    
            if ( dataFontObject ~= nil ) then
                dataFontObject:draw( dataPosition.x, dataPosition.y, dataText )
            end
    
        elseif ( hudDataHasType == HUD_BAR ) then
    
            local dataRatio = 0.0
            local barColor  = { red = 0, green = 0, blue = 0, alpha = 1 }
    
            if     ( hudLinkedWith == LINKED_WITH_HEALTH ) then
    
                dataRatio = g.player.currentHealth / g.player.maxHealth
    
                if ( dataRatio < 0.2 ) then
                    barColor.red = 1
                elseif ( dataRatio < 0.4 ) then
                    barColor.red = 1
                    barColor.green = 1
                else
                    barColor.blue = 1
                end
    
            elseif ( hudLinkedWith == LINKED_WITH_SUGAR ) then
    
                dataRatio = ( g.player.maxSpeed.x - g.player.moveSpeed + 1 ) / ( g.player.moveSpeedCap - g.player.moveSpeed + 1 )
    
                if ( dataRatio < 0.5 ) then
                    barColor.green = 1
                elseif ( dataRatio < 0.70 ) then
                    barColor.green = 1
                else
                    barColor.red = 1
                end
    
            else
    
                dataRatio = 0
    
            end
    
                local barUpperSide  = dataPosition.y - ( dataBarHeight * 0.5 )
                local barLowerSide  = barUpperSide + ( dataBarHeight * 2 ) - dataPosition.y -- do te multiplication times two minus data position because of GuildEd weirdness.
                local barLeftSide   = dataPosition.x
                local barRightSide  = barLeftSide + ( dataRatio * dataBarLength ) - dataPosition.x -- subtract dataPosition.x because of GuildEd weirdness.
    
                local backingColor  = { red = 1, green = 1, blue = 1, alpha = 1 }
                local backUpperSide = barUpperSide - dataBarBackingSize
                local backLowerSide = barLowerSide + dataBarBackingSize * 2
                local backLeftSide  = barLeftSide - dataBarBackingSize
                local backRightSide = dataBarLength + dataBarBackingSize
    
                --draw the bar background
                fillRect( backLeftSide, backUpperSide, backRightSide, backLowerSide, backingColor.red, backingColor.green, backingColor.blue, backingColor.alpha )
    
                --draw the bar itself
                fillRect( barLeftSide, barUpperSide, barRightSide, barLowerSide, barColor.red, barColor.green, barColor.blue, barColor.alpha )
    
        end
    end
    
        
    

  • Player Physics

    Motivation

    From the outset, we wanted our game to be a platformer focused on speed and agility. Thus, our player character has the ability to run, jump, wall jump, and wall slide. My primary focus during development was on making all of the physics for these actions “feel right.”

    Design

    GuildEd had a solid integration setup for its physics, but its collision system had some small issues. In particular, the detection for the ‘on ground’ state was glitchy, and there was no detection for whether the player was touching a wall or not.

    Fortunately, GuildEd did provide a very useful raytracing function, which I used for my solution. I created a pair of raytracing ‘sensors’ on the bottom and both sides of the character. As you can see in the video, they are designed to be very tight to the collision box and shoot in both directions. If either one of the raycasts impacts the environment for that side of the character, it sets the ‘on ground’ or ‘on wall’ state. This setup worked well enough to make it into the final game build.

    Code

    Wall Sliding Detection

        
            
    --Wall-Sliding
    local westSideRay = nil
    local eastSideRay = nil
    local slightlyWestOfObject   = 0
    local slightlyEastOfObject   = 0
    local verticalColliderStart  = 0
    local verticalColliderLength = 0
    timeDirectionHeldWhileSliding = 0
    directionHeldWhileSliding = false
    
    function isNextToWall()
        slightlyInsideWest  = rigidBody:posX() - ( 0.498 * iconWidthInWorldUnits() )
        slightlyOutsideWest = rigidBody:posX() - ( 0.508 * iconWidthInWorldUnits() )
    
        slightlyInsideEast  = rigidBody:posX() + ( 0.498 * iconWidthInWorldUnits() )
        slightlyOutsideEast = rigidBody:posX() + ( 0.508 * iconWidthInWorldUnits() )
    
        colliderUpperPoint  = rigidBody:posY() - ( 0.49 * iconHeightInWorldUnits() )
        colliderLowerPoint  = rigidBody:posY() + ( 0.49 * iconHeightInWorldUnits() )
    
        westSideDownRay = RayB.new( slightlyInsideWest, colliderUpperPoint, slightlyOutsideWest, colliderLowerPoint )
        westSideUpRay   = RayB.new( slightlyInsideWest, colliderLowerPoint, slightlyOutsideWest, colliderUpperPoint )
        eastSideDownRay = RayB.new( slightlyInsideEast, colliderUpperPoint, slightlyOutsideEast, colliderLowerPoint )
        eastSideUpRay   = RayB.new( slightlyInsideEast, colliderLowerPoint, slightlyOutsideEast, colliderUpperPoint )
    
        if ( westSideDownRay:IsCollidingWithWorld() or westSideUpRay:IsCollidingWithWorld() or eastSideDownRay:IsCollidingWithWorld() or eastSideUpRay:IsCollidingWithWorld() ) then
            return true
        end
    
        return false
    end
    
        
    

    Grounding Detection

        
            
    
    function isOnGround()
    
        slightlyInsideBottom  = rigidBody:posY() + ( 0.496 * iconHeightInWorldUnits() )
        slightlyOutsideBottom = rigidBody:posY() + ( 0.506 * iconHeightInWorldUnits() )
    
        colliderWestPoint   = rigidBody:posX() - ( 0.49 * iconWidthInWorldUnits() )
        colliderEastPoint   = rigidBody:posX() + ( 0.49 * iconWidthInWorldUnits() )
    
        footWestRay = RayB.new( colliderEastPoint, slightlyInsideBottom, colliderWestPoint, slightlyOutsideBottom )
        footEastRay = RayB.new( colliderWestPoint, slightlyInsideBottom, colliderEastPoint, slightlyOutsideBottom )
    
        if ( footEastRay:IsCollidingWithWorld() or footWestRay:IsCollidingWithWorld() ) then
            return true
        end
    
        return false
    end
    
        
    

Executables:

Source:

The source for this project is currently unavailable.