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.