Some Solarus lua code understanding questions

Started by linky, August 04, 2014, 03:24:40 PM

Previous topic - Next topic
Hi,

I'm trying to understand the lua code of solarus by reading the differents scripts of zelda solarus dx and i am stucked by some special trick, especially some specific to solarus. So could you please explain me some of them (apologize for the length of this post and if some of my questions are very naïve as i'm new to solarus and lua).

1) The hud code :

For each element of the hud (the rupee, the heart...) it seems you define a lua pseudo class of this element (by using the metatable) in this code that is always at the begining of the lua file (example taken from the rupee.lua in hud folder):


local rupees = {}

function rupees:new(game)

  local object = {}
  setmetatable(object, self)
  self.__index = self
  object:initialize(game)
  return object

end


and then in the hud.lua code you call this constructor to build an instance of this class for all the object of the hud, for example:


local rupees_builder = require("hud/rupees")
local menu = ruppy_builder:new(self)


I have understood what this code does, but what i don't understand is why it is necessary to create such a pseudo class as i don't think you use them elsewhere (i'm not sure of that). To my little knowledge, when you create a class, you have in mind to be able to create many instance of that class to use those objects in differents situation. But here, if i understand well, the rupee class is used only once (for the hud).

So is there a special reason why you use a more complex structure in the constitution of the hud. Because exactly the same code without the method "rupees:new" (just using the rupee table as it is), will work the same. Or the class structure is more adapted for the hud, for any reason. Otherwise maybe is it just because you plan to use this class for something else in a future version of solarus ?

2) The use of menu:

The use of the menus are not very clear to me. In the doc you explain that the utility of a menu is to allow to add some callback functions to any table that already exists (at least this is what i have understood). But is the menu itself an object created automaticaly by the engine when you call some of its function (start, stop...) and that includes the table that called the menu or does it just add some functions to the table that has called him ?

For example, still with the hud example, after having create the hud's element, you allow their showing by runing this function where you use the menu function (taken from hud.lua) :


function game:set_hud_enabled(hud_enabled)
    ...
    loop on menu...
    ...
         sol.menu.start(self, menu)
    ...
    end
end


where menu is one hud element (like the rupee) created before and placed in the hud table.

At the begining i didn't understood why the call to one of the draw function wasn't sufficient to show an item (like the rupee icon), beacause you have also to use a menu.start function for it.

For the example of the rupee, to show the icon of the rupee (near the number of money of the hero at bottom left) you use something like :


local rupee_icon_img = sol.surface.create("hud/rupee_icon.png")
rupee_icon_img:draw_region(x0, y0, width, length, dst_surface, x, y)


the first line create an image object of the icon called a "surface" in solarus environment and the second one is called to draw it on the screen. But when it is called alone nothing happens apparently. To make it works you have to put the "draw_region" in a callback function of a menu:


sol.menu.start(game,rupee_menu)

-- and somewhere create a callback function:

function rupee_menu:on_draw(dst_surface)

   rupee_icon_img:draw_region(x0, y0, width, length, dst_surface, x, y)
   
end   

   
then the image of the rupee is well displayed.

So please correct me if i'm wrong, because what i am understanding from this procedure is maybe completely idiot.

My understanding is :

In fact, unlike what i was first thinking, the "draw_region" function  display only one time the image it have to show. But as the screen is refreshed at a certain frequency by solarus engine, if you display only once, you don't really see it, as the screen is then immediatlely again redrawn and if the draw function is not called again the rupee disappears. To be sure you see it all the time, you have to called this draw function every cycle of redrawn of the screen.

So this is where the role of the menu is coming. The menu add a callback function to the rupee_menu table and each time the engine start a redraw of the screen, if it sees that a table has a callback function "on_draw" (added to the table by the menu or include in a table "menu" ?) then it will run this callback function to draw what is asked in this function.

Does it works like this or i'm completely misleading on the use of menus ?

3) Entity code:

Last thing, completely different subject, i wish to ask you if it is possible to make a lua code for the entity that appears on the map (teleporter, crystal, switch...) to customize their behavior.

For the enemy, item or custom_entity, when you create a new item of them in the quest editor, it automaticaly create a lua file that goes in their respective folder. But for the entity, it seems that when you create a new one, you can only give them a name and choose some specific properties, but you can't give their special behavior in a lua files as the other entities. This is maybe why they don't have their own folder in the quest editor like the others elements, but are placed inside the folder of the map (on the treeview at the left).

In the french tutorial videos I have seen (many thanks for them, they are really more than helpful to understand solarus), when you need to add a new behavior to an entities, you do it directly in the lua file of the map, although with other kind of sprite (enemy etc...) they have their own lua script. I works well also, but i was thinking that with large project it can become difficult to read the lua map file if we put a lot of scripts for all the differents entities added to the map. It could be more clear if they could have also their own lua files.

So even if apparently it is not proposed by the quest editor, is there a way to add some behavior to those kind of entities in an independant lua file, or is it a choice not to proposed this possibility ?

Thanks you very much for your help

August 04, 2014, 03:50:10 PM #1 Last Edit: August 05, 2014, 05:32:32 PM by Christopho
Hi!
First of all, you have a good understanding of all this, there is no doubt.
I am going to answer in 3 posts for more clarity.

1) You are absolutely right in your analysis. The setmetatable and __index magic is only useful to make several instances of the class and when inheritance is needed. This is the usual idiom to mimic object-oriented programming in Lua, so I did that way at first, and the code is still there in ZSDX for historical reasons. But I find it very hard to understand. When there is only one instance, this is too complex for no reason. Keep it simple! There are at least two other approaches:
- The one you mention: simply do a unique table and it will work with only one instance.
- Another approach to allow multiple instances is what I made in the tutorial:

-- The money counter shown in the game screen.

local rupees_builder = {}

function rupees_builder:new(game)

  local rupees = {}

  function rupees:initialize()

    rupees.surface = sol.surface.create(48, 12)
    rupees.surface:clear()
    rupees.digits_text = sol.text_surface.create({
      font = "white_digits",
    })
    rupees.digits_text:set_text(game:get_money())
    rupees.rupee_icons_img = sol.surface.create("hud/rupee_icon.png")
    rupees.rupee_bag_displayed = game:get_item("rupee_bag"):get_variant()
    rupees.money_displayed = game:get_money()

    rupees:check()
    rupees:rebuild_surface()
  end

  -- Checks whether the view displays correct information
  -- and updates it if necessary.
  function rupees:check()

    local need_rebuild = false
    local rupee_bag = game:get_item("rupee_bag"):get_variant()
    local money = game:get_money()

    -- Max money.
    if rupee_bag ~= rupees.rupee_bag_displayed then
      need_rebuild = true
      rupees.rupee_bag_displayed = rupee_bag
    end

    -- Current money.
    if money ~= rupees.money_displayed then
      need_rebuild = true

      if rupees.money_displayed < money then
        rupees.money_displayed = rupees.money_displayed + 1
      else
        rupees.money_displayed = rupees.money_displayed - 1
      end

      if rupees.money_displayed == money  -- The final value was just reached.
          or rupees.money_displayed % 3 == 0 then  -- Otherwise, play sound "rupee_counter_end" every 3 values.
        sol.audio.play_sound("rupee_counter_end")
      end
    end

    -- Redraw the surface only if something has changed.
    if need_rebuild then
      rupees:rebuild_surface()
    end

    -- Schedule the next check.
    sol.timer.start(game, 40, function()
      rupees:check()
    end)
  end

  function rupees:rebuild_surface()

    rupees.surface:clear()

    -- Max money (icon).
    rupees.rupee_icons_img:draw_region((rupees.rupee_bag_displayed - 1) * 12, 0, 12, 12, rupees.surface)

    -- Current rupee (counter).
    -- TODO show in green if the maximum is reached.
    if rupees.money_displayed == game:get_max_money() then
      rupees.digits_text:set_font("green_digits")
    else
      rupees.digits_text:set_font("white_digits")
    end
    rupees.digits_text:set_text(rupees.money_displayed)
    rupees.digits_text:draw(rupees.surface, 16, 5)
  end

  function rupees:set_dst_position(x, y)
    rupees.dst_x = x
    rupees.dst_y = y
  end

  function rupees:on_draw(dst_surface)

    local x, y = rupees.dst_x, rupees.dst_y
    local width, height = dst_surface:get_size()
    if x < 0 then
      x = width + x
    end
    if y < 0 then
      y = height + y
    end

    rupees.surface:draw(dst_surface, x, y)
  end

  rupees:initialize()

  return rupees
end

return rupees_builder

This last approach looks much better, I use it everywhere now for the HUD, the pause menus and the title screen. However, note that if you have a lot of instances, each instance duplicates the functions it contains. This is not the best choice for memory. One way to avoid that is to declare the functions outside the enclosing function rupee_builder:new().

Also note that in ZSDX, we do have multiple instances of the hearts counter in the savegame selection menu.

Quote from: linky on August 04, 2014, 03:24:40 PM
But is the menu itself an object created automaticaly by the engine when you call some of its function (start, stop...) and that includes the table that called the menu or does it just add some functions to the table that has called him ?
A menu is a table created by the user (so, there is no "menu" type). But the engine does not add anything in that table. It just calls functions if they exist in the table. So I understand why it seems confusing. This is very different from the rest of the API because there is no dedicated type. Maybe this is a mistake and maybe it will change one day. A proper type for something displayed on the screen with some size and a position might be more natural.

Anyway, the callbacks (on_draw, on_key_pressed...) are called while the menu is active, that is, after sol.menu.start(your_menu) and before sol.menu.stop(your_menu).

Without the menu API, you can still display things on the screen like a HUD during the game, but the liftetime is more difficult to control. You have to call your_hud_element:on_draw() yourself at each frame (probably from game:on_draw()) and stop doing that when the HUD is disabled. Same thing for on_key_pressed() and its friends, by taking care of the return value. Actually, you would have to forward yourself each callback of game to the HUD elements. This requires a lot of boilerplate code. It would work, but using menus is less painful.

There is also the problem of timers. When you create a timer specific to a menu, for example the title screen that shows some text after 5 seconds, you have to cancel the timer if the title screen is skipped before the delay. The lifetime of a timer can be a menu, and this way you don't have to stop the timer explicitly yourself.
Menus also have a lifetime for the same reason: when you exit the game and go back to the title screen, all menus that belong to the game are stopped. Otherwise they would still be displayed, and worse, they would react to user inputs.
These bugs are a nightmare, I had lots of them. Then I made this menu API.

Also, yes, drawings have to be done again at each frame. The map is redrawn on the screen, so everything that was drawn before is overwritten.

August 04, 2014, 04:23:57 PM #3 Last Edit: August 04, 2014, 04:32:33 PM by Christopho
3) Good remark. The idea with entities is that they are normally created from the editor with some properties, and then controlled by the map script. From a map script, or from any script, you can always call code from external Lua scripts using require.
However, enemies always need models (the "breed") from separate files, so the engine supports enemy scripts natively.
Similarly, custom entities often need models from separte files, so the engine also supports custom entity scripts natively.

For other entities, like I said you can still can require("some_script") and call a function that initializes your entity like you want. Okay, this is a bit repetitive if you have lots of maps with entities that should have the same behavior. The ultimate solution is metatables (since Solarus 1.2). With metatables, you won't even have to call require() for each map script that contains an entity. For example, you can set a behavior to all sensors whose name has a specific pattern. Give the appropriate name from the editor in a map, and the sensor will have the behavior without touching the map script. See nice examples in the tutorials (chapters 54 and 55) and here: https://github.com/christopho/zelda_mercuris_chest/blob/master/data/scripts/quest_manager.lua

Hi,

Thank you very much for all those enormous precisions, it is perfectly clear now on all that points and i have well understood what the menu functions are really doing and why to use them. I really appreciate you take so much time to explain this and hope it can help also one day some other new comers.

Thank you again.