Corona – Tower Defense Game Experiments 8

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 )

Leave a Reply

Your email address will not be published. Required fields are marked *