multi_events.lua: Register multiple functions on an event

Started by Christopho, October 20, 2016, 03:18:55 PM

Previous topic - Next topic
October 20, 2016, 03:18:55 PM Last Edit: October 26, 2016, 03:17:59 PM by Christopho
I already talked about the multi_events.lua script in another topic, but let's share it in this forum because most scripts to be shared by the community will need it. Indeed, scripts that use it can be more self-contained, and therefore easier to share, which is the goal of this forum.

Scripts that I will share it on this forum, as well as the ones you can already find in the Zelda OLB Solarus Edition project, almost always use this feature.

So what is it?

This multi_events.lua script allows to register several functions to a single event, for example game:on_started().

Motivation

You often have a lot of scripts: the dialog box, the HUD, the pause menu, the camera manager, etc., and all these components need to do some stuff in the same event, for example they all need to perform some initialization in game:on_started(). One way to do it is to define the game:on_started() event in your game_manager.lua script, and from there, call the initialization code of each component (the dialog box, the HUD, the pause menu, the camera manager, etc). You can guess the problem: whenever you create a new component that also needs to do something in game:on_started(), you need to modify your game_manager.lua script again. This is because game:on_started() can be defined only once (or it would overwrite the previous definition). And then when you want to share your nice new feature on this forum, you need to tell people how to modifiy they game_manager.lua script. Not as easy as just copying a Lua script file. This is only an exemple, the same is true for a lot of other events (not only game:on_started()).

As an example, our previous games all have this issue. Here is the game:on_started() event from Zelda ROTH SE:
Code (lua) Select

function game:on_started()

  dungeon_manager:create(game)
  equipment_manager:create(game)
  dialog_box = dialog_box_manager:create(game)
  hud = hud_manager:create(game)
  pause_menu = pause_manager:create(game)
  camera = camera_manager:create(game)

  -- Initialize the hero.
  local hero = game:get_hero()
  game:stop_rabbit()  -- In case the game was saved as a rabbit.
  update_walking_speed()

  -- Measure the time played.
  run_chronometer(game)
end

As described above, we initialize all components. Components are usually defined in their own separate Lua scripts of course, but there is still this central initialization code needed. It is a bit delicate to share one of these features with you (like for example, the transformation of Link into a rabbit), because you would have not only to copy the rabbit.lua script, but also this initialization code. And actually even more, because in fact, rabbit.lua also needs to do stuff in game:on_game_over_started() and in hero:on_state_changed()... Very easy to miss things and to introduce bugs when you want the feature in your project.

So, it would be great if there was a way to define multiple functions to be called when a single event occurs, right? This is precisely what multi_events.lua script does. By using a different syntax, you can register several functions to the same event. Which means that in our examples above, you no longer need to modifiy existing scripts every time you import a script from some other project! Well, you still need to require() the script somewhere, but that's it.

Now in Zelda OLB SE, one of our new projects, the game_manager.lua script no longer defines game:on_started() at all. Instead, every component (dungeons, equipment, dialog box, HUD, pause menu, camera, rabbit manager, chronometer, etc.) registers its own stuff to be done in game:on_started(). The game manager remains unchanged when features are added or removed from the project. The whole rabbit code is contained in a single script now (and actually I will share it on this forum very soon).

So, this is extremely useful to make scripts as much independent and self-contained as possible. Basically, they become much easier to share.

How to use it?

- Copy the multi_events.lua script below to your project, into the scripts/ folder.
- Just require("scripts/multi_events.lua") at some point in your project (like from main.lua).
- In your scripts for all events, you now have the choice between two approaches: the traditional one or the multi-functions one, using object:register_event(event_name, callback).

Examples:

Code (lua) Select

-- The traditional way still works:
function my_sprite:on_animation_finished()
  -- blah blah
end


Code (lua) Select

local map = ...

require("multi_events")

-- This approach allows to register several functions to the same event:
map:register_event("on_started", function()
        -- blah blah
end)

map:register_event("on_started", function()
        -- other stuff
end)


The code!

Script: scripts/multi_events.lua
License: GPL v3
Author: Christopho
URL: https://github.com/solarus-games/zelda-olb-se/blob/dev/data/scripts/multi_events.lua
Code (lua) Select

-- This script allows to register multiple functions as Solarus events.
--
-- Usage:
--
-- Just require() this script and then all Solarus types
-- will have a register_event() method that adds an event callback.
--
-- Example:
--
-- local multi_events = require("scripts/multi_events")
--
-- -- Register two callbacks for the game:on_started() event:
-- game:register_event("on_started", my_function)
-- game:register_event("on_started", another_function)
--
-- -- It even works on metatables!
-- game_meta:register_event("on_started", my_function)
-- game_meta:register_event("on_started", another_function)
--
-- The good old way of defining an event still works
-- (but you cannot mix both approaches on the same object):
-- function game:on_started()
--   -- Some code.
-- end
--
-- Limitations:
--
-- Menus are regular Lua tables and not a proper Solarus type.
-- They can also support multiple events, but to do so you first have
-- to enable the feature explicitly like this:
-- multi_events:enable(my_menu)
-- Note that sol.main does not have this constraint even if it is also
-- a regular Lua table.

local multi_events = {}

local function register_event(object, event_name, callback)

  local previous_callbacks = object[event_name] or function() end
  object[event_name] = function(...)
    return previous_callbacks(...) or callback(...)
  end
end

-- Adds the multi event register_event() feature to an object
-- (userdata, userdata metatable or table).
function multi_events:enable(object)
  object.register_event = register_event
end

local types = {
  "game",
  "map",
  "item",
  "surface",
  "text_surface",
  "sprite",
  "timer",
  "movement",
  "straight_movement",
  "target_movement",
  "random_movement",
  "path_movement",
  "random_path_movement",
  "path_finding_movement",
  "circle_movement",
  "jump_movement",
  "pixel_movement",
  "hero",
  "dynamic_tile",
  "teletransporter",
  "destination",
  "pickable",
  "destructible",
  "carried_object",
  "chest",
  "shop_treasure",
  "enemy",
  "npc",
  "block",
  "jumper",
  "switch",
  "sensor",
  "separator",
  "wall",
  "crystal",
  "crystal_block",
  "stream",
  "door",
  "stairs",
  "bomb",
  "explosion",
  "fire",
  "arrow",
  "hookshot",
  "boomerang",
  "camera",
  "custom_entity"
}

-- Add the register_event function to all userdata types.
for _, type in ipairs(types) do

  local meta = sol.main.get_metatable(type)
  assert(meta ~= nil)
  multi_events:enable(meta)
end

-- Also add it to sol.main (which is a regular table).
multi_events:enable(sol.main)

return multi_events



On line 39, why do you create the empty function? Isn't that wasteful? Couldn't you just do the following instead? Lines 39 and 41 are modified.

Code (lua) Select

local function register_event(object, event_name, callback)

  local previous_callbacks = object[event_name]
  object[event_name] = function(...)
    return previous_callbacks and previous_callbacks(...) or callback(...)
  end
end

Both approaches should be equivalent I think. It is a matter of taste. To me, the and+or version is a bit less readable. It makes one additional test. Also, when defining a recursive algorithm it is nice to show explicitly the an initial value, which is often an empty list, or in this particular case, an empty function. Like in functional programming.
But again, this is equivalent, there is no real problem with your suggestion.

Interesting, using the multiple event for game:save() makes the game un-save-able, calling game:save() returns nil

What do you mean? game:save() is a normal function, not an event.