Basic Game Tutorial 6: Items

Congratulations on making it to the final part of this project! Hopefully you've learned a lot throughout these tutorials and feel better about going about creating your own game.

In this part of the tutorial we'll be adding items to collect. As with the previous parts, the way we will achieve this in our program will allow you to add more items very easily.

Let's keep it classic with the good old coin. Our project will begin with just coins for our items, but adding different types will be very simple.

We'll need to begin with a few variables as usual. Just like with the state machine in the previous project, the items need a type and a state. Add the following lines to your program:

 50. coin = 0
 51.
 52. active = 0
 53. collect = 1
 54. inactive = 2

There we go! We've got a variable called coin which stores a 0. This will be used as an index into an array of tiles.

Similarly, we have a number of state variables below this which we will use to determine what happens to the coin during the game.

Now we need to create the array of items. Each item needs its own structure with a number of properties:

 56. items = [
 57.     [ .type = coin, .x =  7, .y = 1, .state = active ],
 58.     [ .type = coin, .x =  8, .y = 0, .state = active ],
 59.     [ .type = coin, .x =  9, .y = 0, .state = active ],
 60.     [ .type = coin, .x = 10, .y = 1, .state = active ] 
 61. ]

As you can see, each item has 4 properties. We have a .type which stores the type of the item. We have a .x and .y which are the level coordinates of the item (different than the screen coordinates, these level coordinates tell us which row and column of the level array the item will appear in) and finally a .state property to store the state.

Next up we'll need the tilesheet information to animate the items, just like we needed for the player:

 63. itemAnim = [
 64.     [ .start = 154, .length = 1 ]
 65. ]

Since we are only using coins at the moment, we don't need any more information in this array. If we were to add another type of item, we would need more information.

This information can be accessed with itemAnim[0].start, or, since we have a coin variable which stores a 0, we can say itemAnim[items[0].type]. This sort of array indexing, despite looking quite complex, is very useful and worth getting your head around!

We are using the item.type property as an index into the itemAnim array.

Lastly, we should create a variable to keep track of the number of coins the player has collected:

 67. playerCoins = 0

Excellent. Now we have everything we need to put the items on screen. Head into the main loop for this next part, just after the for loop which draws the level.

We'll be using a for loop to loop over the array of items and draw each one. This for loop will end up being rather long and complex looking, so let's build it step by step. First we just want to actually draw the coins on screen:

102.     for i = 0 to len( items ) loop
103.         x = items[i].x * tSize
104.         y = ( items[i].y + levelOffset ) * tSize
105.         drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
106.     repeat

Run the program and we should see the coins on screen. Of course, without the code to make it happen, we cannot pick the coins up yet.

Before we do that, let's make sure we understand what's happening. Our for loop counts using an i variable from 0 to the length of our items array. Our items array has 4 elements, so i will count from 0 to 3.

We create some local x and y variables to store the position of the item. This is just to make our code easier to read.

We take the .x and .y properties of the current item in question and multiply ithem by the tSize variable to give us the screen coordinates for the item. With the y position, we must add the levelOffset in order to put them on the correct row.

Then, on line 105, we use the drawSheet() function to draw the item. The tricky part here is the tile index:

itemAnim[items[i].type].start

This is actually one property of a structure within an array of structures indexing another array of structures to give us the correct property with which to index into a tilesheet. Try saying that three times quickly.

Since the only item type we are using is a coin, items[i].type is always a 0. If we use a 0 as an index into the itemAnim array, we get the animation tile for the coin.

As mentioned before, this might seem a little pointless since we could simply use the tile number for the coin in the tilesheet, but then when it comes to adding items we'll have a very difficult time indeed.

Collecting the Coins

If we want to be able to collect the coins, we'll have to make this for loop of ours a little more complicated.

First we'll wrap the calculations and the drawsheet() line in an if statement. We only want to do these things if the item is not inactive.

102.     for i = 0 to len( items ) loop
103.         if items[i].state != inactive then
104.             x = items[i].x * tSize
105.             y = ( items[i].y + levelOffset ) * tSize
106.             drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
107.         endif
108.     repeat

Great! Now we need to add an if statement to check if the player has moved into the range of an item.

Collision If Statement

This if statement is going to be quite long indeed. When writing game code there really is no avoiding this sometimes. Ready?

102.     for i = 0 to len( items ) loop
103.         if items[i].state != inactive then
104.             x = items[i].x * tSize
105.             y = ( items[i].y + levelOffset ) * tSize
106.             if playerX + pSize.x > x and playerX < x + tSize and
107.                playerY + pSize.y > y and playerY < y + tSize and
108.                items[i].state == active
109.             then
110.             drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
111.         endif
112.     repeat

Phew, check that out for an if statement! It's not even finished yet, this is just the condition! It's so large that we've split it up across multiple lines to make it easier to read. Remember, you can format your code however you like! There's nothing stopping you from breaking up long lines into multiple to make things clearer.

We are checking if the right hand side of the player ( playerX + pSize.x) is greater than the left side of the coin (> x), and the the left side of the player playerX is less than the right hand edge of the coin < x + tSize.

We are also checking if the player's feet playerY + pSize.y is greater than the top of the coin > y, and that the top of the player's head (playerY}) is less than the bottom of the coin tile ({< y + tSize).

We are also checking that the coin itself has to be active.

These 5 conditions must all be true for this if statement to begin. Now let's actually make something happen in it!

102.     for i = 0 to len( items ) loop
103.         if items[i].state != inactive then
104.             x = items[i].x * tSize
105.             y = ( items[i].y + levelOffset ) * tSize
106.             if playerX + pSize.x > x and playerX < x + tSize and
107.                playerY + pSize.y > y and playerY < y + tSize and
108.                items[i].state == active
109.             then
110.                 playNote( 0, 3, 1046.50, 1, 20, 0.5 )
111.                 playNote( 1, 3, 1396.71, 1, 10, 0.5 )
112.                 playerCoins += 1
113.                 items[i].state = collect
114.             endif
115.             drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
116.         endif
117.     repeat

There we have it. As you can see, the new lines are from 109 to 114. After the then, we first use two playNote() functions to play a nice coin collection sound.

We also increase the playerCoins variable by 1 and change the .state property of the item to collect.

By having a state other than active and inactive, we can now apply some cool things to happen before the coin vanishes.

When we pick up an item in a game we sometimes see that item shoot into the air a little before vanishing. Let's make this happen by using the .collect state:

102.     for i = 0 to len( items ) loop
103.         if items[i].state != inactive then
104.             x = items[i].x * tSize
105.             y = ( items[i].y + levelOffset ) * tSize
106.             if playerX + pSize.x > x and playerX < x + tSize and
107.                playerY + pSize.y > y and playerY < y + tSize and
108.                items[i].state == active
109.             then
110.                 playNote( 0, 3, 1046.50, 1, 20, 0.5 )
111.                 playNote( 1, 3, 1396.71, 1, 10, 0.5 )
112.                 playerCoins += 1
113.                 items[i].state = collect
114.             endif
115.             if items[i].state == collect then
116.                 items[i].y -= 0.15
117.                 if items[i].y < -1 then
118.                     items[i].state = inactive
119.                 endif
120.             endif
121.             drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
122.         endif
123.     repeat

There we go. That's our for loop all done!

Because of the collect state, we can make something happen to the item before it vanishes. On line 115 we check if the state of an item is collect. If it is, we reduce the y position of the item by a small amount. We then have another if statement within this to check if the y position has gone past a certain number. If it has, we change the state to inactive! Once the state is inactive, the item is no longer drawn due to the if statement on line 103.

Told you it would be a rather large for loop!

We're still missing something... We currently have no way of telling how many coins we have! We need a couple of draw commands to display the number of coins. Let's put these just after our items for loop:

125.     drawSheet( tilesheet, 154, 10, 10, scale )
126.     drawText( 10 + tSize * 0.75 + 10, 10, tSize * 0.75, grey, playerCoins )

The drawSheet() line just above puts an image of the coin in the top left corner of our screen. The drawText() line simply displays the playerCoins variable next to it!

Run the program and pick up a coin! We should hear a little sound, see the coin pop into the air and our coin counter in the top left should increase. If that's all happening, excellent!

The Program So Far

Alright that's all for now. As usual, below is a copy of the whole program. Make sure we're matching and your program works as intended before moving on to the next tutorial, in which we'll be adding an enemy to the game!

  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. moveSpeed = 5
  9.
 10. idle = 0
 11. walk = 1
 12. jump = 2
 13. hit  = 3
 14. 
 15. state = idle
 16.
 17. anim = [
 18.     [ .start = 96, .length = 1 ],
 19.     [ .start = 97, .length = 11 ],
 20.     [ .start = 95, .length = 1 ],
 21.     [ .start = 94, .length = 1 ]
 22. ]
 23.
 24. animationFrame = 0
 25.
 26. gravity = 1
 27. velocity = 0
 28.
 29. jumpTimer = 0
 30. oldA = 0
 31.
 32. screenX = 0
 33. screenY = 0
 34.
 35. tiles = [ 121, 138, 128, 129, 130 ]
 36. 
 37. level = [
 38.     [ -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 ],
 39.     [ -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 ],
 40.     [ -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 ],
 41.     [  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 ],
 42.     [  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 ],
 43.     [  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 ]
 44. ]
 45.
 46. levelHeight = 12
 47. levelOffset = levelHeight - len( level )
 48. tSize = 0
 49. 
 50. coin = 0
 51.
 52. active = 0
 53. collect = 1
 54. inactive = 2
 55.
 56. items = [
 57.     [ .type = coin, .x =  7, .y = 1, .state = active ],
 58.     [ .type = coin, .x =  8, .y = 0, .state = active ],
 59.     [ .type = coin, .x =  9, .y = 0, .state = active ],
 60.     [ .type = coin, .x = 10, .y = 1, .state = active ] 
 61. ]
 62.
 63. itemAnim = [
 64.     [ .start = 154, .length = 1 ]
 65. ]
 66.
 67. playerCoins = 0
 68.
 69. loop
 70.     clear()
 71.    
 72.     c = controls( 0 )
 73.
 74.     screenW = gwidth()
 75.     screenH = gheight()
 76.     scale = screenH / ( tileSize( tilesheet, 121 ).y * levelHeight )
 77.     tSize = scale * tileSize( tilesheet, 121 ).y
 59.     pSize = tileSize( chrSheet, 96 ) * scale
 60.
 61.     if playerX - screenX < screenW * 0.4 then
 62.         screenX -= moveSpeed
 63.     endif
 64.     if playerX - screenX > screenW * 0.6 then
 65.         screenX += moveSpeed
 66.     endif
 67.     if screenX < 0 then
 68.         screenX = 0
 69.     endif
 70.
 71.     drawImage( background, -screenX / 2, -screenY, screenH / imageSize( background ).y )
 72.
 73.     for row = 0 to len( level ) loop
 74.         for col = 0 to len( level[0] ) loop
 75.             if level[row][col] >= 0 then
 76.                 x = col * tSize
 77.                 y = ( row + levelOffset ) * tSize
 78.                 drawSheet( tilesheet, tiles[level[row][col]], x - screenX, y, scale )
 79.             endif
 80.         repeat
 81.     repeat
 82.
102.     for i = 0 to len( items ) loop
103.         if items[i].state != inactive then
104.             x = items[i].x * tSize
105.             y = ( items[i].y + levelOffset ) * tSize
106.             if playerX + pSize.x > x and playerX < x + tSize and
107.                playerY + pSize.y > y and playerY < y + tSize and
108.                items[i].state == active
109.             then
110.                 playNote( 0, 3, 1046.50, 1, 20, 0.5 )
111.                 playNote( 1, 3, 1396.71, 1, 10, 0.5 )
112.                 playerCoins += 1
113.                 items[i].state = collect
114.             endif
115.             if items[i].state == collect then
116.                 items[i].y -= 0.15
117.                 if items[i].y < -1 then
118.                     items[i].state = inactive
119.                 endif
120.             endif
121.             drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
122.         endif
123.     repeat
124.
125.     if c.a and jumpTimer < 12 then
126.         jumpTimer += 1
127.         velocity -= 8 / jumpTimer
128.         state = jump
129.     endif
130.
131.     if oldA and !c.a then
132.         jumpTimer = 12
133.     endif
134.
135.     oldA = c.a
136.
137.     velocity += gravity
138.    
139.     if !collision( playerX + pSize.x / 2, playerY + pSize.y + velocity ) then
140.         playerY += velocity
141.     else
142.         playerY = int( ( playerY + velocity + pSize.y ) / tSize ) * tSize - pSize.y
143.         velocity = 0
144.         jumpTimer = 0
145.         state = idle
146.     endif
147.
148.     if c.right and !collision( playerX + pSize.x / 2 + moveSpeed, playerY + pSize.y - 1 ) then
149.         playerX += moveSpeed
150.         if state != jump then
151.             state = walk
152.         endif
153.     endif
154.
155.     if c.left and !collision( playerX + pSize.x / 2 - moveSpeed, playerY + pSize.y - 1 ) then
156.         playerX -= moveSpeed
157.         if state != jump then
158.             state = walk
159.         endif
160.     endif
161.    
162.     animationStart = anim[state].start
163.
164.     if animationFrame >= anim[state].length then
165.         animationFrame = 0
166.     endif
167.
168.     drawSheet( chrSheet, animationStart + animationFrame, playerX - screenX, playerY, scale )
169.
170.     animationFrame += 0.2
171.
172.     update()
173. repeat
174.
175. function collision( x, y )
176.     tileX = int( x / tSize )
177.     tileY = int( y / tSize ) - levelOffset
178.    
179.     result = true
180.    
181.     if tileY < 0 or tileY >= len( level ) or tileX < 0 or tileX >= len( level[0] ) then
182.         result = false
183.     else
184.         if level[tileY][tileX] < 0 then
185.             result = false
186.         endif
187.     endif
188. return result

Functions and Keywords used in this tutorial

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