Reproduction Steps
AnimDesync.rbxl (38.1 KB)
In Run Mode, there should be several giant characters dancing in synchronization.
In Play Mode, walk around and notice that the characters are not dancing in-sync.
Expected Behavior
As a player walks around the AnimDesync.rbxl
place, the giant characters should be dancing in synchronization. This is the case when viewing them from the server’s perspective.
Actual Behavior
From the client’s perspective, animation playback on each giant starts from the beginning once their model streams in. They are not synchronized with the server’s playback of the animation and therefore look desynchronized.
Workaround
It’s possible to leave a timestamp on the server indicating when an animation’s playback started [ via workspace:GetServerTimeNow()
] to manually correct the TimePosition.
However, there’s no universal clean way to do this without some coordination between the server and client. The AnimationTrack objects on the client are not the same AnimationTrack objects on the server, so you can’t assign attributes to the server AnimationTrack and read them on the client.
I came up with this narrow workaround that works for Animators playing a single looping AnimationTrack. I tag the AssemblyRootPart of the dummy being animated and have the client search for an Animator object to work with from there. The server will set a timestamp for when it started playing the animation, and the client will try to awkwardly synchronize from there:
--!strict
local CollectionService = game:GetService("CollectionService")
local function syncAnim(track: AnimationTrack, clockStart: number)
task.spawn(function ()
repeat task.wait() until track.Length > 0
local now = workspace:GetServerTimeNow()
track.TimePosition = (now - clockStart) % track.Length
end)
end
local function onFixAdded(rootPart: Instance)
if rootPart:IsA("BasePart") then
local character = rootPart:FindFirstAncestorOfClass("Model")
local animator: Animator? = if character
then character:FindFirstChildWhichIsA("Animator", true)
else nil
if animator then
local clockStart = animator:GetAttribute("ClockStart")
if typeof(clockStart) == "number" then
print("sync clock", animator:GetFullName())
for i, track: AnimationTrack in animator:GetPlayingAnimationTracks() do
syncAnim(track, clockStart)
end
end
end
end
end
local fixAdded = CollectionService:GetInstanceAddedSignal("AnimFix")
fixAdded:Connect(onFixAdded)
for i, part in CollectionService:GetTagged("AnimFix") do
task.spawn(onFixAdded, part)
end
In general this feels obnoxiously complicated. It would be comparatively easier to fix this issue on the engine side of things since there’s already some kind of network architecture under the hood synchronizing these things.
Issue Area: Engine
Issue Type: Display
Impact: Moderate
Frequency: Often