A possible bug when creating an entity from another script

Started by Diarandor, February 11, 2017, 08:01:33 PM

Previous topic - Next topic
Hi @Christopho! I need some help. I have a problem with my rain script. I don't kow if this is a bug or something I did wrong. I think the problem started when I began refactoring my code, but I am not sure. My (unfinished) rain script is this one:

Code (Lua) Select

-- Rain manager script.
--[[
To add this script to your game, call from game_manager script:
    require("scripts/weather/rain_manager")

The functions here defined are:
    game:get_rain_type(world)
    game:set_rain_type(world, rain_type)

Rain types: nil (no rain), "rain", "storm".
--]]

-- This script requires the multi_event script:
require("scripts/multi_events")
local rain_manager = {}

local game_meta = sol.main.get_metatable("game")
local map_meta = sol.main.get_metatable("map")


-- Default settings. Change these for testing.
local rain_enabled = true -- Do not change this property, unless you are testing.
local lightning_enabled = false
local rain_speed = 100 -- Default drop speed 100.
local drop_max_distance = 300 -- Max possible distance for drop movements.
local drop_delay = 10 -- Delay between drops, in milliseconds.
local drop_sprite_id = "test/rain"

-- Initialize rain on maps when necessary.
game_meta:register_event("on_map_changed", function()
  if self ~= nil then
    local map = self:get_map()
    rain_manager:update_rain(map)
  end
end)

-- Get the raining state for a given world.
function game_meta:get_rain_type(world)
  local rain_type = self:get_value("rain_state_" .. world)
  return rain_enabled and rain_type
end
-- Set the raining state for a given world.
function game_meta:set_rain_type(world, rain_type)
  -- Update savegame variable.
  self:set_value("rain_state_" .. world, rain_type)
  -- Check if rain is necessary: if we are in that world and rain is needed. 
  local current_world = self:get_map():get_world()
  local rain_needed = (current_world == world) and rain_enabled and rain_type
  if (not rain_needed) then return end -- Do nothing if rain is not needed!
  -- We need to start the rain in the current map.
  local map = self:get_map()
  rain_manager:start_rain(map)
end


-- Create rain if necessary when entering a new map.
function rain_manager:update_rain(map)
  -- Get rain state in this world.
  local world = map:get_world()
  local rain_type = map:get_game():get_rain_type(world)
  -- Start rain if necessary.
  if rain_type == "rain" then
    self:start_rain(map)
  elseif rain_type == "storm" then
    self:start_storm(map)
  end
end

-- Define function to create splash effects.
-- If no parameters x, y are given, the position is random.
local function create_drop_splash(map, x, y)
  local max_layer = map:get_max_layer()
  local min_layer = map:get_min_layer()
  local camera = map:get_camera()
  local cx, cy, cw, ch = camera:get_bounding_box()
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize parameters.
  local x = x or cx + cw * math.random()
  local y = y or cy + ch * math.random()
  local layer = max_layer
  while map:get_ground(x,y,layer) == "empty" and layer > min_layer do
    layer = layer - 1 -- Draw the splash at the lower layer we can.
  end
  -- Do not draw splash over some bad grounds: "hole" and "lava".
  local ground = map:get_ground(x, y, layer)
  if ground ~= "hole" and ground ~= "lava" then
    drop_properties.x = x
    drop_properties.y = y
    drop_properties.layer = layer
    local drop_splash = map:create_custom_entity(drop_properties)
    assert(drop_splash ~= nil)
    local splash_sprite = drop_splash:get_sprite()
    splash_sprite:set_animation("drop_splash")
    splash_sprite:set_direction(0)
    function splash_sprite:on_animation_finished() drop_splash:remove() end
  end
end

-- Define function to create drops.
-- If no parameters x, y are given, the position is random.
local function create_drop(map, x, y)
  local max_layer = map:get_max_layer()
  local min_layer = map:get_min_layer()
  local camera = map:get_camera()
  local cx, cy, cw, ch = camera:get_bounding_box()
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize parameters.
  drop_properties.x = x or cx + cw * math.random() + 30
  drop_properties.y = y or cy + ch * math.random() - 100
  drop_properties.layer = max_layer
  local drop = map:create_custom_entity(drop_properties)
  local m = sol.movement.create("straight")
  m:set_angle(7 * math.pi / 5)
  m:set_speed(rain_speed)
  local random_max_distance = math.random(1, drop_max_distance)
  m:set_max_distance(random_max_distance)
  m:set_ignore_obstacles()
  function m:on_finished() drop:remove() end
  function m:on_obstacle_reached() drop:remove() end
  function drop:on_removed()
    local x, y = drop:get_position()
    create_drop_splash(map, x, y)
  end
  m:start(drop)
end


-- Start rain in the current map.
function rain_manager:start_rain(map)
  local max_layer = map:get_max_layer()
  local min_layer = map:get_min_layer()
  local camera = map:get_camera()
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize random seed for positions.
  math.randomseed(os.time())

  -- Start timer to draw rain drops.
  sol.timer.start(map, drop_delay, function()
    -- Create drops on random positions.
    create_drop(map)
    -- Repeat loop.
    return true
  end)
end

-- Stop rain in the current map.
function rain_manager:stop_rain(map)

end

-- Return rain manager.
return rain_manager


The problem appears when leaving a map with rain to another (where there is no rain, but this is probably not important). The bug appears in the console (no crash) at lines 92-93, because the entity "drop_splash" does not exist in that moment. I know that this can be fixed with an extra condition "if drop_splash ~= nil then blablabla end", but I'd like to know why does this happen.

I had the same problem at lines 31-32, because the "self" variable does not exist sometimes, and the condition "if self ~= nil then" fixed this. Why do not these variables exist when these events are called? Is this a bug?
Thanks in advance for the help.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I don't know if this is related to the problems you are having, but on quick inspection of your code, I see two problems.

Lines 71 - 97
Code (lua) Select
local function create_drop_splash(map, x, y) --line 71
  --skip
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize parameters.
  local x = x or cx + cw * math.random() --these x & y are not used anywhere
  local y = y or cy + ch * math.random()
  local layer = max_layer
  --skip
  local ground = map:get_ground(x, y, layer) --these x & y override the previously defined locals
  if ground ~= "hole" and ground ~= "lava" then
    drop_properties.x = x --these x & y are defined by map:get_ground(x,y,layer)
    drop_properties.y = y
    drop_properties.layer = layer
    --skip
  end


EDIT: Disregard second problem

Quote from: llamazing on February 11, 2017, 10:42:28 PM
I don't know if this is related to the problems you are having, but on quick inspection of your code, I see two problems.

Lines 71 - 97
Code (lua) Select
local function create_drop_splash(map, x, y) --line 71
  --skip
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize parameters.
  local x = x or cx + cw * math.random() --these x & y are not used anywhere
  local y = y or cy + ch * math.random()
  local layer = max_layer
  --skip
  local ground = map:get_ground(x, y, layer) --these x & y override the previously defined locals
  if ground ~= "hole" and ground ~= "lava" then
    drop_properties.x = x --these x & y are defined by map:get_ground(x,y,layer)
    drop_properties.y = y
    drop_properties.layer = layer
    --skip
  end


EDIT: Disregard second problem

I don't see any problem with these variables (x, y). I think the local variables of the inner scope are overriding the ones of the outer scope, so there should not be any problem.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Should I share my rain sprite files? I don't know if that may help.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

self only exists in methods (when using the colon syntax). When you call register_event(), pass a function that takes the game parameter and don't use self.

Thanks a lot Chris! That solved one of the problems. But I still get this error:
Error: In on_removed: [string "scripts/weather/rain_manager.lua"]:94: attempt to index local 'drop_splash' (a nil value)

The new version of the script is this:
Code (Lua) Select

-- Rain manager script.
--[[
To add this script to your game, call from game_manager script:
    require("scripts/weather/rain_manager")

The functions here defined are:
    game:get_raining(world)
    game:set_raining(world, rain_type)

Rain types: nil (no rain), "rain", "storm".
--]]

-- This script requires the multi_event script:
require("scripts/multi_events")
local rain_manager = {}

local game_meta = sol.main.get_metatable("game")
local map_meta = sol.main.get_metatable("map")


-- Default settings. Change these for testing.
local rain_enabled = true -- Do not change this property, unless you are testing.
local lightning_enabled = false
local rain_speed = 100 -- Default drop speed 100.
local drop_max_distance = 300 -- Max possible distance for drop movements.
local drop_delay = 10 -- Delay between drops, in milliseconds.
local drop_sprite_id = "test/rain"

-- Initialize rain on maps when necessary.
game_meta:register_event("on_map_changed", function(game)
    local map = game:get_map()
    rain_manager:update_rain(map)
end)

-- Get the raining state for a given world.
function game_meta:get_rain_type(world)
  local rain_type = nil
  if world then
    rain_type = self:get_value("rain_state_" .. world)
  end
  return rain_enabled and rain_type
end
-- Set the raining state for a given world.
function game_meta:set_rain_type(world, rain_type)
  -- Update savegame variable.
  self:set_value("rain_state_" .. world, rain_type)
  -- Check if rain is necessary: if we are in that world and rain is needed. 
  local current_world = self:get_map():get_world()
  local rain_needed = (current_world == world) and rain_enabled and rain_type
  if (not rain_needed) then return end -- Do nothing if rain is not needed!
  -- We need to start the rain in the current map.
  local map = self:get_map()
  rain_manager:start_rain(map)
end


-- Create rain if necessary when entering a new map.
function rain_manager:update_rain(map)
  -- Get rain state in this world.
  local world = map:get_world()
  local rain_type = map:get_game():get_rain_type(world)
  -- Start rain if necessary.
  if rain_type == "rain" then
    self:start_rain(map)
  elseif rain_type == "storm" then
    self:start_storm(map)
  end
end

-- Define function to create splash effects.
-- If no parameters x, y are given, the position is random.
local function create_drop_splash(map, x, y)
  local max_layer = map:get_max_layer()
  local min_layer = map:get_min_layer()
  local camera = map:get_camera()
  local cx, cy, cw, ch = camera:get_bounding_box()
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize parameters.
  local x = x or cx + cw * math.random()
  local y = y or cy + ch * math.random()
  local layer = max_layer
  while map:get_ground(x,y,layer) == "empty" and layer > min_layer do
    layer = layer - 1 -- Draw the splash at the lower layer we can.
  end
  -- Do not draw splash over some bad grounds: "hole" and "lava".
  local ground = map:get_ground(x, y, layer)
  if ground ~= "hole" and ground ~= "lava" then
    drop_properties.x = x
    drop_properties.y = y
    drop_properties.layer = layer
    local drop_splash = map:create_custom_entity(drop_properties)
--    if drop_splash ~= nil then
      local splash_sprite = drop_splash:get_sprite()
      splash_sprite:set_animation("drop_splash")
      splash_sprite:set_direction(0)
      function splash_sprite:on_animation_finished() drop_splash:remove() end
--    end
  end
end

-- Define function to create drops.
-- If no parameters x, y are given, the position is random.
local function create_drop(map, x, y)
  local max_layer = map:get_max_layer()
  local min_layer = map:get_min_layer()
  local camera = map:get_camera()
  local cx, cy, cw, ch = camera:get_bounding_box()
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize parameters.
  drop_properties.x = x or cx + cw * math.random() + 30
  drop_properties.y = y or cy + ch * math.random() - 100
  drop_properties.layer = max_layer
  local drop = map:create_custom_entity(drop_properties)
  local m = sol.movement.create("straight")
  m:set_angle(7 * math.pi / 5)
  m:set_speed(rain_speed)
  local random_max_distance = math.random(1, drop_max_distance)
  m:set_max_distance(random_max_distance)
  m:set_ignore_obstacles()
  function m:on_finished() drop:remove() end
  function m:on_obstacle_reached() drop:remove() end
  function drop:on_removed()
    local x, y = drop:get_position()
    create_drop_splash(map, x, y)
  end
  m:start(drop)
end


-- Start rain in the current map.
function rain_manager:start_rain(map)
  local max_layer = map:get_max_layer()
  local min_layer = map:get_min_layer()
  local camera = map:get_camera()
  local drop_properties = {direction = 0, x = 0, y = 0, layer = max_layer,
    width = 16, height = 16, sprite = drop_sprite_id}
  -- Initialize random seed for positions.
  math.randomseed(os.time())

  -- Start timer to draw rain drops.
  sol.timer.start(map, drop_delay, function()
    -- Create drops on random positions.
    create_drop(map)
    -- Repeat loop.
    return true
  end)
end

-- Stop rain in the current map.
function rain_manager:stop_rain(map)

end

-- Return rain manager.
return rain_manager


My guess of what might be happening is that when the map is gonna change, the "drop" entities are removed so the event "drop:on_removed()" in line 124 is called, hence the function "create_drop_splash(map, x, y)" tries to create a drop_splash which I think is not created because the map is being destroyed, so that drop_splash becomes nil and the error appears... could this be the problem?
I will now try not to use the event "drop:on_removed()", which is probably a bad idea...
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Yes, that solved my other problem. I guess that my guess was correct, but I could be wrong.

From now on I will be more careful with the "entity:on_removed" events, since I guess these are called by the engine too when leaving maps and, in these particular cases, new entities cannot be created by this function because the map is being removed too...
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."