Corona – Tower Defense Game Experiments 10

Giving the player the ability to place different types of defenses adds interest to the game. It opens up different types of strategies and different types of game play.

Looking at Plants vs Zombies you will see a wide variety of defense types. Here’s a list of the types of plants available: http://plantsvszombies.wikia.com/wiki/Gallery_of_Plants

Defenses can have a variety of features. Core ideas might be:

  • Damage – how much damage the defense applies to an enemy
  • Rate of fire – how often the defense fires
  • Special effect – A special ability only applied to this type of defense
  • Cost – The energy cost to place the defense

Really you could boil the list down cost vs power. This is the essential formula that creates balance and inspires game play. All of the features, except cost, are a measure of power for a defense. Cost is the balancing factor. Energy is collected over time, gives players the choice to build fewer more expensive defenses, or more less powerful defenses, or build a mix of each.

The key factor to making the game playable is a balance of cost vs power. Adding a mix of special features add a lot of interest to the game, provided the special features work well in cost vs power balance. As the programmer you can make anything happen. You can make the most powerful weapons cost very little. But this makes for an uninteresting playing experience. As a game design this is where you your real work. Finding the right balance of cost vs power for each element while providing some interesting effects.

In the example here I’ll keep the features simple. each defense will have a rate of fire, damage, and a cost.

Rate of fire will set the interval, in milliseconds, between shots for a defense.

Damage will determine how much subtracted from an enemies life property with each hit.

Cost sets the amount of energy that is paid to create this type of defense.

I’ve also added one special feature. One type of defense will also momentarily stop an approaching alien.

defense_type_array

This array will hold a description of each defense type. With properties holding values for each of the features above. Here’s an example:

{name="Standard",	rof=1000,	damage=1,	cost=50}

This is standard defense, it all of the features of the original defense. It fires once every 1000 ms, it subtracts 1 from the life of an enemy with each hit, and costs 50 energy to create.

The name property will be used for special abilities.

I created an array of these tables named defense_type_array:

local defense_type_array = {
			{name="Standard",	rof=1000,	damage=1,	cost=50},
			{name="Rapid",		rof=800,	damage=1,	cost=75},
			{name="Heavy",		rof=2000,	damage=3,	cost=100},
			{name="Stun",		rof=1200,	damage=0.5,	cost=80}
							}

There will be one button for each of items in this array.

We need to be able to identify each defense placed on the board. So far I haven’t used any graphics, so there isn’t a picture for each, yet. For now I will match the color of the button to the color of the defense. When the buttons are created I generate a color for each. As a temporary measure I will put the color in the defense type tables for easy reference.

In the make_defense_button() function add the following:

local function make_defense_buttons()
	for i = 1, #defense_type_array, 1 do
		local button = display.newRoundedRect( 0, 0, 40, 40, 6 )
		local r = 255 * ( i / #defense_type_array )

		-- For this example I'll put the red values in each of these tables
		-- this way we can color each of the defense elements to match the buttons.
		defense_type_array[i].red = r 

		button.index = i
		button:setFillColor( r, 0, 0 )
		button:setStrokeColor( 255, 255, 255 )
		button.x = display.contentWidth - 26
		button.y = 40 + ( i * 50 )
		button:addEventListener( "touch", touch_defense_button )
		table.insert(defense_button_array, button )
		control_group:insert( button )
	end
end

touch_tile()

Touching a tile creates a new defense. This is also where we check the available energy and pay the cost for the defense.

local function touch_tile( event ) -- Check the type of defense
	local phase = event.phase
	if phase == "began" then
		local tile = event.target
		local tile_x = tile.x
		local tile_y = tile.y

		local cost = defense_type_array[current_defense_type].cost -- Get the cost for this defense type

		if energy >= cost then -- Use cost here
			energy = energy - cost -- and here
			local defense = make_defense( tile_x, tile_y )
			defense.col = tile.col
		end
	end
end

make_defense()

Each defense will now have a type and each will have different abilities. Each defense should keep track of it’s features: roy, damage and defense_name.

As an alternative, each defense could hold a reference to the table describing the defense features. Here I chose the first option. If there were many defense features the second option might be better.

local function make_defense( x, y )
	local defense = display.newRect( 0, 0, 32, 32 )

	-- Get all of the properties describing this weapon and store them in the new defense
	defense.rof 		 = defense_type_array[current_defense_type].rof
	defense.damage 		 = defense_type_array[current_defense_type].damage
	defense.defense_name = defense_type_array[current_defense_type].name
	defense.red 		 = defense_type_array[current_defense_type].red

	defense:setFillColor( defense.red, 0, 0 )
	defense_group:insert( defense )
	defense.x = x
	defense.y = y
	table.insert( defense_array, defense )
	defense.timer = timer.performWithDelay( defense.rof, function() defense_defend( defense ) end, -1 )
	return defense
end

Here I just transferred the features across and set them as properties of the defense that was created. Notice that I also got the red property and used this to set the color of the defense. The defense should now match the color of button. Later when images are added we can dump the color.

make_bullet()

Bullets created earlier were all the same. Now we want them to have unique properties described by the defense that fired them. The make_bullet() function doesn’t have a reference to the defense. But, it is called from the defense_defend() function that does have a reference to the defense. To share information between the two I’ll have make_bullet() return a reference to the bullet.

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} )
	return bullet -- better return the bullet so the defense_defend() function can work with it
end

defense_defend()

The defense_defend() function creates bullets. Each bullet will need to know it’s damage value. To get the damage we need to have a reference to the defense. Here we get the bullet that was returned and assign it the damage and the name of the defense type that fired it. The name is important since I’ll use it for special effects.

Damage is easy, instead of subtracting 1 from alien.life. Here we replace this with bullet.damage. In this way each bullet can subtract it’s own damage value.

Next we need to check the defense_name. If the name is “Stun” I need to stop the enemy, and then start again after a short delay. Since the movement is handled by a transition we need to cancel the previous transition. Then add a new transition with a delay. Here I used the ALIEN_SPEED constant to make sure the enemy is moving at the same speed.

To get the speed right I needed to know the distance. To get the remaining distance to move the enemy I subtracted the current y from the alien_target_y value.

local function defense_defend( defense )
	for i = 1, #alien_array, 1 do
		local alien = alien_array[i]
		if alien.col == defense.col then
			local bullet = make_bullet( defense.x, defense.y ) -- Get the bullet
			bullet.damage = defense.damage 	-- assign a damage value to the bullet
			bullet.defense_name = defense.defense_name 		-- get the name of the defense
			break
		end
	end
end

make_alien()

I want to add a weapon with a special feature. One of the weapons will “Stun” an enemy. This will have the effect of stopping the enemy momentarily. To make this effect happen the current transition will have to be removed from the enemy and replaced with a new transition. The new transition will have a delay.

The challenge here is making the enemy continue moving at the same speed to the same end point as before it was stopped. To make this happen I need to know the end point, and know the rate of movement.

The end point for enemies was calculated in the make_alien() function previously. To make things more organized and efficient I defined a variable to hold the ending y value for enemies.

local alien_target_y = ( TILE_SIZE + TILE_MARGIN ) * TILE_ROWS -- Ending y value for aliens

This would need to be defined after TILE_SIZE, TILE_MARGIN and TILE_ROWS are defined. This value will probably not change, so it could have be declared as a constant I suppose.

Enemies will also need a rate of movement in px per sec. I defined this at the top as a constant.

local ALIEN_SPEED = 1000 / 20 	-- Assign an alien speed 20px per sec

Multiplying this by the distance will give us the time in ms that enemies will move at a rate of 20 px per sec.

Now we need to retool the make_alien function() to make use of these new variables.

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
	-- Change the alien movement
	local t = alien_target_y * ALIEN_SPEED
	alien.transition = transition.to( alien, {y=alien_target_y, time=t, onComplete=remove_alien} )
	alien_group:insert( alien )
	table.insert( alien_array, alien )
end

Since enemies start at y of 0. The distance then move is alien_target_y. Multiply this by the ALIEN_SPEED and we get the time in ms for the transition. Notice that I removed some of the other code that was here from the previous example.

check_bullet()

This function checks for bullets hitting enemies. We need to add two new features here: apply damage for specific bullet/defense types, and any special features for any specific bullet type. In this example only the “Stun” type defense will have a special effect.

The

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 - bullet.damage -- Use the damage value of the bullet
					local defense_name = bullet.defense_name -- Get the name of the defense
					-- Check the defense name and apply special stuff
					if defense_name == "Stun" then
						transition.cancel( alien.transition ) 				 -- Cancel the current transition
						local t = ( alien_target_y - alien.y ) * ALIEN_SPEED -- Calculate a new speed
						-- Add a new transition with a delay
						alien.transition = transition.to( alien, { y=alien_target_y,
																	time=t,
																	delay=300,	-- Delay
																	onComplete=remove_alien } )
					end 

				else
					remove_alien( alien )
				end
				remove_bullet( bullet )
				break
			end
		end
	end
end

Here’s a listing of the entire code used so far.

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

-- This example allows the creation of different types of defenses.
-- Each defense type has different features. 

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 ALIEN_SPEED = 1000 / 20 	-- Assign an alien speed 20 px per sec.
local ENERGY_RECHARGE_RATE = 1
-- local DEFENSE_ENERGY_COST = 50 -- Remove this the cost is now in the array below
local ENERGY_TIMER_TIME = 150

local alien_timer
local energy_timer
local energy = 0
local current_defense_type = 1
local energy_text
local alien_target_y = ( TILE_SIZE + TILE_MARGIN ) * TILE_ROWS -- Ending y value for aliens

local defense_array = {}
local alien_array = {}
local bullet_array = {}
-- Define an array of tables containing properties to be applied to each weapon
local defense_type_array = {
			{name="Standard",	rof=1000,	damage=1,	cost=50},
			{name="Rapid",		rof=800,	damage=1,	cost=75},
			{name="Heavy",		rof=2000,	damage=3,	cost=100},
			{name="Stun",		rof=1200,	damage=0.5,	cost=80}
							}

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

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

local function select_defense_button()
	for i = 1, #defense_button_array, 1 do
		local button = defense_button_array[i]
		if button.index == current_defense_type then
			button.strokeWidth = 3
		else
			button.strokeWidth = 0
		end
	end
end

local function touch_defense_button( event )
	local button = event.target
	current_defense_type = button.index
	select_defense_button()
end

local function make_defense_buttons()
	for i = 1, #defense_type_array, 1 do
		local button = display.newRoundedRect( 0, 0, 40, 40, 6 )
		local r = 255 * ( i / #defense_type_array )

		-- For this example I'll put the red values in each of these tables
		-- this way we can color each of the defense elements to match the buttons.
		defense_type_array[i].red = r 

		button.index = i
		button:setFillColor( r, 0, 0 )
		button:setStrokeColor( 255, 255, 255 )
		button.x = display.contentWidth - 26
		button.y = 40 + ( i * 50 )
		button:addEventListener( "touch", touch_defense_button )
		table.insert(defense_button_array, button )
		control_group:insert( button )
	end
end

make_defense_buttons()
select_defense_button()

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} )
	return bullet -- better return the bullet so the defense_defend() function can work with it
end

local function defense_defend( defense )
	for i = 1, #alien_array, 1 do
		local alien = alien_array[i]
		if alien.col == defense.col then
			local bullet = make_bullet( defense.x, defense.y ) -- Get the bullet
			bullet.damage = defense.damage 	-- assign a damage value to the bullet
			bullet.defense_name = defense.defense_name 		-- get the name of the defense
			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 )

	-- Get all of the properties describing this weapon and store them in the new defense
	defense.rof 		 = defense_type_array[current_defense_type].rof
	defense.damage 		 = defense_type_array[current_defense_type].damage
	defense.defense_name = defense_type_array[current_defense_type].name
	defense.red 		 = defense_type_array[current_defense_type].red

	defense:setFillColor( defense.red, 0, 0 )
	defense_group:insert( defense )
	defense.x = x
	defense.y = y
	table.insert( defense_array, defense )
	defense.timer = timer.performWithDelay( defense.rof, function() defense_defend( defense ) end, -1 )
	return defense
end

local function touch_tile( event ) -- Check the type of defense
	local phase = event.phase
	if phase == "began" then
		local tile = event.target
		local tile_x = tile.x
		local tile_y = tile.y

		local cost = defense_type_array[current_defense_type].cost -- get the cost for this defense type

		if energy >= cost then -- Use cost here
			energy = energy - cost -- and here
			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
	-- Change the alien movement
	local t = alien_target_y * ALIEN_SPEED
	alien.transition = transition.to( alien, {y=alien_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 - bullet.damage -- Use the damage value of the bullet
					local defense_name = bullet.defense_name -- Get the name of the defense
					-- Check the defense name and apply special stuff
					if defense_name == "Stun" then
						transition.cancel( alien.transition ) 				 -- Cancel the current transition
						local t = ( alien_target_y - alien.y ) * ALIEN_SPEED -- Calculate a new speed
						-- Add a new transition with a delay
						alien.transition = transition.to( alien, { y=alien_target_y,
																	time=t,
																	delay=300,	-- Delay
																	onComplete=remove_alien } )
					end 

				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 *