8-way 2D top down movement (diagonal) idle animation issues

Background

I am working on animations for 8-way movement for my character and have done the 8 directional running animations using a blend tree.

Now I need to do the same for the idle animations.

When the player stops moving, the correct idle animation should play.

For example, if they move right and let go of the 'A' button, then the idle animation of the player facing right is played.

Problem

I have done 4/8 directions (Up, Left, Right & Down), my problem is with the diagonals.

e.g if the player lets go of the 'W' and 'D' keys then the idle animation of the player facing the Upper Right corner should be played.

I can't get this to work though.

My blend tree for the animations:

I realize that it's because the two keys (Whether W+D, S+D, A+S, or W+A) are let go at different times, and with the system I have currently, the player has to let go of any of those two key combinations at exactly the same time to successfully play the right diagonal animation.

Tl;Dr:

How do I accomplish these post-movement idle animations when my current system relies on the player releasing the correct keys simultaneously?

Is there a system for this, add a tiny bit of input lag or map keys to something?

Code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
[SerializeField] private Transform pfDashEffect;

public Animator animator;

Vector2 movement;

public float speed = 10f;
public float dashDistance = 100f;
private Vector3 lastMoveDir;

private float MoveX;
private float MoveY;

private void Update()
{
// Input
movement.x = Input.GetAxisRaw("Horizontal");
movement.y = Input.GetAxisRaw("Vertical");

animator.SetFloat("Horizontal", movement.x);
animator.SetFloat("Vertical", movement.y);
animator.SetFloat("Speed", movement.sqrMagnitude);

Move();
Dash();

}

private void FixedUpdate()
{
PlayerAbilities.Dash();
}

private void Move()
{
MoveX = 0f;
MoveY = 0f;

if (Input.GetKey(KeyCode.W))
{
MoveY = +1f;
}
if (Input.GetKey(KeyCode.A))
{
MoveX = -1f;
}
if (Input.GetKey(KeyCode.S))
{
MoveY = -1f;
}
if (Input.GetKey(KeyCode.D))
{
MoveX = +1f;
}

bool isIdle = MoveX == 0 && MoveY == 0;
if (isIdle)
{
// Play idle animation with the lastMoveDir so that the idle animation plays in the right rotation (left, right, up, down)
}
else
{
Vector3 moveDir = new Vector3(MoveX, MoveY).normalized;

if (TryMove(moveDir, speed * Time.deltaTime))
{
// Play walking
animator.SetFloat("Horizontal", movement.x);
animator.SetFloat("Vertical", movement.y);
animator.SetFloat("Speed", movement.sqrMagnitude);
}
else
{
// Play idle
animator.SetFloat("Speed", 0);
}
}
}

private bool CanMove(Vector3 dir, float distance)
{
return Physics2D.Raycast(transform.position, dir, distance, layerMask).collider == null;
}

private bool TryMove(Vector3 baseMoveDir, float distance)
{
Vector3 moveDir = baseMoveDir;

bool canMove = CanMove(moveDir, distance);
if (!canMove)
{
// Cannot move diagonally
moveDir = new Vector3(baseMoveDir.x, 0f).normalized;
canMove = moveDir.x != 0f && CanMove(moveDir, distance);
if (!canMove)
{
// Cannot move horizontally
moveDir = new Vector3(0f, baseMoveDir.y).normalized;
canMove = moveDir.y != 0f && CanMove(moveDir, distance);
}
}

if (canMove)
{
lastMoveDir = moveDir;
// Play walking animation with the moveDir so that the idle animation plays in the right rotation (left, right, up, down)
// storeLastDirection(new Vector3(MoveX, MoveY));

animator.SetFloat("LastMoveVertical", lastMoveDir.y);
animator.SetFloat("LastMoveHorizontal", lastMoveDir.x);
transform.position += moveDir * distance;
return true;
}
else
{
return fase;
}
}

private void Dash()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Vector3 beforeDashPosition = transform.position;
if(TryMove(lastMoveDir, dashDistance))
{
Transform dashEffectTransform = Instantiate(pfDashEffect, beforeDashPosition, Quaternion.identity);
dashEffectTransform.eulerAngles = new Vector3(0, 0, GetAngleFromVectorFloat(lastMoveDir));
float dashEffectWidth = 5;
dashEffectTransform.localScale = new Vector3(dashDistance / dashEffectWidth, 1f, 1f);
}
}
}

public static float GetAngleFromVectorFloat(Vector3 dir)
{
dir = dir.normalized;
float n = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
if (n < 0) n += 360;

return n;
}
}


At the moment I'm just storing the two x and y coordinates for my blend tree like this:

animator.SetFloat("LastMoveVertical", lastMoveDir.y);
animator.SetFloat("LastMoveHorizontal", lastMoveDir.x);


Which works well for the simple directions (North, East, South, West), just not the diagonal ones as the X/Y values change when each button is released. So if the player lets go of the W and 5 milliseconds later lets go of the D key, that's enough for the value to change which then in turn doesn't show the correct animation.

• Have you considered using Input.GetAxis instead (or alongside) Input.GetAxisRaw and use those values for "LastMoveVertical" and "LastMoveHorizontal" values instead of the moveDir values? The moveDir values are not smooth and with the GetAxis you could get smooth values that are not immediately 0, 1, -1. – schneebuzz Apr 24 at 10:17
• Did you come top with a resolution for this issue? I am having similar problems. – pelagos Apr 27 at 5:54

The best way to solve this problem would be to have a separate vector holding the direction that was just being moved in, and when a key is released to start a coroutine that will update this value after a couple of milliseconds

Vector3 heldDirection; // update this each time there is a new key down event, with all held keys

// for each of your directions
if(Input.GetKeyUp(direction){
BlendToIdleFromDirection(heldDirection); // use the heldDirection to blend into your correct idleAnimation
StartCorutine(ReleaseDirection(direction));
}

// will update the held direction after a small delay
ReleaseDirection(direction){
yield return new WaitForSeconds(0.2f);
if(Vector2.dot(direction,heldDirection) > 0) { // check in case this has been updated by another coroutine
heldDirection -= direction;
}
}


I have found that a cheap lerp can do the trick. Try using a different variable to store the anim parameters, and lerp them to the target values over time.

private Vect3 animParam;

void UpdateAnimDir(Vector3 movementDir) {
animParam = Lerp(animParm, movementDir, Time.deltaTime * 0.1);

animator.SetFloat("Horizontal", animParam.x);
animator.SetFloat("Vertical", animParam.y);
}


You can adjust the factor, or do something more complex to smooth out the value so you don't read the immediate value and react.