A key factor to the tower defense game is the economy. If there is no limiting factor to how fast defenses can be created there isn’t much challenge since a player can quickly fill the board with defenses.
There needs to be a limiting factor in how often a player can place a defense. Here I will set up a simple system with an energy value that increases over time and incur an energy cost for placing defenses.
The key to making this system form an interesting game is balancing all of the factors. All of the factors need to be considered. Here’s a list:
- Speed of enemies – Faster enemies are harder to deal with
- Speed of bullets – Faster bullets can take care of closer range enemies before a defense is destroyed
- Defense rate of fire – Defenses that fire faster are more powerful
- Energy cost to place a defense – Lower cost allows defense to be placed more often
- Energy Recharge rate – The faster your energy recharges the more often you can place a defense
Note: Setting a cap on total available energy would be another idea to interest to game play. I didn’t do that here but it would not be hard to add.
This example will add a variable to track energy. Energy will recharge over time at a fixed rate. Placing a defense incurs an energy cost.
First I added a few constants at the top of the script to set fixed values.
Energy recharge rate sets the amount of energy is added each timer the energy timer updates. Energy defense cost sets the value subtracted from energy each time a defense is placed. Energy timer time sets the time in milliseconds that energy is recharged, added to energy.
local ENERGY_RECHARGE_RATE = 1 -- Set the rate to recharge local DEFENSE_ENERGY_COST = 50 -- Set the cost for a defense local ENERGY_TIMER_TIME = 150 -- Timer to recharge energy
We also need a few variables to keep track of energy and timers. Energy holds your current energy. Energy timer holds a reference to the timer that updates energy.
Note, I moved the alien timer to the top of my script to keep all of the timers together. This variable is not new.
local alien_timer -- Moved this to the top local energy_timer -- reference the energy timer local energy = 0 -- set the starting energy
I needed a text field to display the current energy value. I defined this at the top since it might be accessed from any where.
local energy_text -- Declare a text field to display energy
I thought it might be good to have the energy display and other UI elements in their own group. So I added a group to hold these:
local control_group = display.newGroup() -- Add a group for UI stuff
Next I need to add a function to update the energy display, and a function to increase the energy value. I separated these into two functions thinking it might be more flexible in the future if I need to include code to position and adjust the position of the text field with each update.
The update_energy() function displays the current energy value in the text field.
The energy_recharge() function increase the energy value, then calls update_energy() to refresh the display.
In this arrangement the energy display is only updated when energy is increased. This works if the energy recharge rate is not too slow. If the recharge rate is slow you you could place a defense and not see the energy value update until the recharge timer updates energy. If this is effecting you, add a call to update_energy() to make_defense().
Last I added create the energy timer. Here we create a new timer that updates at the rate set by ENERGY_TIMER_TIME. With each update it calls energy_recharge(), and loops forever.
local function update_energy() energy_text.text = energy end local function energy_recharge() energy = energy + ENERGY_RECHARGE_RATE update_energy() end energy_timer = timer.performWithDelay( ENERGY_TIMER_TIME, energy_recharge, -1 )
Last, I need to incur the energy cost for placing a defense element. Basically this means checking that there is sufficient energy to pay for a defense. If there is sufficient energy, subtract the cost from energy and create the defense.
New defense elements are created in the touch_tile() handler. Here I added a little logic to check the energy, subtract the energy cost and place the new defense.
local function touch_tile( event ) local phase = event.phase if phase == "began" then local tile = event.target local tile_x = tile.x local tile_y = tile.y if energy >= DEFENSE_ENERGY_COST then -- Check the energy energy = energy - DEFENSE_ENERGY_COST -- Pay energy for defense local defense = make_defense( tile_x, tile_y ) defense.col = tile.col end end end
Still the game is lacking many features that would make it truly interesting to play. That said, what we have here is an outline of the mechanics that cover many of the elements that make the game function. Building on these ideas, adding compelling art, and a theme is what will make the game interesting.
Here’s the full source code
I still have the memory display at the bottom of the script. It’s good to keep an eye on this while building the game. If it ever looks like the memory is increase constantly and not leveling off over time we have a problem. Finding these types of problems earlier is better than trying to search them out after you think you are finished.
----------------------------------------------------------------------------------------- -- -- main.lua -- ----------------------------------------------------------------------------------------- -- This version adds an energy economy to limit how often defenses can be placed. display.setStatusBar( display.HiddenStatusBar ) local TILE_ROWS = 9 local TILE_COLS = 5 local TILE_SIZE = 48 local TILE_MARGIN = 1 local BULLET_SPEED = 1000 / 400 local ENERGY_RECHARGE_RATE = 1 -- Set the rate to recharge local DEFENSE_ENERGY_COST = 50 -- Set the cost for a defense local ENERGY_TIMER_TIME = 150 -- Timer to recharge energy local alien_timer -- Moved this to the top local energy_timer -- reference the energy timer local energy = 0 -- set the starting energy local energy_text -- Declare a text field to display energy local defense_array = {} local alien_array = {} local bullet_array = {} local game_group = display.newGroup() local defense_group = display.newGroup() local alien_group = display.newGroup() local tile_group = display.newGroup() local control_group = display.newGroup() -- Add a group for UI stuff game_group:insert( tile_group ) game_group:insert( defense_group ) game_group:insert( alien_group ) -- Draw energy energy_text = display.newText( energy, 0, 0, native.systemFont, 16 ) control_group:insert( energy_text ) energy_text:setTextColor( 0, 255, 0 ) energy_text.x = 300 energy_text.y = 40 local function update_energy() energy_text.text = energy end local function energy_recharge() energy = energy + ENERGY_RECHARGE_RATE update_energy() end energy_timer = timer.performWithDelay( ENERGY_TIMER_TIME, energy_recharge, -1 ) local function remove_bullet( bullet ) local index = table.indexOf( bullet_array, bullet ) transition.cancel( bullet.transition ) table.remove( bullet_array, index ) display.remove( bullet ) end local function make_bullet( x, y ) local bullet = display.newCircle( 0, 0, 5 ) bullet:setFillColor( 0, 0, 0 ) bullet.x = x bullet.y = y table.insert( bullet_array, bullet ) local bt = y * BULLET_SPEED bullet.transition = transition.to( bullet, {y=0, time=bt, onComplete=remove_bullet} ) end local function defense_defend( defense ) for i = 1, #alien_array, 1 do local alien = alien_array[i] if alien.col == defense.col then make_bullet( defense.x, defense.y ) break end end end local function remove_defense( defense ) local index = table.indexOf( defense_array, defense ) timer.cancel( defense.timer ) table.remove( defense_array, index ) display.remove( defense ) end local function make_defense( x, y ) local defense = display.newRect( 0, 0, 32, 32 ) defense:setFillColor( 200, 0, 0 ) defense_group:insert( defense ) defense.x = x defense.y = y table.insert( defense_array, defense ) defense.timer = timer.performWithDelay( 1000, function() defense_defend( defense ) end, -1 ) return defense end local function touch_tile( event ) local phase = event.phase if phase == "began" then local tile = event.target local tile_x = tile.x local tile_y = tile.y if energy >= DEFENSE_ENERGY_COST then -- Check the energy energy = energy - DEFENSE_ENERGY_COST -- Pay energy for defense local defense = make_defense( tile_x, tile_y ) defense.col = tile.col end end end local function make_grid() for row = 1, TILE_ROWS, 1 do for col = 1, TILE_COLS, 1 do local tile = display.newRect( 0, 0, TILE_SIZE, TILE_SIZE ) tile.x = ( TILE_SIZE + TILE_MARGIN ) * col tile.y = ( TILE_SIZE + TILE_MARGIN ) * row tile.col = col tile.has_defense = false tile:addEventListener( "touch", touch_tile ) tile_group:insert( tile ) end end end local function remove_alien( alien ) local index = table.indexOf( alien_array, alien ) transition.cancel( alien.transition ) table.remove( alien_array, index ) display.remove( alien ) end local function make_alien() local alien = display.newRect( 0, 0, 32, 32 ) alien:setFillColor( 0, 200, 0 ) local col = math.random( 1, TILE_COLS ) alien.col = col alien.x = col * ( TILE_SIZE + TILE_MARGIN ) alien.y = 0 alien.life = 5 local target_y = ( TILE_SIZE + TILE_MARGIN ) * TILE_ROWS local t = ( TILE_ROWS + 1 ) * 2000 alien.transition = transition.to( alien, {y=target_y, time=t, onComplete=remove_alien} ) alien_group:insert( alien ) table.insert( alien_array, alien ) end local function hit_test( x, y, bounds ) return x > bounds.xMin and x < bounds.xMax and y > bounds.yMin and y < bounds.yMax end local function hit_test_bounds( bounds1, bounds2 ) return bounds1.xMin < bounds2.xMax and bounds1.xMax > bounds2.xMin and bounds1.yMin < bounds2.yMax and bounds1.yMax > bounds2.yMin end local function check_bullets() for b = 1, #bullet_array, 1 do local bullet = bullet_array[b] if b > #bullet_array then return end for a = 1, #alien_array, 1 do local alien = alien_array[a] if hit_test( bullet.x, bullet.y, alien.contentBounds ) then if alien.life > 0 then alien.life = alien.life - 1 else remove_alien( alien ) end remove_bullet( bullet ) break end end end end local function check_enemies() for a = 1, #alien_array, 1 do local alien = alien_array[a] for d = 1, #defense_array, 1 do local defense = defense_array[d] if hit_test_bounds( alien.contentBounds, defense.contentBounds ) then remove_defense( defense ) break end end end end local function on_enterframe( event ) check_bullets() check_enemies() end Runtime:addEventListener( "enterFrame", on_enterframe ) make_grid() alien_timer = timer.performWithDelay( 5300, make_alien, -1 ) ------------------------------------------------------------------------------------- local memory_text = display.newText( "Hello", 5, 5, native.systemFont, 16 ) memory_text:setTextColor( 255, 0, 0 ) memory_text.x = display.contentCenterX local monitorMem = function() collectgarbage() local textMem = system.getInfo( "textureMemoryUsed" ) / 1000000 memory_text.text = "Mem:"..collectgarbage("count") .. " tex:".. textMem end Runtime:addEventListener( "enterFrame", monitorMem )