Question about dynamic tiles

Started by Starlock, August 27, 2015, 02:26:43 AM

Previous topic - Next topic
If you wanted to make a shovel that was able to dig at dirt to reveal treasures similar to that of the shovel in Link's Awakening would you have to turn all ground tiles into dynamic tiles? Like would you make the ground dynamic and then under it there would be patches of dirt that are revealed after the ground has been dug? I'm just checking to see if this is the basic idea before I attempt to code a shovel

August 27, 2015, 04:07:32 AM #1 Last Edit: August 27, 2015, 04:11:34 AM by Diarandor
Hi! I also wanted to make a shovel for my game, although I haven't thought much how to program it.

I thought that I could detect the ground in some way and create a dynamic tile with hole ground over it. This was not a good idea since you would need to detect a 16x16 grid to create the hole in the correct position, but since sometimes the tiles are shifted 8 pixels in some coordinate, we would not be able to know the exact position where we should create the 16x16 hole (because of these possible shifts of 8 pixels in some coordinate). Thus, we will need to use some 16x16 entity that will be replaced by a 16x16 hole, that is unavoidable.

I think you are right that the best way would be using dynamic tiles. I would try to detect them with a collision test, checking if they are "sand" (checking the pattern id), and in that case removing it and creating a new dynamic tile with hole ground in its place, or something like that.

In this post http://forum.solarus-games.org/index.php/topic,386.0.html, Christopho said that he can do the functions dynamic_tile:get_pattern_id() and dynamic_tile:get_ground() for the next version of Solarus (the version 1.5), so you cannot detect the pattern id of a dynamic tile yet.

I think that the only solution at present would be using custom entities instead of dynamic tiles. In this case, instead of removing it and creating a new one, you can directly change the sprite and ground type of the custom entity.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Yes! I too would like to make the shovel item, but I came across the same problem you did: the entire map would be filled with either dynamic tiles or custom entities. Surely, there must be some easier way.

What I was thinking of at the moment was a tile like the tall grass that would interact if you used the shovel item. So basically, the hole would already be on the map and above the hole the ground tile. When the shovel is used above the ground tile, it disappears producing a treasure (like the tall grass) and then revealing the hole (much like the tall grass would be cut to show the cut grass or the bush cut to show the stump).

September 04, 2015, 02:35:41 PM #3 Last Edit: September 04, 2015, 02:39:41 PM by Christopho
It must be possible to implement the shovel without having dynamic tiles everywhere when the map starts.
Like the ice rod of Zelda Return of the Hylian SE, you could have normal tiles everywhere, and create shovel holes as dynamical tiles (or custom entities) where the hero uses the shovel. Lua code would check if the target 16x16 square is traversable (with map:get_ground() at each corner of the 16x16 square). I don't think that the alignment would be an issue. I would create a 16x16 dynamic tile of hole at some coordinates snapped to the 16x16 or 8x8 grid.

To avoid to create two holes at the same place, maybe what the shovel should create a custom entity rather than a dynamic tile. If there is a collision with such a custom entity, don't make another one.


Now there is another challenge. How to control which tiles are allowed for digging and which tiles are not? Just allowing to make a hole on any traversable tile might not be what you want. There are traversable tiles like cobbled paths where a shovel is not supposed to dig.

To have more control and restrict the use of the shovel to some specific regions of the map (instead of on any traversable ground), here are a few ideas:
- In some games, maybe digging is allowed almost everywhere and forbidding it is the exception. Then, each map script could indicate some explicit rectangle coordinates where to forbid the use of the shovel. This might be enough: the shovel would work on any traversable ground of the map except in these rectangles. Pros: easy to program the shovel. Cons: Some work to do for every map that wants to have non-diggable places.
- Or the contrary: if allowing is the exception, you can specify in the map script where are the only allowed rectangles.
- Or do something equivalent from the map editor, placing custom entities on the forbidden rectangles. The downside of this approach is that the map view in the editor will easily get polluted by a lot of big custom entities.
- A more advanced idea: from your Lua scripts, you can parse the map data file. Powerful, but kind of a hack. You can actually know the pattern of each static tile! If you know this information, you can then easily restrict the use of the shovel to dirt and sand tiles. To see how to parse a map data file from your scripts, you can see the dungeon minimap menu of Zelda Mystery of Solarus DX or Zelda Return of the Hylian SE. In these, I parse map data files to detect the location of chests and show them on the dungeon map when the player has the compass. It is possible to do the same, detecting tiles instead of chests. Performance will be okay as long as you parse once and store the information somewhere. It requires more complicated work, but it is doable.

September 04, 2015, 10:47:11 PM #4 Last Edit: September 04, 2015, 10:51:10 PM by Diarandor
You are right Christopho, it should be possible.

So, as you say, there won't be any problem with the grid alignment since we can check the ground on the four corners of the 16x16 square. And the idea of parsing the map is great, I didn't think of that possibility.

There are two possible behaviours for the shovel in the following particular case: if the ground can be dug in some of the corners of the 16x16 tile (aligned to the 16x16 grid) but not in all of them, then we can either
1) make the shovel to clash with the ground, or
2) make the shovel to dig only in the 8x8 parts (of the 16x16 square) that can be dug (to do this, we create 8x8 tiles in the correct positions).
Although this is not very important, I think the first option is much better and simpler in this particular case.

To check the collision with a custom entity "digging hole", I think that the best/fastest option could be to call once the function map:get_entities_in_rectangle(x, y, width, height) (when Solarus 1.5 is released, with this function), and then check if there is some hole entity overlapping the corresponding 16x16 square.

(Since I'm not having much free time lately, I will wait for someone to write this script, or I will try to do it by myself when I have some free time.)
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I wrote a script to manage the digging ! ;D (but solarus 1.5 is needed)

It provides two methods for the game:

  • game:can_dig(x, y, layer) to check if can digging at the specified position
  • game:dig(x, y, layer) to create a hole with a dynamic tile
And the method digger:on_map_changed(map) that must be called in the game:on_map_changed(map) method.

It uses a 16x16 grid and allows to have different tiles for holes (e.g. for the grass and the sand). To do that you need to specify for each tiles of each tilesets (that allows digging) which tile to use for hole; and you can use the _default value to allow digging where there are no tile. (see the definition of the variable digging_rules in the script)

So here's the script (digging_manager.lua):
Code (lua) Select

-- Script that manages digable positions and makes holes for the shovel.

-- Usage:
-- local digging_manager = require("scripts/digging_manager")
-- digging_manager:create(game)

local digging_manager = {}

-- Rules for each tilesets that allows to dig.
local digging_rules = {
  ["light_world"] = {
    _default = "grass.under_stone",
    ["grass.soil"] = "grass.under_stone",
    ["grass.weed"] = "grass.under_stone",
    ["grass.weed_big"] = "grass.under_stone",
    ["grass.flower"] = "grass.under_stone",
    ["grass.flower_double"] = "grass.under_stone",
    ["grass.soil.diag.1b"] = "grass.under_stone",
    ["grass.soil.diag.2b"] = "grass.under_stone",
    ["grass.soil.diag.3b"] = "grass.under_stone",
    ["grass.soil.diag.4b"] = "grass.under_stone",
    ["sand.soil"] = "sand.under_stone",
    ["sand.weed"] = "sand.under_stone",
    ["sand.flower"] = "sand.under_stone",
    ["sand.soil.diag.1b"] = "sand.under_stone",
    ["sand.soil.diag.2b"] = "sand.under_stone",
    ["sand.soil.diag.3b"] = "sand.under_stone",
    ["sand.soil.diag.4b"] = "sand.under_stone",
  },
}

-- Creates a digger for the specified game.
function digging_manager:create(game)

  local digger = {}
  local current_map = nil
  local digable_mask = {[1] = {}, [2] = {}, [3] = {}}

  -- Loads the digable mask from the current map.
  local function load_digable_mask()

    digable_mask = {[1] = {}, [2] = {}, [3] = {}}

    -- Check the map.
    if current_map == nil then return end

    -- Get the tileset.
    local tileset = current_map:get_tileset()
    if digging_rules[tileset] == nil then
      return
    end

    -- Set default rule for the low layer.
    if digging_rules[tileset]._default then

      local width, height = current_map:get_size()
      width, height = math.floor(width / 16), math.floor(height / 16)

      for x = 0, width do
        for y = 0, height do
          digable_mask[1][x .. ";" .. y] = digging_rules[tileset]._default
        end
      end
    end

    -- Setup an environment to load map tiles.
    local environment = {
      tile = function(tile_properties)

        local layer = tile_properties.layer + 1
        local x = math.floor(tile_properties.x / 8)
        local y = math.floor(tile_properties.y / 8)
        local width = math.floor(tile_properties.width / 8)
        local height = math.floor(tile_properties.height / 8)

        local tile = digging_rules[tileset][tile_properties.pattern]

        for i = 1, width do
          for j = 1, height do
            local mask_x = math.floor((x + i - 1) / 2)
            local mask_y = math.floor((y + j - 1) / 2)
            digable_mask[layer][mask_x .. ";" .. mask_y] = tile
          end
        end
      end
    }

    setmetatable(environment, {
      __index = function()
        return function() end
      end
    })

    -- Load the map.
    local chunk = sol.main.load_file("maps/" .. current_map:get_id() .. ".dat")
    setfenv(chunk, environment)
    chunk()
  end

  -- Call this function to notify the digger that the current map has changed.
  function digger:on_map_changed(map)

    current_map = map
    load_digable_mask()
  end

  -- Checks whether the specified position can be dug in the current map.
  function game:can_dig(x, y, layer)

    if digable_mask == nil then
      return false
    end

    x, y = x - (x % 16), y - (y % 16)

    for entity in current_map:get_entities_in_rectangle(x, y, 16, 16) do
      if entity:get_type() == "dynamic_tile" and entity:is_enabled() then
        return false
      end
    end

    x, y, layer = math.floor(x / 16), math.floor(y / 16), layer + 1

    return digable_mask[layer][x .. ";" .. y] ~= nil
  end

  -- Digs a hole at the specified position in the current map.
  function game:dig(x, y, layer)

    if not self:can_dig(x, y, layer) then
      return false
    end

    local mask_layer = layer + 1
    local mask_x, mask_y = math.floor(x / 16), math.floor(y / 16)
    local tile = digable_mask[mask_layer][mask_x .. ";" .. mask_y]

    current_map:create_dynamic_tile({
      layer = layer,
      x = mask_x * 16,
      y = mask_y * 16,
      width = 16,
      height = 16,
      pattern = tile,
      enabled_at_start = true
    })

    return true
  end

  return digger
end

return digging_manager


The digging_rules of this script is an example based on the alltp pack but you need to modify the light_world.tiles image to add "grass" and "sand" of tiles grass.under_stone and sand.under_stone. (have no transparency to override flowers)

Now, it remains only to write the script of the shovel.  :P

meanwhile here's the script that I've used for my tests (main.lua):
Code (lua) Select

local digging_manager = require("digging_manager")

function sol.main:on_started()

  local game = sol.game.load("save")
  local digger = digging_manager:create(game)

  function game:on_map_changed(map)
    digger:on_map_changed(map)
  end

  function game:on_key_pressed(key)

    local map = self:get_map()
    if map and key == "t" then
      local x, y, layer = map:get_hero():get_position()
      self:dig(x, y, layer)
    end
  end

  game:start()
end

SQE developer

Hello, I had the same idea with footprint on the sand. For now, I just made many sensor (many many !! but I try it with dynamic tiles and it works too!) and when the player overlaps this sensor (or dynamic tiles) the step is draw on the sand but as Christopho said, the map is really polluted (as you can see on the picture). I was hoping that the tile id will be checkable one day and I learn today the it will be possible in the 1.5! Cool, but is it only with dynamic_tile, or normal ones ?

The idea of parsing the map file to get pattern ids already works in 1.4. It is map:get_entities_in_rectangle() that only works in 1.5.

If you want to use my script with solarus 1.4, you can remove the code that check for entities in the game:can_dig(x, y, layer) method. (line 116)

But you need to add code to update the mask when you dig, to not be able to dig in the same place twice.
With something like this (in the game:dig(x, y, layer) method):
Code (lua) Select

function game:dig(x, y, layer)

  -- Check if can dig
  -- Create the dynamic tile

  digable_mask[mask_layer][mask_x .. ";" .. mask_y] = nil
  return true
end
SQE developer