Reset enemies position every time hero is changing region in a map

Started by xavius, May 22, 2014, 05:06:58 AM

Previous topic - Next topic
Hi,

I was wondering if it is possible to reset the enemies position when changing region in a map, when crossing a separator.

What I actually do is put a sensor at the start of each region of the map and reset the not already killed enemies position of the region. Is there an easier way?

I do the same trick instead of using enemy:set_optimization_distance() to stop the enemies of next rooms.

Thank you!

Separators have events called "on_activated(direction)" and "on_activating(direction)" ( http://www.solarus-games.org/doc/1.2/lua_api_separator.html#lua_api_separator_events ). The one you want to use is the on_activating, triggered when the separator begin the movement -> the enemies should have their positions reset before you can see them. An example to reset an enigma when you cross the separator : https://github.com/Renkineko/solarus-nrfh/blob/master/data/maps/hole_clearer.lua#L49 (here I don't check if all entities are in the same region because I always reset the enigma if it is not solved yet, but the idea is the same for you...)

So you can try to use this event with the "is_in_same_region" and "exists" method to know if the enemy must be reset. How do you know the initial position of enemy ? Array initiated at the map:on_started ? If yes, you can also delete elements from this array when enemy is killed, so you don't have to do the test "exists". Only "if enemy:is_in_same_region(hero) then reset_position(enemy) else stop_movement(enemy) end"

But to stop the enemies not in the same region, you also could just do


function enemy:on_position_changed()
if not enemy:is_in_same_region(hero) then
  movement:stop()
end
end


You can initiate this function specifically in the map if you don't want to generalize this behavior.

Because you want to stop enemies not in the same region AND you want to reset position for enemy where you enter, you also can do the hard way : foreach separator:on_activating, you reset position of enemies still alive, no matter if they are in the same region. With the mini function said just above, they will stop anyway if they are not in the same region.

Hope it helps.

I wrote a script that does something similar, except that in my case I re-create the enemies when changing region, even the ones that were dead. I do this only for enemies and separators that have a special prefix.

Here is what I would do in your case (this was not tested though):

Create a file maps/lib/separator_manager.lua with:

-- This script manages enemies when there are separators in a map.
-- Enemies that are prefixed by "auto_enemy" are automatically
-- reset at their initial position when taking separators prefixed by "auto_separator".

local separator_manager = {}

function separator_manager:manage_map(map)

  local enemy_places = {}

  -- Store the position and properties of enemies.
  for enemy in map:get_entities("auto_enemy") do
    local x, y, layer = enemy:get_position()
    enemy_places[#enemy_places + 1] = {
      x = x,
      y = y,
      layer = layer,
      direction = enemy:get_sprite():get_direction(),
      enemy = enemy,
    }
  end

  -- Function called when a separator was just taken.
  local function separator_on_activated(separator)

    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

      if enemy:exists() and
          enemy:is_in_same_region(hero) then
        -- The enemy is still alive: reset its position if he is in the new active region.
        enemy:set_position(enemy_place.x, enemy_place.y, enemy_place.layer)
        enemy:get_sprite():set_direction(enemy_place.direction)
        enemy:restart()
      end
    end
  end

  for separator in map:get_entities("auto_separator") do
    separator.on_activated = separator_on_activated
  end

end

return separator_manager


To activate this script on a map, do this from the map script:

local separator_manager = require("maps/lib/separator_manager")
separator_manager:manage_map(map)


I am not sure that you have to stop enemies in other regions (and by the way, if you want to stop them, use enemy:set_enabled(false)). Just make sure that they can't get too close to the border, otherwise you could see part of their sprite and you could even attack them. When you use separators, all enemy scripts must call enemy:in_in_same_region(hero) before going toward the hero.
Another solution is to destroy all enemies except the ones of the current region. But this does directly not work for you because you don't want to resurrect the dead ones.

Other remarks:
- entity:is_in_same_region(other) is buggy in Solarus 1.2.0. It is fixed in Solarus 1.2.1, the development version. If you use the git version (branch master), you will be okay.
- With my code, perhaps the player will have time to see enemies instantly move back to their initial position, shortly after taking the separator. I have a few ideas about to fix that, but we can talk about it later if you have the problem.

Both ideas fromRenkineko and Christopho are good. I focused more on Christopho script because it better suits my needs.

I tweaked Christopho scprit a little because I was indeed seeing the enemies resetting to their place. I simply disable enemies when entering the separator (separator.on_activating) and reset their position then call separator.on_activated to enable only enemies in current region:

-- This script manages enemies when there are separators in a map.
-- Enemies that are prefixed by "auto_enemy" are automatically
-- reset at their initial position when taking separators prefixed by "auto_separator".

local separator_manager = {}

function separator_manager:manage_map(map)

  local enemy_places = {}

  -- Store the position and properties of enemies.
  for enemy in map:get_entities("auto_enemy") do
    local x, y, layer = enemy:get_position()
    enemy_places[#enemy_places + 1] = {
      x = x,
      y = y,
      layer = layer,
      direction = enemy:get_sprite():get_direction(),
      enemy = enemy,
    }
    enemy:set_enabled(false)
  end

  -- Function called when a separator was just taken.
  local function separator_on_activating(separator)
 
    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

      if enemy:exists() then
        if enemy:is_in_same_region(hero) then
  enemy:set_position(enemy_place.x, enemy_place.y, enemy_place.layer)
  enemy:get_sprite():set_direction(enemy_place.direction)
  enemy:set_enabled(false)
end
      end
    end
  end
 
  -- Function called after a separator was just taken.
  local function separator_on_activated(separator)
    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

      if enemy:exists() then
        if enemy:is_in_same_region(hero) then
  enemy:set_enabled(true)
  enemy:restart()
end
      end
    end
  end

  for separator in map:get_entities("auto_separator") do
    separator.on_activating = separator_on_activating
    separator.on_activated = separator_on_activated
  end

end

return separator_manager



Still, I have a weird problem. I created a simple bat enemy that run toward you when it sees you. We entering the region for the first time, the enemy is alternating between stopping and moving. This only happens on the first time. If I reenter the region later, everything will work as usual. Here is the enemy script. I can provide a video if needed.

local enemy = ...
local going_hero = false
local m

function enemy:on_created()

  self:set_life(1)
  self:set_damage(2)
  self:create_sprite("enemies/fire_bat")
 
  self:set_size(16, 8)
  self:set_origin(8, 0)
  self:set_obstacle_behavior("flying")
  m = sol.movement.create("target")
end

function enemy:stopping()
  local sprite = self:get_sprite()
  sprite:set_animation("stopped")
  --m:stop()
  m:set_speed(0)
  going_hero = false
end



function enemy:on_restarted()
  local sprite = self:get_sprite()
  sprite:set_animation("stopped")
  --m:stop()
  m:set_speed(0)
  going_hero = false

  self:go_random()
  self:check_hero()
end

function enemy:check_hero()
  local hero = self:get_map():get_entity("hero")
  local _, _, layer = self:get_position()
  local _, _, hero_layer = hero:get_position()
  local near_hero = layer == hero_layer
    and self:get_distance(hero) < 150

  if near_hero and not going_hero then
    self:go_hero()
--self:set_speed(40)
sol.audio.play_sound("bat")

  elseif not near_hero and going_hero then
--self:set_speed(32)
    self:go_random()
  end

  sol.timer.stop_all(self)
  sol.timer.start(self, 100, function() self:check_hero() end)
end

function enemy:go_random()
  going_hero = false
  local sprite = self:get_sprite()
  sprite:set_animation("stopped")
  m:set_speed(0)
end

function enemy:go_hero()
  going_hero = true
  local sprite = self:get_sprite()
  sprite:set_animation("walking")
  m:set_speed(88)
  m:set_ignore_obstacles(false)
  m:start(self)
end


Thank you again for all your help!

I think I found out the problem. I forgot to set the movement:set_ignore_obstacles() after creating the movement. I had in the past have a problem related to this.

Here is a cleaned up code of my enemy:
local enemy = ...
local going_hero = false
local m

function enemy:on_created()
  self:set_life(1)
  self:set_damage(2)
  self:create_sprite("enemies/fire_bat")
 
  self:set_size(16, 8)
  self:set_origin(8, 0)
  self:set_obstacle_behavior("flying")
  m = sol.movement.create("target")
  m:set_ignore_obstacles(false)
  m:set_speed(0)
  m:start(self)
end

function enemy:on_restarted()
  local sprite = self:get_sprite()
  sprite:set_animation("stopped")
  m:set_speed(0)
  going_hero = false

  self:go_random()
  self:check_hero()
end

function enemy:check_hero()
  local hero = self:get_map():get_entity("hero")
  local _, _, layer = self:get_position()
  local _, _, hero_layer = hero:get_position()
  local near_hero = layer == hero_layer
    and self:get_distance(hero) < 150

  if near_hero and not going_hero then
    self:go_hero()
sol.audio.play_sound("bat")

  elseif not near_hero and going_hero then
    self:go_random()
  end
sol.timer.stop_all(self)
sol.timer.start(self, 100, function() self:check_hero() end)
end

function enemy:go_random()
  going_hero = false
  local sprite = self:get_sprite()
  sprite:set_animation("stopped")
  m:set_speed(0)
end

function enemy:go_hero()
  going_hero = true
  local sprite = self:get_sprite()
  sprite:set_animation("walking")
  m:set_speed(88)
end

By the way, do you have a compiled version of solarus 1.2.1 for windows? Unless I do not have the right git address, it does not seems to have binaries.

Thank you!

The set_speed(0) in the go_random troubles me. Plus, what's the point of "go_random" in your enemy:on_restarted, if you do "check_hero" right after ? These questions don't explain why there is the alternating move only the first time, at first see I don't see mistakes in code. Did you try to debug the moves with lot of "print" informations ? :) Or do you have a repo where I could try ?

About the compiled version, no there is no bin in git, you have to compile the sources yourself (and in windows it's a pain in the ass :p). There is a big big interest in compiling yourself : you can enable solarus as a "console application", and all your print are seen in real-time in the console (and are logged in error.txt as well).

Solarus 1.2.1 is not released yet, but I can make a windows snapshot the development version.

Renkineko

For the set_speed(0), you could also use stop(). I did not see a difference. Maybe one is fat than the other in time processing?
For the go_random(), I only use it to change the animation of the enemy. I could put this in the check_hero().
For the alternating move, I think it is happening because movement:set_ignore_obstacles() is not set. The second time I enter the room, it is set via enemy:on_restarted(). Maybe there is no default value for this movement? I did not see the source code yet.


Christopho

I would really appreciate if you could. I do not have an environment set yet. I know how hard it can be since I am often cross compiling ARM kernels at work. If I do not mistake, the bug is when entering the room from south. I noticed that event after separator transition, I still was in the room below.


Thank you all.


Thank you very much Christopho. Sorry for the delay. I just tried it, works flawlessly.

I found a bug in the script. In the case you kill an enemy and get out of the screen before the dying animation is finished, it will still exist. Then when reentering the screen, the game will crash because the enemy is supposed to be dead but the script is trying to restart it.

I simply do a check of the life count with the check on exists().

By the way, is it possible to get the enemy entity name in the enemy object itself?

Thank you.

The new code of separator_manager.lua:

-- This script manages enemies when there are separators in a map.
-- Enemies that are prefixed by "auto_enemy" are automatically
-- reset at their initial position when taking separators prefixed by "auto_separator".

local separator_manager = {}

function separator_manager:manage_map(map)

  local enemy_places = {}

  -- Store the position and properties of enemies.
  for enemy in map:get_entities("auto_enemy") do
    local x, y, layer = enemy:get_position()
local spriteTemp = enemy:get_sprite()

    enemy_places[#enemy_places + 1] = {
      x = x,
      y = y,
      layer = layer,
 
      --direction = enemy:get_sprite():get_direction(),
      enemy = enemy,
 
  --Set if enemy should reappear or not
  --reappear = 0,
    }
enemy:set_enabled(false)
  end

  -- Function called when a separator was just taken.
  local function separator_on_activating(separator)
 
    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

  --string.find(s, pattern [, index [, plain]])
 
      if enemy:exists() then
    if enemy:get_life() <= 0 then
          if enemy:is_in_same_region(hero) then

enemy:set_position(enemy_place.x, enemy_place.y, enemy_place.layer)
--enemy:get_sprite():set_direction(enemy_place.direction)
enemy:set_enabled(false)
  end
end
      end
    end
  end
 
  -- Function called after a separator was just taken.
  local function separator_on_activated(separator)
    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

      if enemy:exists() then
    if enemy:get_life() <= 0 then
          if enemy:is_in_same_region(hero) then
enemy:set_enabled(true)
enemy:restart()
  end
end
      end
    end
  end

  for separator in map:get_entities("auto_separator") do
    separator.on_activating = separator_on_activating
separator.on_activated = separator_on_activated
  end

end

return separator_manager

Ok I will fix the crash, thanks for the report.
Yes, you can get the name of any enemy with enemy:get_name(), like for any entity.

Here is a new script.

I added a feature to choose if the enemy will reappear or not. If you want to reset the enemy every time you enter the region, just add "reappear" in the enemy name. If the enemy is dead, it will be recreated. If it is alive, the life counter will be reset.

For a weird reason, when I paste the code, the indentations are not correct. Sorry for this.

manager_separator.lua

-- This script manages enemies when there are separators in a map.
-- Enemies that are prefixed by "auto_enemy" are automatically
-- reset at their initial position when taking separators prefixed by "auto_separator".

local separator_manager = {}

function separator_manager:manage_map(map)

  local enemy_places = {}

  -- Store the position and properties of enemies.
  for enemy in map:get_entities("auto_enemy") do
    local x, y, layer = enemy:get_position()
local spriteTemp = enemy:get_sprite()
local reappear = 0

if( string.find(enemy:get_name(), "reappear") ~= nil ) then
  reappear = 1
else
  reappear = 0
end

    enemy_places[#enemy_places + 1] = {
      x = x,
      y = y,
      layer = layer,
 
      --direction = enemy:get_sprite():get_direction(),
      enemy = enemy,
 
  -- Get breed to recreate it if it has to reappear
  breed = enemy:get_breed(),
  name = enemy:get_name(),
  life = enemy:get_life(),
 
  --Set if enemy should reappear or not
  reappear = reappear,
    }
enemy:set_enabled(false)
  end

  -- Function called when a separator was just taken.
  local function separator_on_activating(separator)
 
    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
 
  local enemy = enemy_place.enemy
 
  --Check if enemy is tagged to reappear unpon entering the screen
  if( enemy_place.reappear == 1 ) then
    -- if enemy is destroyed or about to be
if( enemy:exists() == false ) or ( enemy:get_life() <= 0 ) then
  -- Create enemy
  enemy_place.enemy = map:create_enemy{
  name = enemy_place.name,
  breed = enemy_place.breed,
  x = enemy_place.x,
  y = enemy_place.y,
  layer = enemy_place.layer,
  direction = 0,
}

  enemy = enemy_place.enemy
else
  -- If not destroyed or about to then reset life counter
  enemy:set_life(enemy_place.life)
end
  end

 
      if enemy:exists() then
    if enemy:get_life() > 0 then
          if enemy:is_in_same_region(hero) then
   
enemy:set_position(enemy_place.x, enemy_place.y, enemy_place.layer)
--enemy:get_sprite():set_direction(enemy_place.direction)
enemy:set_enabled(false)
  end
end
      end
    end
  end
 
  -- Function called after a separator was just taken.
  local function separator_on_activated(separator)
    local hero = map:get_hero()
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

      if enemy:exists() then
    if enemy:get_life() > 0 then
          if enemy:is_in_same_region(hero) then
enemy:set_enabled(true)
enemy:restart()
  end
end
      end
    end
  end

  for separator in map:get_entities("auto_separator") do
    separator.on_activating = separator_on_activating
separator.on_activated = separator_on_activated
  end

end

return separator_manager