Basic Game Tutorial 3: The Character

Alright! Here we go.

It's time to bring this game project to life with a character. In this tutorial, we'll be putting our character on screen and making it interact with the level. We want it to walk on the platforms and fall off the edges!

Setup

Before we get our character on screen, we need to do some setup. First, we must load a the character tilesheet:

  1. background = loadImage( "Kenney/backgrounds", false )
  2. tilesheet = loadImage( "Kenney/extraPlatformPack", false )
  3. chrSheet = loadImage( "Kenney/characters", false )

We're using the name chrSheet for the variable which stores the image we need.

Now let's create some player position variables:

  5. playerX = 0
  6. playerY = 0

Nice and simple. We'll begin with our character being drawn at the very top left of the screen.

Let's use these variables in a drawSheet() function to actually get our player character appearing on screen.

We'll be adding this line just before the update() function in our main game loop on line 46:

 46.    drawSheet( chrSheet, 96, playerX, playerY, scale )

Run your program and we should see our character sitting comfortably in the top left of the screen.

Okay! We're done. Enjoy your new game.

Just kidding. There's lots more to do.

First of all let's address a slight problem. Our character tile is a different size to the level building tiles. Because of this, just like our tSize variable, we'll need a player size variable to make things easier for us later.

Add the following line to your code, just beneath the tSize variable in the main loop:

 33. pSize = tileSize( chrSheet, 96 ) * scale

Just like with the tSize variable, we take the tile size of the character tile and multiply it by the scale variable.

Alright, let's move on!

If our game is going to be playable, we need our character to fall until he lands safely on the ground. When he's standing on a platform, he should not fall.

Achieving this is actually quite tricky. However, once complete, we will be able to freely create new parts to the level and it will work just fine!

Gravity and Velocity

You might have noticed that on Earth, when we jump into the air, we unfortunately come back down again. This is because of a rather inconvenient thing called gravity.

When we are in the air, gravity pulls us towards the centre of the Earth. Thankfully, there is some nice solid ground in the way to stop us going too far.

Velocity is the speed of an object in a direction.

Let's say we drop a stone from the top of a building. The force of gravity is making the stone move faster and faster towards the ground. This is called increasing in velocity.

When the stone reaches the ground, its velocity becomes 0. It has stopped. However, gravity has not changed.

We can simulate the effects of gravity and velocity in our game code and achieve some awesome things.

Let's create the variables we'll use for these effects:

  8. gravity = 1
  9. velocity = 0

Done! Now let's use these variables to affect the player. We need to add these next two lines just before the drawSheet() function used to draw the player. To make sure you've got it right, we'll show the end of the main loop too:

 50.     velocity += gravity
 51.     playerY += velocity
 52.
 53.     drawSheet( chrSheet, 96, playerX, playerY, scale )
 54.
 55.     update()
 56. repeat

Run the program to see our character plummet straight past the screen! Excellent!

Okay... So we have gravity. Now let's work on actually making a working floor. Before we explain how this works, we need to delete a line of code. Line 51 to be precise. Take a look at how lines 50 onward should look:

 50.     velocity += gravity
 51.   
 52.     drawSheet( chrSheet, 96, playerX, playerY, scale )
 53.    
 54.     update()
 55. repeat   

We do not always want our character's y position to be affected by the velocity variable all the time, only when there is no ground beneath them.

What exactly do we mean by ground anyway?

Going from Pixel Co-ordinates to Array Co-ordinates

What we need to do is quite complicated, so strap your focusing hats on.

We need to map our level array on to the screen so that it fits correctly in the tiles.

We must check the tile beneath the player to see if it is empty in the level array. As long as we have an empty tile beneath us, our y. position should be affected by velocity.

Let's return to our picture for a minute to illustrate what we mean:

See our cute character on the left side of the level? In the picture, he is above a tile indexed by the number 1. We want him not to fall unless he is above a -1 tile.

But how do we go from pixel co-ordinates to the co-ordinates of our array?

With maths! Hurray...

In all seriousness, this is an incredibly useful technique to learn - with it, you'll be able to create any 2D game with much more confidence.

Figuring out the surrounding tiles is something we'll want to do quite a bit in our project. Not just for falling, but for moving left and right and to interact with items too.

It's the perfect time to write our very own function to to do just that!

A function which does this needs to be given an x and y position to calculate from. We will pass these to the function as variables.

Remember, we write custom functions at the very end of our program:

 57. function collision( x, y )
 58.     tileX = int( x / tSize )
 59.     tileY = int( y / tSize ) - levelOffset
 60.    
 61.     result = true
 62.    
 63.     if tileY < 0 or tileY >= len( level ) or tileX < 0 or tileX >= len( level[0] ) then
 64.         result = false
 65.     else
 66.         if level[tileY][tileX] < 0 then
 67.             result = false
 68.         endif
 69.     endif
 70. return result

Now that's a scary bit of code right there! This function receives an x and a y position of the screen, converts them into array co-ordinates and finally tells us whether the tile at our x and y position is something to collide with (> 0) or not (< 0).

Remember, the tiles in our level array are empty if they are a -1. It tells us this with a single true or false variable called result. We can use this function to check if a tile is not a collision tile with a statement like:

if !collision( playerX, playerY ) then

(Remember, ! means not)

Here's how the function works:

First, we receive an x and y position. We then create two local variables called tileX and tileY which will be our array co-ordinates.

 58.     tileX = int( x / tSize )
 59.     tileY = int( y / tSize ) - levelOffset

We take the x and y positions passed to the function and divide them by the tSize variable to give us the tile coordinates.

We must use the int() function when we do this because we're looking for a whole number. If our result was a decimal, it would not work properly when used as an index into the level array.

For example, imagine our character is at the screen co-ordinates (450, 560).

We take 450 and divide it by the tSize variable. When undocked, the tSize variable is 60. 450 divided by 60 is 7.5. With the int() function, this becomes the number 7. So, our tileX variable now holds a 7!

Doing the same with the y position gives us a result of 9. We subtract the levelOffset variable and this gives us a 3. So tileY now stores a 3!

This operation tells us any grid position from any pair of screen co-ordinates. Now let's put those tileX and tileY variables to work.

The next part is a little tricky. First, we create the variable which will tell us whether the tile we are checking is solid (true) or empty (false). The variable is called result and at first it simply stores true.

 61.     result = true

Next, we check if the tile in question is actually in the range of our level array. We must do this because if our character falls down a pit, or walks somewhere on screen where there is no tile data in the level array, we will get an out of bounds error. This is because the system is trying to check for a place in the level array which does not exist.

To solve this, we use an if statement which checks whether the tileX or tileY variables are in the correct range. If they are not, we simply make our result variable false to indicate that we will not collide with the tile in question.

 63.     if tileY < 0 or tileY >= len( level ) or tileX < 0 or tileX >= len( level[0] ) then
 64.         result = false
 65.     else
 66.         if level[tileY][tileX] < 0 then
 67.             result = false
 68.         endif
 69.     endif
 70. return result

If it is in range, we check if that position in the array is solid or empty, then make the result variable true or false accordingly.

Using our Collision Function in the Program

Time to put this function to good use. We want to apply the gravity effect to our player only if the tile underneath them is empty.

Let's write this into our code. We're looking at lines 52 to 57 below:

 52.     if !collision( playerY + pSize / 2, playerY + pSize + velocity ) then
 53.         playerY += velocity
 54.     else
 55.         playerY = int( ( playerY + velocity + pSize.y ) / tSize ) * tSize - pSize.y
 56.         velocity = 0
 57.     endif

Here, we are using our collision() function to check if the tile below the player is not solid. We use an exclamation mark (!) before the function call to check if the result is not true.

Let's take a quick look at the arguments for the function call. We want to pass the player's x and y positions to the function, but we need a couple of other things.

First, we must add pSize / 2 to the x position, because we want to check the middle of the tile, not the corner. We also add tSize to the y position to check the bottom of the tile rather than the top. Check out the graphics below to see what we mean by this.

The image below shows the origin point of the tile in yellow. This point is ( playerX, playerY ).

If we add pSize.x / 2 to the x position, we are describing this point (shown in yellow):

Finally, we add pSize.y to the y position:

That gives us the bottom of the character's feet, but we need to check where they are going to be, not where they currently are. For this reason, we add the velocity variable to the y position. We are checking where the character would be if velocity was added to their position.

Back to the if statement:

 52.     if !collision( playerX + pSize.x / 2, playerY + pSize.y + velocity ) then
 53.         playerY += velocity
 54.     else
 55.         playerY = int( ( playerY + velocity + pSize.y ) / tSize ) * tSize - pSize.y
 56.         velocity = 0
 57.     endif

So, if the collision() function returns a false, we know that the tile in question is empty, and we can apply velocity to the character's y position.

We use an else on line 54 to give an instruction if the tile in question is not empty.

On line 55 we set the y position of the player to be exactly where we want it to be and set velocity to 0.

Run the program to see the character fall perfectly on to the platform!

If you've made it this far, you deserve a huge congratulations!

Believe it or not, that's all of the collision code complete. This will allow us to create new parts to our level and they will work perfectly.

End Result

Let's double check we're at the same point in the project. Below is a list of exactly how the program should look. If yours is all up to scratch and works without errors then we'll see you in the next tutorial where we'll be making our player move and jump! Exciting!

  1. background = loadImage( "Kenney/backgrounds", false )
  2. tilesheet  = loadImage( "Kenney/superPlatformPack", false )
  3. chrSheet   = loadImage( "Kenney/characters", false )
  4.
  5. playerX = 0
  6. playerY = 0
  7.
  8. gravity = 1
  9. velocity = 0
 10.
 11. screenX = 0
 12. screenY = 0
 13.
 14. tiles = [ 121, 138, 128, 129, 130 ]
 15. 
 16. level = [
 17.     [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
 18.     [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
 19.     [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  2,  3,  4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
 20.     [  1,  1,  1,  1, -1, -1,  1,  1,  1,  1,  1,  1, -1, -1, -1, -1, -1,  1,  1,  1,  1,  1,  1,  1,  1,  1 ],
 22.     [  0,  0,  0,  0, -1, -1,  0,  0,  0,  0,  0,  0, -1, -1, -1, -1, -1,  0,  0,  0,  0,  0,  0,  0,  0,  0 ],
 22.     [  0,  0,  0,  0, -1, -1,  0,  0,  0,  0,  0,  0, -1, -1, -1, -1, -1,  0,  0,  0,  0,  0,  0,  0,  0,  0 ]
 23. ]
 24.
 25. levelHeight = 12
 26. levelOffset = levelHeight - len( level )
 27. tSize = 0
 28. 
 29. loop
 30.     clear()
 31.    
 32.     screenW = gwidth()
 33.     screenH = gheight()
 34.     scale = screenH / ( tileSize( tilesheet, 121 ).y * levelHeight )
 35.     tSize = scale * tileSize( tilesheet, 121 ).y 
 36.     pSize = tileSize( chrSheet, 96 ) * scale
 37.
 38.     drawImage( background, -screenX, -screenY, screenH / imageSize( background ).y )
 39.
 40.     for row = 0 to len( level ) loop
 41.         for col = 0 to len( level[0] ) loop
 42.             if level[row][col] >= 0 then
 43.                 x = col * tSize
 44.                 y = ( row + levelOffset ) * tSize
 45.                 drawSheet( tilesheet, tiles[level[row][col]], x, y, scale )
 46.             endif
 47.         repeat
 48.     repeat
 49.
 50.     velocity += gravity
 51.    
 52.     if !collision( playerX + pSize.x / 2, playerY + pSize.y + velocity ) then
 53.         playerY += velocity
 54.     else
 55.         playerY = int( ( playerY + velocity + pSize.y ) / tSize ) * tSize - pSize.y
 56.         velocity = 0
 57.     endif
 58.
 59.     drawSheet( chrSheet, 96, playerX, playerY, scale )
 60.
 61.     update()
 62. repeat
 63.
 64. function collision( x, y )
 65.     tileX = int( x / tSize )
 66.     tileY = int( y / tSize ) - levelOffset
 67.    
 68.     result = true
 69.    
 70.     if tileY < 0 or tileY >= len( level ) or tileX < 0 or tileX >= len( level[0] ) then
 71.         result = false
 72.     else
 73.         if level[tileY][tileX] < 0 then
 74.             result = false
 75.         endif
 76.     endif
 77. return result

Functions and Keywords used in this tutorial

clear(), drawImage(), drawSheet(), else, endIf, for, function, gHeight(), gWidth(), if, int(), len(), loadImage(), loop, repeat, return, tileSize(), then, to, update()