Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.


Topics - Christopho

Pages: [1]
1
Your scripts / chronometer.lua: Measure the time played
« on: October 26, 2016, 03:24:58 pm »
This chronometer script allows to know how much time a game was played.

How to use it

- Copy the chronometer.lua script below to the scripts folder of your project.
- Just do require("scripts/chronometer") at some point, for example in your game_manager script.

That's it! Your game objects will now have two additional functions: game:get_time_played() (to get the value in seconds) and game:get_time_played_string() (to get a formatted string).
Then it is up to you to display the time in a dialog or in a menu of your choice.

Prerequisites

The multi_events.lua script is necessary for the chronometer to work.

The code!

Script: scripts/chronometer.lua
License: GPL v3
Author: Christopho
URL: https://github.com/solarus-games/zelda-olb-se/blob/dev/data/scripts/chronometer.lua
Code: Lua
  1. -- Adds chronometer features to games.
  2. -- The following functions are provided:
  3. -- - game:get_time_played():            Returns the game time in seconds.
  4. -- - game:get_time_played_string():     Returns a string representation of the game time.
  5.  
  6. -- Usage:
  7. -- require("scripts/chronometer")
  8.  
  9. require("scripts/multi_events")
  10.  
  11. -- Measure the time played.
  12. local function initialize_chronometer_features(game)
  13.  
  14.   -- Returns the game time in seconds.
  15.   function game:get_time_played()
  16.     local milliseconds = game:get_value("time_played") or 0
  17.     local total_seconds = math.floor(milliseconds / 1000)
  18.     return total_seconds
  19.   end
  20.  
  21.   -- Returns a string representation of the game time.
  22.   function game:get_time_played_string()
  23.     local total_seconds = game:get_time_played()
  24.     local seconds = total_seconds % 60
  25.     local total_minutes = math.floor(total_seconds / 60)
  26.     local minutes = total_minutes % 60
  27.     local total_hours = math.floor(total_minutes / 60)
  28.     local time_string = string.format("%02d:%02d:%02d", total_hours, minutes, seconds)
  29.     return time_string
  30.   end
  31.  
  32.   local timer = sol.timer.start(game, 100, function()
  33.     local time = game:get_value("time_played") or 0
  34.     time = time + 100
  35.     game:set_value("time_played", time)
  36.     return true  -- Repeat the timer.
  37.   end)
  38.   timer:set_suspended_with_map(false)
  39. end
  40.  
  41. -- Set up chronometer features on any game that starts.
  42. local game_meta = sol.main.get_metatable("game")
  43. game_meta:register_event("on_started", initialize_chronometer_features)
  44.  
  45. return true
  46.  

2
Your scripts / multi_events.lua: Register multiple functions on an event
« on: October 20, 2016, 03:18:55 pm »
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
  1. function game:on_started()
  2.  
  3.   dungeon_manager:create(game)
  4.   equipment_manager:create(game)
  5.   dialog_box = dialog_box_manager:create(game)
  6.   hud = hud_manager:create(game)
  7.   pause_menu = pause_manager:create(game)
  8.   camera = camera_manager:create(game)
  9.  
  10.   -- Initialize the hero.
  11.   local hero = game:get_hero()
  12.   game:stop_rabbit()  -- In case the game was saved as a rabbit.
  13.   update_walking_speed()
  14.  
  15.   -- Measure the time played.
  16.   run_chronometer(game)
  17. end
  18.  
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
  1. -- The traditional way still works:
  2. function my_sprite:on_animation_finished()
  3.   -- blah blah
  4. end
  5.  

Code: Lua
  1. local map = ...
  2.  
  3. require("multi_events")
  4.  
  5. -- This approach allows to register several functions to the same event:
  6. map:register_event("on_started", function()
  7.         -- blah blah
  8. end)
  9.  
  10. map:register_event("on_started", function()
  11.         -- other stuff
  12. end)
  13.  

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
  1. -- This script allows to register multiple functions as Solarus events.
  2. --
  3. -- Usage:
  4. --
  5. -- Just require() this script and then all Solarus types
  6. -- will have a register_event() method that adds an event callback.
  7. --
  8. -- Example:
  9. --
  10. -- local multi_events = require("scripts/multi_events")
  11. --
  12. -- -- Register two callbacks for the game:on_started() event:
  13. -- game:register_event("on_started", my_function)
  14. -- game:register_event("on_started", another_function)
  15. --
  16. -- -- It even works on metatables!
  17. -- game_meta:register_event("on_started", my_function)
  18. -- game_meta:register_event("on_started", another_function)
  19. --
  20. -- The good old way of defining an event still works
  21. -- (but you cannot mix both approaches on the same object):
  22. -- function game:on_started()
  23. --   -- Some code.
  24. -- end
  25. --
  26. -- Limitations:
  27. --
  28. -- Menus are regular Lua tables and not a proper Solarus type.
  29. -- They can also support multiple events, but to do so you first have
  30. -- to enable the feature explicitly like this:
  31. -- multi_events:enable(my_menu)
  32. -- Note that sol.main does not have this constraint even if it is also
  33. -- a regular Lua table.
  34.  
  35. local multi_events = {}
  36.  
  37. local function register_event(object, event_name, callback)
  38.  
  39.   local previous_callbacks = object[event_name] or function() end
  40.   object[event_name] = function(...)
  41.     return previous_callbacks(...) or callback(...)
  42.   end
  43. end
  44.  
  45. -- Adds the multi event register_event() feature to an object
  46. -- (userdata, userdata metatable or table).
  47. function multi_events:enable(object)
  48.   object.register_event = register_event
  49. end
  50.  
  51. local types = {
  52.   "game",
  53.   "map",
  54.   "item",
  55.   "surface",
  56.   "text_surface",
  57.   "sprite",
  58.   "timer",
  59.   "movement",
  60.   "straight_movement",
  61.   "target_movement",
  62.   "random_movement",
  63.   "path_movement",
  64.   "random_path_movement",
  65.   "path_finding_movement",
  66.   "circle_movement",
  67.   "jump_movement",
  68.   "pixel_movement",
  69.   "hero",
  70.   "dynamic_tile",
  71.   "teletransporter",
  72.   "destination",
  73.   "pickable",
  74.   "destructible",
  75.   "carried_object",
  76.   "chest",
  77.   "shop_treasure",
  78.   "enemy",
  79.   "npc",
  80.   "block",
  81.   "jumper",
  82.   "switch",
  83.   "sensor",
  84.   "separator",
  85.   "wall",
  86.   "crystal",
  87.   "crystal_block",
  88.   "stream",
  89.   "door",
  90.   "stairs",
  91.   "bomb",
  92.   "explosion",
  93.   "fire",
  94.   "arrow",
  95.   "hookshot",
  96.   "boomerang",
  97.   "camera",
  98.   "custom_entity"
  99. }
  100.  
  101. -- Add the register_event function to all userdata types.
  102. for _, type in ipairs(types) do
  103.  
  104.   local meta = sol.main.get_metatable(type)
  105.   assert(meta ~= nil)
  106.   multi_events:enable(meta)
  107. end
  108.  
  109. -- Also add it to sol.main (which is a regular table).
  110. multi_events:enable(sol.main)
  111.  
  112. return multi_events
  113.  
  114.  

3
Development / Solarus 1.5 development snapshot
« on: March 15, 2016, 10:49:12 pm »
Here is an updated snapshot for Windows because you were a lot to ask!
http://www.solarus-games.org/downloads/solarus/win32/

This is a development version, so take extra precautions, backup your files, etc. The format of some data files has changed between previous development snapshots and this one, so if you have syntax errors in data files, don't worry, I will help you on this thread.

4
Development / Separator manager
« on: August 11, 2015, 02:32:38 pm »
Here is a script that automatically restores any enemy, destructible object and block when taking a separator!

maps/lib/separator_manager.lua:
Code: Lua
  1. -- This script restores entities when there are separators in a map.
  2. -- When taking separators prefixed by "auto_separator", the following entities are restored:
  3. -- - Enemies prefixed by "auto_enemy".
  4. -- - Destructibles prefixed by "auto_destructible".
  5. -- - Blocks prefixed by "auto_block".
  6. --
  7. -- Usage from a map script:
  8. -- local separator_manager = require("maps/lib/separator_manager.lua")
  9. -- separator_manager:manage_map(map)
  10. -- If you prefer, you can also enable it automatically on all maps from game:on_map_changed().
  11.  
  12. local separator_manager = {}
  13.  
  14. function separator_manager:manage_map(map)
  15.  
  16.   local enemy_places = {}
  17.   local destructible_places = {}
  18.   local game = map:get_game()
  19.   local zelda = map:get_entity("zelda")
  20.  
  21.   -- Function called when a separator was just taken.
  22.   local function separator_on_activated(separator)
  23.  
  24.     local hero = map:get_hero()
  25.  
  26.     -- Enemies.
  27.     for _, enemy_place in ipairs(enemy_places) do
  28.       local enemy = enemy_place.enemy
  29.  
  30.       -- First remove any enemy.
  31.       if enemy:exists() then
  32.         enemy:remove()
  33.       end
  34.  
  35.       -- Re-create enemies in the new active region.
  36.       if enemy:is_in_same_region(hero) then
  37.         local old_enemy = enemy_place.enemy
  38.         local enemy = map:create_enemy({
  39.           x = enemy_place.x,
  40.           y = enemy_place.y,
  41.           layer = enemy_place.layer,
  42.           breed = enemy_place.breed,
  43.           direction = enemy_place.direction,
  44.           name = enemy_place.name,
  45.         })
  46.         enemy:set_treasure(unpack(enemy_place.treasure))
  47.         enemy.on_dead = old_enemy.on_dead  -- For door_manager.
  48.         enemy_place.enemy = enemy
  49.       end
  50.     end
  51.  
  52.     -- Blocks.
  53.     for block in map:get_entities("auto_block") do
  54.       -- Reset blocks in regions no longer visible.
  55.       if not block:is_in_same_region(hero) then
  56.         block:reset()
  57.       end
  58.     end
  59.  
  60.   end
  61.  
  62.   -- Function called when a separator is being taken.
  63.   local function separator_on_activating(separator)
  64.  
  65.     local hero = map:get_hero()
  66.  
  67.     -- Enemies.
  68.     if not map.used_separator then
  69.       -- First separator: remove enemies from other regions like on_activated() does.
  70.       -- Because on_activated() was not called when the map started.
  71.       for _, enemy_place in ipairs(enemy_places) do
  72.         local enemy = enemy_place.enemy
  73.         if enemy:exists() and not enemy:is_in_same_region(hero) then
  74.           enemy:remove()
  75.         end
  76.       end
  77.     end
  78.  
  79.     -- Destructibles.
  80.     for _, destructible_place in ipairs(destructible_places) do
  81.       local destructible = destructible_place.destructible
  82.  
  83.       if not destructible:exists() then
  84.         -- Re-create destructibles in all regions except the active one.
  85.         if not destructible:is_in_same_region(hero) then
  86.           local destructible = map:create_destructible({
  87.             x = destructible_place.x,
  88.             y = destructible_place.y,
  89.             layer = destructible_place.layer,
  90.             name = destructible_place.name,
  91.             sprite = destructible_place.sprite,
  92.             destruction_sound = destructible_place.destruction_sound,
  93.             weight = destructible_place.weight,
  94.             can_be_cut = destructible_place.can_be_cut,
  95.             can_explode = destructible_place.can_explode,
  96.             can_regenerate = destructible_place.can_regenerate,
  97.             damage_on_enemies = destructible_place.damage_on_enemies,
  98.             ground = destructible_place.ground,
  99.           })
  100.           -- We don't recreate the treasure.
  101.           destructible_place.destructible = destructible
  102.         end
  103.       end
  104.     end
  105.   end
  106.  
  107.   for separator in map:get_entities("auto_separator") do
  108.     separator.on_activating = separator_on_activating
  109.     separator.on_activated = separator_on_activated
  110.   end
  111.  
  112.   -- Store the position and properties of enemies.
  113.   for enemy in map:get_entities("auto_enemy") do
  114.     local x, y, layer = enemy:get_position()
  115.     enemy_places[#enemy_places + 1] = {
  116.       x = x,
  117.       y = y,
  118.       layer = layer,
  119.       breed = enemy:get_breed(),
  120.       direction = enemy:get_sprite():get_direction(),
  121.       name = enemy:get_name(),
  122.       treasure = { enemy:get_treasure() },
  123.       enemy = enemy,
  124.     }
  125.   end
  126.  
  127.   local function get_destructible_sprite_name(destructible)
  128.     -- TODO the engine should have a destructible:get_sprite() method.
  129.     -- As a temporary workaround we use the one of custom entity, fortunately
  130.     -- it happens to work for all types of entities.
  131.     local sprite = sol.main.get_metatable("custom_entity").get_sprite(destructible)
  132.     return sprite ~= nil and sprite:get_animation_set() or ""
  133.   end
  134.  
  135.   -- Store the position and properties of destructibles.
  136.   for destructible in map:get_entities("auto_destructible") do
  137.     local x, y, layer = destructible:get_position()
  138.     destructible_places[#destructible_places + 1] = {
  139.       x = x,
  140.       y = y,
  141.       layer = layer,
  142.       name = destructible:get_name(),
  143.       treasure = { destructible:get_treasure() },
  144.       sprite = get_destructible_sprite_name(destructible),
  145.       destruction_sound = destructible:get_destruction_sound(),
  146.       weight = destructible:get_weight(),
  147.       can_be_cut = destructible:get_can_be_cut(),
  148.       can_explode = destructible:get_can_explode(),
  149.       can_regenerate = destructible:get_can_regenerate(),
  150.       damage_on_enemies = destructible:get_damage_on_enemies(),
  151.       ground = destructible:get_modified_ground(),
  152.       destructible = destructible,
  153.     }
  154.   end
  155.  
  156. end
  157.  
  158. return separator_manager
  159.  

5
Development / Enemy: Zora
« on: July 29, 2015, 09:35:43 am »
I made a Zora enemy recently, who shoots three aligned fireballs.
It is essentially the same as ALTTP, except that the fireballs bounce when the hero hits them with the sword (but this feature can easily be removed if you want the real ALTTP behavior).

enemies/zora_water.lua:
Code: Lua
  1. -- A water enemy who shoots fireballs.
  2.  
  3. local enemy = ...
  4. local sprite
  5.  
  6. function enemy:on_created()
  7.  
  8.   enemy:set_life(1)
  9.   enemy:set_damage(2)
  10.   enemy:set_obstacle_behavior("swimming")
  11.   enemy:set_pushed_back_when_hurt(false)
  12.   enemy:set_size(16, 16)
  13.   enemy:set_origin(8, 13)
  14.  
  15.   sprite = enemy:create_sprite("enemies/" .. enemy:get_breed())
  16.   function sprite:on_animation_finished(animation)
  17.     if animation == "shooting" then
  18.       sprite:set_animation("walking")
  19.     end
  20.   end
  21. end
  22.  
  23. function enemy:on_restarted()
  24.  
  25.   local sprite = enemy:get_sprite()
  26.   local hero = enemy:get_map():get_hero()
  27.   sol.timer.start(enemy, 3000, function()
  28.     if enemy:get_distance(hero) < 300 then
  29.       sol.audio.play_sound("zora")
  30.       sprite:set_animation("shooting")
  31.       enemy:create_enemy({
  32.         breed = "fireball_red_small",
  33.       })
  34.     end
  35.     return true  -- Repeat the timer.
  36.   end)
  37. end
  38.  

enemies/fireball_red_small.lua
Code: Lua
  1. -- 3 fireballs shot by enemies like Zora and that go toward the hero.
  2. -- They can be hit with the sword, this changes their direction.
  3. local enemy = ...
  4.  
  5. local sprites = {}
  6.  
  7. function enemy:on_created()
  8.  
  9.   enemy:set_life(1)
  10.   enemy:set_damage(1)
  11.   enemy:set_minimum_shield_needed(2)  -- Shield 2 can block fireballs.
  12.   enemy:set_size(8, 8)
  13.   enemy:set_origin(4, 4)
  14.   enemy:set_can_hurt_hero_running(true)
  15.   enemy:set_obstacle_behavior("flying")
  16.   enemy:set_invincible()
  17.   enemy:set_attack_consequence("sword", "custom")
  18.  
  19.   for i = 0, 2 do
  20.     sprites[#sprites + 1] = enemy:create_sprite("enemies/" .. enemy:get_breed())
  21.   end
  22. end
  23.  
  24. local function go(angle)
  25.  
  26.   local movement = sol.movement.create("straight")
  27.   movement:set_speed(192)
  28.   movement:set_angle(angle)
  29.   movement:set_smooth(false)
  30.  
  31.   function movement:on_obstacle_reached()
  32.     enemy:remove()
  33.   end
  34.  
  35.   -- Compute the coordinate offset of follower sprites.
  36.   local x = -math.cos(angle) * 10
  37.   local y = math.sin(angle) * 10
  38.   sprites[2]:set_xy(x, y)
  39.   sprites[3]:set_xy(2 * x, 2 * y)
  40.   sprites[2]:set_animation("following_1")
  41.   sprites[3]:set_animation("following_2")
  42.  
  43.   movement:start(enemy)
  44. end
  45.  
  46. function enemy:on_restarted()
  47.  
  48.   local hero = enemy:get_map():get_hero()
  49.   local angle = enemy:get_angle(hero:get_center_position())
  50.   go(angle)
  51. end
  52.  
  53. -- Destroy the fireball when the hero is touched.
  54. function enemy:on_attacking_hero(hero, enemy_sprite)
  55.  
  56.   hero:start_hurt(enemy, enemy_sprite, enemy:get_damage())
  57.   enemy:remove()
  58. end
  59.  
  60. -- Change the direction of the movement when hit with the sword.
  61. function enemy:on_custom_attack_received(attack, sprite)
  62.  
  63.   if attack == "sword" and sprite == sprites[1] then
  64.     local hero = enemy:get_map():get_hero()
  65.     local movement = enemy:get_movement()
  66.     if movement == nil then
  67.       return
  68.     end
  69.  
  70.     local old_angle = movement:get_angle()
  71.     local angle
  72.     local hero_direction = hero:get_direction()
  73.     if hero_direction == 0 or hero_direction == 2 then
  74.       angle = math.pi - old_angle
  75.     else
  76.       angle = 2 * math.pi - old_angle
  77.     end
  78.  
  79.     go(angle)
  80.     sol.audio.play_sound("enemy_hurt")
  81.  
  82.     -- The trailing fireballs are now on the hero: don't attack temporarily
  83.     enemy:set_can_attack(false)
  84.     sol.timer.start(enemy, 500, function()
  85.       enemy:set_can_attack(true)
  86.     end)
  87.   end
  88. end
  89.  
Sprites are attached.

6
Development / How to show a night overlay on a map
« on: August 08, 2013, 10:01:39 am »
Solarus has a few built-in mechanisms, and you can customize things through the Lua API.
For example, there is no built-in day/night system. But it is easy to create one. In this sample script, we will make a simple dark overlay on the map to make a night visual effect.

More generally, you can use the same code to show any kind of semi-transparent surface on the map, even a PNG image.

For more details, see the documentation of surfaces.

First, create a surface for your map, for example in map:on_started() in your map script:

Code: [Select]
function map:on_started()
  self.night_overlay = sol.surface.create()  -- Create an empty surface of the size of the screen.
  self.night_overlay:set_opacity(192)        -- Make it semi-transparent (0: transparent, 255: opaque).
  self.night_overlay:fill_color({0, 0, 64})  -- Fill it with dark blue to obtain a night effect.
end

The night surface is initialized, we just have to draw it when the map is drawn:
Code: [Select]
function map:on_draw(destination_surface)
  self.night_overlay:draw(destination_surface)
end

And voilà !

So this is the basic idea.

If you want more:
- (Easy) To make the opacity change with time, use a timer.
- (Easy) To show an image from an external file instead, replace sol.surface.create() by sol.surface.create("path/to/file.png").
- (Advanced) To make the image move when the player moves (like the lost woods overlay in ALTTP), there are two optional parameters x and y in map.night_overlay:draw(). How to determine appropriate x and y values is left as an exercise to the reader ;)
- (Advanced) To make the image move automatically (like clouds), create a movement on your image.
- (Advanced) You can also combine the previous two remarks.

7
General discussion / Welcome!
« on: August 05, 2013, 02:31:00 pm »
Since the Solarus 1.0 release, several people suggested to create a forum to talk about game development with Solarus, get help, share scripts and game resources.
So that's done! Feel free to make any suggestion.
Welcome everybody!

Pages: [1]