Custom Entity: Moving platform [Solved with code]

Started by wizard_wizzle (aka ZeldaHistorian), January 31, 2015, 05:14:26 PM

Previous topic - Next topic
January 31, 2015, 05:14:26 PM Last Edit: February 01, 2015, 09:49:26 PM by wrightmat
How would someone recommend implementing moving platforms (over water or holes only) in Solarus? I've tried a dynamic tile with movement applied, but I can only get it to traverse what the hero could and not water. I've tried a custom entity, but I can't get it to be traversable by the hero - if the platform sits on water, the hero just jumps into the water rather than walking on the platform. I'm probably just doing something wrong, but was hoping for some direction either way.

Hi!
I see a solution to both approaches :
- Using a dynamic tile: Set up your movement with movement:set_ignore_obstacles(true). The fact that dynamic tiles cannot traverse water is probably a bad choice, could you report this problem on github? Thanks!
- Using a custom entity: By  defaut, custom entities have no ground property. It means that the map keeps the ground of whatever entity is below. But you can set a ground to your custom entity with custom_entity:set_modified_ground("traversable").

To make a moving platform, the recommended choice is your first idea: dynamic tiles, because it is simpler.

Thanks! I'll report that problem on github.

I knew the dynamic tile solution was simpler, but I may go with the custom entity approach to give me more flexibility to create multiple types of platforms that can be reused. The problem with the dynamic tile is that I want the platform to traverse ONLY water and holes - when it reached regular ground that the hero can traverse, it would reverse direction (or whatever other action). Using a custom entity, I can now get it to be traversable by the hero, but it won't traverse water.

local entity = ...
local map = entity:get_map()
local hero = map:get_entity("hero")

local ex, ey, el, hx, hy, hl
local recent_obstacle = 0
local timer

-- Platform: entity which moves in either horizontally or
-- vertically (depending on direction) and carries the hero on it.

function entity:on_created()
  self:create_sprite("entities/platform")
  self:set_size(32, 32)
  self:set_origin(20, 20)
  self:set_can_traverse_ground("hole", true)
  self:set_can_traverse_ground("deep_water", true)
  self:set_can_traverse_ground("traversable", false)
  self:set_can_traverse_ground("shallow_water", false)
  self:set_can_traverse_ground("wall", false)
  self:set_modified_ground("traversable")

  self:add_collision_test("overlapping", function(platform, other)
    -- This callback will be repeatedly called while other is overlapping the platform
    if other:get_type() ~= "hero" then
      return
    end
    local hero = other

    -- Only do this in some specific states (in particular, don't do it while jumping, flying with the hookshot, etc.)
    if hero:get_state() ~= "free" and hero:get_state() ~= "sword loading" then
      return
    end
   
    -- Keep the hero on the platform as it moves
    if timer == nil then
      timer = sol.timer.start(self, 50, function()
hx, hy, hl = hero:get_position()
        ex, ey, el = entity:get_position()
        local ox = hx - ex
        local oy = hy - ey
        hero:set_position(hx-(ox/5), hy-(oy/5))
        timer = nil  -- This variable "timer" ensures that only one timer is running.
      end)
    end

  end)

  local direction4 = self:get_sprite():get_direction()
  local m = sol.movement.create("path")
  m:set_path{direction4 * 2}
  m:set_speed(32)
  m:set_loop(true)
  m:start(self)

end

function entity:on_obstacle_reached()
  local direction4 = self:get_sprite():get_direction()
  self:get_sprite():set_direction((direction4 + 2) % 4)

  local x, y = self:get_position()
  recent_obstacle = 8

  local direction4 = self:get_sprite():get_direction()
  local m = sol.movement.create("path")
  m:set_path{direction4 * 2}
  m:set_speed(32)
  m:set_loop(true)
  m:start(self)

end

function entity:on_position_changed()
  if recent_obstacle > 0 then
    recent_obstacle = recent_obstacle - 1
  end
end

function entity:on_movement_changed(movement)
  local direction4 = movement:get_direction4()
  self:get_sprite():set_direction(direction4)
end

P.S. This probably isn't the best way to keep the hero on the platform either, but it works for now.

Nice!
To keep the hero on the platform more precisely, you can do the hero:set_position() call from entity:on_position_changed().

I'll give that a try. How would that work to set the hero's position only if he's on the platform though? Move the whole collision test to entity:on_position_chanted()?

Any thoughts on why the platform won't traverse water? My self:set_can_traverse_ground("deep_water", true) call doesn't seem to be functioning. Am I doing something wrong or is it a bug in the engine?

I would add a function is_hero_on_plaftorm() that returns true if timer is not nil.

The platform should traverse water if you call self:set_can_traverse_ground("deep_water", true), so if not, this may be a bug.

I finally got moving platforms working! They still won't traverse water if it's on the same layer, but I placed the water on low layer, with the platform and hero on intermediate, and it worked fine. Code, for anyone who's interested:

local entity = ...
local map = entity:get_map()
local hero = map:get_entity("hero")

local ex, ey, el, hx, hy, hl
local recent_obstacle = 0
local timer

-- Platform: entity which moves in either horizontally or
-- vertically (depending on direction) and carries the hero on it.

function entity:on_created()
  self:create_sprite("entities/platform")
  self:set_size(32, 32)
  self:set_origin(20, 20)
  self:set_can_traverse("jumper", true)
  self:set_can_traverse_ground("hole", true)
  self:set_can_traverse_ground("deep_water", true)
  self:set_can_traverse_ground("traversable", false)
  self:set_can_traverse_ground("shallow_water", false)
  self:set_can_traverse_ground("wall", false)
  self:set_modified_ground("traversable")
  self:set_layer_independent_collisions(false)

  self:add_collision_test("overlapping", function(platform, other)
    -- This callback will be repeatedly called while other is overlapping the platform
    if other:get_type() ~= "hero" then
      return
    end
    local hero = other

    -- Only do this in some specific states (in particular, don't do it while jumping, flying with the hookshot, etc.)
    if hero:get_state() ~= "free" and hero:get_state() ~= "sword loading" then
      return
    end
   
    -- Keep the hero on the platform as it moves
    if timer == nil then
      timer = sol.timer.start(self, 50, function()
        timer = nil  -- This variable "timer" ensures that only one timer is running.
      end)
    end
  end)

  local direction4 = self:get_sprite():get_direction()
  local m = sol.movement.create("path")
  m:set_path{direction4 * 2}
  m:set_speed(32)
  m:set_loop(true)
  m:start(self)

  self:add_collision_test("containing", function(platform, other)
    if other:get_type() == "wall" and other:get_type() ~= "jumper" then
      self:on_obstacle_reached(m)
    end
  end)

end

function entity:on_obstacle_reached(movement)
  movement:stop()

  local direction4 = self:get_sprite():get_direction()
  if direction4 == 0 then
    direction4 = 2
  elseif direction4 == 2 then
    direction4 = 0
  elseif direction4 == 1 then
    direction4 = 3
  elseif direction4 == 3 then
    direction4 = 1
  end

  movement:set_path{direction4 * 2}
  movement:set_speed(32)
  movement:set_loop(true)
  movement:start(self)

  local x, y = self:get_position()
  recent_obstacle = 8
end

function entity:on_position_changed()
  if timer ~= nil then
    hx, hy, hl = hero:get_position()
    ex, ey, el = entity:get_position()
    local ox = hx - ex
    local oy = hy - ey
    hero:set_position(hx-(ox/10), hy-(oy/10))
  end

  if recent_obstacle > 0 then
    recent_obstacle = recent_obstacle - 1
  end
end

function entity:on_movement_changed(movement)
  local direction4 = movement:get_direction4()
  self:get_sprite():set_direction(direction4)
end

Hi! Thanks for the code, I found it very useful. I have modified (and improved a bit) the functions of the script. Now the hero is not centered in the platform and can move more freely over it. (I tested the platform over traversable ground and it works fine.) I post my customized code, just in case someone find it useful:


local entity = ...

-- Platform: entity which moves in either horizontally or
-- vertically (depending on direction) and carries the hero on it.

local speed = 50
local time_stopped = 1000

function entity:on_created()
  self:create_sprite("entities/platform")
  self:set_size(32, 32)
  self:set_origin(16, 16)
  self:set_can_traverse("jumper", true)
  self:set_can_traverse_ground("hole", true)
  self:set_can_traverse_ground("deep_water", true)
  self:set_can_traverse_ground("traversable", false)
  self:set_can_traverse_ground("shallow_water", false)
  self:set_can_traverse_ground("wall", false)
  self:set_modified_ground("traversable")
  self:set_layer_independent_collisions(false)

  local m = sol.movement.create("path")
  local direction4 = self:get_sprite():get_direction()
  m:set_path{direction4 * 2}
  m:set_speed(speed)
  m:set_loop(true)
  m:start(self)
 
  self:add_collision_test("containing", function(platform, other)
    if other:get_type() == "wall" and other:get_type() ~= "jumper" then
      self:on_obstacle_reached(m)
    end
  end)

end

function entity:on_obstacle_reached(movement)
  --Make the platform turn back.
  movement:stop()
  movement = sol.movement.create("path")   
  local direction4 = self:get_sprite():get_direction()
  direction4 = (direction4+2)%4
  movement:set_path{direction4 * 2}
  movement:set_speed(speed)
  movement:set_loop(true)
  sol.timer.start(self, time_stopped, function() movement:start(self) end)
end

function entity:on_position_changed()
  -- Moves the hero if located over the platform.
  if not self:is_on_platform(hero) then return end
  local hx, hy, hl = hero:get_position()
  local direction4 = self:get_sprite():get_direction()
  local dx, dy = 0, 0 --Variables for the translation.
  if direction4 == 0 then dx = 1
  elseif direction4 == 1 then dy = -1
  elseif direction4 == 2 then dx = -1
  elseif direction4 == 3 then dy = 1
  end
  if not hero:test_obstacles(dx, dy, hl) then hero:set_position(hx + dx, hy + dy, hl) end
end

function entity:on_movement_changed(movement)
  --Change direction of the sprite when the movement changes.
  local direction4 = movement:get_direction4()
  self:get_sprite():set_direction(direction4)
end

function entity:is_on_platform(other_entity)
  --Returns true if other_entity is on the platform.
  local ox, oy, ol = other_entity:get_position()
  local ex, ey, el = self:get_position()
  if ol ~= el then return false end
  local sx, sy = self:get_size()
  if math.abs(ox - ex) < sx/2 -1 and math.abs(oy - ey) < sy/2 -1 then return true end
  return false
end
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Much improved - thank you! An addition you need is local hero = entity:get_map():get_entity("hero") for the "hero" call in on_position_changed.

One issue I'm having... I'm adding walls that affect only NPCs in order to limit the path of the platform. I've found that with "containing" as the collision test, it doesn't cause the platform to reverse direction at all, like the wall isn't there. I can get it to work with "touching" as the collision test. Not sure if you saw the same thing, or if it works okay for you.

There are also issues with the platform moving correctly after the first bounce. The first one is perfect - it hits the obstacle, pauses for a second, and reverses direction. On the second wall, it moves past the wall, finally switched direction a little bit later then bounces back only a little bit and kind of "juggles" around the wall. Did you encounter anything like this?

Yes, you are right, I forgot that line (I found that I had a global variable hero in other script, by mistake), and the "touching" collision should be the best choice for the platform. Thank you!

I only have tested the platform between two normal walls, and it works fine, at least in that case. I didn't find the last issue that you said.
(Maybe the problem is with that kind of wall that only affects NPC's, I don't know...)
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I really like this idea and want to implement it! How about instead of making the water layers into low when making the map, you change the hero's layer to high (and make the platform become high as well) when the hero activates the platform? That way it's gonna move in the high layers. Then, when the hero dismounts the platform, you can change the layer settings back to normal for both the hero and the platform?