AnimationController Animation not immediately playing after cloning into Workspace

Hi everyone, i’ve been running into a quite interesting issue when building my First Person Gun/Item Framework

  1. What do you want to achieve? → Get rid of the flickering

  2. What is the issue? → Animation flickering straight after cloning the Viewmodel to the Camera instance

  3. What solutions have you tried so far?

  • Parenting the Viewmodel after loading the anims (does not work cause AnimationController’s cannot load anims outside of workspace)
  • Parenting the Item Model after loading the anims (does not work cause the arms/viewmodel still flickers)
  • Prevent rendering the first frame in the update function (still flickers)
  • Look for solution on the devforum - nothing that worked for my case

Let’s dive a little bit deeper into my specific Situation:
I am building a First Person Item/Gun Framework which utilizes Viewmodels inside the player Camera to display the items client sided in first person

I have a central ModuleScript that handles all the functionality, here is a simplified version of it:

-- This function gets called once when the equip signal is triggered
-- In my case from my custom hotbar
function module.equipItem(itemInstance : Instance)
	-- Unequip current item before equipping another
	-- this just resets some values and destroys the view model
	module.unequip()
	
	local item = itemInstance:Clone()

	-- Unrelated code Removed for simplicity
	
	-- Move Item to Viewmodel --
	local viewModel = ViewmodelTemplate:Clone()
	
	local handleMotor = viewModel:WaitForChild("HumanoidRootPart").Handle
	handleMotor.Part1 = handle
	item.Parent = viewModel
	
	-- Add Default Sounds --
	-- Unrelated code Removed for simplicity (Creates sound instances etc)
	
	-- Init Runtime Vars --
	-- currentItem and currentViewmodel are global vars here
	currentViewmodel = viewModel
	viewModel.Parent = Camera
	currentItem = item

	-- Unrelated code Removed for simplicity (Sets some unrelated vars for the current item)

	
	task.spawn(function()
		local equipAnimationTrack = nil
		local holdAnimationTrack = nil
		
		if (animations:FindFirstChild("Equip")) then
			equipAnimationTrack = viewModel.AnimationController.Animator:LoadAnimation(animations.Equip)
		end
		if (animations:FindFirstChild("Hold")) then
			holdAnimationTrack = viewModel.AnimationController.Animator:LoadAnimation(animations.Hold)
		end
		
		-- This is a thing i saw in a different devforum post
		-- https://devforum.roblox.com/t/animationtrackplay-should-immediately-initialize-the-first-animation-pose/2965721
		-- Does not change behaviour
		game:GetService("RunService").PreAnimation:Wait()
		
		equipSound:Play()
		if (equipAnimationTrack) then
			equipAnimationTrack:Play(0)
			wait(equipAnimationTrack.Length)
		end
		if (holdAnimationTrack) then
			local fade = 0
			if (equipAnimationTrack) then
				fade = 0.1
			end
			holdAnimationTrack:Play(fade)
		end

		-- Check if the item is still the one we started equipping
		-- This prevents setting ready=true if unequipped during the animation/wait
		if (currentItem ~= item) then return end
		itemIsReady = true
		
		-- Play Hold Animation --
		local holdAnimation = animations:FindFirstChild("Hold")
		if (holdAnimation) then
			local holdTrack = viewModel.AnimationController:LoadAnimation(holdAnimation)
			holdTrack:Play(0)
		end
	end)
end

-- Method gets called on RenderStepped
function module.updateClient(delta)
	local Character = LocalPlayer.Character
	
	if (not Character) then return end
	if (not currentViewmodel) then return end
	if (not currentItem) then return end
	
	-- Position the viewmodel to the camera --
	currentViewmodel.HumanoidRootPart.CFrame = game.Workspace.Camera.CFrame
	
	-- Unrelated code Removed for simplicity
end

To explain it a bit
When i call the equip method a new instance of the viewmodel gets created along with the item model
then the model gets motored with the viewmodel etc.
then the item gets parented to the viewmodel

after that a new task gets spawned (this is needed because i want to wait for the animations etc and the set the itemIsReady var)
inside that task the whole equip animation logic is handeled

Here is a video of the issue happening:


Basically when i equip a item the viewmodel gets cloned into the camera but the animation only plays one frame later - this means my viewmodel is in the idle position for one frame.

Can anyone think of a solution to resolve this issue?

1 Like

defered events moment
Blud you literally doing task.spawn
It only resumes on next update cycle (usually second frame)
Also please take in mind that creating a function every time will make Garbage Collector get a PTSD
Either just do

do
--code here
end

(Acording to your setup i see ^)
Or move to more raw method by using coroutines becouse they resume instantly (kinda)
Also please cache AnimationTracks becouse they are heavy to create every time

1 Like

Thank’s for the input on task.spawn()
I’m not rellay aware of luau lifecycles and stuff

That thing with creating functions and the Garbage Collector makes sense, i am trying to avoid that now.

Regarding to that animation caching i prob gotta rewrite some stuff so i don’t clone the Viewmodel every time - cause from my knowledge AnimationTracks are only valid per Animator instance → but i will try that

However, i’ve tried to get rid of the task.spawn and use coroutines (with a now “outsourced” function) → still flickers
Then i’ve tried to just directly call the equipAnimation function (which would make the equipItem function yield) but that also didn’t work

Can you think of any other possible problem sources/solutions when it comes to my issue?

1 Like

You should probably just cache the animation track from the :LoadAnimation and see if that fixes it, or you could set the position of the viewmodel somewhere else briefly till the animation is loaded.

Using task.spawn is not a problem here.

1 Like

task.defer does what you just explained
task.spawn and coroutines will create/resume the thread immediately on the same frame

1 Like

Okay i fixed it, here is what i did:

  1. Clone the Viewmodel when the Framework loads, not when i equip a item
  2. Parent the Viewmodel to workspace at an position invisible for the player
  3. Load all available Animation tracks for all available Items
  4. Parent the Viewmodel to ReplicatedStorage so its not chilling in workspace all the time
-- Initialize Viewmodel
	print("[FPS] Loading viewmodel")
	currentViewmodel = ViewmodelTemplate:Clone()
	currentViewmodel.Parent = game.Workspace
	
	-- Prelaod Animations --
	print("[FPS] Preloading animations")
	for _,item in pairs(ReplicatedStorage.Items:GetChildren()) do
		if (not item:IsA("Model")) then continue end
		if (not item:FindFirstChild("Animations")) then continue end

		animationCache[item.Name] = {}
		for _,animation in pairs(item.Animations:GetChildren()) do
			if (not animation:IsA("Animation")) then continue end
			ContentProvider:PreloadAsync({animation})
			animationCache[item.Name][animation.Name] = currentViewmodel.AnimationController.Animator:LoadAnimation(animation)
		end
	end

	currentViewmodel.Parent = ReplicatedStorage

When the item then gets equipped i play the animation track from the cache:

local equipAnimationTrack = animationCache[item.Name]["Equip"]
	local holdAnimationTrack = animationCache[item.Name]["Hold"]

	RunService.PreAnimation:Wait()
	currentViewmodel.Parent = Camera

	equipSound:Play()
	if (equipAnimationTrack) then
		equipAnimationTrack:Play(0)
		wait(equipAnimationTrack.Length)
	end
	
	if (holdAnimationTrack) then
		local fade = 0
		if (equipAnimationTrack) then
			fade = 0.1
		end
		holdAnimationTrack:Play(fade)
	end

But i wait for the PreAnimation part of the lifecyle before i parent the viewmodel to the camera
This way the model does not get parented before the animation plays.

This way all animation tracks are available at all times + the viewmodel can load the animations before the item is being equipped

1 Like

nope
Do research before saying anything please.
Coroutine part is true tho which cant be said to task.spawn

1 Like

yup you are right
image

local function e(a)
	print(`Task.spawn: {os.clock()-a}`)
end
local function a(a)
	print(`Coroutine: {os.clock()-a}`)
end
task.spawn(e,os.clock())
local t = os.clock()
coroutine.resume(coroutine.create(a),t)