DESCRIPTION

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!

QUICK STATISTICS:

Role: Sole Programmer

Team: Team Yarnball

  • Size: 6 Developers
    • 1 Artist
    • 3 Level Designers
    • 1 Producer
    • 1 Programmer

Engine: GuildEd

Platforms: Windows

Development Time: 3 months

 

Sole 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.
 

  • 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
    

    Back to top of code tabs

  • 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 Snippets:

    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
    

    Back to top of code tabs

  • 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
    

    Back to top of code tabs
 

Source:

Due to the fact that GuildEd is a proprietary engine that is exclusive to the Guildhall, I cannot provide the full source.