Animation events firing dozens or hundreds of times when they should only fire once (GetMarkerReachedSignal)

Reproduction Steps

1. Have Player2 call GetMarkerReachedSignal on Player1’s humanoid and print something to the output when the event fires
2. Have Player1 play a curves animation with a couple animation events on their humanoid
3. Observe how the event will fire dozens of times with a single playthrough of the non-looping animation despite the event only being in the animation once. My high score is 122 fires.

My setup:
Included is a Studio reproduction file:
ANIMATION_EVENTDUPLICATION_RECREATE.rbxl (38.2 KB)

There are two scripts; “ToolScript” in a Tool in StarterPack to play the animation, and “AnimationEventsScript” in StarterPlayerScripts to call GetMarkerReachedSignal on each player’s character.

ToolScript:

local Tool = script:FindFirstAncestorOfClass("Tool")

local fireAnimation = game.ReplicatedStorage:WaitForChild("TESTANIMATION") --Instance.new("Animation")
--fireAnimation.AnimationId = "rbxassetid://10436236672" --10398262856"

Tool.Activated:Connect(function()
	local humanoid : Humanoid = Tool.Parent:FindFirstChildOfClass("Humanoid")
	
	local fireTrack = humanoid:LoadAnimation(fireAnimation)
	fireTrack:Play(0)
end)

AnimationEventsScript:

function connectToPlayer(player : Player)
	local function connectToCharacter(character : Model)
		local hummanoid : Humanoid = character:WaitForChild("Humanoid")
		
		local function markerReached(parameter : string)
			print("    |marker reached with parameter", parameter)
		end
		
		hummanoid.AnimationPlayed:Connect(function(animTrack : AnimationTrack)
			print(character, " | ", animTrack.Animation)
			animTrack:GetMarkerReachedSignal("EVENT"):Connect(markerReached)
		end)
	end
	player.CharacterAdded:Connect(connectToCharacter)
	if player.Character then connectToCharacter(player.Character) end
end
game.Players.PlayerAdded:Connect(connectToPlayer)
for i, player : Player in ipairs(game.Players:GetPlayers()) do
	connectToPlayer(player)
end

Finally, there is an Animation object in ReplicatedStorage called “TESTANIMATION” with assetId “rbxassetid://10436236672”.
This animation has two animation events close to the start of the animation. Both are named “EVENT”, and one has the parameter “PARAMETER” while the other has the parameter “OTHER PARAMETER”

Here is a screenshot of the hierarchy:
image

Once set up, go to the test tab of Roblox Studio, and start a local server with 2 clients connected. Then, click with the tool selected on one client and watch the output of the second client. Notice how the event will fire multiple times on each client but on the remote client it will fire dozens of times as opposed to just a couple (neither are expected behavior)

Expected Behavior

When there is an animation event within an animation and I connect to GetMarkerReachedSignal, I expect that event to fire once for every occurrence of the event within the animation. Not 122 times.


  12:50:19.722  Noobot9k  |  TESTANIMATION  -  Client - AnimationEvents:13
  12:50:19.726      |marker reached with parameter PARAMETER  -  Client - AnimationEvents:9
  12:50:19.757      |marker reached with parameter OTHER PARAMETER  -  Client - AnimationEvents:9

.

Actual Behavior

The animation event fires a couple times when observing from the same client who owns the humanoid and played the animation, and occurs dozens or hundreds of times when observing from another client.

  12:51:54.413  Player1  |  Animation  -  Client - AnimationEvents:13
  12:51:54.414   ▶     |marker reached with parameter PARAMETER (x61)  -  Client - AnimationEvents:9
  12:51:54.417   ▶     |marker reached with parameter OTHER PARAMETER (x122)  -  Client - AnimationEvents:9

This is just one playthrough of the animation.

Workaround

Only act on animation events once per animation. This is a large limitation as you can’t - for example - have an animation play multiple footstep sounds in a single playthrough of the animation.

See this comment for a better workaround.
Animation events firing dozens or hundreds of times when they should only fire once (GetMarkerReachedSignal) - #5 by Noobot9k

Issue Area: Engine
Issue Type: Other
Impact: High
Frequency: Constantly
Date First Experienced: 2022-07-21 11:07:00 (-06:00)

6 Likes

Thanks for the detailed report, we’ll look into this. Also were able to confirm that the curve animation crash you reported earlier was fixed in version 539.

3 Likes

While we work on this, a workaround is to manually disconnect the MarkerReachedSignal connection when the animation is stopped. This issue would also cause the stopped connection to leak so it’s a good idea to manually disconnect that as well:

		hummanoid.AnimationPlayed:Connect(function(animTrack : AnimationTrack)
			print(character, " | ", animTrack.Animation)
			local markerConnection = animTrack:GetMarkerReachedSignal("EVENT"):Connect(markerReached)
			local stoppedConnection
			stoppedConnection = animTrack.Stopped:Connect(function()
				markerConnection:Disconnect()
				stoppedConnection:Disconnect()
			end)
		end)

3 Likes

Apologies for the long delay in following up on this. This is an unfortunate side effect of how we replicate animations between clients. It’s definitely a bug, but because it has worked this way for so long fixing it would introduce potentially game-breaking issues in existing experiences, so we are not planning to address it at this time. If the workaround posted above doesn’t solve the problem for you let us know and we can try and come up with another solution.

2 Likes

Your workaround only seems to work if you carefully play the animation one at a time and never have an animation interrupt itself (load and play a new AnimationTrack with the same AnimationId again before it has finished playing the first time.)

In my provided example place if I apply your solution and spam click with the tool everything will look fine on the owner player but for the observing player every time the animation plays each event will fire an additional time more than it should. This means if you interrupt the playback of the animation with itself 20 times in a row then on that final playback when it plays through the animation each event will fire 20 times when they should only fire once.

It seems if an animation is interrupted by itself before AnimationTrack.Stopped can fire then the MarkerReachedSignal will fire on the first AnimationTrack even though it’s the second animation track that has reached that marker. This means when just one AnimationTrack with that AnimationId reaches that marker, all the tracks with the same AnimationId will fire their MarkerReachedSingals.

I also notice that, on the observing client, the AnimationTrack.Stopped event never fires until all AnimationTracks with that AnimationId have stopped playing on that animator. This means if I click 20 times and play the animation 20 times, even though the time it took me to click that many times was far longer than the length of the animation, the Stopped event won’t fire even once until every instance of that animation has stopped playing and then the Stopped event will fire for all of them all at once.

Here’s my setup now. I’m using the same example place provided above but with the following changes:

I added a disabled LocalScript to StarterPlayer.StarterCharacterScripts named Animate to disable character animation.

I changed the code in the AnimationEvents script to the following: (using your solution)

function connectToPlayer(player : Player)
	local function connectToCharacter(character : Model)
		local hummanoid : Humanoid = character:WaitForChild("Humanoid")
		
		local function markerReached(parameter : string)
			print("    |marker reached with parameter", parameter)
		end
		
		hummanoid.AnimationPlayed:Connect(function(animTrack : AnimationTrack)
			print(character, " | ", animTrack.Animation)
			local markerConnection = animTrack:GetMarkerReachedSignal("EVENT"):Connect(markerReached)
			local stoppedConnection
			stoppedConnection = animTrack.Stopped:Connect(function()
				print("Animation Stopped")
				markerConnection:Disconnect()
				stoppedConnection:Disconnect()
			end)
		end)
	end
	player.CharacterAdded:Connect(connectToCharacter)
	if player.Character then connectToCharacter(player.Character) end
end
game.Players.PlayerAdded:Connect(connectToPlayer)
for i, player : Player in ipairs(game.Players:GetPlayers()) do
	connectToPlayer(player)
end

With this example, try spam-clicking forever on one client. On the client performing the action, you should see various messages denoting when the animation is played, when animation events are reached, and when the animation stops, all scrambled together. You should also see a few Stopped events pile up at the end, but only as many as there were copies of the same animation playing at once at the time you stopped spam-clicking. With the length of the example animation I’m suing this shouldn’t be more than maybe 4 or 5. This is all expected behavior.

...
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
  Animation Stopped
      |marker reached with parameter OTHER PARAMETER
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
  Animation Stopped
      |marker reached with parameter OTHER PARAMETER
  Animation Stopped
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
      |marker reached with parameter OTHER PARAMETER
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
  Animation Stopped
      |marker reached with parameter OTHER PARAMETER
  Animation Stopped
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
      |marker reached with parameter OTHER PARAMETER
  Animation Stopped
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
      |marker reached with parameter OTHER PARAMETER
  Animation Stopped
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
      |marker reached with parameter OTHER PARAMETER
  Animation Stopped
  Player1  |  TESTANIMATION
      |marker reached with parameter PARAMETER
      |marker reached with parameter OTHER PARAMETER
   ▶ Animation Stopped (x4)

On the observing client however, you will never see “Animation Stopped” in the console, despite the fact that as you go for longer and longer that animation will definitely have had time to finish many times over.

It’s only when you stop spamming the tool on the owner client that the observer client will get a flood of Stopped events all at once. If you were just spamming the mouse for several minutes you could rack up hundreds of stop events that will all fire at once instead of as each animation finishes.

...
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x166)
   ▶     |marker reached with parameter OTHER PARAMETER (x166)
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x167)
   ▶     |marker reached with parameter OTHER PARAMETER (x167)
  Player1  |  Animation
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x169)
   ▶     |marker reached with parameter OTHER PARAMETER (x169)
  Player1  |  Animation
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x171)
   ▶     |marker reached with parameter OTHER PARAMETER (x171)
  Player1  |  Animation
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x173)
   ▶     |marker reached with parameter OTHER PARAMETER (x173)
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x174)
   ▶     |marker reached with parameter OTHER PARAMETER (x174)
  Player1  |  Animation
   ▶     |marker reached with parameter PARAMETER (x175)
   ▶     |marker reached with parameter OTHER PARAMETER (x175)
   ▶ Animation Stopped (x175)

Every time I clicked, from the observing client the animation would start playing, reach each marker signal/event, fire the events n number of times instead of just once, where n is the current number of times I’ve interrupted the animation in a row without giving it a rest, and not fire the stop event when the animation has fnished. When I stop clicking and allow the final animation to finish then suddenly the stop event will fire n number of times all at once.

This means if I click 5 times quickly, then each time the animation plays for the observing client each marker signal/event will fire 1 time, 2 times, 3 times, 4 times, and then 5 times in a single playback of the animation, having fired each event 15 times total when each event should have only fired 5 times, once ber playback of the animation. And instead of a couple of Stopped events firing somewhere in the middle of all this as the first and second animations finish while I’m still clicking the fourth and fifth times, no Stopped events will fire until all 5 playbacks of the animation have finished and then the Stopped event will fire 5 times all at once.

As you can see, this workaround doesn’t really work that well. What might be better is to disconnect all events related to an animation track when another animation of that same AnimationId is played.

To do this I updated the AnimationEvents script to contain the following code:

function connectToPlayer(player : Player)
	local function connectToCharacter(character : Model)
		local hummanoid : Humanoid = character:WaitForChild("Humanoid")
		
		local playingAnimationTracksById = {}
		local eventsConnectedToAnimationId = {}
		
		local function disconnectEvents(animId : string)
			if not eventsConnectedToAnimationId[animId] then return end
			
			for i, event in ipairs(eventsConnectedToAnimationId[animId]) do
				event:Disconnect()
			end
		end
		
		local function markerReached(parameter : string)
			print("    |marker reached with parameter", parameter)
		end
		
		hummanoid.AnimationPlayed:Connect(function(animTrack : AnimationTrack)
			local animId = animTrack.Animation.AnimationId
			local existingTrack = playingAnimationTracksById[animId]
			if not eventsConnectedToAnimationId[animId] then
				eventsConnectedToAnimationId[animId] = {} 
			end
			if existingTrack then disconnectEvents(animId) end
			playingAnimationTracksById[animId] = animTrack
			
			print(character, " | ", animTrack.Animation)
			table.insert(eventsConnectedToAnimationId[animId], animTrack:GetMarkerReachedSignal("EVENT"):Connect(markerReached))
			
			table.insert(eventsConnectedToAnimationId[animId], animTrack.Stopped:Connect(function()
				print("Animation Stopped")
				disconnectEvents(animId)
			end))
		end)
	end
	player.CharacterAdded:Connect(connectToCharacter)
	if player.Character then connectToCharacter(player.Character) end
end
game.Players.PlayerAdded:Connect(connectToPlayer)
for i, player : Player in ipairs(game.Players:GetPlayers()) do
	connectToPlayer(player)
end

This is still a sub-optimal workaround though because if an animation is played over itself it will prevent the first playback of the animation from firing the stopped event or any markers that would occur after the part of the animation it was at when the second instance of the animation started playing, even though the first instance is still playing and Roblox is blending between the two, but it at least prevents duplicate firing of events.

This workaround is also getting very convoluted and complicated with the size of the script ballooning when it should only need to be half the size.

With further experimentation, I’ve confirmed that the Stopped event does fire when expected if the animation is interrupted by an AnimationTrack with a different AnimationId so this workaround does seem to work well enough, but it’s very suboptimal. I can’t imagine any games relying on this broken functionality. It’s taken me all this time just to get an understanding of what is going on. I think it’s more likely developers found that the system is broken and instead of handling client-side events with animation marker signals, they would handle all the events on the owner client and use BindableEvents to propagate to other clients, which would cause effects to often end up out of sync with the animations but there was no better option. This is at least what I observe in games. Effects tied to animations playing out of sync.

I will use this workaround if I decide I need to use MarkerReachedSignals, though to be honest I’ve been avoiding them with new projects because they’re so unpredictable. I don’t think it’s acceptable for Roblox to leave this feature in this state. No developer is going to ever understand how it works. Just that it doesn’t work at all.

3 Likes

Exactly this! The animation system is tough to figure out already, especially when you aren’t sure if behavior is intended or if it’s one of the many bugs. We do footstep sounds using animation markers on other player’s Animators, so this makes me wonder if we have unknown memory leaks / bugs.

Leaving bugs in the system because 1 or 2 games might rely on it is a really bad way to plan for the future. This would be trivial for them to fix, but save hundreds if not thousands of develop hours wondering why these events don’t work.

At the very least the documentation should be updated to specify that these are broken with no planned fix, if that is Roblox’s position.

4 Likes

Can you give examples of cases where this behavior is being relied on? Or a number of places that somehow do rely on it? This seems super esoteric and not something anyone could ever come to rely on even by accident. I’m skeptical fixing it would affect anyone’s workarounds.

Not cool to leave all these obscure insane bugs in the animation system.

3 Likes