AnimationTrack event behavior inconsistent between server and client

Hey developers. I recently encountered a strange bug where I’m not sure what’s going on. In my game, I need to listen to events (or signals) from AnimationTracks on both the server and client. To accomplish this, I play animations on the server, and on the client I listen to the AnimationPlayed event of the player’s humanoid to get a reference to the AnimationTrack on the client.
The problem is, even when the animation has stopped and the AnimationTrack has been destroyed on the client, future playbacks of the same animation still trigger the signals. What’s even stranger is that on the server, the signals no longer trigger after the track is destroyed. To explain myself better, here is a reproducible example:

On the server-side, I have the following script:

function playAnimation(animation, humanoid)
	local track = humanoid:LoadAnimation(animation)
	
	track:GetMarkerReachedSignal("activateForce"):Connect(function()
		print('Event received on server.')
	end)
	
	track.Stopped:Connect(function()
		track:Destroy()
	end)
	
	track:Play()
end

game.Players.PlayerAdded:Connect(function(player)
	-- wait for player's character to load
	wait(1)
	local character = player.Character
	if not character then character = player.CharacterAdded:Wait() end
	
	-- load and play the animation (on the server)
	local animation = game.ReplicatedStorage.Animation
	
	playAnimation(animation, character.Humanoid)
	
	-- after a while, play it again
	wait(2)
	
	playAnimation(animation, character.Humanoid)
end)

And on the client, I have the following script:

local character = game.Players.LocalPlayer.Character
local animation = game.ReplicatedStorage.Animation

character.Humanoid.AnimationPlayed:Connect(function(track)
	if track.Animation.AnimationId == animation.AnimationId then
		track:GetMarkerReachedSignal("activateForce"):Connect(function()
			print('Event received on client.')
		end)
		
		track.Stopped:Connect(function()
			track:Destroy()
		end)
	end
end)

Running this game yields the following console output:

Event received on server.
Event received on client.
Event received on server.
Event received on client. (x2)

Trying to play the animation multiple times leads to the event firing on the client every time the animation is played, regardless if it was on a new AnimationTrack. This leads me to believe that the track is possibly cached on the client, so the tracks in the callback to AnimationPlayed are actually the same, but I have no way of verifying this.
Any help is appreciated. I would like the event to fire only once on the client when I play the animation again.

Edit: Would like to add that even explicitly destroying the track on the client does not solve this problem.

1 Like

I dont know if this would solve your issue but animations are the character are suppose to be done from a local script not a server one and normally the animation played event is used on the server. BTW animations played on the character will replicate because its being controlled by a humanoid and not a animation controller, you can read up on it here: AnimationController | Documentation - Roblox Creator Hub . So the correct way to do it would be the opposite of what your doing.

**edited wrong link

1 Like

I know that animations replicate, that’s why I’m doing it the way I do it (otherwise this method would be impossible). I play the animation from the server because I need to listen to more game-critical events on the server (such as spawning a damage hitbox). I cannot rely on the client to do this because it would make it insecure.
The reason why people say to play animations on the client is because the client has no network latency between requesting to play the animation and actually playing the animation. In my case, this latency will be seen anyways because I need to do a server-sided check before allowing them to play the animation (I use animations in my game for spells).
My question is about why destroying the track on the client doesn’t actually destroy it and why the track continues to fire events.

1 Like

Im not sure about the track issue, but cant you use remote events to accomplish what your doing?

I did try that before. The timing of RemoteEvents are unpredictable when they arrive on the client. Using an AnimationTrack allows the timing of events to be accurate.

1 Like

I think it might be because of how your coding it on the server try doing this instead:

function playAnimation(animation, humanoid)
	local track = humanoid:LoadAnimation(animation)
	
	track:GetMarkerReachedSignal("activateForce"):Connect(function()
		print('Event received on server.')
	end)
	
	track:Play()
    track.Stopped:Wait()
    track:Destroy()
end

2 Likes

Can you explain why this would solve the problem? It looks to be doing the same thing, but I’ll try it anyway.

1 Like

I tested it, the code doesn’t solve the problem. I appreciate the help, but next time please explain why you are suggesting something and your reasoning behind it, it would help everyone a lot.

2 Likes

with my way its gonna wait until the animation is finished playing before you can play it again

2 Likes

I tested it, the problem still remains. The animation was less than 2 seconds so this was not the problem.

1 Like

Update: it seems that the AnimationTrack actually is cached on the client, so subsequent callbacks to the AnimationPlayed event return a reference to the same track.
It also seems that the track is only truly destroyed when the player dies (and their humanoid is destroyed).
It’s a bit strange this behavior isn’t documented and varies from the server, but it can be worked around.

4 Likes

I don’t think you mentioned how you fixed it, I am interested to know.

There is an Animator instance in the Humanoid which controls Animation. This is used for AnimationControllers as well and it allows the client to control animation on their character (since they have network ownership). Deleting it and calling :LoadAnimation from the server should reset it.

The problem you’re seeing could be a bug but I’m not 100% sure.

1 Like

For my game I kept a table on the client. On the callback to AnimationPlayed, I checked if the track was already in the table and avoided connecting the events twice if it already existed.