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