We've already come a long way in building our endless runner game. At this point in the series, you should already have a playable and enjoyable experience, but this tutorial will teach you how to add one more twist: boss battles!
By now our game is actually starting to look like a real game! We have a controllable character with different types of obstacles that are randomly generated,we have parrallax scrolling implemented in three different layers, and we have a basic scoring system that gives our players a feeling of accomplishment. So far, so good. Now maybe this is enough for the kind of game you are designing and this is all you need. However, let's say you want to spice up your game with something new. Something like...a boss battle!!! While most endless scroll games don't have any kind of boss battle, it will be a good practice to look at how we would go about designing this kind of event. So, let's get started!
You will notice that we are again following the same format with an old folder in the attached download containing the entire project that we have created thus far and a new folder containing the project after we have finished this tutorial. The first thing to do is to be sure that we have the images in the correct folder. You can grab boss.png and bossSpit.png from the new folder in the attached download file. Notice the boss is not animated, and is just a single image. If you want an animated boss and forgot how to animate a sprite in Corona, refer to the sprite animation tutorial released earlier in this series.
Now that we have those in the same folder as our main.lua file, go ahead and open up main.lua in your favorite text editor. I am not going to explain very much of what is going on here(Be sure to read the comments for hints and small explanations!) as we have, at this point, already talked about everything several times and I'm sure you would just skip over it anyway. This will also help us to get through the tutorial quickly as everything we do here we have done before, so it's just a lot of little changes. So let's get started.
The first thing that we will do is get our images loaded in the game. Again, this should all look familiar. Add this code up top where we load all of the other images. Just be sure bossSpits is declared before you try to put something in it!
local boss = display.newImage("boss.png", 150, 150) boss.x = 300 boss.y = 550 boss.isAlive = false boss.health = 10 boss.goingDown = true boss.canShoot = false --spitCycle is the only thing that is not self explantory --every time we move a ground piece back to the right of the --screen we update the score by one. Now we also update the --spite cycle. Every time spitCycle is a multiple of three --the boss will shoot his projectile. This just keeps track --of that for us! boss.spitCycle = 0 for a=1, 3, 1 do bossSpit = display.newImage("bossSpit.png") bossSpit.x = 400 bossSpit.y = 550 bossSpit.isAlive = false bossSpit.speed = 3 bossSpits:insert(bossSpit) end
Then be sure to add them into the screen next to the other additions, a good place to put them would be right before you insert the collisionRect.
Save that and run it. Go ahead and check it out in the iPhone 4 version and you should see something like this:
Now, let's make them do something. The way we are going to handle the boss is that rather randomly coming into the game like the other events we want this to happen at a specific time. For this example we are going to set it to a low number so it will get to the event quickly, but you'll probably want to make it a more reasonable time in your game. So, what will happen is every time your monster has to run a distance that is equal to a multiple of 10. The boss will spawn. As we spawn the boss we make the ground become flat at the groundMin. We also stop ghosts and spike walls from spawning that way there are not distractions from the fight! When he spawns he will enter the scene from the top and start bobbing up and down while spitting heat seeking yellow balls...of death!
So, as the boss shoots at the player, the player will need to kill the boss by shooting him ten times. The player may also defend himself from the spit by shooting them. So the basic strategy for winning is going to be jumping and spamming his shooting skill! A couple other things to note is that in this game I decided to not update the score while the boss is alive, that way the player is forced to defeat the boss to keep getting higher scores. The last thing before we move on is that I have the boss set to fire a shot every three blockUpdates. This again has the benefit that the difficulty automatically scales with the game. It also provides an easy way to get consistent shots off. Nothing too crazy, but overall it adds a nice twist to the game.
In order to make more room for the boss on the screen I moved the player a little bit further back in the screen. I now start the hero off like so:
monster.x = 60
This will open up the level a little bit. The next thing we should do is go ahead and plop our functions in that will update the boss and the boss' spit:
function updateBoss() --check to make sure that the boss hasn't been killed if(boss.health > 0) then --check to see if the boss needs to change direction if(boss.y > 210) then boss.goingDown = false end if(boss.y < 100) then boss.goingDown = true end if(boss.goingDown) then boss.y = boss.y + 2 else boss.y = boss.y - 2 end else --if the boss has been killed make him slowly disappear boss.alpha = boss.alpha - .01 end --once the monster has been killed and disappear officially --kill him off and reset him back to where he was if(boss.alpha <= 0) then boss.isAlive = false boss.x = 300 boss.y = 550 boss.alpha = 1 boss.health = 10 inEvent = 0 boss.spitCycle = 0 end end function updateBossSpit() for a = 1, bossSpits.numChildren, 1 do if(bossSpits[a].isAlive) then (bossSpits[a]):translate(speed * -1, 0) if(bossSpits[a].y > monster.y) then bossSpits[a].y = bossSpits[a].y - 1 end if(bossSpits[a].y < monster.y) then bossSpits[a].y = bossSpits[a].y + 1 end if(bossSpits[a].x < -80) then bossSpits[a].x = 400 bossSpits[a].y = 550 bossSpits[a].speed = 0 bossSpits[a].isAlive = false; end end end end
With those in there we just need to make some adjustments to some of our other functions and add them to our update() function:
updateBossSpit() if(boss.isAlive == true) then updateBoss() end
Add those in the update function then go to the checkEvent() function and where you see the the code that looks like this:
if(inEvent > 0 and eventRun > 0) then --Do nothing else
Right below that else statement empty everything that was in there and change it to this:
--this is where we spawn the boss after every 10 blocks --also control the boss's health from here if(boss.isAlive == false and score%10 == 0) then boss.isAlive = true boss.x = 400 boss.y = -200 boss.health = 10 end --if the boss is alive then keep the event set to 15 --this will prevent the other events from spawning if(boss.isAlive == true) then inEvent = 15 else --everything down here should be the same as it was before check = math.random(100) if(check > 80 and check < 99) then inEvent = math.random(10) eventRun = 1 end if(check > 98) then inEvent = 11 eventRun = 2 end if(check > 72 and check < 81) then inEvent = 12 eventRun = 1 end if(check > 60 and check < 73) then inEvent = 13 eventRun = 1 end end
There shouldn't be anything too crazy in there. If you are new to programming you might not be familiar with the modulus symbol: %. What this does is take whatever numbers you have and divides the first number by the second number and returns the remainder. So, if score is 15, score % 10 will return 5, which is not 0, so it will not spawn the boss event. The same goes for any number that is not a multiple of 10. That is how we easily can control the rate of how often something spawns. The next step is going to be back in updateBlocks(). Right beneath the if statement that says:
if((blocks[a]).x < -40) then
You are going to take the scoreText section and put it inside of an if statement that checks to see if the boss is alive or not (again we are not going to update the score while he is alive! As part of the if statement you are going to add an else that gives the boss some of his behavior). Make that section look like this:
--only update the score if the boss is not alive if (boss.isAlive == false) then score = score + 1 scoreText.text = "score: " .. score scoreText:setReferencePoint(display.CenterLeftReferencePoint) scoreText.x = 0 scoreText.y = 30 else --have the boss spit every three block passes boss.spitCycle = boss.spitCycle + 1 if(boss.y > 100 and boss.y < 300 and boss.spitCycle%3 == 0) then for a=1, bossSpits.numChildren, 1 do if(bossSpits[a].isAlive == false) then bossSpits[a].isAlive = true bossSpits[a].x = boss.x - 35 bossSpits[a].y = boss.y + 55 bossSpits[a].speed = math.random(5,10) break end end end end if(inEvent == 15) then groundLevel = groundMin end
Still nothing too terribly hard, like I said there shouldn't be a lot of new material from here. The next part is just more of the same and then we are done. The next thing that we are going to add is the collision detection checks to see kills. Let's start with the monster's blast, we need to check against the boss' spit as well as the monster itself.
Add these lines to the updateBlasts() function:
--check for collisions with the boss if(boss.isAlive == true) then if(blasts[a].y - 25 > boss.y - 120 and blasts[a].y + 25 < boss.y + 120 and boss.x - 40 < blasts[a].x + 25 and boss.x + 40 > blasts[a].x - 25) then blasts[a].x = 800 blasts[a].y = 500 blasts[a].isAlive = false --everything is the same only 1 hit will not kill the boss so just take a little health away boss.health = boss.health - 1 end end --check for collisions between the blasts and the bossSpit for b = 1, bossSpits.numChildren, 1 do if(bossSpits[b].isAlive == true) then if(blasts[a].y - 20 > bossSpits[b].y - 120 and blasts[a].y + 20 < bossSpits[b].y + 120 and bossSpits[b].x - 25 < blasts[a].x + 20 and bossSpits[b].x + 25 > blasts[a].x - 20) then blasts[a].x = 800 blasts[a].y = 500 blasts[a].isAlive = false bossSpits[b].x = 400 bossSpits[b].y = 550 bossSpits[b].isAlive = false bossSpits[b].speed = 0 end end end
One more collision detection to go and that is checking the boss's spit against our player. Add this to the checkCollisions() function:
--make sure the player didn't get hit by the boss's spit! for a = 1, bossSpits.numChildren, 1 do if(bossSpits[a].isAlive == true) then if((( ((monster.y-bossSpits[a].y))<45)) and (( ((monster.y-bossSpits[a].y))>-45)) and (( ((monster.x-bossSpits[a].x))>-45)) ) then --stop the monster speed = 0 monster.isAlive = false --this simply pauses the current animation monster:pause() gameOver.x = display.contentWidth*.65 gameOver.y = display.contentHeight/2 end end end
Again that should all look pretty familiar. The last thing that we need to do is go into our restartGame() function and update it with the boss' information along with his saliva.
Also note that because we changed the monster.x position in this tutorial it is important that instead of resetting the monster.x to 110, we should now update the monster.x to reflect his new position, which is 60.
--reset the boss boss.isAlive = false boss.x = 300 boss.y = 550 --reset the boss's spit for a = 1, bossSpits.numChildren, 1 do bossSpits[a].x = 400 bossSpits[a].y = 550 bossSpits[a].isAlive = false end
Save that, run it, and enjoy the new addition to the game! You should be rocking something like this now:
Like I said several times, there really wasn't a lot of crazy new content, but I thought it would be a good exercise to show you how easy it is to update your game with new content. Also it shows new approaches to adding events so they are more on demand that random. Most of these kind of games will probably implement a mixture of both. A good example of a game that does this well is bit.trip runner. Check it out to get a good feel for what can be done with this style of game. If we did go over something too fast and you have any questions let me know, if not get ready to get our game a bit better organized next time with a nice new menu system that will make this game feel complete. Thanks for following along and we'll see you next time!