Advertisement
  1. Code
  2. Mobile Development
  3. Corona

Build an Endless Runner Game From Scratch: Boss Battles

Scroll to top
Read Time: 12 min
This post is part of a series called Corona SDK: Build an Endless Runner Game From Scratch.
Build an Endless Runner Game from Scratch: Game Over & Scoring
Build an Endless Runner Game From Scratch: The Game Menu

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!

1
2
local boss = display.newImage("boss.png", 150, 150)
3
boss.x = 300
4
boss.y = 550
5
boss.isAlive = false
6
boss.health = 10
7
boss.goingDown = true
8
boss.canShoot = false
9
--spitCycle is the only thing that is not self explantory
10
--every time we move a ground piece back to the right of the
11
--screen we update the score by one. Now we also update the
12
--spite cycle. Every time spitCycle is a multiple of three
13
--the boss will shoot his projectile. This just keeps track
14
--of that for us!
15
boss.spitCycle = 0
16
for a=1, 3, 1 do
17
bossSpit = display.newImage("bossSpit.png")
18
bossSpit.x = 400
19
bossSpit.y = 550
20
bossSpit.isAlive = false
21
bossSpit.speed = 3
22
bossSpits:insert(bossSpit)
23
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.

1
2
screen:insert(boss)
3
screen:insert(bossSpits)

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:

1
2
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:

1
2
function updateBoss()
3
	--check to make sure that the boss hasn't been killed
4
	if(boss.health > 0) then
5
		--check to see if the boss needs to change direction
6
		if(boss.y > 210) then
7
			boss.goingDown = false
8
		end
9
		if(boss.y < 100) then
10
			boss.goingDown = true
11
		end
12
		if(boss.goingDown) then
13
			boss.y = boss.y + 2
14
		else
15
			boss.y = boss.y - 2
16
		end
17
	else
18
		--if the boss has been killed make him slowly disappear
19
		boss.alpha = boss.alpha - .01
20
	end
21
		--once the monster has been killed and disappear officially
22
	--kill him off and reset him back to where he was
23
	if(boss.alpha <= 0) then
24
		boss.isAlive = false
25
		boss.x = 300
26
		boss.y = 550
27
		boss.alpha = 1
28
		boss.health = 10
29
		inEvent = 0
30
		boss.spitCycle = 0
31
	end
32
	end
33
function updateBossSpit()
34
	for a = 1, bossSpits.numChildren, 1 do
35
                if(bossSpits[a].isAlive) then
36
		        (bossSpits[a]):translate(speed * -1, 0)
37
		        if(bossSpits[a].y > monster.y) then
38
			        bossSpits[a].y = bossSpits[a].y - 1
39
		        end
40
		        if(bossSpits[a].y < monster.y) then
41
			        bossSpits[a].y = bossSpits[a].y + 1
42
		        end
43
		        if(bossSpits[a].x < -80) then
44
			        bossSpits[a].x = 400
45
			        bossSpits[a].y = 550
46
			        bossSpits[a].speed = 0
47
			        bossSpits[a].isAlive = false;
48
		        end
49
                 end
50
        end
51
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:

1
2
3
updateBossSpit()
4
5
	if(boss.isAlive == true) then
6
7
			updateBoss()
8
9
		end

Add those in the update function then go to the checkEvent() function and where you see the the code that looks like this:

1
2
if(inEvent > 0 and eventRun > 0) then
3
	--Do nothing
4
else

Right below that else statement empty everything that was in there and change it to this:

1
2
--this is where we spawn the boss after every 10 blocks
3
4
--also control the boss's health from here
5
6
if(boss.isAlive == false and score%10 == 0) then
7
8
boss.isAlive = true
9
10
boss.x = 400
11
12
boss.y = -200
13
14
boss.health = 10
15
16
end
17
18
--if the boss is alive then keep the event set to 15
19
20
--this will prevent the other events from spawning
21
22
if(boss.isAlive == true) then
23
24
inEvent = 15
25
26
else
27
28
--everything down here should be the same as it was before
29
30
check = math.random(100)
31
32
if(check > 80 and check < 99) then
33
34
inEvent = math.random(10)
35
36
eventRun = 1
37
38
end
39
40
if(check > 98) then
41
42
inEvent = 11
43
44
eventRun = 2
45
46
end
47
48
if(check > 72 and check < 81) then
49
50
inEvent = 12
51
52
eventRun = 1
53
54
end
55
56
if(check > 60 and check < 73) then
57
58
inEvent = 13
59
60
eventRun = 1
61
62
end
63
64
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:

1
2
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:

1
2
--only update the score if the boss is not alive
3
if (boss.isAlive == false) then
4
score = score + 1
5
	scoreText.text = "score: " .. score
6
	scoreText:setReferencePoint(display.CenterLeftReferencePoint)
7
	scoreText.x = 0
8
	scoreText.y = 30
9
else
10
	--have the boss spit every three block passes
11
	boss.spitCycle = boss.spitCycle + 1
12
	if(boss.y > 100 and boss.y < 300 and boss.spitCycle%3 == 0) then
13
		for a=1, bossSpits.numChildren, 1 do
14
			if(bossSpits[a].isAlive == false) then
15
				bossSpits[a].isAlive = true
16
				bossSpits[a].x = boss.x - 35
17
				bossSpits[a].y = boss.y + 55
18
				bossSpits[a].speed = math.random(5,10)
19
				break
20
			end
21
		end
22
	end
23
end
24
if(inEvent == 15) then
25
        groundLevel = groundMin
26
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:

1
2
--check for collisions with the boss
3
if(boss.isAlive == true) then
4
	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
5
		blasts[a].x = 800
6
		blasts[a].y = 500
7
		blasts[a].isAlive = false
8
		--everything is the same only 1 hit will not kill the boss so just take a little health away
9
		boss.health = boss.health - 1
10
	end
11
end
12
--check for collisions between the blasts and the bossSpit
13
for b = 1, bossSpits.numChildren, 1 do
14
	if(bossSpits[b].isAlive == true) then
15
		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
16
			blasts[a].x = 800
17
			blasts[a].y = 500
18
			blasts[a].isAlive = false
19
			bossSpits[b].x = 400
20
			bossSpits[b].y = 550
21
			bossSpits[b].isAlive = false
22
			bossSpits[b].speed = 0
23
		end
24
	end
25
end

One more collision detection to go and that is checking the boss's spit against our player. Add this to the checkCollisions() function:

1
2
--make sure the player didn't get hit by the boss's spit!
3
for a = 1, bossSpits.numChildren, 1 do
4
if(bossSpits[a].isAlive == true) then
5
	if(((  ((monster.y-bossSpits[a].y))<45)) and ((  ((monster.y-bossSpits[a].y))>-45)) and ((  ((monster.x-bossSpits[a].x))>-45)) ) then
6
		--stop the monster
7
			speed = 0
8
			monster.isAlive = false
9
			--this simply pauses the current animation
10
			monster:pause()
11
			gameOver.x = display.contentWidth*.65
12
			gameOver.y = display.contentHeight/2
13
		end
14
	end
15
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.

1
2
--reset the boss
3
boss.isAlive = false
4
boss.x = 300
5
boss.y = 550
6
--reset the boss's spit
7
for a = 1, bossSpits.numChildren, 1 do
8
bossSpits[a].x = 400
9
	  bossSpits[a].y = 550
10
	  bossSpits[a].isAlive = false
11
end

Save that, run it, and enjoy the new addition to the game! You should be rocking something like this now:

and

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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.