Corona – Tower Defense Game Experiments 6.2

plants vs Zombies works on a strict grid with enemies advancing in rows, and defenses firing only on their row. Other tower defense games allow defenses to fire at any enemy within range. The last post tested an example that had defenses firing only within their row. In this example I will create defenses that fire at any enemy within range.

Get Distance to enemy

To get the distance to an enemy in pixels we can take the Pythagorean formula: a2 + b2 = c2. In Lua terms this might look like:

c = math.sqrt((a*a)+(b*b))

To make this more useful and easier to work with we can wrap it in a function that returns the distance between two points. The example below takes two sets of x and y values and returns the distance between these points.

local function get_distance( x1, y1, x2, y2 )
	local dx = x1 - x2
	local dy = y1 - y2
	return math.sqrt( (dx*dx) + (dy*dy) ) -- Return the distance
end

If you’re feeling Object Oriented, you could make a a point object and pass that to the function. Lua is not an Object Oriented language so we would need to use a table and factory function, but this has a very similar arrangement.

local function Point( x, y )
	return {x=x, y=y}
end

local function get_distance( point_1, point_2 )
	local dx = point_1.x - point_2.x
	local dy = point_1.y - point_2.y
	return math.sqrt( (dx*dx) + (dy*dy) ) -- Return the distance
end

With this method, using either of the two functions above, a defense can make the decision to fire on an enemy when that enemy is within range.


Deciding when to fire

The defense_defend( defense ) function is called from the timer assigned to a defense. Here we need to examine each enemy and measure the distance between the enemy and the defense. If the range is less a specified range we have the defense fire at that enemy.

I set the range of the defense as a “constant” at the top of script for easy use.

local DEFENSE_RANGE = 250

Next I need to modify defense_defend(defense) to work with the get_distance(x1,y1,x2,y2) function above. I used the first function in this example. Here we loop through the alien_array and look at the distance to each alien. If there is an alien within range we fir on it and break the loop.

-- This new function will look at the board and find eligible targets
local function defense_defend( defense )
	for i = 1, #alien_array, 1 do 		-- Loop through alien_array
		local alien = alien_array[i]	-- get the alien

		if get_distance( defense.x, defense.y, alien.x, alien.y ) < DEFENSE_RANGE then -- Check the distance
			make_bullet( defense.x, defense.y, alien.x, alien.y )
			break
		end
	end
end

Since get_distance(x1,y1,x2,y2) returns the distance as a number we can place this function call inside the if expression:

if get_distance( defense.x, defense.y, alien.x, alien.y ) < DEFENSE_RANGE then ...

I will also have to modify the make_bullet() function to allow it fire on a target anywhere on the screen. You’ll notice this function has been modified from make_bullet(x,y) to make_bullet(x1,y1,x2,y2).

Sending bullet toward the target

Using this method bullets will be fired across columns. This means I will have to animate both the x and the y of the bullet. In the previous versions I had only animated the y value of the bullet. In these versions bullets were animated to a y of 0, targeting the top edge of the screen.

To make this happen I need to modify the make_bullet( x, y ) function. Before the function only had to send a bullet to the top of the screen, in which case all that was needed was a starting position. Now it needs to know the position of the target also. So here we’ll modify make_bullet(x,y) to make_bullet( start_x, start_y, target_x, target_y ).

Since the distance to the target will always be different we’ll need to get the distance again and multiply by the constant BULLET_SPEED, thus making sure the bullets move at a consistent speed no matter what the distance.

Here’s a new version of the make_bullet() function

local function make_bullet( start_x, start_y, target_x, target_y )
	local bullet = display.newCircle( 0, 0, 5 )
	bullet:setFillColor( 0, 0, 0 )
	bullet.x = start_x
	bullet.y = start_y
	table.insert( bullet_array, bullet )

	-- The bullet speed and transition need to be retooled
	-- We will end up calling get_distance twice, we could optimize by passing the distance from defense_defend
	local d = get_distance( start_x, start_y, target_x, target_y )  -- Get the distance to the target
	local bt = d * BULLET_SPEED 

	bullet.transition = transition.to( bullet, {y=target_y, x=target_x, time=bt, onComplete=remove_bullet} )
end

Following the flow of code here, the defense_defend() function calls on get_distance(). If an enemy is within the distance we move on to make_bullet(). Where make_bullet() calls on get_distance() again. There’s room for a small performance optimization here. We could pass the distance to make_bullet() and not have to do the math again. I’ll things as they are for now, and may be address this in a future update.

Here’s a full listing of the code for this example:

-----------------------------------------------------------------------------------------
--
-- main.lua
--
-----------------------------------------------------------------------------------------

-- Experiments controlling the firing habits of defense elements
-- This time defense elements only fire at enemies in their row

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 DEFENSE_RANGE = 250 -- Set the range for defense elements in pixels

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()

game_group:insert( tile_group )
game_group:insert( defense_group )
game_group:insert( alien_group )

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

-- This function gets the distance between two points
local function get_distance( x1, y1, x2, y2 )
	local dx = x1 - x2
	local dy = y1 - y2
	return math.sqrt( (dx*dx) + (dy*dy) ) -- Return the distance
end 

local function make_bullet( start_x, start_y, target_x, target_y )
	local bullet = display.newCircle( 0, 0, 5 )
	bullet:setFillColor( 0, 0, 0 )
	bullet.x = start_x
	bullet.y = start_y
	table.insert( bullet_array, bullet )

	-- The bullet speed and transition need to be retooled
	-- We will end up calling get_distance twice, we could optimize by passing the distance from defense_defend
	local d = get_distance( start_x, start_y, target_x, target_y )  -- Get the distance to the target
	local bt = d * BULLET_SPEED 

	bullet.transition = transition.to( bullet, {y=target_y, x=target_x, time=bt, onComplete=remove_bullet} )
end

-- This new function will look at the board and find eligible targets
local function defense_defend( defense )
	for i = 1, #alien_array, 1 do 		-- Loop through alien_array
		local alien = alien_array[i]	-- get the alien

		if get_distance( defense.x, defense.y, alien.x, alien.y ) < DEFENSE_RANGE then -- Check the distance  			make_bullet( defense.x, defense.y, alien.x, alien.y ) 			break 		end  	end  end local function remove_defense( defense ) 	local index = table.indexOf( defense_array, defense )  	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 )  	 	-- Change the timer handler to call defense_defend( 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 	 		local defense = make_defense( tile_x, tile_y ) -- Get a reference to the new defense 		defense.col = tile.col 						   -- assign the col to the new defense 	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 -- Tiles need to know 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 ) -- Oops! I had previously used row instead of col here! 	alien.col = col 	-- Assign this alien his col position 	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 ) 	if x > bounds.xMin and x < bounds.xMax and y > bounds.yMin and y < bounds.yMax then 		return true 	else  		return false 	end end  local function on_enterframe( event )  	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

Runtime:addEventListener( "enterFrame", on_enterframe )

make_grid()

local 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 *