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 )