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") (http://www.solarus-games.org/doc/latest/lua_api_custom_entity.html#lua_api_custom_entity_set_modified_ground).
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
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...)
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?