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:
-- 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.
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
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
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.
Should I share my rain sprite files? I don't know if that may help.
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:
-- 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...
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...