Build an Endless Runner Game From Scratch: Sprite Interaction
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.
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.
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.
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!