Viewmodel weird animation bug

So,
Its my first time making an FPS System and everything was working fine except once i got to equipping animation

the bug i encountered was that whenever the player equips a weapon it creates a new viewmodel and parents it to the camera
and then i play an equip animation

when i did that the viewmodel spawned in the default pose (no animation) and then the animation starts playing weirdly, my explanation of the situation is not that good

but i’ll show a video of my problem and the code

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Modules = ReplicatedStorage.Modules
local Assets = ReplicatedStorage.Assets

local Framework = Modules.Framework
local SharedFramework = Framework.Shared
local PackagesFramework = Framework.Packages

local Utility = require(SharedFramework.Utility)
local Spring = require(PackagesFramework.Spring)

local currentCamera = workspace.CurrentCamera

local recoilPositionSpring = Spring.new(Vector3.zero)
local recoilRotationSpring = Spring.new(Vector3.zero)

recoilPositionSpring.Speed = 10
recoilRotationSpring.Speed = 10

recoilPositionSpring.Damper = 0.8
recoilRotationSpring.Damper = 0.8

local bobTime = 0
local currentIntensity = 0 

local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()

local Viewmodels = {}
Viewmodels.Viewmodel = nil
Viewmodels.Connection = nil
Viewmodels.Animations = {}
Viewmodels.Sounds = {}

local function updateApperance(viewmodel:Model)
	local RightArm:MeshPart = viewmodel.RightArm
	local LeftArm:MeshPart = viewmodel.LeftArm
	
	RightArm.Color = character:FindFirstChild("Body Colors").RightArmColor3
	LeftArm.Color = character:FindFirstChild("Body Colors").LeftArmColor3
end

function Viewmodels:Equip(weapon : string)
	-- UnEquip Old Viewmodel so we dont cause any Duplication Errors
	if self.Viewmodel then self:UnEquip() end
	
	-- Ensure the weapon is valid
	if not weapon then return end
	assert(typeof(weapon) == "string", string.format("Expected (%s) got (%s)", "string", typeof(weapon)))
	
	-- Get the Viewmodel and Module from Utility Module
	local weaponViewmodel = Utility.GetViewmodel(weapon)
	if not weaponViewmodel then return end
	
	-- Clone the viewmodel since it only returns the model from ReplicatedStorage without Cloning
	local viewmodelClone = weaponViewmodel:Clone()
	
	-- Assign the Viewmodel and Module so we can get data about the viewmodel or weapon for customizations 
	self.Viewmodel = viewmodelClone
	self.Animator = viewmodelClone.Humanoid.Animator
	self.Module = Utility.GetWeaponModule(weapon)

	self.Viewmodel.Parent = currentCamera
	updateApperance(self.Viewmodel)
	-- PreLoad Animations and Sounds before parenting the viewmodel
	-- to ensure there's no flickering or weird animations or other weird bugs
	self:LoadAnimations()
	self:LoadSounds()

	-- play animation
	local equipAnim: AnimationTrack = self:GetAnimation("Equip")
	equipAnim:Play()
	equipAnim.Priority = Enum.AnimationPriority.Action4
	equipAnim:GetMarkerReachedSignal("End"):Connect(function()
		local idleAnim = self:GetAnimation("Idle")
		idleAnim:Play()
	end)
	
	-- Adjust Recoils
	recoilPositionSpring.Speed = self.Module.Recoil.Speed
	recoilRotationSpring.Speed = self.Module.Recoil.Speed
	
	recoilPositionSpring.Damper = self.Module.Recoil.Damper
	recoilRotationSpring.Damper = self.Module.Recoil.Damper
	
	-- Connect to RenderStepped to update the viewmodel position and other data if needed
	self.Connection = RunService.RenderStepped:Connect(function(dt)
		self:Update(dt)
	end)
	
	print("[Viewmodels] Equipped:", weapon)
end

function Viewmodels:UnEquip()
	local unequipAnim: AnimationTrack = self:GetAnimation("UnEquip")
	unequipAnim:Play()
	unequipAnim:GetMarkerReachedSignal("End"):Wait()
	
	-- Disconnect RenderStepped Connection
	if self.Connection then
		self.Connection:Disconnect()
		self.Connection = nil
	end
	
	-- Remove the Viewmodel
	if self.Viewmodel then
		self.Viewmodel:Destroy()
		self.Viewmodel = nil
	end
	
	-- Clear Animations Table and Module
	for _, anim in self.Animations do
		anim:Stop()
	end
	
	-- Clear Sounds Table
	for _, sound in self.Sounds do
		if sound.IsPlaying then
			task.spawn(function()
				sound.Ended:Wait()
				sound:Destroy()
			end)
			
			continue
		end
		sound:Destroy()
	end
	
	-- Cleanup
	self.Sounds = {}
	self.Animations = {}
	self.Animator = nil
	self.Module = nil

end

function Viewmodels:LoadSounds()
	if not self.Viewmodel then return end

	for name, id in self.Module.Sounds do
		local sound = Instance.new("Sound")
		sound.Name = name
		sound.SoundId = id
		sound.Parent = self.Viewmodel.PrimaryPart

		-- Save Sound
		self.Sounds[name] = sound
	end

	print("[Viewmodels] PreLoadded all Sounds:", self.Sounds)
end

function Viewmodels:LoadAnimations()
	if not self.Viewmodel then return end
	
	for name, id in self.Module.Animations do
		local anim = Instance.new("Animation")
		anim.Name = name
		anim.AnimationId = id
		anim.Parent = self.Viewmodel.PrimaryPart
		
		self.Animations[name] = self.Animator:LoadAnimation(anim)
	end

	print("[Viewmodels] PreLoadded all Animations:", self.Animations)
end

function Viewmodels:GetAnimation(animation : string)
	print('got animation:', animation)
	
	-- Check if Animation is already Loaded, Then Return it
	if self.Animations[animation] then
		print("[Viewmodels] Got Cached", animation, "Animation:", self.Animations[animation])
		return self.Animations[animation]
	end
	
	if not self.Viewmodel then return end
	assert(typeof(animation) == "string", string.format("Expected (%s) got (%s)", "string", typeof(animation)))
	
	-- Create Animation
	local anim: Animation = Instance.new("Animation")
	anim.AnimationId = self.Module.Animations[animation]
	anim.Parent = self.Viewmodel.PrimaryPart
	
	-- Load Animation and Save it in Animations Table so we can use it anytime later
	local track: AnimationTrack = self.Animator:LoadAnimation(anim)
	self.Animations[animation] = track
	
	-- Cleanup Animation because we dont really need it
	-- because the animation is loaded and we got the track now we can play animations
	-- even if the animation itself got destroyed.
        -- anim:Destroy()
	
	print("[Viewmodels] Created New", animation , "Animation")
	return track
end

function Viewmodels:PlaySound(soundName : string)
	if not self.Sounds[soundName] then return end
	assert(typeof(soundName) == "string", string.format("Expected (%s) got (%s)", "string", typeof(soundName)))
	
	-- Play Sound
	self.Sounds[soundName]:Play()
end

function Viewmodels:AddRecoil(position, rotation)
	recoilPositionSpring.Impulse(recoilPositionSpring, position)
	recoilRotationSpring.Impulse(recoilRotationSpring, rotation)
end

function Viewmodels:Update(dt)
	if not self.Viewmodel then return end

	local moving = Utility.IsPlayerMoving()

	-- 1. Smooth Intensity Interpolation
	-- Target is 1 if moving, 0 if standing still.
	local targetIntensity = moving and 1 or 0
	-- Lerp the intensity so the gun smoothly returns to center when stopping
	currentIntensity = currentIntensity + (targetIntensity - currentIntensity) * math.min(dt * 10, 1)

	-- 2. Continuous Time
	-- Time always moves, but slightly faster when walking
	local speedMultiplier = moving and 8 or 2
	bobTime += dt * speedMultiplier

	-- 3. Idle Breathing (Always active, ultra-minimal)
	local breathX = math.sin(bobTime * 0.5) * 0.01
	local breathY = math.cos(bobTime * 0.5) * 0.025

	-- 4. Walk Bobbing (Scaled by currentIntensity)
	-- Using sin(time) for X and cos(time * 2) for Y creates a perfectly smooth Figure-8
	local walkX = math.sin(bobTime) * 0.035 * currentIntensity
	local walkY = math.cos(bobTime * 2) * 0.035 * currentIntensity
	local walkRoll = math.sin(bobTime) * math.rad(0.5) * currentIntensity

	-- Combine the motions
	local finalX = breathX + walkX
	local finalY = breathY + walkY
	local finalRoll = walkRoll
	
	-- Recoil
	local recoilPos = recoilPositionSpring.Position
	local recoilRot = recoilRotationSpring.Position
	
	-- Final CFrames
	local bobCF = CFrame.new(finalX, finalY, 0) * CFrame.Angles(0, 0, finalRoll)
	local recoilCF = CFrame.new(recoilPos) * CFrame.Angles(recoilRot.X, recoilRot.Y, recoilRot.Z)

	self.Viewmodel:PivotTo(currentCamera.CFrame * bobCF * recoilCF )
end

return Viewmodels

Hi, just asking because I’m curious if you’ve tried making/creating a “equiping a weapon” animation?
As much as I know, the issue can be related with a few things, at first you can just create/add the equip anim(smooth) or check if anim is playing after the weapon is equiped in anim or just in the game (if something from this is true, due to it the look of “buggy start of animation” playing looks like that.

Hi, i’m pretty sure this is an issue how you first setup your viewmodels. If the arms are facing forward to the HRP, that’ll small bug will show. Instead have the arms going downward so when you load the viewmodel onto the camera the first thing you will see won’t be the arms.

i tried that but doesn’t work the arms are now below the camera and cant see it

i made the equip animation and it looks fine the issue is that when i copy the viewmodel and parent it to the camera and then play the equip animation it appears with extended arms and the weapon between the arms with no equip animations or anything in a split second then and then it starts playing the equip animation

Oh, okay, then you can try a few different thing if you have not tried it before.

1) Change Animation Priority Before Playing

-- may cause some problems during playback
equipAnim:Play()
equipAnim.Priority = Enum.AnimationPriority.Action4


-- for more practicable use, i think so (setting at first before playing it)
equipAnim.Priority = Enum.AnimationPriority.Action4
equipAnim:Play()

2) Use Caching for save [used anims] to make them ready to use in any play-time. Update your LoadAnimations function to handle priorities natively, and adjust the Equip order.


--function Viewmodels:LoadAnimations()
--	if not self.Viewmodel then return end
--	
--	for name, id in self.Module.Animations do
--		local anim = Instance.new("Animation")
--		anim.Name = name
--		anim.AnimationId = id
--		anim.Parent = self.Viewmodel.PrimaryPart

		local track = self.Animator:LoadAnimation(anim)

		-- so there they cached and can be used (set as you need)
		if name == "Equip" then
			track.Priority = Enum.AnimationPriority.Action4
		elseif name == "Idle" then
			track.Priority = Enum.AnimationPriority.Idle
		else
			track.Priority = Enum.AnimationPriority.Action
		end

		self.Animations[name] = track
--	end
--
--	print("[Viewmodels] PreLoadded all Animations:", self.Animations)
--end

And if we are doing this thing, you should update your Viewmodels:Equip .

--function Viewmodels:Equip(weapon : string)
--	-- UnEquip Old Viewmodel so we dont cause any Duplication Errors
--	if self.Viewmodel then self:UnEquip() end

--	-- Ensure the weapon is valid
--	if not weapon then return end
--	assert(typeof(weapon) == "string", string.format("Expected (%s) got (%s)", "string", typeof(weapon)))

--	-- Get the Viewmodel and Module from Utility Module
--	local weaponViewmodel = Utility.GetViewmodel(weapon)
--	if not weaponViewmodel then return end

--	-- Clone the viewmodel since it only returns the model from ReplicatedStorage without Cloning
--	local viewmodelClone = weaponViewmodel:Clone()

--	-- Assign the Viewmodel and Module so we can get data about the viewmodel or weapon for customizations 
--	self.Viewmodel = viewmodelClone
--	self.Animator = viewmodelClone.Humanoid.Animator
--	self.Module = Utility.GetWeaponModule(weapon)

--	self.Viewmodel.Parent = currentCamera
--	updateApperance(self.Viewmodel)
--	-- PreLoad Animations and Sounds before parenting the viewmodel
--	-- to ensure there's no flickering or weird animations or other weird bugs
--	self:LoadAnimations()
--	self:LoadSounds()


	-- we gives a small pause to make sure everything is initialized
	task.wait()

	-- check if the player switch weapons during the pause
	if not self.Viewmodel or viewmodelClone ~= self.Viewmodel then return end

	-- locking the postion to camera before the animation starts
	self.Connection = RunService.RenderStepped:Connect(function(dt)
		self:Update(dt)
	end)

--	-- play animation
--	local equipAnim: AnimationTrack = self:GetAnimation("Equip")
--	equipAnim:Play()

	equipAnim:GetMarkerReachedSignal("End"):Connect(function()
		-- be sure we are holding the same weapon
		if self.Viewmodel == viewmodelClone then
			local idleAnim = self:GetAnimation("Idle")
			idleAnim:Play()
		end
	end)

--	-- Adjust Recoils
--	recoilPositionSpring.Speed = self.Module.Recoil.Speed
--	recoilRotationSpring.Speed = self.Module.Recoil.Speed

--	recoilPositionSpring.Damper = self.Module.Recoil.Damper
--	recoilRotationSpring.Damper = self.Module.Recoil.Damper

--	print("[Viewmodels] Equipped:", weapon)
--end

And this should help solve this problem, if not then I have no idea what the problem really is and how this script is built. I did my best

I was just saying instead of setting up the viewmodel like in Image 1 below, set it up like Image 2. And redo your animations. This is how I fixed that bug (I had it before), so when you load an animation the viewmodel would go from the standing (no animation) to the actual animation.