Root motion looses momentum when changing animations

Godot Version

4.7 stable

Question

Hello, I have root motion project where i set character velocity from animation root motion (bone).

Problem arises when my statemachine changing states from one animation to another, the character velocity goes back to 0.0 instead of continuing from previous root velocity. I have blending enabled with values, i have tested without animation blending, same result.

What is odd is that in one case the velocity resets, in another case it works as expected.

Here is captured data that i printed out.

State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
State: walk_start Velocity: 1.128 RM: (0.0, 0.0, 0.000376)
Changing state to: walk
State: walk Velocity: 0.000 RM: (0.0, 0.0, 0.0)
State: walk Velocity: 0.000 RM: (0.0, 0.0, 0.0)
State: walk Velocity: 0.000 RM: (0.0, 0.0, 0.0)
State: walk Velocity: 0.004 RM: (0.0, 0.0, 0.000001)
State: walk Velocity: 0.008 RM: (0.0, 0.0, 0.000003)
State: walk Velocity: 0.012 RM: (0.0, 0.0, 0.000004)
State: walk Velocity: 0.016 RM: (0.0, 0.0, 0.000005)
State: walk Velocity: 0.020 RM: (0.0, 0.0, 0.000007)
State: walk Velocity: 0.024 RM: (0.0, 0.0, 0.000008)
State: walk Velocity: 0.028 RM: (0.0, 0.0, 0.000009)
State: walk Velocity: 0.032 RM: (0.0, 0.0, 0.000011)


this happens in character process function.


func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	var input_dir := Input.get_vector("k_left", "k_right", "k_forward", "k_backward")
	direction = (player_camera.transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	get_root_velocity(delta)
	state_machine.physics_update(delta)
	print("State: ", state_machine.current_state_name, " Velocity: %.3f" % velocity.length(), " RM: ",animation_player.get_root_motion_position())
	
	

Here is a demo video

In the mid playback i have slowed down engine to show that velocity resets.

And when that happens you get that nasty bump feel/look on your character which im trying to avoid.

Now the problem is when i go from Walk_start to Walking states/animations

The problem does not exist when going from walking to running states/animation - here velocity continues to increase .

Here is debug printout with that:

State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
Changing state to: run_16
State: run_16 Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: run_16 Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: run_16 Velocity: 1.745 RM: (0.0, 0.0, 0.029086)
State: run_16 Velocity: 2.311 RM: (0.0, 0.0, 0.038518)
State: run_16 Velocity: 2.877 RM: (0.0, 0.0, 0.047951)
State: run_16 Velocity: 3.443 RM: (0.0, 0.0, 0.057383)
State: run_16 Velocity: 4.009 RM: (0.0, 0.0, 0.066814)
State: run_16 Velocity: 4.009 RM: (0.0, 0.0, 0.066815)
State: run_16 Velocity: 4.009 RM: (0.0, 0.0, 0.066815)
State: run_16 Velocity: 4.009 RM: (0.0, 0.0, 0.066815)

Velocity gets set the same way across all states with:


	character.set_quaternion(character.get_quaternion() * character.animation_player.get_root_motion_rotation())
		character.velocity = (character.animation_player.get_root_motion_rotation_accumulator().inverse() * character.get_quaternion()) * character.animation_player.get_root_motion_position() / _delta
	

Inside states physics update function.

Animations were done in blender with rigify rig by hand.

Root animations are all linear and loopable.

Not sure what else could be the problem.

PC is windows11 nvidia 4070s, 60hz monitor.

EDIT:

Forgot to mention how many frames are animations:

Walk = 32

Run = 16

Walk_start = 8

That looks like a problem with the animations. You say that walk_start to walk causes the velocity to reset to zero, but not when going from walking to running?

exactly

Well i dont know what else can i do in the animation as it loops, and you saw blender outputs the line goes straight - constant before and after the keyed scene. This approach was done on all of my animations including run.

How else should one set the animation in blender or maybe what exactly am i doing wrong?

I keyed in blender frame 1 and frame (end) and inbetween the transformation of the root is linear.

When played separately single animation it loops without dropping velocity, so this suggests that its good.

Im no expert, but am desperate

Are those lines the F-curves?

Basically the root bone just defines the location for the animation at each keyframe.

For walking, the animation should hop back to the start at the end of the sequence.

Can you post a video or GIF of each of the animations running in blender?

The problem is probably due to the animation blend between walk_start and walk, I think the animation might not synchronize properly, or perhaps the blend takes the walk start animation back to the start and the stationary idle pose at the beginning overwrites the motion to walk at the start of the blend, so perhaps you should try snapping to the walk animation and using timing to switch when the footstep is at the right position.

So the simple test is - do not blend the animations.

i think i have already stated, that i tried with animation blendig and without, its the same result, when animation/state switch the velocity goes to 0.0 and then ramps up again.

As for blender animation, you saw the screenshots, they are linear - straight line for the root bone translation in one direction - straight.

Here is a video.

One thing that i notice but not sure what it means, the blender animation keys - sometimes they are circular, sometimes square. maybe that is something that messes it up? I dont know why are they different

Ok, you should try editing the walk_start animation to have more keyframes, maybe a complete half cycle or a couple of steps, try both.

Keyframe Shape Meanings :

  • Diamond: Standard/Free handle type or normal keyframe.
  • Circle / Round: Automatic or auto-clamped handle type.
  • Square: Vector handle type (creates linear, sharp transitions).
  • Rhombus: Aligned handle type for smooth transitions.
  • Cyan Diamond (Small): Breakdown state used for transition poses.
  • Dark Gray/Orange Diamond: Moving hold keyframe adding minor motion around a pose.
  • Pink Diamond (Big): Extreme keyframe marking a primary pose peak.
  • Green Diamond (Tiny): Jitter or baked filler keyframe

i dont see how that would do anything as when you import it into godot, godot reframes all the interpolated keys (bakes them in) or something..

Another thing i was thinking maybe not enough frames per start animation? i have 5 frames maybe for a 60fps thats too little as it translates to sub 0.2s or something, but still it shouldnt go back to 0.0 velocity..

Oh i see what you mean.. to have the walk_start not end like i have it now, but animate it a half step or a full step or something then transition to normal walk?

But then we get into situations where you would press movement very rapidly and character would not inch forward but would make full/half steps based on animation and thus you kinda lose precision like for example if you want to get near ledge or something.

I wonder how other 3D games does it with root motion..

I know some games do simple animations with basic transitions and no root motion. I kinda want to have not as detailed as red dead redemption/gta5 type animations but similar, so that it feels like locomotion.

I have made it simple to implement necessary states/inbetween states if needed and they are easy to hook up. Im just stuck with this transition.. If its a godot bug then all my efforts are down the drain in im back to sq1.

Any suggestion?

Yeah, make a walk_stop animation that does several steps and blend it fast in the situation where the user taps the key then releases. Maybe have a special case for that.

You blend in using time seek nodes

So you just blend at the time that lines up with the current footstep.

The walk_start animations on mixamo dont have root motions built in, they can be edited in blender, and they often have several paces.

I have seen a walk start animation in Unreal that has as many keyframes as the walk animation and they handle it without any issues.

I’m sure a lot of triple A games use root motion to measure position -e.g. Assassins creed, Spiderman, basically any third person game.

I dont really use time seek nodes for anything, and i am usually happy with the blend from idle to walk instead of walk_start.

Just imagine the puzzle has a simple solution … You play walk_start when the walk starts, then it directly switches to walk if the button is held. If the key is released you blend to the end of walk stop, and quickly. The walk_start can be a long animation.

well another problem is when you have just idle > walk states, with /witout blending, when you rapidly tap the forward button you create something like one legged gallop, which looks odd.. and not very gamery.. so thats why i wanted to have walk start animation to have that first step, thats why it has so little frames. Im not worried about animation looks atm, im focusing on this root motion velocity reset, and this is stopping me from progressing.

also im not gonna use mixamo because of garbage licenses they try to set you up for, which is not what i have in mind for my projects. Also they ask for account registration and etc etc..

i had a setup where i had left right footstep triggers where when you press jump the animation would wait for any foot to be on ground then play leap animation based whic foot was the leap foot.. but that system didnt use root motion and i did some mathematical formulations to calculate character velocity to not have sliding feet effect.

Now back to root motion, it is all sloved as long as i do a good animation job in blender and no need for doobious calculations.. much simpler.

might look into it, and then i will create different animations.

Locomotion is crazy.. you have to have hundreds of animations to make it look real and feel good when controlling.. many small cases, like we discusses - case for rapid button press, etc.. sheesh..

Hi!

I didn’t read the whole thread but i had the same problem and found no clear solution.
I used a statemachine and “traveled” to the animation. As temporary solution i saved the last root velocity and applied it if the velocity was zero while i was in walking/running state.

But with godot 4.7 came the function to sync your animations in a blendspace1D/2D, which i used from there on, because the four states are different in length and i wanted the feet’s synced left/right while walk/run and it solved this Problem by chance.

Hope it helps somehow

Yeah I think you have the answer. I was trying to suggest travel to the animation when I said to play it straight after. The new feature in 4.7 is something I havent looked at - there was a sync checkbox in thr blendspace anyway but it didnt always work for different length animations pre-4.7.

Can you expand? What are blend spaces?

What am i looking at in your screen shot?

In my case i use animationplayer. Very simple and versitile.

I use the AnimationTree to handle the animations, there you can use different AnimationNode types. One type is AnimationNodeBlendSpace2D.

How do you use the animationplayer to say its versatile? In my memory it’s very limited and i kinda hated godot to force me to use an AnimationTree. I wanted to code everything, like i could in unity and not to draw lines :smiley:

I don’t know how could you miss this in the documentation, but its as simple as animation_player.play(“my_animation”, blend_ammount)

For me its simpler and i can set this in each state as per my states. So whenever a state is active, then play this animation.

And you don’t need no animation trees and all that nonsense that you kinda set it up with spaghetti connections and after few months you forget how it all works and its a pain to rework.. in code its much simpler. You can read through pretty fast and understand what it was supposed to do.

Seems like i found a hunch, to the culprit of my demise.

So the walk_start animation trigger switch for next state is animation_finished signal. that unblocks the state decision maker and evaluates what to do next (what state next).

Now in this process the velocity gets set to 0.0 no matter how long your animation is, and until the next animation starts playing, essentially your player is stuck.

Now this solution i have tried before, to add a function call animation track before animation finished but given that previous walk_start animation was only 5 frames, the rounding error and approximation and blending all did me no favors. So it wasnt working as i expected it to work and were giving me false positives resetting the velocity to zero.

I reanimated the walk_start to be very long with whole step cycle and then added unblock state to then change the state and animation.

Ofc this was quickily done so i didnt take my time to match the root vel speeds to two animations. But here you can see that it does not go to zero, instead it blends like it should.

State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020912)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020912)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020912)
State: walk_start Velocity: 1.255 RM: (0.0, 0.0, 0.020912)
Unblocked state
Changing state to: walk
State: walk Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk Velocity: 1.255 RM: (0.0, 0.0, 0.020911)
State: walk Velocity: 1.240 RM: (0.0, 0.0, 0.02066)
State: walk Velocity: 1.225 RM: (0.0, 0.0, 0.020409)
State: walk Velocity: 1.209 RM: (0.0, 0.0, 0.020157)
State: walk Velocity: 1.194 RM: (0.0, 0.0, 0.019906)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)
State: walk Velocity: 1.179 RM: (0.0, 0.0, 0.019654)

extends StateInterface

class_name WalkStartState

var character: Player
var is_blocking:bool = false

func enter(_prev_state: String = "")-> void:
	character = state_machine.owner
	character.animation_player.animation_finished.connect(on_animation_finished)
	character.animation_player.play("Walk_start_long")
	character.is_root_motion = true
	is_blocking = true
	
func exit() -> void:
	if character.animation_player.animation_finished.is_connected(on_animation_finished):
		character.animation_player.animation_finished.disconnect(on_animation_finished)
		
func physics_update(_delta: float) -> void:
	if character.direction:
		var up := Vector3.UP
		# For root motion
		var forward : Vector3 = character.direction # Check this value if its in correct way
		var right := up.cross(forward).normalized()
		var corrected_up := forward.cross(right).normalized()
		var target_basis := Basis(right, corrected_up, forward).orthonormalized() 
		var a = character.transform.basis.get_rotation_quaternion()
		var b = target_basis.get_rotation_quaternion()
		var c = a.slerp(b,0.3)
		character.transform.basis = Basis(c).orthonormalized()

		character.set_quaternion(character.get_quaternion() * character.animation_player.get_root_motion_rotation())
		character.velocity = (character.animation_player.get_root_motion_rotation_accumulator().inverse() * character.get_quaternion()) * character.animation_player.get_root_motion_position() / _delta

	if not character.is_on_floor():
		state_machine.change_state("Jump_midair_90")
		return
		
	if not is_blocking:
		_decide_next_state()

	character.move_and_slide()
	
	
func on_animation_finished(_animation_name):
	character.animation_player.animation_finished.disconnect(on_animation_finished)
	is_blocking = false
	
func _decide_next_state():
	if Input.get_vector("k_left", "k_right", "k_forward", "k_backward"):
		if Input.is_action_pressed("k_sprint"):
			state_machine.change_state("Run_16")
		
			return
		if Input.is_action_pressed("k_crouch"):
			state_machine.change_state("Crouch_walk_32")
			
			return
		state_machine.change_state("walk")
	
		return
		
	if Input.is_action_just_pressed("k_jump") and character.is_on_floor():
		state_machine.change_state("Jump_ascend_9")
		
		return
		
	if Input.is_action_pressed("k_crouch"):
		state_machine.change_state("Crouch_90")
		
		return
			
	state_machine.change_state("walk_stop")
	
	return

It is a bummer that the animation finished signal takes one step too long to process and thus results with a reset to 0.0 while that engine cycle frame gets processed. And now i have to manually add these unblock function calls into every oneshot animation. i could do this in code but from my tests it is less precise and the code execution often overshoots the targeted value of trigger due to process time deltas and etc etc..

Maybe someone knows a better way to reliably capture animation finished and do the state transition to next animation before the properties of the previous animation gets reset?

You just use animation tree and link the states and set to travel to the next state automatically.

set_travel_to in code? or how?