Menu

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.

Show posts Menu

Topics - Christopho

#1
Your scripts / chronometer.lua: Measure the time played
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) Select

-- Adds chronometer features to games.
-- The following functions are provided:
-- - game:get_time_played():            Returns the game time in seconds.
-- - game:get_time_played_string():     Returns a string representation of the game time.

-- Usage:
-- require("scripts/chronometer")

require("scripts/multi_events")

-- Measure the time played.
local function initialize_chronometer_features(game)

  -- Returns the game time in seconds.
  function game:get_time_played()
    local milliseconds = game:get_value("time_played") or 0
    local total_seconds = math.floor(milliseconds / 1000)
    return total_seconds
  end

  -- Returns a string representation of the game time.
  function game:get_time_played_string()
    local total_seconds = game:get_time_played()
    local seconds = total_seconds % 60
    local total_minutes = math.floor(total_seconds / 60)
    local minutes = total_minutes % 60
    local total_hours = math.floor(total_minutes / 60)
    local time_string = string.format("%02d:%02d:%02d", total_hours, minutes, seconds)
    return time_string
  end

  local timer = sol.timer.start(game, 100, function()
    local time = game:get_value("time_played") or 0
    time = time + 100
    game:set_value("time_played", time)
    return true  -- Repeat the timer.
  end)
  timer:set_suspended_with_map(false)
end

-- Set up chronometer features on any game that starts.
local game_meta = sol.main.get_metatable("game")
game_meta:register_event("on_started", initialize_chronometer_features)

return true

#2
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


#3
Development / Solarus 1.5 development snapshot
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
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) Select

-- This script restores entities when there are separators in a map.
-- When taking separators prefixed by "auto_separator", the following entities are restored:
-- - Enemies prefixed by "auto_enemy".
-- - Destructibles prefixed by "auto_destructible".
-- - Blocks prefixed by "auto_block".
--
-- Usage from a map script:
-- local separator_manager = require("maps/lib/separator_manager.lua")
-- separator_manager:manage_map(map)
-- If you prefer, you can also enable it automatically on all maps from game:on_map_changed().

local separator_manager = {}

function separator_manager:manage_map(map)

  local enemy_places = {}
  local destructible_places = {}
  local game = map:get_game()
  local zelda = map:get_entity("zelda")

  -- Function called when a separator was just taken.
  local function separator_on_activated(separator)

    local hero = map:get_hero()

    -- Enemies.
    for _, enemy_place in ipairs(enemy_places) do
      local enemy = enemy_place.enemy

      -- First remove any enemy.
      if enemy:exists() then
        enemy:remove()
      end

      -- Re-create enemies in the new active region.
      if enemy:is_in_same_region(hero) then
        local old_enemy = enemy_place.enemy
        local enemy = map:create_enemy({
          x = enemy_place.x,
          y = enemy_place.y,
          layer = enemy_place.layer,
          breed = enemy_place.breed,
          direction = enemy_place.direction,
          name = enemy_place.name,
        })
        enemy:set_treasure(unpack(enemy_place.treasure))
        enemy.on_dead = old_enemy.on_dead  -- For door_manager.
        enemy_place.enemy = enemy
      end
    end

    -- Blocks.
    for block in map:get_entities("auto_block") do
      -- Reset blocks in regions no longer visible.
      if not block:is_in_same_region(hero) then
        block:reset()
      end
    end

  end

  -- Function called when a separator is being taken.
  local function separator_on_activating(separator)

    local hero = map:get_hero()

    -- Enemies.
    if not map.used_separator then
      -- First separator: remove enemies from other regions like on_activated() does.
      -- Because on_activated() was not called when the map started.
      for _, enemy_place in ipairs(enemy_places) do
        local enemy = enemy_place.enemy
        if enemy:exists() and not enemy:is_in_same_region(hero) then
          enemy:remove()
        end
      end
    end

    -- Destructibles.
    for _, destructible_place in ipairs(destructible_places) do
      local destructible = destructible_place.destructible

      if not destructible:exists() then
        -- Re-create destructibles in all regions except the active one.
        if not destructible:is_in_same_region(hero) then
          local destructible = map:create_destructible({
            x = destructible_place.x,
            y = destructible_place.y,
            layer = destructible_place.layer,
            name = destructible_place.name,
            sprite = destructible_place.sprite,
            destruction_sound = destructible_place.destruction_sound,
            weight = destructible_place.weight,
            can_be_cut = destructible_place.can_be_cut,
            can_explode = destructible_place.can_explode,
            can_regenerate = destructible_place.can_regenerate,
            damage_on_enemies = destructible_place.damage_on_enemies,
            ground = destructible_place.ground,
          })
          -- We don't recreate the treasure.
          destructible_place.destructible = destructible
        end
      end
    end
  end

  for separator in map:get_entities("auto_separator") do
    separator.on_activating = separator_on_activating
    separator.on_activated = separator_on_activated
  end

  -- Store the position and properties of enemies.
  for enemy in map:get_entities("auto_enemy") do
    local x, y, layer = enemy:get_position()
    enemy_places[#enemy_places + 1] = {
      x = x,
      y = y,
      layer = layer,
      breed = enemy:get_breed(),
      direction = enemy:get_sprite():get_direction(),
      name = enemy:get_name(),
      treasure = { enemy:get_treasure() },
      enemy = enemy,
    }
  end

  local function get_destructible_sprite_name(destructible)
    -- TODO the engine should have a destructible:get_sprite() method.
    -- As a temporary workaround we use the one of custom entity, fortunately
    -- it happens to work for all types of entities.
    local sprite = sol.main.get_metatable("custom_entity").get_sprite(destructible)
    return sprite ~= nil and sprite:get_animation_set() or ""
  end

  -- Store the position and properties of destructibles.
  for destructible in map:get_entities("auto_destructible") do
    local x, y, layer = destructible:get_position()
    destructible_places[#destructible_places + 1] = {
      x = x,
      y = y,
      layer = layer,
      name = destructible:get_name(),
      treasure = { destructible:get_treasure() },
      sprite = get_destructible_sprite_name(destructible),
      destruction_sound = destructible:get_destruction_sound(),
      weight = destructible:get_weight(),
      can_be_cut = destructible:get_can_be_cut(),
      can_explode = destructible:get_can_explode(),
      can_regenerate = destructible:get_can_regenerate(),
      damage_on_enemies = destructible:get_damage_on_enemies(),
      ground = destructible:get_modified_ground(),
      destructible = destructible,
    }
  end

end

return separator_manager
#5
Development / Enemy: Zora
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) Select

-- A water enemy who shoots fireballs.

local enemy = ...
local sprite

function enemy:on_created()

  enemy:set_life(1)
  enemy:set_damage(2)
  enemy:set_obstacle_behavior("swimming")
  enemy:set_pushed_back_when_hurt(false)
  enemy:set_size(16, 16)
  enemy:set_origin(8, 13)

  sprite = enemy:create_sprite("enemies/" .. enemy:get_breed())
  function sprite:on_animation_finished(animation)
    if animation == "shooting" then
      sprite:set_animation("walking")
    end
  end
end

function enemy:on_restarted()

  local sprite = enemy:get_sprite()
  local hero = enemy:get_map():get_hero()
  sol.timer.start(enemy, 3000, function()
    if enemy:get_distance(hero) < 300 then
      sol.audio.play_sound("zora")
      sprite:set_animation("shooting")
      enemy:create_enemy({
        breed = "fireball_red_small",
      })
    end
    return true  -- Repeat the timer.
  end)
end


enemies/fireball_red_small.lua
Code (lua) Select

-- 3 fireballs shot by enemies like Zora and that go toward the hero.
-- They can be hit with the sword, this changes their direction.
local enemy = ...

local sprites = {}

function enemy:on_created()

  enemy:set_life(1)
  enemy:set_damage(1)
  enemy:set_minimum_shield_needed(2)  -- Shield 2 can block fireballs.
  enemy:set_size(8, 8)
  enemy:set_origin(4, 4)
  enemy:set_can_hurt_hero_running(true)
  enemy:set_obstacle_behavior("flying")
  enemy:set_invincible()
  enemy:set_attack_consequence("sword", "custom")

  for i = 0, 2 do
    sprites[#sprites + 1] = enemy:create_sprite("enemies/" .. enemy:get_breed())
  end
end

local function go(angle)

  local movement = sol.movement.create("straight")
  movement:set_speed(192)
  movement:set_angle(angle)
  movement:set_smooth(false)

  function movement:on_obstacle_reached()
    enemy:remove()
  end

  -- Compute the coordinate offset of follower sprites.
  local x = -math.cos(angle) * 10
  local y = math.sin(angle) * 10
  sprites[2]:set_xy(x, y)
  sprites[3]:set_xy(2 * x, 2 * y)
  sprites[2]:set_animation("following_1")
  sprites[3]:set_animation("following_2")

  movement:start(enemy)
end

function enemy:on_restarted()

  local hero = enemy:get_map():get_hero()
  local angle = enemy:get_angle(hero:get_center_position())
  go(angle)
end

-- Destroy the fireball when the hero is touched.
function enemy:on_attacking_hero(hero, enemy_sprite)

  hero:start_hurt(enemy, enemy_sprite, enemy:get_damage())
  enemy:remove()
end

-- Change the direction of the movement when hit with the sword.
function enemy:on_custom_attack_received(attack, sprite)

  if attack == "sword" and sprite == sprites[1] then
    local hero = enemy:get_map():get_hero()
    local movement = enemy:get_movement()
    if movement == nil then
      return
    end

    local old_angle = movement:get_angle()
    local angle
    local hero_direction = hero:get_direction()
    if hero_direction == 0 or hero_direction == 2 then
      angle = math.pi - old_angle
    else
      angle = 2 * math.pi - old_angle
    end

    go(angle)
    sol.audio.play_sound("enemy_hurt")

    -- The trailing fireballs are now on the hero: don't attack temporarily
    enemy:set_can_attack(false)
    sol.timer.start(enemy, 500, function()
      enemy:set_can_attack(true)
    end)
  end
end

Sprites are attached.
#6
Development / How to show a night overlay on a map
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:


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:
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!
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!