2
\$\begingroup\$

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:

enter image description here

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;

    public LayerMask layerMask;

    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.

\$\endgroup\$
2
  • \$\begingroup\$ 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. \$\endgroup\$
    – schneebuzz
    Commented Apr 24, 2020 at 10:17
  • \$\begingroup\$ Did you come top with a resolution for this issue? I am having similar problems. \$\endgroup\$
    – pelagos
    Commented Apr 27, 2020 at 5:54

3 Answers 3

1
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

I didn't opt for the solutions mentioned since I wanted the character movement to be as snappy as possible. I chose to animate my player using a script based state machine, but the same logic should apply.

I got this to work by using GetAxis (not raw) as an indication for what buttons were previously pressed.

For example, assume WASD are used for controls. If W and D were held down and W was released 1 frame before D, that would cause the animation to default to Idle looking up. If Input.GetAxis("Horizontal") is slightly less than 1, then it had been pressed very recently. Knowing this, we set the animation to idle looking in the north east direction.

\$\endgroup\$
0
\$\begingroup\$

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;
    }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .