Arriving AnimationTrack playback does not synchronize with the server

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

25 Likes

Thanks for the report. I filed a ticket to our internal database.

2 Likes