This article has been archived and is no longer being updated. It may not work with the most recent OS versions.
Home Archive Tutorials

How To Make a Game Like Doodle Jump with Corona Tutorial Part 2

This is a blog post by iOS Tutorial Team member Jacob Gundersen, an indie game developer who runs the Indie Ambitions blog. Check out his latest app – Factor Samurai! In this tutorial series, you’ll learn how to use the Corona game engine to create a neat game like Doodle Jump. In the previous tutorial, […]


  • Other, Other, Other

This is a blog post by iOS Tutorial Team member Jacob Gundersen, an indie game developer who runs the Indie Ambitions blog. Check out his latest app – Factor Samurai!

Create a game like Doodle Jump with Corona!

Create a game like Doodle Jump with Corona!

In this tutorial series, you’ll learn how to use the Corona game engine to create a neat game like Doodle Jump.

In the previous tutorial, you learned how to use LevelHelper with Corona. We created a jumping character, different kinds of clouds, and gave him some arms.

In this second and final part of the tutorial series, we’re going to:

  • Give our hero the ability to shoot arrows
  • Create monsters and give them paths to follow
  • Make the level move as our player jumps upwards
  • Add even more epic win! :]

If you don’t have it already, here’s the example project where we left it off last time.

So fire up Level Helper and your text editor, and get ready to fire some arrows! :]

Fire It Up!

If you’ve been creating your own level with LevelHelper, open it up and drag the arrow sprite onto the gray area outside the level. Give the ‘arrow’ sprite the following attributes.

Attributes for arrow in LevelHelper

We’re now going to add a function to the player object that shoots an arrow. This code should go in the newPlayer() function, after the p:addEventListener(“collision”, p) line. Here’s the code:

	function p:shootArrow(x, y)
		local target = {}
		target.x = x
		target.y = -localGroup.y + y
		arrow = loader:newObjectWithUniqueName("arrow", physics)
		arrow.x = self.x
		arrow.y = self.y
		distanceY = target.y - self.y
		distanceX = target.x - self.x 
		arrow:setLinearVelocity(distanceX * 6, distanceY * 6)
		local firedAngle = angleBetween(self, target)

Once again we are scoping the shootArrow function within the p object. Our first line creates a table named target. In Lua, the data container is called a table. Tables are hybrid array/dictionary objects. Tables can have keys or indexes added to them. We are creating a target table so we can create our calculated destination (it will be a touch).

We will be passing in the touch coordinates from the screen when we call this function. The x coordinate will be fine, but the y coordinate will need to be translated to localGroup coordinates in order to be compared to the position of the player to calculate the angle for both the rotation of the arms and for the physics call in order to apply a force to the arrow.

We next instantiate a new copy of the arrow object with the newObjectWithUniqueName call. Our single arrow in LevelHelper is already in the level (outside of the world boundaries), but what we are doing here is creating a copy of that arrow. This call doesn’t automatically add the arrow to our localGroup display group, so we need to do that.

If we didn’t add it to the localGroup all the positioning code for the arrow will be relative to the screen or ‘stage’ (what corona calls the master parent display group that all instantiated objects are automatically added to). This would make it hard to position the arrow.

The startAnimationWithUniqueNameOnSprite is the call we use to finally use the animation we created in LevelHelper. This will cycle through the four frames of the bow to look like we are drawing and releasing the bowstring.

Next we place the arrow at the center of the player, this is the starting position of the arrow.

The next three lines calculate the distance between the x and y coordinates of the player and the touch. These values are reduced and used to set the linear velocity of the arrow. This means that a close touch applies less force to the arrow than a far touch.

The angleBetween function is a helper function that calculates the angle between two points in our game. You should place the following code at the very end of the file:

function angleBetween ( srcObj, dstObj )
    local xDist = dstObj.x-srcObj.x ; local yDist = dstObj.y-srcObj.y
    local angleBetween = math.deg( math.atan( yDist/xDist ) )
    if ( srcObj.x < dstObj.x ) then angleBetween = angleBetween+90 else angleBetween = angleBetween-90 end
    return angleBetween - 90

Rotating the Bow to Shoot

The rotate method on the arrow is a built in function that sets the rotation of the sprite. The next method is one we'll define next. It rotates the arms and bow so they match up with shooting angle. That code should be placed in before the p:shootArrow function:

	function p:rotateArms(angle)
		if angle < -90 then
			frontarm.xScale = -1
			backarm.xScale = -1
			self.xScale = -1
			backarm.rotation = angle + 180
			frontarm.rotation = angle + 180
			frontarm.xScale = 1
			backarm.xScale = 1
			self.xScale = 1
			backarm.rotation = angle
			frontarm.rotation = angle

This function should be easy to understand. We're flipping the player, frontarm, and backarm and rotating the arms so they point in the shooting direction.

Finally, we need to create a collision function for the arrow. This code should reside within the shootArrow function:

		local function arrowCollision(self, event)
			object = event.other
		arrow.collision = arrowCollision
		arrow:addEventListener("collision", arrow)

This should look familiar. The call to removeSpriteWithUniqueName call to the loader object is LevelHelper code that will delete and clean up after the object that the arrow collides with. We set the arrow mask bit to 4 in LevelHelper, so the only thing that the arrow can collide with is a monster.

The LevelHelper remove functions are preferable for any sprite instantiated in our initial call to instantiateObjectsInGroup. If we remove sprites (using removeSelf()) that were created in that initial call, we will have errors later on when we try to clean up everything in the level at the end of the game.

One last thing we need to do, the arrows need to remove themselves once they fly off screen. We'll do this with an enterFrame function on the arrow object. This code should also appear inside the shootArrow function:

		function arrow:enterFrame(event)
			if localGroup ~= nil then
				yStart = -localGroup.y
				if self.y > yStart + 480 or
					self.y < yStart or
					self.x < 0 or
					self.x > 320 then
						Runtime:removeEventListener("enterFrame", self)
		Runtime:addEventListener("enterFrame", arrow)

That first line checks to see if localGroup still exists. In the case that we've been killed or fallen, an arrow may still exist. However, the localGroup may have been removed. In this case we'd throw an error. This first if statement avoids that problem.

Next, we set yStart to the negative of the localGroup.y position. We set that to negative because the arrows position is relative to the localGroup. So the y position of localGroup gets us to the position of the level relative to the screen and the negative of this value tells us how far down from the very top of the level the arrow is.

If you find that any of this positioning code is confusing, I recommend playing with it a little. A great way to do this is with the print() function. If you add the following line of code to the enterFrame function will print the y position of the level (localGroup) and the arrow:

		print(localGroup.y, self.y)

Watch these values in the Corona Terminal window as the arrows are shot. This should give you an idea of how the y positions change as arrows move through the level and the level scrolls.

Now the player has all the required code to shoot an arrow, but we still don't have a way to trigger it. That requires touch handling code.

Adding Touch Handling

We're ready to move on to handling touch functions. Touch events can either be handled globally by registering the event with the Runtime object or the touch events can be directed towards specific objects. Because we are using the screen to shoot and to move the player, we are going to use the Runtime object.

We will be adding our touch code function at the root level of our program, so this code can be added anywhere as long as it's not inside another function. Our touch code function looks like this:

function touchListener(event)
	if event.phase == "began" then 
		player:shootArrow(event.x, event.y)
		local vx, vy = player:getLinearVelocity()
		if event.x > player.x then
			player:setLinearVelocity(70, vy)
		elseif event.x < player.x then
			player:setLinearVelocity(-70, vy)
	if event.phase == "moved" then
		local vx, vy = player:getLinearVelocity()
		if event.x > player.x then
			player:setLinearVelocity(70, vy)
		elseif event.x < player.x then
			player:setLinearVelocity(-70, vy)
	if event.phase == "ended" then
		local vx, vy = player:getLinearVelocity()
		player:setLinearVelocity(0, vy)

This code looks long, but it's actually the same logic repeated several times. The touch function has an argument, that we have named event. Event has several properties, including a phase property. The phase corresponds to touchBegan, touchMoved, and touchEnded events in Cocos2d or UIKit touch handling.

We want to fire one arrow each time the screen is touched, so we'll place that code in the "began" phase of our code.

The event object inside this function has an x and y property that correspond to the touch position on the screen. We call our shootArrow method on our player property and pass in the touch coordinates. This will now shoot an arrow.

We'll also be using touch code to move our player across the screen. This is a convenience for those who wish to play the game in the simulator, where the accelerometer isn't available.

First we need to get the y linear velocity of the player so that we can pass that value in to retain the constant velocity of the player in the vertical dimension. The getLinearVelocity call always returns a pair of values, so we need to set the variables up as a pair, even though we won't need the current x velocity.

Once we have the vy variable we test to see whether we touched to the left or right of our player, and based on that we set the linear velocity with our current y velocity (vy) and a strength of 70 in the x dimension toward the touch.

We apply this logic on both the touch began and the touch moved, this way we can drag the touch around and change the direction of the player. Once we release the touch, we want the player to no longer move toward the previous touch so in that case, event.phase == "ended", we set the x velocity to 0.

That's are touch listener. We just need to add it to the Runtime object. Add this code beneath the touch listener code:

Runtime:addEventListener("touch", touchListener)

Lets go through the anatomy of an addEventListener call. It will always be called on an object, object:addEventListener. The first argument is the event type. The event type will determine what kinds of arguments that the function will pass through to the body of the function.

The second argument is either the object or the name of the function. In the case of a listener added to a specific object, any other than Runtime, this argument will be the name of the object. If the call is on the Runtime object, then the second parameter is the name of the function without the () included.

You may wonder how the listener knows what function to call if we don't pass a function in. In the case of a collision listener, we must set the .collision property before we call the addEventListener method. In the case of an enterFrame function, the name of the function must be called 'enterFrame.'

Save and run now. You should be able to shoot arrows and control the movement of your character by touching (or clicking in the simulator) on the screen.

Player shooting arrows in our level

It's starting to get pretty awesome, huh! Just a bit more to go to wrap up this game!

Gratuitous Music and Sound Effects

Lets take a quick break and do something easy, add audio. Audio calls all start with a method call to the audio object, this object is built into Corona and doesn't require and special imports to use it.

Start by copying all of the sound files from the resources for this project into the same folder as the main.lua file.

Then go back to the top of the file and after the 'require("LevelHelperLoader")' line add the following:

bgMusic = audio.loadStream("Enchanted Journey.mp3")
backgroundMusicChannel = bgMusic, { channel=2, loops=-1, fadein=5000 })
audio.setMaxVolume(0.5, {channel = 2})
shoot = audio.loadSound("shoot.wav")
explode = audio.loadSound("explode.wav")
jump = audio.loadSound("jump3.wav")
monsterKill = audio.loadSound("monsterkill.wav")

Adding audio is pretty straight forward. The first line preloads the Enchanted Journey.mp3 file into memory and prepares to play it. In our second line we are calling the play method. This call takes two arguments, the first is our object we created when we preloaded the music file.

The second is actually a table (tables are like dictionaries and arrays combined in Lua). This table has a number of parameters, not all of which are included in this call. We are just giving it a channel, telling it to loop forever, and asking it to fade in over 5000 ms. We set the channel to 2 in order to reduce the volume of that channel in the next line.

The next four lines are preloading four audio sound effects that we will use to play at different events in our code. We will call on each of these variables to play the audio event.

Place the following lines of code in the following places to add sound effects to those events:

--pCollision function - inside newPlayer function

			if vy > 0 then 
				if object.tag == LevelHelper_TAG.CLOUD then
					self:setLinearVelocity(0, -350)
				elseif object.tag == LevelHelper_TAG.BCLOUD then

--p:shootArrow function - inside newPlayer function

		arrow = loader:newObjectWithUniqueName("arrow", physics)
--arrowCollision function - inside shootArrow function

		object = event.other

One other thing that we want to add is a blue sky background. Add the following code to the top after the audio file loading:

blueSky = display.newRect(0, 0, 320, 480)
blueSky:setFillColor(160, 160, 255)
score = display.newText("0", 30, 10, "Helvetica", 20)

These first two lines create a rectangle the size of the screen. There are a number of display functions that draw primitives, load sprites, or create groups (as you've seen). The newRect function takes an x and y position along with a width and height as arguments.

The second call fills the screen with the color r = 160, g = 160, and b = 255.

Finally, we create a new display object, a text label with the newText call. This function takes the string, an x and y position, the font name, and font size as its arguments.

The fonts available on the device natively are available through this call as well as additional fonts included in the folder. Additional fonts need to be added to the build.settings file which mirrors the info.plist. That discussion is beyond the scope of this tutorial.

If you run it now, it should look more like your hero is jumping around in the sky, and the score will be onscreen. We're going to slowly change the background as we climb up the level to have fewer clouds and look more like outer space by adding stars.

Adding a blue background for the level

Scrolling the Layer

We want to scroll the layer as the player jumps. At any give time the player should have half a screen worth of level above him. We'll accomplish this by adding a global enterFrame function.

Add the following code after the touchListener funtion, but before the call to 'Runtime:addEventListener("touch", touchListener)':

function runtimeListener(e)
	score.text = string.format('%d', worldHeight - 480 + localGroup.y)
	if player.y < -localGroup.y + 240 then 
		localGroup.y = -(player.y - 240)
	elseif player.y > -localGroup.y + 480 then
	backGroundValue = (localGroup.y + (worldHeight - 480)) / (worldHeight - 480)
	blueSky.alpha = math.max(1 - backGroundValue, 0)


This function accomplishes a couple of things. The first updates the score based on how far we advanced in our level.

The next if statement first checks to see if the player is above half the height of the screen. If he is, the position of localGroup.y is updated based on the player's position.

If the player isn't above half the height of the screen, the if statement checks if the player is below the bottom of the screen. If so, the gameOver() function is called. We haven't built that yet so lets keep it commented out for now. Double dashes -- denote a single line comment in Lua.

The next two lines calculate how far we are in the level and set the alpha of the blueSky rectangle to the percentage complete. This is to give the impression that we are ascending into outer space. Go back to LevelHelper and remove any background clouds and add stars to the last two or three screens.

Then add the following line of code to call your new runtimeListener function each frame right after the other call to addEventListener:

Runtime:addEventListener("enterFrame", runtimeListener)

If you save and run you'll now be able to climb up the level!

Player climbing the clouds with a scrolling layer

Monsters and Paths, Oh My!

Now that we can shoot and move through the level, we are actually pretty close to being finished. We just need to add some enemies to our level to spice it up!

One great thing about LevelHelper is the ability to add enemies and give them premade paths to follow - with no code required!

Return to LevelHelper and go to the animation pane. Create a new animation and add monster1 and monster2 to it. Make sure loop remains ticked. The standard properties are fine for the rest of the options. Click finish animation.

Double click to rename the animation 'monster.' Drag a monster from the animation pane into the level, I'd place him at the very top. Give him the tag 'MONSTER' and make sure that his physics attributes match those below.

Settings for the monster in LevelHelper

Click on the 'clone and align' button and make copies. I'm gonna make 11 clones, 480 y pixels apart.

Once you've got your desired number of monsters, we are going to set paths up for each one. Click on the paths pane. Click 'New' to create a new path. Start clicking on the level. Each click will create a new point in our path.

When you've laid out your square path, press 'Finish.' Highlight the first path and tick the 'Is Path' box. It must be a path in order to be assignable to a monster. If your happy with a square path, you can stop here.

But, if you want a smooth curved path, you click the 'Edit' button, control points and path points will show up in your path. You can drag the points around to make the path curvy. If you want to add more points or remove points, click the plus and minus buttons. Each click puts you in a new mode, so you can't move point in the add/subtract modes. When you're happy with your path, click finish.

Creating a path for the monster in LevelHelper

Now we are going to assign a path to a monster. Click on the monster you want to follow that path. Click the path button in the Sprite Properties pane. Choose the name of the path, the default is BezierShape.

The speed is how long the monster takes to move across the entire path in seconds. This time is divided up evenly across all of the path points, so if you have a five second path with five segments between points, each segment will be traversed in one second, regardless of the length of the segment, creating a monster that can potentially speed up and slow down through its path.

'Is cyclical' will cause the sprite to constantly move along the path, if it's not ticked it will only make the journey once. 'Restart at other end' will cause the sprite to restart each cycle at the chosen end of the path. If this is not ticked it will cycle beginning to end then end to beginning.

You can choose whether the sprite starts at the beginning of the path and moves toward the end or vice versa. Paths are absolute in LevelHelper. Wherever the path is within the level, is where that sprite/monster will be, regardless of the initial placement of the sprite in LevelHelper.

If you assign multiple monsters to the same path, they will move along that path together, unless you assign different values, ie. one monster could move front to back the other back to front, one could move faster than the other, etc.

Setting the monster path attributes in LevelHelper

Go through and give each monster a path.

Any sprite that has a path must be of physics type 'static', so if you don't give a monster a path, it will just float in the air at the initial LevelHelper position.

Once you've got paths for your monsters, save and run. You'll see that your monsters are moving around and you can shoot them with your arrows to destroy them.

Monsters are in our level!

We need to give the player some logic when he collides with a monster. Put the following code in after the section for colliding with the clouds. The entire pCollision function should look like this:

	function pCollision(self, event)
		object = event.other					
		if event.phase == "began" then
			vx, vy = self:getLinearVelocity()
			if vy > 0 then 
				if object.tag == LevelHelper_TAG.CLOUD then
					self:setLinearVelocity(0, -350)
				if object.tag == LevelHelper_TAG.BCLOUD then
			if object.tag == LevelHelper_TAG.MONSTER then

This should now understand what is going on here. If the collided object has a tag of MONSTER, we call the game over function. Lets go ahead and write that function now.

Game Over, Man!

Insert the game over code after the runtimeListener function:

function gameOver()
	gameOverText = display.newText("Game Over", 50, 240, native.systemFontBold, 40)

	local function removeGOText()

	timer.performWithDelay(2000, removeGOText)
	Runtime:removeEventListener("enterFrame", runtimeListener)
	Runtime:removeEventListener("accelerometer", accelerometerCall)
	Runtime:removeEventListener("touch", touchListener)
	localGroup = nil
	timer.performWithDelay(2000, startOver)

The first line should look familiar from the very beginning of the tutorial. We're using the display.newText function to display the text "Game Over" to the screen.

Next we create a function to remove the text, for when we restart, and we schedule that function to run after 2000 ms.

Then we start cleaning up all the objects that we've created. The player is removed first, with a removePlayer function that we need to build. Next all the global Runtime listeners are removed. Don't worry about the accelerometer listener, we'll add that in a second.

Next we use a LevelHelper function 'removeAllSprites.' This is a great way to remove all the objects created by LevelHelper. As stated earlier, this function will clean everything up that was initially created by LevelHelper. If we removed something without using the LevelHelper remove functions, this would throw an error.

We set localGroup to nil. This allows the garbage collector to do it's work. It should be empty, but we do it as good practice. Finally, we call a new function, startOver. It will reinitialize our level by invoking the loadLevel function, creating a new player, etc.

Lets create the code that removes the player. The following code should appear right before the return p line in the newPlayer() function:

	function p:removePlayer()
		Runtime:removeEventListener("enterFrame", self)

Most callback functions will remove themselves when the sprite is removed. However, the enterFrame function is an exception to that. It will continue to fire and throw an error if the sprite has been removed from memory (referring to a nil variable). So, we remove that first.

When that's done we can go ahead and remove the rest of the sprites from our level.

Here's the startOver function, you can place it before or after the gameOver() function:

function startOver()
	Runtime:addEventListener("enterFrame", runtimeListener)
	Runtime:addEventListener("accelerometer", accelerometerCall)
	Runtime:addEventListener("touch", touchListener)

This should make sense. We're just calling the same method to load the level as we did in the first place. Then we're adding the listeners back to the Runtime object.

One thing we haven't done yet is create the accelerometer code that will allow us to use tilt to move our player like Doodle Jump. We can touch/click to move him, but it would be more fun if we could use the tilt. That's an easy fix. Lets go ahead create the accelerometer function now:

function accelerometerCall(e)
	px, py = player:getLinearVelocity()
	player:setLinearVelocity(e.xGravity * 700, py)

Runtime:addEventListener("accelerometer", accelerometerCall)

The accelerometer event has an xGravity and a yGravity property. Here we are simply getting the y velocity, so we don't interrupt the momentum in the vertical direction, and then applying the force of the e.xGravity value times 700 to the player. The 700 value is arbitrary, I just played with it until it felt about right. This provides a tilt control of our player if we are playing on a device.

In order to build for the device in corona, you must have an apple developer account. If you are using the trial version of Corona, you can use a development certificate to create an app build for the device. The resulting .app file can be installed on the device through itunes or Xcode organizer. For more information on this process go here.

Go back to the runtime listener now and remove the comment dashes before the call to gameOver(). Save and run in the simulator. You should now be able to die and restart the game by running into a monster or falling off screen.

Game Over screen

Finishing Touches

We're almost finished, there are just a few odds and ends left. We want to have the monsters look where they are going, currently they always look to the left. We'll do that with a flipMonsters function in our runtimeListener function. Go ahead and uncomment that call in the runtimeListener function and add the following code before the newPlayer function towards the beginning of the file:

function flipMonsters()
	local myMonsters = loader:spritesWithTag(LevelHelper_TAG.MONSTER)
	for n = 1, #myMonsters do
		if myMonsters[n].prevX == nil then
			myMonsters[n].prevX = myMonsters[n].x
		elseif myMonsters[n].prevX - myMonsters[n].x > 0 then	
			myMonsters[n].xScale = -1
			myMonsters[n].xScale = 1
		myMonsters[n].prevX = myMonsters[n].x

The spritesWithTag call will generate an array, technically a table, of any of the tagged sprites in the level. Next we create a for loop. A couple of things of note in lua. Lua tables are indexed starting with a 1 instead of 0. Any array length can be accessed by prefixing the array variable name with #.

In this loop we have to create a new variable the was the previous x position of the sprite. Because the sprites are static in type, they don't have linear velocity values. We need to first check if the monster has a prevX attribute already populated. The first time this function runs trying to access the prevX attribut will throw an error (prevX will be nil).

In Lua, all objects are tables and we can add attributes at any time.

On the second time around, once prevX exists, we test whether the sprite is moving left or right (prevX - x is negative=left or positive=right). We then set the xScale property accordingly. Finally we reset the prevX value to the current x, in preparation for the next time around.

If you save and run now your monsters should face the direction they are moving. Make sure that you have removed the comment dashes in front of the flipMonster() call in the runtimeListener function.


Now we'e done right? Well, almost - we have to let the user win (so Charlie Sheen can play!)

So let's create a gameWon function and fire it when we get to the top of the level.

Here's the function, place it somewhere after the startOver() function:

function gameWon()
	print("you win")
	timer.performWithDelay(2000, function() physics.pause() end)
	gameWonText = display.newText("YOU WIN!", 50, 240, native.systemFontBold, 40)

We simply print to the console and on screen that you have won. Also, after a two second delay, we pause the physics engine so you don't keep bouncing. Notice that in lua we can create a function declaration and pass it in as an argument to a function. When we do this we needn't give it a name.

We'll fire this function by creating one last bezier path. Go back to LevelHelper and create a new bezier shape. A line created by two points will do. Highlight the new shape and click 'Is Sensor.' Also, give it the CHECKPOINT tag. Make sure it has a category bit of 1. That's right, ladies and gents, bezier shapes can have physics properties.

Creating a checkpoint line with LevelHelper

Now add the following code to the pCollision function (in the newPlayer() function) to call the gameWon() function. This code should appear right after the object = event.other line:

		if object.tag == LevelHelper_TAG.CHECKPOINT then

Save and run, you should now be able to climb to the top of your level and WIN THE GAME!!! Feel the tiger blood running through your veins.

You win!

Congratulations, you have the skills to create level upon level of 2D scrolling goodness!

Where To Go From Here?

Here is the example project with all of the code from the tutorial so far.

As mentioned in the last tutorial, here are some great resources to learn more about Corona:

Also, if you have any questions or comments about this tutorial, please join the forum discussion below!

This is a blog post by iOS Tutorial Team member Jacob Gundersen, an indie game developer and co-founder of Third Rail Games. Check out his latest app - Factor Samurai!




More like this