Basic Game Tutorial 7: Enemies

Welcome back! We're close to completion with this last part of the tutorial!

Creating enemies is going to be very similar indeed to the way we have programmed the items. We'll be using the exact same techniques here, even re-using the state variables we created in the last part.

However, we will need an extra state for the enemies. This state will be death and we will use it when we jump on an enemy. Add the following line to the item state variables:

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

All done. Now we need to create a type for the enemy along with a couple of arrays just like before. This next bit of code should go just before the main loop:

 70. slime = 0

Just like with coin, we create a variable to store an index into an array. Our enemy is going to be a slime, so that's what we've called the variable!

Next let's create the enemy information array:

 72. enemies = [
 73.     [ .type = slime, .x = 20, .y = 1, .state = active, .frame = 0, .velocity = 0, .dir = 0.05 ]
 74. ]

You might notice that we have more properties for the enemies than the items. We want our enemy to move around and to be affected by gravity just like the player. Because of this, each enemy needs a .velocity and a .dir to store its movement speed and direction. Since the enemies have multiple frames of animation and each enemy might be animated at different times, each enemy needs its own .frame property too.

Now we need an array to store the animation details for the enemies:

 76. enemyAnim = [
 77.     [ .start = 165, .length = 2 ]
 78. ]

Given that we only have one type of enemy (a slime), we only need one element in this array for now. If we added more enemy types, we would need more elements in this array with the tile information for each one.

For now we have everything we need to put a slime on screen and make it move.

Just as with the items, this will be a for loop which ends up being quite large. Ready? Of course you are.

This for loop will go after the draw commands for the items. Just like before, we'll start simple and add features as we go:

139.     for i = 0 to len( enemies ) loop
140.         if enemies[i].state != inactive then
141.             x = enemies[i].x * tSize
142.             y = ( enemies[i].y + levelOffset ) * tSize
143.             eAnimStart = enemyAnim[enemies[i].type].start
144.             eSize = tileSize( chrSheet, eAnimStart + enemies[i].frame ) * scale )
145.             drawSheet( chrSheet, eAnimStart + enemies[i].frame, x - screenX, y, scale )
146.         endif
147.     repeat

This for loop counts over the enemies array. First we check to see if the .state property is not inactive. If it is not, we calculate x and y positions just like with items.

On line 143 we create a variable to make our code easier to read. eAnimStart stores the starting tile of the enemy animation from the tilesheet.

We also create a variable to store the size of the scaled up enemy tile on line 144. Since each frame of the enemy animation might be a different size, we use eAnimStart + enemies[i].frame in the tileSize() function to give us the correct size for each frame.

Finally, on line 145 we use the drawSheet() function to draw the enemy at its calculated position.

Run the program to see our slimy friend sitting comfortably in mid air above the third platform of the level.

We still have some ways to go!

First, let's get the slime to animate. We'll need to adjust the enemies[i].frame property to achieve this:

139.     for i = 0 to len( enemies ) loop
140.         if enemies[i].state != inactive then
141.             x = enemies[i].x * tSize
142.             y = ( enemies[i].y + levelOffset ) * tSize
143.             eAnimStart = enemyAnim[enemies[i].type].start
144.             eSize = tileSize( chrSheet, eAnimStart + enemies[i].frame ) * scale )
145.             drawSheet( chrSheet, eAnimStart + enemies[i].frame, x - screenX, y, scale )
146.             enemies[i].frame += 0.1
147.             if enemies[i].frame >= enemyAnim[enemies[i].type].length then
148.                 enemies[i].frame = 0
149.             endif
150.         endif
151.     repeat

We have added lines 146 to 149 above. Just as we did with the player animation, we use an increasing animation frame variable to animate the enemy, then an if statement checks to see if the animation frame is greater than the length of the animation stored in the enemyAnim array. If it is, we reset it to 0.

Run the program to see our slime sliming about from one frame to the next. Let's get them out of the air and apply some gravity. First, we'll need an if statement:

139.     for i = 0 to len( enemies ) loop
140.         if enemies[i].state != inactive then
141.             x = enemies[i].x * tSize
142.             y = ( enemies[i].y + levelOffset ) * tSize
143.             eAnimStart = enemyAnim[enemies[i].type].start
144.             eSize = tileSize( chrSheet, eAnimStart + enemies[i].frame ) * scale )
145.
146.             if enemies[i].state != death then
147.             endif
148.
149.             drawSheet( chrSheet, eAnimStart + enemies[i].frame, x - screenX, y, scale )
150.             enemies[i].frame += 0.05
151.             if enemies[i].frame >= enemyAnim[enemies[i].type].length then
152.                 enemies[i].frame = 0
153.             endif
154.         endif
155.     repeat

We've created some lines of space around our new if statement on line 146 to make things clearer. Everything we add to this from here onward will be inside this if statement, since we only want the enemy to move or be jumped on if they are not already in the death state.

The death state for the enemies will be very similar to the collect state for items, since it will be used to make something specific happen before the enemy becomes inactive.

Let's get gravity and velocity working first of all:

146.             if enemies[i].state != death then
147.                 enemies[i].velocity += gravity
148.                 if !collision( x + eSize.x / 2, y + eSize.y + enemies[i].velocity / eSize.y ) then
149.                     enemies[i].y += enemies[i].velocity / tSize
150.                 else
151.                     enemies[i].y = int( ( enemies[i].y + enemies[i].velocity / tSize + eSize.y / tSize ) ) - eSize.y / tSize
152.                     enemies[i].velocity = 0
153.                 endif
154.             endif

There we have it! Run the program and our slimy friend should fall down to the ground and land safely.

To achieve this we are using almost exactly the same section of code as we did to make the player land safely on a platform. We use the custom collision() function again to check if the tile beneath the enemy is one to collide with. If it is not ( ! ), we apply the enemy's .velocity to the y position.

Colliding with Enemies

In order to interact with the enemies, we'll need a gigantic if statement just like we did for the items. Again, we'll split this up across a few lines for clarity:

146.             if enemies[i].state != death then
147.                 enemies[i].velocity += gravity
148.                 if !collision( x + eSize.x / 2, y + eSize.y + enemies[i].velocity / eSize.y ) then
149.                     enemies[i].y += enemies[i].velocity / tSize
150.                 else
151.                     enemies[i].y = int( ( enemies[i].y + enemies[i].velocity / tSize + eSize.y / tSize ) ) - eSize.y / tSize
152.                     enemies[i].velocity = 0
153.                 endif
154.                 if playerX + pSize.x > x and playerX < x + eSize.x and
155.                    playerY + pSize.y > y and playerY < y + eSize.y and
156.                    enemies[i].state == active and velocity > 0
157.                 then
158.                     enemies[i].state = death
159.                 endif
160.             endif

Our monster of an if statement begins at line 154. Similar to the items we are checking if the x and y positions of the player are in range of the x and y positions of the enemy. We also check if the enemy is in the active state, since the enemy must be alive and well in order for us to collide with them.

One extra condition in the if statement is that we must have a velocity greater than 0 (and velocity > 0). This means we cannot hurt the enemy unless we are jumping, since jumping is the only way to increase our velocity!

If all of these 6 conditions are true, the whole if statement is true and we set the state of the enemy to death. Similar to the collide function, we will use this as a way of applying specific effects to the enemy before it becomes inactive.

Making the Enemy Move

An enemy which just stands still isn't very exciting! We need to make our slime move around the platform and turn around if they're about to fall off the edge.

We'll be using the enemies[i].dir property for this.

146.             if enemies[i].state != death then
147.                 enemies[i].velocity += gravity
148.                 if !collision( x + eSize.x / 2, y + eSize.y + enemies[i].velocity / eSize.y ) then
149.                     enemies[i].y += enemies[i].velocity / tSize
150.                 else
151.                     enemies[i].y = int( ( enemies[i].y + enemies[i].velocity / tSize + eSize.y / tSize ) ) - eSize.y / tSize
152.                     enemies[i].velocity = 0
153.                 endif
154.                 if playerX + pSize.x > x and playerX < x + eSize.x and
155.                    playerY + pSize.y > y and playerY < y + eSize.y and
156.                    enemies[i].state == active and velocity > 0
157.                 then
158.                     enemies[i].state = death
159.                 endif
160.                 if !collision( x + eSize.x / 2 + enemies[i].dir * tSize, y + eSize.y ) then
161.                     enemies[i].dir = -enemies[i].dir
162.                 else
163.                     enemies[i].x += enemies[i].dir
164.                 endif
165.             endif

Our new if statement begins at line 160 and ends at 164. We use the custom collision() function again to check if the tile underneath the tile the enemy is about to walk into is empty. If it is, we use enemies[i].dir = -enemies[i].dir to change the direction the enemy travels in. If the tile in question is not empty, we simply keep moving!

We're almost done!

We just need something to happen when the enemy enters the death state. Since this entire section is wrapped in an if enemies[i].state != death, we can simply put an else before the endif to make something happen when enemies[i].state == death:

165.             else
166.                 enemies[i].y += 8 / tSize
167.                 y += 8
168.                 if y > screen_h then
169.                     enemies[i].state = inactive
170.                 endif
171.             endif

This last secion of the enemy code tells the enemy what to do when it enters the death state. We increase the y position of the enemy (moving it down the screen) and a simple if statement checks to see if the y position has become greater than the screen height. If it is, we set the enemy's state to inactive, preventing it from being drawn!

The Whole For Loop

WOW! That was a lot of code. Let's take a look at the whole enemies for loop to make sure we've got this right:

139.     for i = 0 to len( enemies ) loop
140.         if enemies[i].state != inactive then
141.             x = enemies[i].x * tSize
142.             y = ( enemies[i].y + levelOffset ) * tSize
143.             eAnimStart = enemyAnim[enemies[i].type].start
144.             eSize = tileSize( chrSheet, eAnimStart + enemies[i].frame ) * scale )
145.
146.             if enemies[i].state != death then
147.                 enemies[i].velocity += gravity
148.                 if !collision( x + eSize.x / 2, y + eSize.y + enemies[i].velocity / eSize.y ) then
149.                     enemies[i].y += enemies[i].velocity / tSize
150.                 else
151.                     enemies[i].y = int( ( enemies[i].y + enemies[i].velocity / tSize + eSize.y / tSize ) ) - eSize.y / tSize
152.                     enemies[i].velocity = 0
153.                 endif
154.                 if playerX + pSize.x > x and playerX < x + eSize.x and
155.                    playerY + pSize.y > y and playerY < y + eSize.y and
156.                    enemies[i].state == active and velocity > 0
157.                 then
158.                     enemies[i].state = death
159.                 endif
160.                 if !collision( x + eSize.x / 2 + enemies[i].dir * tSize, y + eSize.y ) then
161.                     enemies[i].dir = -enemies[i].dir
162.                 else
163.                     enemies[i].x += enemies[i].dir
164.                 endif
165.             else
166.                 enemies[i].y += 8 / tSize
167.                 y += 8
168.                 if y > screen_h then
169.                     enemies[i].state = inactive
170.                 endif
171.             endif
172.
173.             drawSheet( chrSheet, eAnimStart + enemies[i].frame, x - screenX, y, scale )
174.             enemies[i].frame += 0.05
175.             if enemies[i].frame >= enemyAnim[enemies[i].type].length then
176.                 enemies[i].frame = 0
177.             endif
178.         endif
179.     repeat

The Program So far

Let's take a look at the entirety of the project so far. Make sure you're matching up, then in the next and final tutorial we'll cover how to add your own ideas into the project:

  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. death = 3
 56.
 57. items = [
 58.     [ .type = coin, .x =  7, .y = 1, .state = active ],
 59.     [ .type = coin, .x =  8, .y = 0, .state = active ],
 60.     [ .type = coin, .x =  9, .y = 0, .state = active ],
 61.     [ .type = coin, .x = 10, .y = 1, .state = active ] 
 62. ]
 63.
 64. itemAnim = [
 65.     [ .start = 154, .length = 1 ]
 66. ]
 67.
 68. playerCoins = 0
 69.
 70. slime = 0
 71.
 72. enemies = [
 73.     [ .type = slime, .x = 20, .y = 1, .state = active, .frame = 0, .velocity = 0, .dir = 0.05 ]
 74. ]
 75.
 76. enemyAnim = [
 77.     [ .start = 165, .length = 2 ]
 78. ]
 79.
 80. loop
 81.     clear()
 82.    
 83.     c = controls( 0 )
 84.
 85.     screen_w = gwidth()
 86.     screen_h = gheight()
 87.     scale = screen_h / ( tileSize( tilesheet, 121 ).y * levelHeight )
 88.     tSize = scale * tileSize( tilesheet, 121 ).y
 89.     pSize = tileSize( chrSheet, 96 ) * scale
 90.
 91.     if playerX - screenX < screen_w * 0.4 then
 92.         screenX -= moveSpeed
 93.     endif
 94.     if playerX - screenX > screen_w * 0.6 then
 95.         screenX += moveSpeed
 96.     endif
 97.     if screenX < 0 then
 98.         screenX = 0
 99.     endif
100.
101.     drawImage( background, -screenX / 2, -screenY, screen_h / imageSize( background ).y )
102.
103.     for row = 0 to len( level ) loop
104.         for col = 0 to len( level[0] ) loop
105.             if level[row][col] >= 0 then
106.                 x = col * tsize
107.                 y = ( row + levelOffset ) * tsize
108.                 drawSheet( tilesheet, tiles[level[row][col]], x - screenX, y, scale )
109.             endif
110.         repeat
111.     repeat
112.
113.     for i = 0 to len( items ) loop
114.         if items[i].state != inactive then
115.             x = items[i].x * tSize
116.             y = ( items[i].y + levelOffset ) * tSize
117.             if playerX + pSize.x > x and playerX < x + tSize and
118.                playerY + pSize.y > y and playerY < y + tSize and
119.                items[i].state == active
120.             then
121.                 playNote( 0, 3, 1046.50, 1, 20, 0.5 )
122.                 playNote( 1, 3, 1396.71, 1, 10, 0.5 )
123.                 playerCoins += 1
124.                 items[i].state = collect
125.             endif
126.             if items[i].state == collect then
127.                 items[i].y -= 0.15
128.                 if items[i].y < -1 then
129.                     items[i].state = inactive
130.                 endif
131.             endif
132.             drawSheet( tilesheet, itemAnim[items[i].type].start, x - screenX, y, scale )
133.         endif
134.     repeat
135.
136.     drawSheet( tilesheet, 154, 10, 10, scale )
137.     drawText( 10 + tSize * 0.75 + 10, 10, tSize * 0.75, grey, playerCoins )
138.
139.     for i = 0 to len( enemies ) loop
140.         if enemies[i].state != inactive then
141.             x = enemies[i].x * tSize
142.             y = ( enemies[i].y + levelOffset ) * tSize
143.             eAnimStart = enemyAnim[enemies[i].type].start
144.             eSize = tileSize( chrSheet, eAnimStart + enemies[i].frame ) * scale
145.
146.             if enemies[i].state != death then
147.                 enemies[i].velocity += gravity
148.                 if !collision( x + eSize.x / 2, y + eSize.y + enemies[i].velocity / eSize.y ) then
149.                     enemies[i].y += enemies[i].velocity / tSize
150.                 else
151.                     enemies[i].y = int( ( enemies[i].y + enemies[i].velocity / tSize + eSize.y / tSize ) ) - eSize.y / tSize
152.                     enemies[i].velocity = 0
153.                 endif
154.                 if playerX + pSize.x > x and playerX < x + eSize.x and
155.                    playerY + pSize.y > y and playerY < y + eSize.y and
156.                    enemies[i].state == active and velocity > 0
157.                 then
158.                     enemies[i].state = death
159.                 endif
160.                 if !collision( x + eSize.x / 2 + enemies[i].dir * tSize, y + eSize.y ) then
161.                     enemies[i].dir = -enemies[i].dir
162.                 else
163.                     enemies[i].x += enemies[i].dir
164.                 endif
165.             else
166.                 enemies[i].y += 8 / tSize
167.                 y += 8
168.                 if y > screen_h then
169.                     enemies[i].state = inactive
170.                 endif
171.             endif
172.
173.             drawSheet( chrSheet, eAnimStart + enemies[i].frame, x - screenX, y, scale )
174.             enemies[i].frame += 0.05
175.             if enemies[i].frame >= enemyAnim[enemies[i].type].length then
176.                 enemies[i].frame = 0
177.             endif
178.         endif
179.     repeat
180.
181.     if c.a and jumpTimer < 12 then
182.         jumpTimer += 1
183.         velocity -= 8 / jumpTimer
184.         state = jump
185.     endif
186.
187.     if oldA and !c.a then
188.         jumpTimer = 12
189.     endif
190.
191.     oldA = c.a
192.
193.     velocity += gravity
194.    
195.     if !collision( playerX + psize.x / 2, playerY + pSize.y + velocity ) then
196.         playerY += velocity
197.     else
198.         playerY = int( ( playerY + velocity + pSize.y ) / tSize ) * tSize - pSize.y
199.         velocity = 0
200.         jumpTimer = 0
201.         state = idle
202.     endif
203.
204.     if c.right and !collision( playerX + pSize.x / 2 + moveSpeed, playerY + pSize.y -1 ) then
205.         playerX += moveSpeed
206.         if state != jump then
207.             state = walk
208.         endif
209.     endif
210.
211.     if c.left and !collision( playerX + pSize.x / 2 - moveSpeed, playerY + pSize.y - 1 ) then
212.         playerX -= moveSpeed
213.         if state != jump then
214.             state = walk
215.         endif
216.     endif
217.    
218.     animationStart = anim[state].start
219.
220.     if animationFrame >= anim[state].length then
221.         animationFrame = 0
222.     endif
223.
224.     drawSheet( chrSheet, animationStart + animationFrame, playerX - screenX, playerY, scale )
225.
226.     animationFrame += 0.2
227.
228.     update()
229. repeat
230.
231. function collision( x, y )
232.     tileX = int( x / tsize )
233.     tileY = int( y / tsize ) - levelOffset
234.    
235.     result = true
236.    
237.     if tileY < 0 or tileY >= len( level ) or tileX < 0 or tileX >= len( level[0] ) then
238.         result = false
239.     else
240.         if level[tileY][tileX] < 0 then
241.             result = false
242.         endif
243.     endif
244. 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()