Build an Endless Runner Game From Scratch: Sprite Interaction

Build an Endless Runner Game From Scratch: Sprite Interaction

Tutorial Details
  • Technology: iOS SDK
  • Difficulty: Beginner
  • Completion Time: 30 - 45 Minutes
This entry is part 4 of 10 in the series Build an Endless Runner Game From Scratch

Welcome to the fourth tutorial in our series on building a running-game from scratch with the Corona SDK. In this section, we are going to be adding gravity, collision detection, and the ability to jump to the game sprite. Let’s go!

Hopefully the tutorials so far have been helpful and easy to follow. As always, if you have any questions be sure to leave a comment! Last time we went over how to create nice sprites from spritesheets. Today, we are going to be taking what we learned in the last tutorial and getting that sprite monster into our actual game. Once he is in, we will learn how to control him and make our game interactive. The way I am going to do this is to take the source code that we had from the background motion tutorial and add our monster animation stuff in first. If you download the files for the tutorial, you will notice that there are 2 folders, called “old” and “new”. Old contains all the files from the background motion tutorial that you need to get started working on this tutorial. New contains all the code and everything you will have once this tutorial has been completed. So, go ahead and download the files and open the main.lua file from the old folder. I am going to break up everything we do into three sections. The first is going to cover organization of our game, to which we will make a few changes. The second will cover taking what we learned in the using sprites tutorial and implementing it here. We will go over that section fairly quickly as the details of what is happening have already been covered. The third section will be giving our little guy gravity, collision detection, and the ability to jump!

Up until now we put our images in the code in the order that we want them to appear on the screen. The earlier they are called, the further back they will appear in the layers on the screen. This works, but there is a better way to do this. It will not always be realistic to put every single image in the exact order you want them to appear, and you may at some point want to change the order to which objects appear on the screen. So, open up the main.lua file from the old folder and we’ll start making some changes.

The first thing we are going to do is add the following line to the top of the page right below the display.setStatusBar line.

local sprite = require("sprite")

Next, add the following two lines  right beneath where we created the display group blocks:

local player = display.newGroup()
local screen = display.newGroup()

The display group player is going to be the display group that holds our hero sprite and the screen group will be a display group that holds everything else. Let’s put some more code in and then I will finish explaining.

Insert the following code beneath the for loop where we instantiate our ground blocks:

--create our sprite sheet
local spriteSheet = sprite.newSpriteSheet("monsterSpriteSheet.png", 100, 100)
local monsterSet = sprite.newSpriteSet(spriteSheet, 1, 7)
sprite.add(monsterSet, "running", 1, 6, 600, 0)
sprite.add(monsterSet, "jumping", 7, 7, 1, 1)
--set the different variables we will use for our monster sprite
--also sets and starts the first animation for the monster
local monster = sprite.newSprite(monsterSet)
monster:prepare("running")
monster:play()
monster.x = 110
monster.y = 200
--these are 2 variables that will control the falling and jumping of the monster
monster.gravity = -6
monster.accel = 0
--rectangle used for our collision detection
--it will always be in front of the monster sprite
--that way we know if the monster hit into anything
local collisionRect = display.newRect(monster.x + 36, monster.y, 1, 70)
collisionRect.strokeWidth = 1
collisionRect:setFillColor(140, 140, 140)
collisionRect:setStrokeColor(180, 180, 180)
collisionRect.alpha = 0
--used to put everything on the screen into the screen group
--this will let us change the order in which sprites appear on
--the screen if we want. The earlier it is put into the group the
--further back it will go
screen:insert(backbackground)
screen:insert(backgroundfar)
screen:insert(backgroundnear1)
screen:insert(backgroundnear2)
screen:insert(blocks)
screen:insert(monster)
screen:insert(collisionRect)

Now, let’s  go over that huge block of code.

The first two sections we are going to skip over. Those sections just create our monster sprite from our sprite sheet. If you have any questions about what is going on there do a quick review of the last tutorial where we went went over creating sprites from sprite sheets. In the next section, I have created a basic rectangle shape called collisionRect, this is how we are going to do our collision detection so our monster can interact with the world. Essentially, what is happening is we are creating an invisible square that goes in front of our monster. If you make the rectangle visible (i.e. just change the alpha to 100) you will see that it sits right in front of the monster and is floating a little above the ground.

Figure 1

The reason we do this is that it will give us a collision system that is easy to manage. Because we are doing an endless running game we are mainly concerned with what goes on right in front of the monster (normally what comes behind him won’t kill him). Also, we lift it a little off the ground so that box never collides with the ground below the monster, only things in front of it. The monster itself will handle the collisions with the ground, we just need something to handle the collisions with external objects that could hit him in the front. This will make even more sense in the next couple tutorials as we add things for out monster to run into.

The section after that is where we insert everything into the screen. So, the screen is just a display group -there is nothing magical about it. However, by doing this, it gives us one huge advantage over how we put things on the screen before, and that is how we have control over how things are ordered. Now, regardless of when we created the sprites they will now appear in the order we put them into the display group screen. So, if we decided that the monster should go behind the the backgroundnear objects, we would simply need to insert them into screen group after we have inserted the monster.

Figure 2

Next, modify your update() function to look like this one:

local function update( event )
    updateBackgrounds()
    updateSpeed()
    updateMonster()
    updateBlocks()
    checkCollisions()
end

This will call the remainder of the functions that we need to run to make sure everything is well updated. One thing to note while working with your update function is that order does matter. For our little running game the order isn’t that important as it is called thirty times a second, so anything that is updated will be caught extremely quickly.  Also there is no crucial data that the functions ca ruin for each other. However, there will be times when you need to be careful about what order you put things in. This is especially true when you have multiple functions that update the same variables in different ways. Usually though this is really just a matter of using common sense though and you will be able to logically step through which things should be handled first.

Here are the rest of the functions that will do the work that we just called from the update function. Put them beneath the update function. Be sure to read the comments as I will use them to describe what is going on.

function checkCollisions()
     wasOnGround = onGround
     --checks to see if the collisionRect has collided with anything. This is why it is lifted off of the ground
     --a little bit, if it hits the ground that means we have run into a wall. We check this by cycling through
     --all of the ground pieces in the blocks group and comparing their x and y coordinates to that of the collisionRect
     for a = 1, blocks.numChildren, 1 do
          if(collisionRect.y - 10 > blocks[a].y - 170 and blocks[a].x - 40 < collisionRect.x and blocks[a].x + 40 > collisionRect.x) then
               speed = 0
          end
     end
     --this is where we check to see if the monster is on the ground or in the air, if he is in the air then he can't jump(sorry no double
     --jumping for our little monster, however if you did want him to be able to double jump like Mario then you would just need
     --to make a small adjustment here, by adding a second variable called something like hasJumped. Set it to false normally, and turn it to
     --true once the double jump has been made. That way he is limited to 2 hops per jump.
     --Again we cycle through the blocks group and compare the x and y values of each.
     for a = 1, blocks.numChildren, 1 do
          if(monster.y >= blocks[a].y - 170 and blocks[a].x < monster.x + 60 and blocks[a].x > monster.x - 60) then
               monster.y = blocks[a].y - 171
               onGround = true
               break
          else
               onGround = false
          end
     end
end
function updateMonster()
     --if our monster is jumping then switch to the jumping animation
     --if not keep playing the running animation
     if(onGround) then
          --if we are alread on the ground we don't need to prepare anything new
          if(wasOnGround) then
          else
               monster:prepare("running")
               monster:play()
          end
     else
          monster:prepare("jumping")
          monster:play()
     end
     if(monster.accel > 0) then
          monster.accel = monster.accel - 1
     end
     --update the monsters position accel is used for our jump and
     --gravity keeps the monster coming down. You can play with those 2 variables
     --to make lots of interesting combinations of gameplay like 'low gravity' situations
     monster.y = monster.y - monster.accel
     monster.y = monster.y - monster.gravity
     --update the collisionRect to stay in front of the monster
     collisionRect.y = monster.y
end
--this is the function that handles the jump events. If the screen is touched on the left side
--then make the monster jump
function touched( event )
     if(event.phase == "began") then
          if(event.x < 241) then
               if(onGround) then
                    monster.accel = monster.accel + 20
               end
          end
     end
end

Notice that the touched function is never called. At the bottom of the code right below where you use the timer that calls the update function, put this code:

Runtime:addEventListener("touch", touched, -1)

Let’s review a couple things from the code we just put in. The touched function passes in an event. Each “event” has properties of its own. When we say event.phase == “began” we are telling Corona that we want to be notified as soon as the user touches the screen. On the contrary if we had said “ended” instead of began we would be telling Corona not to notify us until the user had lifted his finger from the screen. Also stored in the event is the coordinates of the touch. This is why using began and ended is important. Do you want the location of when the user first touches the screen, or when he lets go? In most game situations you want to know as soon as the user touches the screen, but not always. For a full reference of the touch events you can go here(https://developer.anscamobile.com/reference/index/events/touch).

So, when you run this you will notice that you only jump if you touch the left side of the screen. The reason why we do that is because we want to reserve the right side of the screen for other things, like shooting fireballs. That does not fall under the scope of this project, but we will get there soon enough! With all of that in there we should now be good to go.

Figure 3

With everything in its place you should now have a monster that can run and jump on the ground! Slowly, our little tutorial is starting to feel like a game! As always, if you have any questions let me know in the comments section below and thanks for following along!

Series Navigation«Build an Endless Runner Game From Scratch: Using SpritesBuild an Endless Runner Game from Scratch: Adding Events»

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Misha

    Hello!

    Is there a way to use spritesheets also for non-animated images? Combining all images in the same spritesheet would save performance and memory, in most circumstances. But I cannot find a function in Corona to use spritesheets for anything else but animations.

    Thanks!

  • http://- Luis

    Excelent series!! please continue working on this. My favorite mobile game style is the endless runner :)

  • John

    Good but at the time not resolution independent.Do you plan a tutorial on this ?

    • Tyler King
      Author

      @ Misha and John

      So both of you have great questions and I’m sorry it took so long to answer, the reason why I held off is that recently(yesterday I believe) Ansca has released some new features in Corona SDK. The biggest change that is relevant to these tutorial series is that instead of using spriteSheet, they use imageSheet. ImageSheets are much more flexible in what they can do and actually solve both problems you two brought up. They handle multiple resolutions and handle using grabbing a single image from a spriteSheet(now imageSheet). The methods we talk about in here still work and are supported. However the new methods are much more flexible and efficient. Again everything we do here will work fine and be smooth, but there is a new more efficient way as of today. I will see about making a new tutorial that will cover using the new imageSheets. For a look at them check out this blog entry:

      http://blog.anscamobile.com/2012/03/image-sheets-image-groups-and-sprites/

  • Walt

    These tutorials are awesome! They are helping me a lot…

    Can you please explain a little more what goes on in the checkCollisions function? I understand almost everything except what values you compare in both “if” statements. Why “-10″, “170″, “40″? What are them exactly?

    for example: collisionRect.y – 10 > blocks[a].y – 170
    what is that checking?

    I’m having a hard time to understand this.

    • Tyler King
      Author

      Hey Walt thanks for following the tutorials so far! So when you have an image that is let’s say 10×10 and you set x to be 0 and y to be 5. You would see that the image is at the top left of the screen. The entire height of the image would be on the screen, but only half of the width would be on the screen. This is because x and y point to the center of the image.

      So when we have:

      collisionRect.y – 10 > blocks[a].y – 170
      and
      blocks[a].x – 40 collisionRect.x

      we are actually checking each of those conditions to see if those specific points match up. For example for blocks[a].x -40, we are going to go to the blocks[a].x then check 40 pixels to the left, then do the same but 40 pixels to the right. This checks to see if the collionsRect.x is inside of the entire width of the current block. If it does fall in between the blocks pixels width-wise then check the y coordinate. If that is below the top of the block(blocks[a].y – 170 gives us the top of the block) then we know that our box has collided with the ground moving forward, so we have run into a wall and our game should be over!

      Let me know if that doesn’t make sense, what you are really doing is just using those numbers to figure from the center of the sprite where collisions should be happening and checking there. Thanks for the question!

      • Walt

        Thanks for your reply Tyler! That made things clearer for me!
        Just one more question. Your blocks are 240 px tall. So from the center to the top there are 120 px. Are you adding those extra 50 px to check the collision using only the top of the collisionRect?
        Does this mean that if my character’s legs hit a wall, nothing will happen because the top of the collisionRect is still above the top of the block?
        You are awesome man, all this helps a lot!

  • Bran Mikkonos

    Hey Tyler, first of all, these tutorials are great for me to learn!
    I have a doubt. I made a jumping monster like you, but made an animation for the jump (my monster has some wings and they move as if there is wind). My problem is that i can’t get the animation running. Whenever it jumps, it just plays the first frame of the whole sequence until the jump ends and then my monster starts running again. The jumping animation never plays apart from the first frame.
    I checked if my jumping animation is right. In order to do that, i changed the walking animation to the jumping animation. It worked, the animation played well, as the monster advanced horizontally on ground level. So i’m not declaring the animation wrongly. This problem only happens when the monster is actually jumping and changing his elevation.
    Any guidance will be extremely appreciated.
    Thanks a lot for this awesome help!!

    • Tyler King
      Author

      So when you set up your animation for the jumping how do you do it? If you have a jumping animation that is 4 frames long your setup should look like this:
      sprite.add(monsterSet, “jumping”, 1, 4, 400, 0)

      1 is the starting frame so if the first frame of your animation is the 5th frame and it is 4 frames long you would put this:
      sprite.add(monsterSet, “jumping”, 5, 9, 400, 0)

      400 is how long you want the animation to run for, so 400 would be 100 milliseconds per frame
      0 is how many times you want the animation to run for, 0 means keep running until you say stop

      then when you start your jump you should do this
      monster:prepare(“jumping”)
      monster:play()

      Is this how you have it set up? It sounds like maybe you might need to edit those values a bit, lemme know if that is the problem!

  • Tyler King
    Author

    @Walt, your assumption is correct. There are certain angles and positions in which an arm or a leg might brush through an object. When you use bounding box collisions you have to make the decision do I want there to be the possibility that an obstacle can kill my player when it does not hit exactly the player but hits the square box around it or do I want small(insignificant) parts of my player to be able to pass through things without killing the player. The only time bounding box collision detection will work perfectly is when your sprite is a perfect box. This is something that you can take into account when you design your characters and objects. In this game I just messed around with the numbers until I found numbers that felt pretty good, and is what you will want to do in your own game. Play with it until it feels good!

  • Bilen

    Hi, thanks for an very good tutorial.

    im wondering why you dont use the corona physics engine for the collision and gravity parst of the game?

    is there any cons/pros of doing it your way vs physics?

  • http://mobile.tutsplus.com/tutorials/corona/build-an-endless-runner-from-scratch-sprite-interaction/ Dev

    wouldn’t it be easier to have names for the block so you know if the hero hit the floor like:

    <pre name="code"

    –in the block generation function
    block.name="block"

    –in the checkCollision function
    if(event.name=="block")then
    onGround=true
    end

  • Rahul Mohan

    Hey! I’m having some trouble with the updating. I added the following code under the update background function.

    backgroundnear2.x = backgroundnear2.x – (speed/5)
    if(backgroundnear2.x < -239) then
    backgroundnear2.x = 760
    end
    end

    However, the trees are not updating. Thanks for the help!