Solarus Forum

Solarus => Development => Topic started by: wrightmat on May 18, 2014, 09:07:11 pm

Title: Referencing global timers
Post by: wrightmat on May 18, 2014, 09:07:11 pm
If I have a timer that I've declared in the "game" context, and my character moves to a different map while the timer is still active, I can't figure out how to reference that timer.

Code: [Select]
race_timer = sol.timer.start(game, 140000, end_race_lost)
race_timer:set_with_sound(true)

Timer declared as above. I know the timer is still active when the map changes because the sound still plays. I also have the time displayed on screen using race_timer:get_remaining_time(). When the hero changes maps, the timer display disappears.

Code: [Select]
  if race_timer ~= nil then
    function map:on_draw(dst_surface)
      local timer_icon = sol.surface.create("hud/timer.png")
      local timer_time = race_timer:get_remaining_time() / 1000
      local timer_text = sol.text_surface.create{
        font = "white_digits",
        horizontal_alignment = "left",
        vertical_alignment = "top",
      }
      timer_icon:draw(dst_surface, 20, 55)
      timer_text:set_text(timer_time)
      timer_text:draw(dst_surface, 40, 60)
    end
  end

The display is disappearing because the timer reference goes to nil, which I'm intentionally checking for. If I comment out the check, I get an error message of "attempt to index global 'race_timer' (a nil value), but the value shouldn't be nil since the timer is in a "game" context, right? Should I be referencing it a different way, or is this a bug in the engine?
Title: Re: Referencing global timers
Post by: Christopho on May 18, 2014, 09:44:22 pm
Hi,

There seems to be a bug in the engine: when you declare a global variable in a map script, and try to access it from another map script, it is nil. So this is not a timer problem.
I never found this bug before because I don't use global variables :)

I am going to fix it, but in the meantime, what you can do is declaring this variable in the game object. This is also better because you will avoid a global variable.

Code: [Select]
game.race_timer = sol.timer.start(game, 140000, end_race_lost)
game.race_timer:set_with_sound(true)

Code: [Select]
if game.race_timer ~= nil then
EDIT: fixed! https://github.com/christopho/solarus/issues/507
Thanks for the report.
Title: Re: Referencing global timers
Post by: wrightmat on May 18, 2014, 11:30:54 pm
Wow, that was fast! Thanks Christopho!

EDIT: Even with the new code in place, the callback of the timer (in the case above, "end_race_lost") won't run on another map. Erro is "bad argument #3 to '__index' (This map is not running).
Title: Re: Referencing global timers
Post by: Christopho on May 19, 2014, 09:22:26 am
In your function end_race_lost, you are probably looking up names that refer to map entities from the previous map. Since that map is not running anymore, this does not work.

For example, assuming that you have a map entity called my_npc in the map where the timer is created:
Code: [Select]
local function end_race_lost
  sol.audio.play_sound("too_late")
  my_npc:get_sprite():set_animation("walking")
end

It does not make sense to access my_npc when its map is no longer the current map.
But here is what happens exactly: the name my_npc was not declared before: therefore, it is resolved lazily when the function end_race_lost is executed. This generates the cryptic error that you get. In 1.3, it will get a nil value instead of this weird error message.

There is a way to access it anyway:
Code: [Select]
local some_npc_safe = some_npc  -- Resolve the entity while the map is still running.
local function end_race_lost
  sol.audio.play_sound("too_late")
  some_npc_safe:get_sprite():set_animation("walking")
end

or if you prefer:

Code: [Select]
local some_npc = map:get_entity("some_npc")  -- Resolve the entity while the map is still running.
local function end_race_lost
  sol.audio.play_sound("too_late")
  some_npc:get_sprite():set_animation("walking")
end

Anyway, in your case, I don't think you really want to access entities from the previous map. So instead, you should probably just check if the map is the current one, with something like:

Code: [Select]
local function end_race_lost
  sol.audio.play_sound("too_late")
  if map == game:get_map() then  -- If this is still the current map.
    some_npc:get_sprite():set_animation("walking")
  end
end
Title: Re: Referencing global timers
Post by: wrightmat on May 19, 2014, 02:06:39 pm
Hmm... I think I understand what you're saying. My case is a tricky one though. It's basically a race that takes place over two different maps. So regardless of which map you're on, when the timer runs out, you lose the race. This is why I was hoping the timer callback would be persistent. I had even declared that end_race_lost function in each map, and it still didn't appear to be working. For reference, here's the function (those torch entities exist on both maps):

Code: [Select]
sol.audio.play_sound("wrong")
game:set_value("i1028", 4);
torch_1:get_sprite():set_animation("unlit")
torch_2:get_sprite():set_animation("unlit")
torch_3:get_sprite():set_animation("unlit")
game.race_timer = nil


Do you know of any way to make this work?
Title: Re: Referencing global timers
Post by: Christopho on May 19, 2014, 02:23:26 pm
The timer callback is persistent indeed. However, the callback you pass to the timer is a function value, not a string. Its name has no importance: the timer stores its code. (All functions are anonymous in Lua, but you can assign a function to a variable). So the end_race_lost function that you declare on the second map has no effect. The one of the first map is the one called, even if you are in the second map. This is why you have the bug.

So, if I understand correctly, torch_1, torch_2 and torch_3 exist on both maps? And you want to unlight them? If yes, here is what you can do:

Code: [Select]
sol.audio.play_sound("wrong")
game:set_value("i1028", 4)
local map = game:get_map()
map:get_entity("torch_1"):get_sprite():set_animation("unlit")
map:get_entity("torch_2"):get_sprite():set_animation("unlit")
map:get_entity("torch_3"):get_sprite():set_animation("unlit")
game.race_timer = nil

This callback should work for both maps :)

PS: "i1028" is a poor savegame variable name, you can use more meaningful names for your savegame variables. ZSDX use this kind of names for historical reasons (in very old Solarus versions, savegame variables were identified by numbers only).
Title: Re: Referencing global timers
Post by: wrightmat on May 20, 2014, 03:19:33 am
Works perfectly now - you're amazing! Thanks!

As far as the savegame variables - I fall into the same category as ZSDX on this one. I've been following the engine from the beginning and developing very early on, so I already had a large number of savegame variables established and it was easier to stick with the numbers. I have a system in place now and I actually like them! :-)
Title: Re: Referencing global timers
Post by: Christopho on May 20, 2014, 02:25:14 pm
I've been following the engine from the beginning and developing very early on
Wow! That's right, I found an e-mail from you of June 2010. The engine and the API have changed a lot since then! I'm glad to realize that you successfully upgraded to each new version and always carried on :)
Title: Re: Referencing global timers
Post by: wrightmat on May 25, 2014, 11:07:02 pm
Still chugging along! Thanks your all your great work on this engine!