Problem statement
Given an enemy with a walkspeed of ‘n’, that receives position updates around every 1/10 of a second, how would you interpolate the enemy to move seamlessly? The critical part of this statement is the ‘around every 1/10 of a second’. This number is variable because it could take 1/10 of a second to receive an update or it could be a lot longer than that (due to the nature of task scheduling and network traffic/network speed).
Scenario
The current situation that I’m running into is that I have a bunch of enemies that are represented on the server simply as { position = Vector2 }. Every frame, an enemy’s position is updated and every 1/10 of a second, position updates are sent to clients. Given the problem’s context, we know that the time between packets will not always be the same. I’m running into a problem where enemies stutter while waiting for the next packet. I’ve tried researching online and checking out devforum posts, but none of them can quite fix the issue that I have.
Attempt #1
My first attempt consists of “rubber-banding”-- continuing an enemy’s movement, and once a new update is received, set the start_position back to the server’s start position immediately. Due to the nature of the updates, this looks terrible and is not at all the experience I want for my players.
-- enemy lerping
-- extra code that is irrelevant to the problem has been removed
function EnemyRender:Step(dt)
local moved = false
-- update model's physical position
self.alpha += dt * SETTINGS.ServerUpdateFrequency
self.position = self.start_position:Lerp(
self.end_position,
self.alpha
)
end
-- receive an update from the server
function EnemyRender:UpdatePosition(packet)
local target = packet[1]
target = Vector3.new(target.X, 3, target.Y)
self.start_position = self.target_position
self.target_position = target
self.alpha = 0
end
Attempt #2
My second attempt consists of using a spring to ‘fix’ the start position. I’ll use the enemy’s current position on the client as a start and “spring” it back to the actual start position on the server. While not entirely mimicking the server’s movement, it gets a very close fit. This solution is extremely close to what I’m trying to accomplish, but small errors accumulate over time, which cause very large inconsistencies (ESPECIALLY when changing directions).
-- update position
function EnemyRender:Step(dt)
-- update position
self.dt += dt
local direction = self.target_position - self.start_position.p
if direction.Magnitude > 10^-16 then
local velocity = direction.Unit * self.enemy_info.walkspeed
self.position = self.start_position.p + velocity * self.dt
end
end
-- receive an update from the server
function EnemyRender:UpdatePosition(packet)
local target = packet[1]
target = Vector3.new(target.X, 3, target.Y)
self.start_position.p = self.position
self.start_position.t = self.target_position
self.target_position = target
self.dt = 0
end
I feel like there’s a simple solution to this problem that I’m just not thinking of. If anybody has had experience with this, I’d really appreciate some guidance. If you need any more information, please feel free to ask.