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 )