Remember That Movie From The 80s

Coming up with the title or this post made me remember that movie Enemy, Mine from the 80s. I think it was starring Dennis Quaid and Lewis Gossit Jr. They were enemies crash landed on a hostile deserted world and had to work together to survive. Then of course they become friends and have a bunch of adventures.

Interesting that they were both starship pilots and I’m working on a space shooter game… cause you know I liked them when I was a kid… in the 80s…

Making Sparks

To copy another element from Cave Berries, because I really like the way the laser sparks when it hits a wall, I added a sparks function/method to the bulletconstruct function:

obj.sparks = function(this)
  if (this.des > 0) then
    for i = 1,4 do
      rnd_x = flr(rnd(12))
      modx = 2
      rnd_y = flr(rnd(4))
      c = 11

      if (i % 2 == 0) then rnd_x += modx, c = 3 end

      pset(this.position.x - 4 + rnd_x, this.position.y + rnd_y, c)
    end
  end
end

The sparks method (I’m going to go ahead and refer to a function on an object/table as a method) also needs to be called in the _draw method in order to actually have the pset function work:

foreach(bullets, function(obj)
    spr(obj.sprite, obj.position.x, obj.position.y)
    obj.sparks(obj)
end)

The above is the updated foreach bullets in the _draw function. Also, inside the bulletconstruct destroy method call it:

obj.destroy = function(this)
  this.des += 1
  this.sparks(this)
  this.hitbox = {x=0,y=0,w=0,h=0}
end

There now we have a cool visual… but no sound effect cause you know… space and all.

First Enemy

As you might be able to tell I whipped up an enemy ship that shoots out a red and orange bullet/bomb/beam out our plucky hero. To get things working we’ll copy ‘n paste a lot of code from the asteroid and bullet objects. Add an enemy function:

function enemy()
  local obj = {}

  -- only come in from random top area.
  x = flr(rnd(127))

  -- set the direction randomly left, right, or center
  xs = {0, 1, 2}
  rnd_x = xs[flr(rnd(3)) + 1]
  if(rnd_x == 2)then rnd_x = -1 end
  obj.dir = rnd_x

  obj.position = {x=x, y=0}
  obj.sprite = 22

  obj.update = function(this)
    this.position.y += 1
    this.position.x += obj.dir

    if(this.position.x == hero.position.x or this.position.x + 8 == hero.position.x + 8)then obj.fire(obj) end

    -- remove it when it goes off screen
    if(this.position.y>127)then del(asteroids, this)end
    if(this.hp <= 0)then this.des += 1 end
    if(this.des >= 20)then this.sprite = 24 end
    if(this.des >= 40)then del(enemies, this) end
  end

  obj.explode = function(this)
    this.sprite = 23
  end

  obj.hitbox = {x=0, y=2, w=8, h=8}
  obj.hp = 10
  obj.des = 0

  obj.fire = function(this)
    if (this.des == 0) then
      text = "enemy fire!!! ..."

      add(bullets, bullet(this.position.x, this.position.y + 10, 'down', 6))
    end
  end

  return obj
end

At this point the enemy is pretty much the same as an asteroid. Notice the single line if in the update method though. If the enemy happens to line up with the hero on the x axis, then FIRE! The fire method is new and basically adds a bullet to the bullets array.

Notice that the function call is different than what we used before. I did some refactoring of the $construct functions and made them “first class”. Not sure what assigning a function to a variable gets you in Lua, but it seems to work the same as using the function keyword.

Also, I setup the symbols-tree-view package for Atom to be able to list the functions in a list in the right-hand side of the editor window. I find it super useful when jumping around between functions. The details can be found in this thread. Thanks to @Sanju for pointing it out to me.

Updated bullets

Here’s the updated bullet definition:

function bullet(x, y, dir, sprite)
  local obj = {}
  obj.position = {x=x, y=y}
  obj.sprite = sprite
  obj.des = 0
  obj.dir = dir

  obj.update = function(this)
    if(this.des == 0 and this.dir == 'up')then this.position.y -= 6 end
    if(this.des == 0 and this.dir == 'down' and this.sprite == 6)then this.position.y += 4 end

    -- it's hit something
    if(this.des > 0)then this.destroy(this) end
    if(this.des >= 5)then del(bullets, this) end

    -- remove it when it goes off screen
    if(this.position.y<0)then del(bullets, this)end
  end

  obj.hitbox = {x=0, y=0, w=8, h=8}

  obj.destroy = function(this)
    this.des += 1

    this.sparks(this)
    this.hitbox = {x=0,y=0,w=0,h=0}
  end

  obj.sparks = function(this)
    -- if(this.des > 0)then pset(this.position.x, this.position.y, 10) end
    if (this.des > 0) then
      for i = 1,4 do
        rnd_x = flr(rnd(12))
        modx = 2
        rnd_y = flr(rnd(4))
        c = 11

        if (i % 2 == 0) then rnd_x += modx, c = 3 end

        pset(this.position.x - 4 + rnd_x, this.position.y + rnd_y, c)
      end
    end
  end

  --return the bullet
  return obj
end

The main changes are in the parameter list. Since enemies will be firing down and will use different beams we need to specify a direction and a sprite. Then in the update method the direction is checked and the bullet’s y position is either incremented or decremented.

Updating update

Next, the _update function needs adjusted to take into account enemies and having enemy bullets hit our hero. Before we dive into that let’s take a look at the each_enemies function. The _update function was starting to get a little crowded so I moved some looping code into another function:

function each_enemies()
  foreach(enemies, function(enemy)
      enemy.update(enemy)

      -- collide enemies with bullets
      foreach(bullets, function(bullet)
        if (enemy.hp > 0) then
          if (collide(enemy, bullet)) then
            text = "enemy hit!!!"
            -- display bullet sprite
            bullet.destroy(bullet)

            enemy.hp -= 1

            -- knock it back a little
            enemy.position.y -= 2

            if(enemy.hp <= 0) then enemy.explode(enemy)end
          end
        end
      end)

      -- enemy collides with hero?
      if (collide(hero, enemy) and enemy.des == 0 and hero.des == 0) then
        sfx(01)
        enemy.hp -= 1
        hero.hp -= 1

        -- knock it back a litte
        enemy.position.y -= 2
        enemy.position.x -= enemy.dir + enemy.dir

        if(enemy.hp <= 0) then enemy.explode(asteroid) end
        if(hero.hp <= 0) then hero.explode(hero) end
      end
  end)
end

Basically the same as the asteroids loop this function uses a foreach to loop the enemies array (added to the _init function) and inside the enemies loop the bullets array is looped an collide is used to check for hits on the enemy ship. Like the astroids loop collide is also used to check for collisions with the hero ship. Thinking to have a “drone” type enemy that will try to blast into the hero, but we’re not quite there yet.

Then inside the _update function add:

each_enemies()

Getting due for some major refactoring, but that’ll probably be a post all by itself.

Hero Bullet hits

In the hero object setup inside the _init function we need to do another bullet loop:

-- bullet hits hero
foreach(bullets, function(bullet)
  if (this.hp > 0) then
    if (collide(this, bullet)) then
      text = "hero hit!!!"
      sfx(4)

      -- display bullet sprite
      bullet.destroy(bullet)

      this.hp -= 1

      -- knock it back a little
      this.position.y += 2

      if(this.hp <= 0) then this.explode(this)end
    end
  end
end)

The loop is placed in the hero’s update method and is similar to the other bullet loops. This loop uses collide to check if they are hitting the hero, sets the text for debug purposes, plays a sound cause there’s probably be some noise when something hits the outside of a ship in space, subtracts a hp while knocking the hero back a little, and then a check to see if the hero is out of hp and if so call the explode method.

Conclusion

Currently the enemies only enter the screen when the btn(5) is pressed. So I think next I’ll work on some AI/attack patterns. Should make the game more fun.

Then the great refactor might happen. Not sure when I’ll hit the token ceiling, but I feel like I’ve still got some space… (heh)

Party On!