Some enemies

Started by garsim, September 11, 2013, 11:41:55 PM

Previous topic - Next topic
Hello, it's me again. I said in another topic that since I don't have enough stuff to present the project I'm working on, I'd share some scripts, so here I am.  :)
From now on, maybe it's not very spectacular, but maybe it can help beginners with some skills. ^^

I was working on some enemies from a game named Chip's Challenge (a really good retro game by the way) and tried to implement them as enemies for the Solarus quest editor.

Fireballs and gliders

First, I did the fireball and the glider, two enemies which go straight until they hurt an obstacle. In that case, they go to another direction :
- the fireball goes to its right by default ; and if it's not possible, it goes to his left ; and if it's not possible (if there are walls on its left and on its right) it comes back.
- the glider goes to its left by default ; and if it's not possible, it goes to his right ; and if it's not possible (if there are walls on its left and on its right) it comes back.

If you don't see what I mean, I suggest you watch this video of a Chip's Challenge level (the 100th one), where there are only fireballs (the orange monsters) and gliders (the other ones) : http://www.youtube.com/watch?v=12E6DDiHpVE&hd=1 . You should see their movements.


I'll begin with the fireball (the glider is not very different).
First, create a sprite with 4 directions. For the example, I took the pike one, in order to have a more "Zeldastic" sprite instead of the Chip's Challenge one.

walking enemies/pike.png 4 0 -1
0 0 16 16 8 13 1 1
0 0 16 16 8 13 1 1
0 0 16 16 8 13 1 1
0 0 16 16 8 13 1 1


Put the sprite in the appropriate place, then create the .lua script associated.

First, we declare some local variables :

local enemy = ...
local sprite
local next_direction


Then we do the enemy:on_created event. Do whatever you want, the only important thing is the line I put in bold : indeed, it will set a value for the sprite variable.

function enemy:on_created()

  self:set_life(1)
  self:set_damage(2)
  [b]sprite = self:create_sprite("enemies/pike_clockwise")[/b]
  self:set_size(16, 16)
  self:set_origin(8, 13)
  self:set_can_hurt_hero_running(true)
  self:set_invincible()
  self:set_attack_consequence("sword", "protected")
  self:set_attack_consequence("thrown_item", "protected")
  self:set_attack_consequence("arrow", "protected")
  self:set_attack_consequence("hookshot", "protected")
  self:set_attack_consequence("boomerang", "protected")
end



Let's do the enemy:on_restarted() event now. In this one, we'll tell our "pikeball" to go straight in its direction.
For this, we'll take the sprite direction and create a movement based on it :

function enemy:on_restarted()
  [b]next_direction = sprite:get_direction() * 2[/b]
  local m = sol.movement.create("path")
  m:set_path{next_direction}
  m:set_speed(128)
  m:set_loop(true)
  m:start(self)
end


Be careful with the line I put in bold, because it's a trap. You probably wonder why there is a "* 2". In fact, it's because of the numbering used in the Solarus engine.
You probably know there are eight possible directions, each one corresponding to a number :
  • 0 : right
  • 1 : top-right
  • 2 : top
  • 3 : top-left
  • 4 : left
  • 5 : bottom-left
  • 6 : bottom
  • 7 : bottom-right

In our case, the interesting directions/numbers are only horizontal and vertical ones (0, 2, 4 and 6).
The sprite we defined has four directions : nevertheless, they don't match with the ones we want. Indeed, the directions of the sprite are 0, 1, 2 and 3. So we have to multiply these directions by 2 to have the correct ones.


Then we can define the enemy:on_obstacle_reached() event, the most interesting one.
We can divide it into three parts :
  • Testing if the monster can go where it should (i.e. no obstacle on its right)
  • Define the monster's direction
  • Restart its movement

For the first part, we'll need a complex condition such like this :

if (next_direction == 2 and self:test_obstacles(16, 0)) -- If the monster was going up
    or (next_direction == 0 and self:test_obstacles(0, 16)) -- Was going right
    or (next_direction == 6 and self:test_obstacles(-16, 0)) -- Was going down
    or (next_direction == 4 and self:test_obstacles(0, -16)) -- Was going left
then 


If true, it means that there is an obstacle on the monster's right ; so it will have to go left instead.
Let's define the direction for the monster's left :

-- Then go to the monster's left
    next_direction = next_direction + 2
    if next_direction > 7 then
      next_direction = 0
    end


As we don't want the monster to have a diagonal movement, we add 2 to the next_direction variable. We also have to "reset" it if it was equal to 6 : indeed, 6 + 2 = 8, but "8" isn't a good direction. So we restart it.
Now, let's define the "normal" monster's behavior :

else -- The monster can go on its right
    next_direction = next_direction - 2
    if next_direction < 0 then
      next_direction = 6
    end
  end


It's like the previous piece of code, but in the other way.
Finally, let's make the monster restart in the new next_direction :

-- Set the direction and restart the movement
  sprite:set_direction(next_direction / 2)
  self:restart()
end


Note that you have to divide next_direction by 2 this time, for the same reason I was talking about some lines earlier.

Here's a tip. I previously said that the "pikeball" shall go back if it cannot go on this left nor on its right. But I didn't write anything about that case.
In fact, it's not necessary to write someting for it. Indeed, the first condition is quite efficient for that : it checks whether you can go on your right or not, and if not, the "pikeball" goes on its left. But if there's also something on the left, the enemy:on_obstacle_reached() event is activated again, and will test again if there is an obstacle on the right. And it will be the case since that obstacle is the one the "pikeball" hurt at first ; so its only choice will be left. And left + left = go back.
If you didn't understand what I said, don't worry, the most important thing to remember is that you don't have to bother with the "come back" case.  ;)


You can now insert your enemy in the quest editor and... enjoy !

Here is the complete .lua script for the fireball type enemy :

local enemy = ...

-- Enemy that always moves, horizontally or vertically depending on its direction
-- When it hurts an obstacle, it goes on its right ; if impossible, goes to the left or come back
-- If you know Chip's Challenge, it's like the fireball's movement ^^

local sprite
local next_direction

-- Do whatever you want for this part
function enemy:on_created()
 
  self:set_life(1)
  self:set_damage(2)
  sprite = self:create_sprite("enemies/pike_clockwise") -- But don't forget to define "sprite" and set a value
  self:set_size(16, 16)
  self:set_origin(8, 13)
  self:set_can_hurt_hero_running(true)
  self:set_invincible()
  self:set_attack_consequence("sword", "protected")
  self:set_attack_consequence("thrown_item", "protected")
  self:set_attack_consequence("arrow", "protected")
  self:set_attack_consequence("hookshot", "protected")
  self:set_attack_consequence("boomerang", "protected")
end

-- In this part, we define the straight movement
function enemy:on_restarted()

  next_direction = sprite:get_direction() * 2 -- Don't forget the "* 2" factor
  local m = sol.movement.create("path")
  m:set_path{next_direction}
  m:set_speed(128)
  m:set_loop(true)
  m:start(self)
end

-- In this part, we define the monster behavior when it hurts an obstacle
function enemy:on_obstacle_reached()

  -- Test : is there an obstacle on the monster's right ?
  if (next_direction == 2 and self:test_obstacles(16, 0)) -- If the monster was going up
    or (next_direction == 0 and self:test_obstacles(0, 16)) -- Was going right
    or (next_direction == 6 and self:test_obstacles(-16, 0)) -- Was going down
    or (next_direction == 4 and self:test_obstacles(0, -16)) -- Was going left
  then 
    -- Then go to the monster's left
    next_direction = next_direction + 2
    if next_direction > 7 then
      next_direction = 0
    end

  else -- The monster can go on its right
    next_direction = next_direction - 2
    if next_direction < 0 then
      next_direction = 6
    end
  end

  -- Set the direction and restart the movement
  sprite:set_direction(next_direction / 2)
  self:restart()
end



So much for the fireball type.
You can do the glider in order to practice. You'll just have to switch the directions in the enemy:on_obstacle_reached() event, it's as easy as pie.  ;)

Nice! And such enemies also exist in ALTTP and Link's Awakening.
Your code is clean, your explanations are very clear. This will probably help people. Thanks!

I didn't remember there were such enemies in Alttp (it's been a while since I played it now :-[ ), maybe it was the caterpillar fireballs in the dark world dungeon 3 ?

Anyway, thank you for this comment.  ;) I'll probably post some other scripts in the next days (and I think the next one will be the "walker" from Chip's Challenge, since it's a variant of the fireball/glider with a random component, it may be quite easy to do).