Welcome to step four. In this step we’ll make plants fire and eventually kill zombies.
Let’s start defining when a plant can fire:
* When there is at least one zombie on the same row the plant is placed
* When it’s not already firing (plants can fire only one bullet at once)
* When a certain amount of time passed since the last time the plant fired
Now let’s define the bullet life:
* The bullet flies from left to right
* The bullet is removed when it hits a zombie
* The bullet is removed when it flies outside the stage
These six concepts bring some great changes in our script, and I tried to organize it in the clearest way possible, while being conscious that putting all the code in a single class starts making the script a bit messy. Anyway, I tried to do my best to keep it readable.
Before showing you the script, you have to say I created a new object called bulletMc
which is the bullet itself.
Ready to see an almost 300 lines long code?
package { import flash.display.Sprite; import flash.utils.Timer; import flash.events.TimerEvent; import flash.events.MouseEvent; import flash.events.Event; import flash.text.TextField; public class Main extends Sprite { // // arrays to store game field information // private var plantsArray:Array;// plants placed in the game field private var zombiesArray:Array;//zombies placed in the game field // // timers // private var flowersTimer:Timer=new Timer(5000);// timer to make flowers fall down private var zombieTimer:Timer=new Timer(5000);// timer to make zombies come in // // containers // private var sunContainer:Sprite=new Sprite();// container for all suns private var plantContainer:Sprite=new Sprite();// container for all plants public var bulletContainer:Sprite=new Sprite();// container for all bullets private var zombieContainer:Sprite=new Sprite();// container for all zombies private var overlayContainer:Sprite=new Sprite();// container of all overlays // // actors // private var movingPlant:plantMc;// plant the player can drag on game field private var selector:selectorMc;// the selector square to show the playere where he's going to place the plant // // other variables // private var money:uint=0;// amout of money owned by the player private var moneyText:TextField=new TextField ;// dynamic text field where to show player's money private var playerMoving:Boolean=false;// Boolean variable to tell us if the player is moving a plant (true) or not (false) public function Main():void { setupField();// initializes the game drawField();// draws the game field fallingSuns();// initializes the falling suns addPlants();// initialized the plants addZombies();// initializes the zombies addEventListener(Event.ENTER_FRAME,onEnterFrm); } // // game field setup. arrays to store plants and zombies information are created // private function setupField():void { plantsArray=new Array(); for (var i:uint=0; i<5; i++) { plantsArray[i]=new Array(); for (var j:uint=0; j<9; j++) { plantsArray[i][j]=0; } } zombiesArray=new Array(0,0,0,0,0); } // // showing the amount of money // private function updateMoney():void { moneyText.text="Money: "+money.toString(); } // // game field drawing and children hierarchy management // private function drawField():void { var fieldSprite:Sprite=new Sprite(); var randomGreen:Number; addChild(fieldSprite); fieldSprite.graphics.lineStyle(1,0xFFFFFF); for (var i:uint=0; i<5; i++) { for (var j:uint=0; j<9; j++) { randomGreen=(125+Math.floor(Math.random()*50))*256; fieldSprite.graphics.beginFill(randomGreen); fieldSprite.graphics.drawRect(25+65*j,80+75*i,65,75); } } addChild(sunContainer); addChild(plantContainer); addChild(bulletContainer); addChild(zombieContainer); addChild(overlayContainer); overlayContainer.addChild(moneyText); updateMoney(); moneyText.textColor=0xFFFFFF; moneyText.height=20; } // // zombies initialization // private function addZombies():void { zombieTimer.start(); zombieTimer.addEventListener(TimerEvent.TIMER,newZombie); } // // adding a new zombie // private function newZombie(e:TimerEvent):void { var zombie:zombieMc=new zombieMc();// constructs the zombie zombieContainer.addChild(zombie);// adds the zombie zombie.zombieRow=Math.floor(Math.random()*5);// chooses a random row where to place the zombie zombiesArray[zombie.zombieRow]++;// increases the number of zombies in the row-th row zombie.x=660;// places the zombie on the board, outside the stage to the right zombie.y=zombie.zombieRow*75+115; } // // suns initialization // private function fallingSuns():void { flowersTimer.start(); flowersTimer.addEventListener(TimerEvent.TIMER, newSun); } // // adding a new sun // private function newSun(e:TimerEvent):void { var sunRow:uint=Math.floor(Math.random()*5);// random row var sunCol:uint=Math.floor(Math.random()*9);// random column var sun:sunMc = new sunMc();// constructs the sun sun.buttonMode=true;// makes the mouse change shape when over the plant sunContainer.addChild(sun);// adds the sun sun.x=52+sunCol*65;// places the sun in the proper column sun.destinationY=130+sunRow*75;// definines the sun y destination point sun.y=-20;// places the sun out to the upper side of the stage sun.addEventListener(MouseEvent.CLICK,sunClicked);// listener to be triggered when the sun is clicked } // // handling clicks on suns // private function sunClicked(e:MouseEvent):void { e.currentTarget.removeEventListener(MouseEvent.CLICK,sunClicked);// removes the CLICK listener money+=5;// makes the player earn money (5) updateMoney();// updates money text var sunToRemove:sunMc=e.currentTarget as sunMc;// defines which sun we need to remove sunContainer.removeChild(sunToRemove);// removes the sun } // // building the plant toolbar (only 1 plant at the moment) // private function addPlants():void { var plant:plantMc=new plantMc();// constructs a new plant overlayContainer.addChild(plant);// adds the plant plant.buttonMode=true;// makes the mouse change shape when over the plant plant.x=90; plant.y=40; plant.addEventListener(MouseEvent.CLICK,onPlantClicked);// listener to be triggered once the plant is clicked } // // handling clicks on plants // private function onPlantClicked(e:MouseEvent):void { // let's see if the player has enough money (10) to afford the plant and isn't currently dragging a plant if (money>=10&&! playerMoving) { money-=10;// pays the plant updateMoney();// updates money text selector=new selectorMc();// constructs a new selector selector.visible=false;// makes the selector invisible overlayContainer.addChild(selector);// adds the selector movingPlant=new plantMc();// constructs a new moving plant movingPlant.addEventListener(MouseEvent.CLICK,placePlant);// lister to be triggered once the moving plant is clicked overlayContainer.addChild(movingPlant);// adds the moving plant playerMoving=true;// tells the script the player is actually moving a plant } } // // placing the plant on the game field // private function placePlant(e:MouseEvent):void { var plantRow:int=Math.floor((mouseY-80)/75); var plantCol:int=Math.floor((mouseX-25)/65); // let's see if the tile is inside the game field and it's free if (plantRow>=0&&plantCol>=0&&plantRow<5&&plantCol<9&&plantsArray[plantRow][plantCol]==0) { var placedPlant:plantMc=new plantMc();// constructs the plant to be placed placedPlant.fireRate=75;// plant fire rate, in frames placedPlant.recharge=0;// plant recharge. When recharge is equal to fireRate, the plant is ready to fire placedPlant.isFiring=false;// Boolean value to tell if the plant is firing placedPlant.plantRow=plantRow;// plant row plantContainer.addChild(placedPlant);// adds the plant placedPlant.x=plantCol*65+57; placedPlant.y=plantRow*75+115; playerMoving=false;// tells the script the player is no longer moving movingPlant.removeEventListener(MouseEvent.CLICK,placePlant);// removes the CLICK listener from the draggable plant overlayContainer.removeChild(selector);// removes the selector overlayContainer.removeChild(movingPlant);// removes the plant itself plantsArray[plantRow][plantCol]=1;// updates game array adding the new plant } } // // core function to be executed at every frame. The whole game is managed here // private function onEnterFrm(e:Event):void { var i:int; var j:int; // // plants management // for (i=0; i<plantContainer.numChildren; i++) { var currentPlant:plantMc=plantContainer.getChildAt(i) as plantMc; // let's see if a plant can fire if (zombiesArray[currentPlant.plantRow]>0&¤tPlant.recharge==currentPlant.fireRate&&! currentPlant.isFiring) { var bullet:bulletMc=new bulletMc();// constructs a new bullet bulletContainer.addChild(bullet);// adds the bullet bullet.x=currentPlant.x; bullet.y=currentPlant.y; bullet.sonOf=currentPlant;// sets the bullet as a son of the current plant currentPlant.recharge=0;// the plant must recharge currentPlant.isFiring=true;// the plant is firing } // let's see if the plant has to recharge if (currentPlant.recharge<currentPlant.fireRate) { currentPlant.recharge++;// recharges the plant } } // // bullets management // for (i=0; i<bulletContainer.numChildren; i++) { var movingBullet:bulletMc=bulletContainer.getChildAt(i) as bulletMc; movingBullet.x+=3;//moves each bullet right by 3 pixels var firingPlant:plantMc=movingBullet.sonOf as plantMc;// finds the plant which shot the bullet // let's see if the bullet flew out of the screen if (movingBullet.x>650) { firingPlant.isFiring=false;// the plant is not longer firing bulletContainer.removeChild(movingBullet);// removes the bullet } else { for (j=0; j<zombieContainer.numChildren; j++) { var movingZombie:zombieMc=zombieContainer.getChildAt(j) as zombieMc; // let's see if a zombie has been hit by a bullet if (movingZombie.hitTestPoint(movingBullet.x,movingBullet.y,true)) { movingZombie.alpha-=0.3;// decreases zombie energy (alpha) firingPlant.isFiring=false;// the plant is not longer firing bulletContainer.removeChild(movingBullet);// removes the bullet // let's see if zombie's energy (alpha) reached zero if (movingZombie.alpha<0) { zombiesArray[movingZombie.zombieRow]--;// decreases the number of zombies in the row zombieContainer.removeChild(movingZombie);// removes the zombie } break; } } } } // // zombies management // for (i=0; i<zombieContainer.numChildren; i++) { movingZombie=zombieContainer.getChildAt(i) as zombieMc; movingZombie.x-=0.5;// moves each zombie left by 1/2 pixels } // // suns management // for (i=0; i<sunContainer.numChildren; i++) { var fallingSun:sunMc=sunContainer.getChildAt(i) as sunMc; // let's see if the sun is still falling because it did not reach its destination if (fallingSun.y<fallingSun.destinationY) { fallingSun.y++;// moves the sun down by one pixel } else { fallingSun.alpha-=0.01;// makes the sun fade away // let's see if the sun disappeared if (fallingSun.alpha<0) { fallingSun.removeEventListener(MouseEvent.CLICK,sunClicked);// removes the CLICK listener from the sun sunContainer.removeChild(fallingSun);// removes the sun } } } // // placing plant process // if (playerMoving) { movingPlant.x=mouseX; movingPlant.y=mouseY; var plantRow:int=Math.floor((mouseY-80)/75); var plantCol:int=Math.floor((mouseX-25)/65); // let's see if the plant is inside the game field if (plantRow>=0&&plantCol>=0&&plantRow<5&&plantCol<9) { selector.visible=true;// shows the selector selector.x=25+plantCol*65; selector.y=80+plantRow*75; } else { selector.visible=false;// hide the selector } } } } }
If you followed previous steps you will notice I changed a bit the code but the old concepts remain the same.
I will focus on new concepts, starting from the creation of a new array called zombiesArray
(line 18) which will store the number of zombies for every row. This is very useful when we want to know if a plant can fire.
At this time the plant can fire only when there is a zombie on its same row, so I don’t care if the zombie is on its left or on its right, but it’s something I will have to face during next step. I don’t want to place a plant on the right of a zombie and see it firing to the right, to an empty row.
When a new zombie is added with newZombie
function (line 100) these two lines
zombie.zombieRow=Math.floor(Math.random()*5);// chooses a random row where to place the zombie zombiesArray[zombie.zombieRow]++;// increases the number of zombies in the row-th row
assign a zombieRow
property to the zombie and increase the zombieRow
-th element in zombiesArray
array. Thanks to this array, I always know how many zombies are roaming in all possible rows.
In placePlant
function (line 170), the function I use to place a plant, I am adding some custom properties:
placedPlant.fireRate=75;// plant fire rate, in frames placedPlant.recharge=0;// plant recharge. When recharge is equal to fireRate, the plant is ready to fire placedPlant.isFiring=false;// Boolean value to tell if the plant is firing placedPlant.plantRow=plantRow;// plant row
I want to define the plant fire rate, in frames. It means the plant will fire a bullet every fireRate
frames. I’ve chosen to measure the fire rate in frames rather than in milliseconds because if there are too much objects on the stage and the game slows down, the fire rate too will slow down, rather than remaining constant. recharge
will be increased at every frame, and when it’s equal to fireRate
, the plant is ready to fire. isFiring
tells us if the plant is already firing, and plantRow
saves the row where the plant is placed, useful when it’s time to check how many zombies are walking in its row.
To see if a plant can fire we use this if
statement:
if (zombiesArray[currentPlant.plantRow]>0&¤tPlant.recharge==currentPlant.fireRate&&! currentPlant.isFiring) { var bullet:bulletMc=new bulletMc();// constructs a new bullet bulletContainer.addChild(bullet);// adds the bullet bullet.x=currentPlant.x; bullet.y=currentPlant.y; bullet.sonOf=currentPlant;// sets the bullet as a son of the current plant currentPlant.recharge=0;// the plant must recharge currentPlant.isFiring=true;// the plant is firing }
It checks for the plant to be on the same row of at least one zombie, to be fully recharged and not to be already firing. Then a new bullet is created, some properties are changed to tell the script the plant is firing and has to recharge, but above all we need a sonOf
property to know which plant fired the bullet. This is very useful once the bullet is about to be removed from the stage and we want to update isFiring
property of the plant.
Then the core code for zombie killing management is this for loop:
for (j=0; j<zombieContainer.numChildren; j++) { var movingZombie:zombieMc=zombieContainer.getChildAt(j) as zombieMc; // let's see if a zombie has been hit by a bullet if (movingZombie.hitTestPoint(movingBullet.x,movingBullet.y,true)) { movingZombie.alpha-=0.3;// decreases zombie energy (alpha) firingPlant.isFiring=false;// the plant is not longer firing bulletContainer.removeChild(movingBullet);// removes the bullet // let's see if zombie's energy (alpha) reached zero if (movingZombie.alpha<0) { zombiesArray[movingZombie.zombieRow]--;// decreases the number of zombies in the row zombieContainer.removeChild(movingZombie);// removes the zombie } break; } }
I am scanning through all zombies performing an hit test to each one, and if a zombie has been hit, we remove the bullet, set isFiring
property of the plant which fired it to false
and decrease zombie’s energy… the alpha in this case. If the alpha reaches zero, we remove the zombie and decrease the corresponding element in zombiesArray
array to update the number of zombies in that row.
At this time you can test the game:
Collect money, buy plants and kill zombies.
Next time, we’ll make zombies attack too, and we will give some environment to the game. No more circles Vs squares, going to make a real game out of it.
Want to learn more? Learn by example!
Get the full commented source code of an actual commercial cross platform HTML5 game!!