2D Top Down 8-Directional Animation Issues

Godot Version

4.4.1

Question

I’ve been working on a top down 2D game (I’m fairly new to the engine). I’m having difficulties with some of the 2D animation. I’ve set up the animation tree which I think is working as intended so I believe the issue must be with my script. See when the character goes from walking → idle when walking in a diagonal direction the character refuses to play the idle animation on the diagonal. I believe this is due to key press release and how it’s extremely difficult to release both the keys at the exact same time. I’ve tried many solutions at this point but nothing I was satisfied with. My current best solution is keeping track of the previous frames direction and making a guess which direction the player intended however there is still 1-2 frames where the wrong animation plays. Anyone have any solutions that I’m too dumb to see myself?

extends Node2D

@onready var animation_tree: AnimationTree = $"../AnimationTree"
@onready var animated_sprite: AnimatedSprite2D = $"../AnimatedSprite2D"
@onready var commit_timer: Timer = $CommitTimer
@onready var player: CharacterBody2D = $".."

var last_facing_direction := Vector2(0, -1)
var direction_history: Array[Vector2] = []

const HISTORY_LENGTH := 10

func _physics_process(delta: float) -> void:
	var idle = !player.velocity
	
	if !idle:
		last_facing_direction = player.velocity.normalized().round()
		_update_direction_history(last_facing_direction)
	else:
		last_facing_direction = _get_most_frequent_direction()
		
	animated_sprite.flip_h = last_facing_direction.x == -1
	
	animation_tree["parameters/Idle/blend_position"] = last_facing_direction
	animation_tree["parameters/Walk/blend_position"] = last_facing_direction
	
	animation_tree["parameters/conditions/walk"] = !idle
	animation_tree["parameters/conditions/idle"] = idle

func _update_direction_history(dir: Vector2) -> void:
	direction_history.push_front(dir)
	if direction_history.size() > HISTORY_LENGTH:
		direction_history.pop_back()

func _get_most_frequent_direction() -> Vector2:
	var counts := {}
	for dir in direction_history:
		if dir == Vector2.ZERO:
			continue
		counts[dir] = counts.get(dir, 0) + 1

	var most_frequent := Vector2.ZERO
	var max_count := 0
	for dir in counts.keys():
		if counts[dir] > max_count:
			max_count = counts[dir]
			most_frequent = dir

	return most_frequent

func is_diagonal(vector: Vector2) -> bool:
	return abs(vector) == Vector2(1, 1)

For anyone wondering I did come to a solution. I’m pretty happy with. I switched the direction logic to be based on the player’s input_direction instead of their velocity, and that definetly made things a lot easier to manage.

What I ended up doing was this: whenever the player changes direction, I store the direction that they were previously facing and start a short timer. If the timer expires and the player has stopped moving, the animation idles in that last stored direction. If the player keeps moving, then the animation updates to match the new input_direction.

There’s still a slight downside, this approach makes the animation feel a tiny bit less responsive but overall, it’s working well enough for my needs right now.

I’m always open to ideas so if you have any improvements let me know!

extends Node2D

@onready var animation_tree: AnimationTree = $"../AnimationTree"
@onready var animated_sprite: AnimatedSprite2D = $"../AnimatedSprite2D"
@onready var commit_timer: Timer = $CommitTimer
@onready var movement_controller: Node2D = $"../MovementController"
@onready var player: CharacterBody2D = $".."

@export var inital_facing_direction := Vector2.DOWN

var facing_direction := Vector2.ZERO
var last_facing_direction := Vector2.ZERO

func _ready() -> void:
	movement_controller.input_direction = inital_facing_direction #Set input_direction to inital direction to avoid animation flicker
	facing_direction = inital_facing_direction	#Set current direction to

func _process(_delta: float) -> void:
	var input_direction = movement_controller.input_direction
	var idle = !player.velocity
	
	#Only change player direction if the player is moving and the commit timer is stopped
	if !idle && commit_timer.is_stopped() && input_direction != facing_direction:
		last_facing_direction = facing_direction #The angle we wish to face is the angle of the value right before we swapped
		commit_timer.start() 
		
	animated_sprite.flip_h = facing_direction.x == -1
	
	### NOTE: blend position should never be (0, 0) as it causes incorrect animation
	animation_tree["parameters/Idle/blend_position"] = facing_direction
	animation_tree["parameters/Walk/blend_position"] = facing_direction
	
	animation_tree["parameters/conditions/walk"] = !idle
	animation_tree["parameters/conditions/idle"] = idle

func _on_commit_timer_timeout() -> void:
	if !player.velocity:
		facing_direction = last_facing_direction
		last_facing_direction = Vector2.ZERO
	else:
		facing_direction = movement_controller.input_direction

It feels like this script is too complicated for what youre trying to achieve. I think all you need is the animated sprite and check the input directions components

#Set player direction
if !input:
    direction = last_facing
else:
    direction = input

#Flips sprite
if player.direction.x < 0:
    animated_sprite.flip_h = true
elif player.direction.x > 0:
    animated_sprite.flip_h = false

#Here use match to play idle/walk anims based on input i.e Vector2(0, 1) would play walk right or idle right depending on velocity
if !player.velocity:
    match direction:
        #vectors to match go here
else:
    match direction:
        #and here

I like the animation tree but it might be a bit much for what youre trying to accomplish but if youre just trying to learn it by all means use it

Hey!

Thanks for the response. I started with a script that was very similar to this, and you are right that my script is a little too complex for what is going on. However, the problem I had was when I pressed multiple keys, upon key release for a slight second, there would be an added input from the key that was released later than the key before, leading to an extra input and having the character face the wrong way when idling, this drove me mad so the slight delay before animation change fixed my issue. If you have any other suggestions its always much appreciated!