Corona – Tower Defense Game Experiments 5

Destroying Aliens with bullets

In the last post I created bullets that fire from a defense at advancing enemies. I still need to check if an enemy is hit, and if so reduce it’s life, and remove it if it has reached 0 life.

I see many example that would use physics for the collision detection for this. I feel that physics may be over kill for what I am doing here. it’s also my guess that using physics would add a lot of processing overhead. Physics also brings up other problems that need to be solved. All in all I physics, in this case will create more problems than it solves. So, I’m going to opt to use a hit test and manage bullets and alien enemies in arrays.

A few notes on code styles. The code is now getting longer and so I decided to make a few style changes to help me keep organized. Constants, these are variables that will not change value. These will be UPPER_CASE. Lua doesn’t have actual constants, I’m using upper case as a reminder.

Hit Test – Bullets vs Aliens

For each bullet fired, I will need to check if that bullet hits an enemy. To determine a hit take the x and y location of the bullet and check if it is inside the bounds of the enemy. The bounds of an object is represented by four values that define the edges of the rectangle surrounding the object. Use display.contentBounds to get the bounds object. This object will have properties of:

  • xMin (defines the left edge)
  • xMax (defines the right edge)
  • yMin (defines the top edge)
  • yMax (defines the bottom edge)

To determine if a point, an x and y pair, is within the bounds of an object use an if statement to compare the bounds properties and the x and y of the point. Check if the x value is greater than xMin and less than xMax, while the y value is greater than yMin and less than yMax. Something like this:

if x > bounds.xMin and x < bounds.xMax and y > bounds.yMin and y < bounds.yMax then
... HIT! ...
end

Checking for a single hit is simple and straight forward. With multiple bullets and enemies on the screen at the same time the problem is a more complicated. For each bullet we need to check for a hit against each enemy. To organize this, all bullets need to be in an array and all enemies in another array. In the future I may want enemies to attack defenses so, I made an array to hold defenses also.

local defense_array = {}
local alien_array = {}
local bullet_array = {}

With the bullets in an array and the enemies in another array we can look at each bullet and perform a hit test against each enemy with that bullet.

Making Bullets

I used the same strategy for bullets that was used for other display elements tiles, aliens and defenses. I need a function to make bullets and another to remove them.

BULLET_SPEED

I used a transition to move the bullets up the screen. The time it takes a bullet to move up the screen will vary by the distance the bullet has to travel. To get the bullets to move at a consistent rate I created the variable BULLET_SPEED. This is a “constant”, that is it’s value will NOT change while the program runs. Corona and Lua do not support actual constants, but this is the intent for this variable, so I will use a style to mark it as such.

I set the speed in pixels per millisecond (1/1000 of a second). I decided on a speed of 400 pixels per second. In milliseconds this becomes 1000 / 400.

local BULLET_SPEED = 1000 / 400

To create transition that moves at this constant rate multiply the distance by BULLET_SPEED and set this value as the time of the transition.

Bullets will be black circles, for now.

Here are two functions to make and remove bullets.

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

To detect a hit I need to look at the position of each bullet and alien and apply a hit test each frame. I added an enterFrame event and handler. I also separated the hit test into it’s own function since it might be useful for other purposes in the future. Note the hit test returns true on a hit or false. This way you can place a hit test inside of an if statement, as in the code below:

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 )

Here’s a full code listing so far.

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

-- This example adds bullets fired by defense elements. 

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 -- ms / distance in px 

local defense_array = {} 	-- Make an array to hold the defenses
local alien_array = {}		-- make an array to hold enemies.
local bullet_array = {}		-- make an array for bullets 

local game_group = display.newGroup()
local defense_group = display.newGroup() 	-- make a group to hold each class
local alien_group = display.newGroup()		--
local tile_group = display.newGroup()

game_group:insert( tile_group )		-- Add each group to the main 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

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 remove_defense( defense )
	local index = table.indexOf( defense_array, defense ) -- get the index of this element
	table.remove( defense_array, index )				  -- delete this element from the array
	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 ) -- Add defense to the proper group

	defense.x = x
	defense.y = y

	table.insert( defense_array, defense ) -- Add this defense to the array

	defense.timer = timer.performWithDelay( 1000, function() make_bullet( defense.x, defense.y ) 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

		make_defense( tile_x, tile_y )
	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.has_defense = false -- mark this tile as NOT having a defense when we begin. 

		 	tile:addEventListener( "touch", touch_tile )
		 	tile_group:insert( tile ) -- Add tiles to the tile group
		 end
	end
end

local function remove_alien( alien )
	local index = table.indexOf( alien_array, alien ) -- Get the index
	transition.cancel( alien.transition )
	table.remove( alien_array, index )	-- Remove the element
	display.remove( alien )
end 

local function make_alien()
	local alien = display.newRect( 0, 0, 32, 32 )
	alien:setFillColor( 0, 200, 0 )

	local row = math.random( 1, TILE_COLS )
	alien.x = row * ( TILE_SIZE  + TILE_MARGIN )
	alien.y = 0
	alien.life = 5 -- Assign each alien a life of 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 ) -- Add aliens to the alien group
	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 *