How to make a PICO-8 game: Part 8

How to make a PICO-8 game: Part 8

Last time, we got halfway through butchering our previously working game, leaving it in a state where it refused to even run. This time, we’ll fix that.

Part 8: Enemy Objects

Now that there are multiple enemies, and they’re all objects, we can no longer use any code that refers to the old variables of ex and ey, or any other “single enemy” legacy code. Currently, that also means that collisions no longer work.

Luckily, the function checkcol() only needs to be sent two lots of x and y values, so still works fine providing we send it the right variables, so we can leave that alone. However, we need to replace the code that calls it.

At the moment, it says this in _update():

 -- check for collision
 if checkcol(x,y,ex,ey) then
  stop()
 end

which doesn’t work – note the ex and ey. Instead, lets call this function for each enemy, and pass that enemy’s x and y to the function. Like this:

 -- check for collision
 foreach(enemies, function(en)
  if checkcol(x,y,en.x,en.y) then
   stop()
  end
 end)

Be extra careful with the brackets here – the foreach encloses the code in them!

Hopefully by now what this code does is clear, as it’s similar to the enemy  draw routine: It runs through each enemy in enemies{} and triggers checkcol for each.

That’s collisions sorted. Our code won’t run yet, because we still have to fix the enemy move and enemy speed increase routines.

At the moment, moves are dealt with here:

 -- move enemy
 if x < ex then
  ex -= d
 end
 if x > ex then
  ex += d
 end
 if y < ey then
  ey -= d
 end
 if y > ey then
  ey += d
 end

and again, ex and ey need replacing.

Why not include a move function as part of the enemy object itself, like how we included a draw function? After the en.draw function in make_enemy(), we can do this:

 en.move = function(this)
  if x < this.x then this.x -= this.d end
  if x > this.x then this.x += this.d end
  if y < this.y then this.y -= this.d end
  if y > this.y then this.y += this.d end
 end

You’ll notice this is essentially the same code, only ex and ey are now this.x and this.y (as they refer to en as “this”), and it’s laid out on fewer lines.

Of course, we need to call this in turn like before, so replace the move code in _update() with:

 foreach(enemies, function(en) en:move() end)

Now, we need to replace the d += dinc code in _update(). It currently still refers to a single “d”, whereas every enemy has it’s own. It should be easy enough now:

foreach(enemies, function(en) en.d += dinc end)

And the code works again! Only… there are no enemies. Let’s put some in.

To start with, we’ll create one enemy in _init(). After the enemies = {} code, we’ll call make_enemy() with some attributes:

 make_enemy(2,120,120,0.05,0,5)

Remember, in order, these values are:

Sprite number (2), x and y coordinates (120 and 120), d (“speed” of the enemy, 0.05), how many “animation ticks” there are currently (0), and how many animation ticks there are in total (5).

Now we’re back to where we were before we broke the game. But we did that to create more enemies whenever we wanted, right? Right. Let’s do that then. For simplicity, we’ll create one every 200 ticks, at the same time we speed the enemies up. So, after ticks=0 in _update():

  make_enemy(2,rnd(128),rnd(128),0.05,0,4)

“rnd” means random number, so “rnd(128)” means pick a random number between 0 and 128. It actually means fractions of numbers too, with 0 being the lowest possible number, and 127.999 the highest. That’s accurate enough for us here, and it provides a way of randomly generating x and y coordinates for our new enemy.

Now run the game. Every 200 “ticks”, we get a new enemy! That seems like a lot of work just to get multiple enemies, but you can see if you implement something like this at the start, updating, moving and drawing many, many enemies (or other objects) all in one go is a breeze.

Next time, we’ll look at making our game a bit more visually interesting, by adding animation to the enemies.

All the code so far:

function _init()
 -- player start location
 x = 64
 y = 64
 
 ticks = 0
 tickstrigger = 200
 dinc = 0.05
 -- score
 score = 0
 
 -- initialise enemies table
 enemies = {}
 
 -- make first enemy
 make_enemy(2,120,120,0.05,0,5)
end

function _update()
 -- check for collision
 foreach(enemies, function(en)
  if checkcol(x,y,en.x,en.y) then
   stop()
  end
 end)

 --[[ check button presses and screen edge
  btn(0) is left
  btn(1) is right
  btn(2) is up
  btn(3) is down
 ]]
 if btn(0) and x > 0 then
  x -= 1
 end
 if btn(1) and x < 120 then
  x += 1
 end
 if btn(2) and y > 0 then
  y -= 1
 end
 if btn(3) and y < 120 then
  y += 1
 end

 -- move enemies
 foreach(enemies, function(en) en:move() end)
 
 -- increment score
 score += 1
 
 -- if ticks are triggered, make enemy faster
 ticks += 1
 if ticks > tickstrigger then
  foreach(enemies, function(en) en.d += dinc end)
  ticks = 0
  make_enemy(2,rnd(128),rnd(128),0.05,0,4)
 end
 
end

function _draw()
 cls(2) -- clear the screen to mauve
 print(score,100,0,7) -- print score
 spr(1,x,y) -- draw sprite 1 at x, y
 -- draw all enemies
 foreach(enemies, function(en) en:draw() end)
end

function checkcol(ax,ay,bx,by)
 if bx+8>ax and bx<ax+8 and by+8>ay and by<ay+8 then
  return true
 else
  return false
 end
end

function make_enemy(espr,ex,ey,ed,eticks,eticksmax)
 en = {}
 en.sprite = espr
 en.x = ex
 en.y = ey
 en.d = ed
 en.frame = 0
 en.ticks = eticks
 en.ticksmax = eticksmax
 
 en.draw = function(this)
  spr(this.sprite,this.x,this.y)
 end
 
 en.move = function(this)
  if x < this.x then this.x -= this.d end
  if x > this.x then this.x += this.d end
  if y < this.y then this.y -= this.d end
  if y > this.y then this.y += this.d end
 end
 
 add(enemies,en)
end

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.