Sine wave animation glitching out due to velocity change

So, I’m writing an fps framework, and for the walk cycle, I wanted to use sine waves to simulate the bobbing of the viewmodel. Now, it works perfectly fine, but the issue is when velocity of the character changes ( When the character turns / jumps ) the change causes the animation to go all glitchy and weird. Any feedback is appreciated on how to stop this from happening!

Code:

self.viewmodel.PrimaryPart.CFrame *= CFrame.new(math.sin(time() * (velocity.Magnitude/offsetX))/bobbleOffset, math.sin(time() * (velocity.Magnitude/offsetY))/bobbleOffset, 0) 
-- Velocity.Magnitude is the velocity of the character's HumanoidRootPart, bobbleOffset is set to 10 by default, and offsetX / offset Y are constant.

Issue:

Edit: I disabled character jumping, so the only issue now is climbing onto different height levels and turning, based on tests.

You can multiply the velocity with vector 1,0,1 to remove the effect of the y effect velocity.

Basically set y velocity to zero for calculation purposes.

1 Like

I have an idea… would lerping to the desired position every frame work?

Seems unnecessary. math.sin is already acting as a smoothing function. Like @dthecoolest said, you’re using the total velocity magnitude, which means vertical velocity (jumping) will impact this where you don’t want it to.

Instead of using Velocity.Magnitude, you could do:

local CharacterXZVelocity = (Velocity *Vector3.new(1, 0, 1)).Magnitude

which will remove the Y-component and only consider movement left/right and forward/backwards

3 Likes

What about turning? Turning seems to cause the view model to slightly glitch out before repositioning itself and running smoothly again

Strange, Velocity.Magnitude shouldn’t vary with rotation, and all of your other variables are constants.

By ‘glitch out’ do you mean similar behaviour to what was visible in the video where the animation just played out at a very high speed? Or does it look different?

1 Like

Yes, the same shaking effect when jumping. I’ll try and send a video later, but it might be tomorrow.

So, the problem seems to occur when the character starts moving diagonally. Like when you’re moving straight and you turn by holding A or D.

Very strange, normal character movement shouldn’t exceed walkspeed with multiple inputs:

Try printing Velocity.Magnitude and see what’s happening to it when you press both. If it’s exceeding the normal walkspeed then you could try using math.min() to ensure it never exceeds to maximum walkspeed value.

Weird, the velocity does seem to change with direction. Before that, though, is there a way I can make it so the animation doesn’t rigidly go back to its original offset when a character stops / stops moving?

You may want a tween or lerp for that, or when a character stops you continue running the function until math.sin(…) is approximately equal to 0 (i.e. your rest state)

Edit: Actually it might be -1 as you’re using *=. A lerp or tween will probably be easier and more reliable.

I tried tweening, but since this is all in a renderstepped loop the tween cant actually play. How should I handle this?

can’t you just detect Humanoid.Magnitude and if it’s not equal to zero, play animation? It’s not realistic that if you run faster your arms move faster too xd, it’s sometimes true but still, you can do:

if Humanoid.MoveDirection.Magnitude > 0 then -- something like this, idk if this property name is correct

    ArmsSpeed = 10
else
    ArmsSpeed = 0
end
1 Like

Also sorry for another reply, but it’s not weird, velocity is vector, every vector is composed of value and direction, soo it’s logical that if we change direction, vector changes too

.Magnitude converts it to a scalar. And the Roblox movement controller limits the velocity magnitude to the walkspeed.

That could work, but it still doesn’t fix the issue of stopping and starting to walk. Rn I’m updating a value of the object every RenderStepped, and I’m comparing it, but it still doesn’t seem to work sometimes. I think it’s because the tween doesn’t finish by the time the next frame renders

i didn’t saw magnitude here, my mistake

you can lerp it instead of tweening, for instance to 0.5 of distance, animation will be overwritten anyways, but it will look smoother

local isTransitioning;

RunService.RenderStepped:Connect(function()
    if (Velocity.Magnitude > 0) and (not isTransitioning) then
        self.viewmodel.PrimaryPart.CFrame *= CFrame.new(...)
    elseif (not isTransitioning) then
        isTransitioning = true;
        -- lerp loop, or tween and tween.Completed:Wait() the CFrame back to its default position
       isTransitioning = false;
    end
end)

Something along these lines might work using a debounce.

It will stop the animation until it’s fully reverted back to its default position. This may not be perfect, but it’s probably the easiest way if you insist on using *= on the CFrame for the animation. I’d personally probably just manually set the CFrame (self.viewmodel.PrimaryPart.CFrame = CFrame.new(...)) using a separate timing function based on (time()-startTime) as then you’ll have more control over its position at any one time.

Alternatively instead of using the Velocity.Magnitude value, just do a: isMoving = (Velocity.Magnitude > 1 and 1) or 0, then multiplying your value like that. Then when the player stops moving you can keep that isMoving value at 1 until: math.sin(time()...) is close to the point where the resultant viewmodel CFrame is in its default state.

Edit: Something along these lines might work better without changing a significant amount of your code:

local isTransitioning;
local startTime = time();

RunService.RenderStepped:Connect(function()
    if (Velocity.Magnitude > 0) and (not isTransitioning) then
        local animationTime = time()-startTime;
        self.viewmodel.PrimaryPart.CFrame *= CFrame.new(...) -- Use 'animationTime' instead of 'time()' here
    elseif (not isTransitioning) then
        isTransitioning = true;
        -- lerp loop, or tween and tween.Completed:Wait() the CFrame back to its default position, check somewhere to make sure it's not already in its default position (i.e. if the character is standing still for an extended period of time)
       isTransitioning = false;
       startTime = time();
    end
end)