Interpolation

This commit is contained in:
Karamel 2017-08-03 23:12:36 +02:00
parent 1ba52f42d7
commit 9c52feed90
2 changed files with 129 additions and 19 deletions

@ -39,8 +39,8 @@ Use poschangelib.add_player_pos_listener(name, my_callback)
Name is the identifier the listener, to use in remove_player_pos_listener. You should
follow the naming convention like for node names. See http://dev.minetest.net/Intro
The my_callback is a function that takes 3 arguments: the player, last known position
and new position.
The my_callback is a function that takes 4 arguments: the player, last known position,
new position and some metadata.
On first call (once a player joins) the last known position will be nil. If your
listener does something in that case, it will be called shortly after the player
@ -52,7 +52,7 @@ When on teleporting, programatic moves with setpos or moving fast it may be far
Quick code sample:
local function my_callback(player, old_pos, new_pos)
local function my_callback(player, old_pos, new_pos, meta)
if old_pos == nil then
minetest.chat_send_player(player:get_player_name(), 'Welcome to the world!')
else
@ -156,6 +156,33 @@ _on_walk is affected by the same issue about non-filled nodes. You can use the 4
argument to check the trigger metadata to adjust your callback.
More on metadata
----------------
The metadata are a table that can contain the following elements:
interpolated
Is true when the position was assumed and not observed. Most of the time because the
player moved too fast to check all nodes in real time.
player_pos
Is set for walk listeners, it contains the player's position. Not set when
interpolated.
source
Contains the name of the node or group that triggered the walk listener.
This is one of thoses passed on registration.
source_level
Contains the level of the group when source is a node group.
redo
Is true when it was detected that the listener was previously called on that position.
See <Watch player walking on particular nodes, the fine way>.
Configuration/Performances tweaking
-----------------------------------

115
init.lua

@ -17,6 +17,12 @@ end
-- more than once per loop (player) if they are registered for more than one event
-- (for example triggered on walk on multiple groups)
local triggered_listeners = {}
local function set_listener_triggered(name, pos)
if not triggered_listeners.name then
triggered_listeners.name = {}
end
table.insert(triggered_listeners.name, pos)
end
--- Internal utility to create an empty table on first registration.
-- @param mothertable The main table that will hold other tables.
@ -30,9 +36,15 @@ local function get_subtable_or_create(mothertable, item)
end
--- Check if a listener can be triggered
local function is_callable(name)
local function is_callable(name, pos)
-- Check if not aleady called
if triggered_listeners.name then return false end
if triggered_listeners.name then
for _, trigg_pos in ipairs(triggered_listeners.name) do
if vector.equals(trigg_pos, pos) then
return false
end
end
end
-- Other checks will come here when required
return true
end
@ -64,11 +76,11 @@ end
--- Trigger registered callbacks if not already triggered.
-- Reset triggered_listeners to be able to recall the callback.
local function trigger_player_position_listeners(player, old_pos, pos)
local function trigger_player_position_listeners(player, old_pos, pos, trigger_meta)
for name, callback in pairs(player_listeners) do
if is_callable(name) then
callback(player, old_pos, pos)
triggered_listeners[name] = true
if is_callable(name, pos) then
callback(player, old_pos, pos, trigger_meta)
set_listener_triggered(name, pos)
end
end
end
@ -120,9 +132,9 @@ end
local function trigger_player_walk_listeners(trigger_name, player, pos, node, node_def, trigger_meta)
for name, callback in pairs(walk_listeners[trigger_name]) do
if is_callable(name) then
if is_callable(name, pos) then
callback(player, pos, node, node_def, trigger_meta)
triggered_listeners[trigger_name] = true
set_listener_triggered(trigger_name, pos)
end
end
end
@ -138,20 +150,79 @@ local function remove_last_pos_on_leave(player)
player_last_pos[player:get_player_name()] = nil
end
minetest.register_on_leaveplayer(remove_last_pos_on_leave)
--- Erratically get a path from start_pos and end_pos. This won't be 100%
-- accurate for many reasons.
-- - We don't know if a node is passable or not.
-- - There may be multiple options to get from one point to an other with the
-- same length
-- - The player may not even walk straight
-- This function is recursive, start will move toward end.
-- @param start_pos Full coortinate of starting point (recursive)
-- @param end_pos The goal
-- @param path Empty at start, will contains all points between start and end
-- at the last call, then return up all the way to the first call.
function poschangelib.get_path(start_pos, end_pos, path)
-- Try to get closer to end_pos by moving one block in the axis that
-- is the further from end. If at the same distance for more than one
-- axis, pick randomly between them.
if path == nil then path = {} end
table.insert(path, start_pos)
local distance = vector.subtract(end_pos, start_pos)
local dX = math.abs(distance.x)
local dY = math.abs(distance.y)
local dZ = math.abs(distance.z)
if (dX + dY + dZ <= 1) then -- Next step will reach end_pos
table.insert(path, end_pos)
return path
end
local d = {} -- List of candidates axis for next move
if dX >= dY and dX >= dZ then table.insert(d, 'x') end
if dY >= dX and dY >= dZ then table.insert(d, 'y') end
if dZ >= dX and dZ >= dY then table.insert(d, 'z') end
local axis = d[math.random(1, table.getn(d))]
local next_pos = nil
if axis == 'x' then
if distance.x > 0 then
next_pos = vector.add(start_pos, vector.new(1,0,0))
else
next_pos = vector.add(start_pos, vector.new(-1,0,0))
end
elseif axis == 'y' then
if distance.y > 0 then
next_pos = vector.add(start_pos, vector.new(0,1,0))
else
next_pos = vector.add(start_pos, vector.new(0,-1,0))
end
elseif axis == 'z' then
if distance.z > 0 then
next_pos = vector.add(start_pos, vector.new(0,0,1))
else
next_pos = vector.add(start_pos, vector.new(0,0,-1))
end
end
if axis == nil then
minetest.log('error', 'poschangelib interpolator is lost')
return path
end
return poschangelib.get_path(next_pos, end_pos, path)
end
--- Check if position has changed for the player.
-- @param player The player object.
-- @returns {old_pos, new_pos} if the position has changed, nil otherwise
-- @returns List of positions from last known to current
-- (with guessed interpolation) if the position has changed, nil otherwise.
local function get_updated_positions(player)
local pos = vector.round(player:getpos())
local old_pos = player_last_pos[player:get_player_name()]
local ret = nil
if old_pos == nil then
-- Position of the player was set
ret = {old = old_pos, new = pos}
ret = {pos}
elseif pos then
-- Check for position change
if not vector.equals(old_pos, pos) then
ret = {old = old_pos, new = pos}
ret = poschangelib.get_path(old_pos, pos)
end
end
player_last_pos[player:get_player_name()] = pos
@ -159,7 +230,8 @@ local function get_updated_positions(player)
end
--- Check and call on_walk triggers if required.
local function check_on_walk_triggers(player, old_pos, pos, raw_pos)
local function check_on_walk_triggers(player, old_pos, pos, trigger_meta)
if trigger_meta == nil then trigger_meta = {} end
-- Get the node at current player position to check if in mid-air
-- or on a half-filled node.
local pos_below = pos
@ -169,7 +241,9 @@ local function check_on_walk_triggers(player, old_pos, pos, raw_pos)
-- in-air or standing on a non-filled walkable block.
-- Pass this information to the listener in case they want a fine
-- collision checking.
local trigger_meta = { player_pos = pos }
if not trigger_meta.interpolated then
trigger_meta.player_pos = pos
end
if not node_def.walkable then
-- Player not standing in a non-filled node
-- Check node below, if walkable consider the player is walking
@ -228,9 +302,18 @@ local function loop(dtime)
for _, player in ipairs(minetest.get_connected_players()) do
local poss = get_updated_positions(player)
if poss then
trigger_player_position_listeners(player, poss.old, poss.new)
if poss.old then -- Don't trigger on join
check_on_walk_triggers(player, poss.old, poss.new, player:getpos())
if table.getn(poss) == 1 then
trigger_player_position_listeners(player, nil, poss[0])
else
local poss_end_couple = table.getn(poss) - 1
for i = 1, poss_end_couple do
local trigger_meta = {}
if i > 1 and i <= poss_end_couple then
trigger_meta.interpolated = true
end
trigger_player_position_listeners(player, poss[i], poss[i+1], trigger_meta)
check_on_walk_triggers(player, poss[i], poss[i+1], trigger_meta)
end
end
-- Reset the triggered listener to allow the next player to trigger them
triggered_listeners = {}