How do I finish a looping animationtree and statemachine

Godot Version

Godot 4

Question

I’m currently using an AnimationTree and I’m having some problems. I want to make it so that when you are holding down shift and a directional button and the player is at full speed, an animation plays and switches the state from bare hands to running in my statemachine. However, whenever these conditions are met (except for full speed) it will continuously play that animation and not switch to the running state. During the animations loop, when I fulfill the conditions again, then it will turn to the running state and not go back. I have no idea what is going on.

Here my nodes for the CharacterBody3D:

Here is what my AnimationTree looks like:

Here is what my CharacterBody3D code looks like:

extends CharacterBody3D

var speed
var Running
const WALK_SPEED = 10.0
const SPRINT_SPEED = 20.0
const JUMP_VELOCITY = 7.8
var accelerationx = 5.0
var dead = false
var full_speed = false

var animation_lock : bool = false

# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = 18.8

@onready var camera = $Camera3D
@onready var Hand = $"CanvasLayer/Bare Hands/Hand"
@onready var Reaction = $CanvasLayer/LiveReaction/Reaction
@onready var PlayerTree : AnimationTree = $"CanvasLayer/Bare Hands/AnimationTree"
@onready var StateMachine : PlayerStateMachine = $PlayerStateMachine
	
func _ready():
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	camera.current = true
	PlayerTree.active = true

func _unhandled_input(event: InputEvent) -> void:
	if dead:
		return
	if event is InputEventMouseButton:
		Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	elif event.is_action_pressed("ui_cancel"):
		Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		
	if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		if event is InputEventMouseMotion:
			rotate_y(-event.relative.x * 0.005)
			camera.rotate_x(-event.relative.y * 0.005)
			camera.rotation.x = clamp(camera.rotation.x, -PI/4, PI/3)
			

func _physics_process(delta):
	var input_dir = Input.get_vector("left", "right", "forward", "backward")
	var direction = (camera.transform.basis * transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if not is_on_floor():
		velocity.y -= gravity * delta

	# Handle Jump.
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = JUMP_VELOCITY
	
	# Handle Sprint.
	if Input.is_action_pressed("sprint") and Input.get_vector("left", "right", "forward", "backward"):
		speed = move_toward(speed, SPRINT_SPEED, accelerationx * delta)
		if speed == SPRINT_SPEED:
			full_speed = true
	else:
		speed = WALK_SPEED

	if is_on_floor():
		if direction:
			velocity.x = direction.x * speed
			velocity.z = direction.z * speed
		else:
			velocity.x = lerp(velocity.x, direction.x * speed, delta * 7.0)
			velocity.z = lerp(velocity.z, direction.z * speed, delta * 7.0)
		
	else:
		velocity.x = lerp(velocity.x, direction.x * speed, delta * 3.0)
		velocity.z = lerp(velocity.z, direction.z * speed, delta * 3.0)
	
	move_and_slide()
	punch()
	run()
			
func punch():
	if Input.is_action_pressed("Attack") and StateMachine.check_if_can_punch():
		pass

func run():
	if full_speed == true and StateMachine.check_if_can_run():
		pass

Here is what my StateMachine looks like:

extends Node

class_name PlayerStateMachine

@export var Player : CharacterBody3D
@export var animation_tree : AnimationTree
@export var CurrentState : State

var states : Array[State]

func _ready():
	for child in get_children():
		if(child is State):
			states.append(child)
			
			#Set the states up with what they need to function
			child.Player = Player
			child.playback = animation_tree["parameters/playback"]
			
		else:
			push_warning("Child " + child.name + " is not a State for PlayerStateMachine")
			
func _physics_process(delta):
	if(CurrentState.next_state != null):
		switch_states(CurrentState.next_state)
		
	CurrentState.state_process(delta)

func check_if_can_run():
	return CurrentState.can_run
	
func check_if_can_punch():
	return CurrentState.can_punch

func switch_states(new_state : State):
	if(CurrentState != null):
		CurrentState.on_exit()
		CurrentState.next_state = null
	
	CurrentState = new_state
	
	CurrentState.on_enter()
	
func _input(event : InputEvent):
	CurrentState.state_input(event)

Here is what the BareHands state looks like:

extends State

class_name Bare_HandsState

@export var punch_state : State 
@export var punch_animation : String = "Punch"
@export var running_state : State
@export var running_animation : String = "Running"

func state_input(event : InputEvent):
	if(event.is_action_pressed("Attack")):
		punch()
	if(event.is_action_pressed("sprint") and Input.get_vector("left", "right", "forward", "backward")):
		run()
		
func punch():
	next_state = punch_state
	playback.travel(punch_animation)
	
func run():
	next_state = running_state
	playback.travel(running_animation)

Here is what my Running state looks like:

extends State

class_name RunningState

@export var bare_hands_state : State

func state_process(_delta):
	Player.run()
	
func _on_animation_tree_animation_finished(String):
	next_state = bare_hands_state

Here is what the State code looks like:

extends Node

class_name State

@export var can_run : bool = true
@export var can_punch : bool = true

var Player : CharacterBody3D
var next_state : State
var playback : AnimationNodeStateMachinePlayback

func state_process(_delta):
	pass

func state_input(_event : InputEvent):
	pass
	
func on_enter():
	pass
	
func on_exit():
	pass

So, I had some struggles following the logic of your code, there were some redundancies in your code, and part of it that were being called that don’t do anything yet. I am not super familiar with AnimationTree, I have generally used more of just AnimationPlayer. However, I did write a mockup of StateMachine and your player code using what you had there. There are things I deleted that don’t yet do anything for clarity.

I would also recommend adding a jump state since that is a state that your player has, so because of that I wrote up a jump state as well, and a punch state to just give more examples.

Player Script

class_name PlayerCharacter extends CharacterBody3D

enum State{IDLE,RUNNING,PUNCH,JUMP}

const WALK_SPEED = 10.0
const SPRINT_SPEED = 20.0
const JUMP_VELOCITY = 7.8

var speed
var running
var acceleration_x : float = 5.0
var dead : bool = false
var full_speed : bool = false
var animation_lock : bool = false
var gravity = 18.8 # Get the gravity from the project settings to be synced with RigidBody nodes.

@onready var camera := $Camera3D
@onready var hand := $"CanvasLayer/Bare Hands/Hand"
@onready var reaction := $CanvasLayer/LiveReaction/Reaction
@onready var player_tree : AnimationTree = $"CanvasLayer/Bare Hands/AnimationTree"
@onready var state_machine : StateMachine = $StateMachine

func _ready():
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	camera.current = true
	player_tree.active = true
func _unhandled_input(event: InputEvent) -> void:
	if dead:
		return
	if event is InputEventMouseButton:
		Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	elif event.is_action_pressed("ui_cancel"):
		Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		
	if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		if event is InputEventMouseMotion:
			rotate_y(-event.relative.x * 0.005)
			camera.rotate_x(-event.relative.y * 0.005)
			camera.rotation.x = clamp(camera.rotation.x, -PI/4, PI/3)
func _physics_process(delta):
	var input_dir = Input.get_vector("left", "right", "forward", "backward")
	var direction = (camera.transform.basis * transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if not is_on_floor():
		velocity.y -= gravity * delta
	# Handle Jump.
	if state_machine.current_state == State.JUMP:
		velocity.y = JUMP_VELOCITY
if state_machine.current_state == State.RUNNING:
	speed = move_toward(speed, SPRINT_SPEED, acceleration_x * delta)
	if speed == SPRINT_SPEED:
		full_speed = true
	else:
		full_speed = false
else:
	speed = WALK_SPEED
	if is_on_floor():
		if direction:
			velocity.x = direction.x * speed
			velocity.z = direction.z * speed
		else:
			velocity.x = lerp(velocity.x, direction.x * speed, delta * 7.0)
			velocity.z = lerp(velocity.z, direction.z * speed, delta * 7.0)
	else:
		velocity.x = lerp(velocity.x, direction.x * speed, delta * 3.0)
		velocity.z = lerp(velocity.z, direction.z * speed, delta * 3.0)
	move_and_slide()
func play_animations(animation):
	match animation:
		State.IDLE:
			pass #Play idle animation here
		State.RUNNING:
			pass #Play running animation here
		State.PUNCH:
			pass #Play punch animation here
		State.JUMP:
			pass #Play jump animation here

StateMachine

class_name StateMachine extends Node

enum {IDLE,RUNNING,PUNCH,JUMP}

@export var actor : CharacterBody3D
@export var animation : AnimationPlayer
@export var state_list : Array[State] = [$Idle,$Running,$Punch,$Jump]
@export var current_state: int = IDLE

func _physics_process(delta):
	_set_state(state_list[current_state].check_state(actor, animation))

func _set_state(new_state) -> void:
	if current_state == new_state:
		return
	state_list[current_state].state_exited(actor, animation)
	state_list[new_state].state_entered(actor, animation)
	current_state = new_state

State

class_name State extends Node

enum {IDLE,RUNNING,PUNCH,JUMP}

func check_state(actor, animation):
	pass

func run_state(actor, animation):
	pass

func state_entered(actor, animation):
	pass

func state_exited(actor, animation):
	pass

IdleState

class_name Idle extends State

@export var connected_states: Array[int] = [RUNNING,PUNCH,JUMP]

func check_state(actor, animation):
	if Input.is_action_pressed("Attack"):
		return PUNCH
	if Input.is_action_pressed("sprint") and Input.get_vector("left", "right", "forward", "backward"):
		return RUNNING
	if Input.is_action_just_pressed("jump") and actor.is_on_floor():
		return JUMP
	return IDLE


func state_entered(actor, animation):
	actor.play_animation(IDLE)

RunningState: if animation isn’t fixed time you need another way to switch out of state.

class_name RunningState extends State

@export var connected_states: int = IDLE

func check_state(actor, animation):
	if animation.running.is_playing():
		return RUNNING
	return IDLE


func state_entered(actor, animation):
	actor.play_animation(RUNNING)

PunchState

class_name PunchState extends State

@export var connected_states: int = IDLE

func check_state(actor, animation):
	if animation.punch.is_playing():
		return PUNCH
	return IDLE


func state_entered(actor, animation):
	actor.play_animation(PUNCH)

JumpState

class_name JumpState extends State

@export var connected_states: int = IDLE

func check_state(actor, animation):
	if animation.jumping.is_playing():
		return PUNCH
	return IDLE


func state_entered(actor, animation):
	actor.play_animation(JUMP)

I don’t know all your functionality or needs, so this might very well not be exactly how you need it set up, but the form is there for tweeking.

The basis is that each physics tick you run the current_states check_state(). This checks to see if it needs to be changed or stay the same. It can also call any run_state() needs at this point (I didn’t see anything that needed necessarily to be run from your state so I left those out). The state_entered() I use for setting animation via the players script when the state starts. I also added a state_exited script if you need it for anything.

The check_state() returns an enum value that is associated to your states. In your statemachine you use that returned value in _set_state(new_state): to change states, and to run exit/enter state functions.

In your player script I changed it so rather than checking for inputs again (which is redundant since your state already does that), now it just checks if you are in the right state.

Your player script also controls animation changes, and uses a match statement with int values to find which state you are in and to play proper animation.

All of this I threw together really quick, I didn’t do any testing since I don’t have any of your resources or know exactly how you have your game setup. This code is for an example to how you might be able to streamline your code.

1 Like

Seems like you are using “At End” transitions between idle and running, this means your animation will not travel immediately, only once the animation ends, which the animation wont end if it’s looping. Set this to a “Immediate” transition.

Are you using conditions or expressions for the animation tree?

1 Like

I copy and pasted all the code but I now I have been getting errors in my statemachine code:
Screenshot (73)
I’m unsure of how to fix this.

My code wasn’t designed to be plug and play. It was to give you an example of setting up a StateMachine. I am surprised those are the only errors it is throwing.

That error is saying that player_state_machine doesn’t have those nodes as children. That error is line 7 of your state machine code. Which if you copy pasted is your state list.

General tip, never copy paste, except if you want to # comment it out and use it as an example to build it yourself so you know what it is doing. If you are uskng code that you don’t know what it does it is so easy to break it.

My code was for an example, it wasn’t meant to necessarily work with what you have going on.

1 Like