You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity角色快速点击攻击时动画卡顿问题求助

问题描述

我编写了一个PlayerMovement脚本用于调用角色各类动画,需求是实现不同方向的攻击动画,并设置与动画时长匹配的延迟来防止玩家重复攻击。尝试过协程、Invoke方法、传入Lambda函数的外部Wait函数等多种延迟实现方式,但快速点击攻击按钮时动画始终出现卡顿。也调整过代码位置、简化布尔变量,怀疑是代码冗余导致问题,但并未解决。

原脚本代码
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.AdaptivePerformance.Provider;
using UnityEngine.EventSystems;
public class PlayerMovement : MonoBehaviour
{
    // Private variables
    private float moveHorizontal = 0;
    private float moveVertical = 0;
    private float attackDelay;

    // Public variables
    public float moveSpeed = 1.0f;
    public float movePenalty = 0.7f;

    // Action state flags
    private bool isAttacking = false;

    // Components
    public MobileJoystick mobileJoystick;
    private Rigidbody2D rb;
    private Animator animator;

    // Animation States
    private string currentState;
    const string IDLE = "HeroIdle";
    const string WALK_X = "WalkHoriz";
    const string WALK_Y = "WalkVertical";
    const string WALK_DIAG = "WalkDiagonal";
    const string ATTACK = "MeleeAttack";
    const string BLOCK = "ShieldBlock";
    const string THRUST = "Thrust";
    const string UPSLASH = "UpSlash";
    const string DOWNSLASH = "DownSlash";

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        ChangeAnimationState(IDLE);
    }

    void Update()
    {
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);

        if (isAttacking)
        {
            moveHorizontal = Input.GetAxis("Horizontal") + mobileJoystick.InputDirection.x;
            moveVertical = Input.GetAxis("Vertical") + mobileJoystick.InputDirection.y;
            rb.velocity = movement * movePenalty;

            attackDelay = animator.GetCurrentAnimatorStateInfo(0).length;
            StartCoroutine(StopAttacking(attackDelay));
        }
        else
        {
            moveHorizontal = Input.GetAxis("Horizontal") + mobileJoystick.InputDirection.x;
            moveVertical = Input.GetAxis("Vertical") + mobileJoystick.InputDirection.y;
            rb.velocity = movement * moveSpeed;

            if (moveHorizontal != 0 && Mathf.Abs(moveHorizontal) > Mathf.Abs(moveVertical))
            {
                ChangeAnimationState(WALK_X);
            }
            else if (moveVertical != 0 && Mathf.Abs(moveVertical) > Mathf.Abs(moveHorizontal))
            {
                ChangeAnimationState(WALK_Y);
            }
            else if (moveVertical != 0 && Mathf.Abs(moveVertical) == Mathf.Abs(moveHorizontal))
            {
                ChangeAnimationState(WALK_DIAG);
            }
            else
            {
                ChangeAnimationState(IDLE);
            }
        }
        
        // Flip character based on direction
        if (moveHorizontal < 0 && transform.localScale.x > 0)
        {
            Vector3 flippedScale = transform.localScale;
            flippedScale.x = -0.05f;
            transform.localScale = flippedScale;
        }
        else if (moveHorizontal > 0 && transform.localScale.x < 0)
        {
            Vector3 unflippedScale = transform.localScale;
            unflippedScale.x = 0.05f;
            transform.localScale = unflippedScale;
        }

        // Attack input handling
        if (!isAttacking && Input.GetButtonDown("Fire1"))
        {
            isAttacking = true;

            if(moveHorizontal != 0 && Mathf.Abs(moveHorizontal) > Mathf.Abs(moveVertical))
            {
                ChangeAnimationState(THRUST);
            }
            else if(moveVertical > 0 && Mathf.Abs(moveHorizontal) < Mathf.Abs(moveVertical))
            {
                ChangeAnimationState(UPSLASH);
            }
            else if(moveVertical < 0 && Mathf.Abs(moveHorizontal) < Mathf.Abs(moveVertical))
            {
                ChangeAnimationState(DOWNSLASH);
            }
            else
            {
                ChangeAnimationState(ATTACK);
            }
        }

        // Debug logs
        Debug.Log("IsAttacking: " + isAttacking);
        Debug.Log("currentState: " + currentState);
        Debug.Log("attackDelay: " + attackDelay);
    }

    void ChangeAnimationState(string newState)
    {
        if (currentState == newState) return;
        animator.Play(newState);
        currentState = newState;
    }

    IEnumerator StopAttacking(float waitTime)
    {
        yield return new WaitForSeconds(waitTime);
        isAttacking = false;
    }
}
问题根源与修复方案

核心问题

在Update方法的isAttacking分支中,每帧都会启动一次StopAttacking协程,导致同一时间存在大量协程等待执行。当第一个协程结束将isAttacking设为false后,后续协程还会继续修改这个值,同时攻击动画也会被频繁干扰,最终引发卡顿。

修复步骤

  1. 将协程启动逻辑移到攻击触发点:只在玩家按下攻击按钮、切换攻击动画后,获取一次动画时长并启动协程,避免重复触发。
  2. 锁定攻击状态期间的动画切换:攻击过程中不再执行移动相关的动画切换逻辑,防止攻击动画被打断。
  3. 优化输入读取时机:统一在Update开头读取输入,避免重复代码。

修改后的脚本代码

using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;

public class PlayerMovement : MonoBehaviour
{
    // Private variables
    private float moveHorizontal = 0;
    private float moveVertical = 0;

    // Public variables
    public float moveSpeed = 1.0f;
    public float movePenalty = 0.7f;

    // Action state flags
    private bool isAttacking = false;

    // Components
    public MobileJoystick mobileJoystick;
    private Rigidbody2D rb;
    private Animator animator;

    // Animation States
    private string currentState;
    const string IDLE = "HeroIdle";
    const string WALK_X = "WalkHoriz";
    const string WALK_Y = "WalkVertical";
    const string WALK_DIAG = "WalkDiagonal";
    const string ATTACK = "MeleeAttack";
    const string BLOCK = "ShieldBlock";
    const string THRUST = "Thrust";
    const string UPSLASH = "UpSlash";
    const string DOWNSLASH = "DownSlash";

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        ChangeAnimationState(IDLE);
    }

    void Update()
    {
        // 统一读取输入,避免重复代码
        moveHorizontal = Input.GetAxis("Horizontal") + mobileJoystick.InputDirection.x;
        moveVertical = Input.GetAxis("Vertical") + mobileJoystick.InputDirection.y;
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);

        if (isAttacking)
        {
            rb.velocity = movement * movePenalty;
            // 移除每帧启动协程的逻辑
        }
        else
        {
            rb.velocity = movement * moveSpeed;

            // 移动动画切换逻辑
            if (moveHorizontal != 0 && Mathf.Abs(moveHorizontal) > Mathf.Abs(moveVertical))
            {
                ChangeAnimationState(WALK_X);
            }
            else if (moveVertical != 0 && Mathf.Abs(moveVertical) > Mathf.Abs(moveHorizontal))
            {
                ChangeAnimationState(WALK_Y);
            }
            else if (moveVertical != 0 && Mathf.Abs(moveVertical) == Mathf.Abs(moveHorizontal))
            {
                ChangeAnimationState(WALK_DIAG);
            }
            else
            {
                ChangeAnimationState(IDLE);
            }
        }
        
        // 角色翻转逻辑
        if (moveHorizontal < 0 && transform.localScale.x > 0)
        {
            Vector3 flippedScale = transform.localScale;
            flippedScale.x = -0.05f;
            transform.localScale = flippedScale;
        }
        else if (moveHorizontal > 0 && transform.localScale.x < 0)
        {
            Vector3 unflippedScale = transform.localScale;
            unflippedScale.x = 0.05f;
            transform.localScale = unflippedScale;
        }

        // 攻击输入处理
        if (!isAttacking && Input.GetButtonDown("Fire1"))
        {
            isAttacking = true;
            string attackAnim = ATTACK;

            // 判定攻击方向动画
            if(moveHorizontal != 0 && Mathf.Abs(moveHorizontal) > Mathf.Abs(moveVertical))
            {
                attackAnim = THRUST;
            }
            else if(moveVertical > 0 && Mathf.Abs(moveHorizontal) < Mathf.Abs(moveVertical))
            {
                attackAnim = UPSLASH;
            }
            else if(moveVertical < 0 && Mathf.Abs(moveHorizontal) < Mathf.Abs(moveVertical))
            {
                attackAnim = DOWNSLASH;
            }

            ChangeAnimationState(attackAnim);
            // 仅在攻击触发时获取动画时长并启动一次协程
            float attackDelay = animator.GetCurrentAnimatorStateInfo(0).length;
            StartCoroutine(StopAttacking(attackDelay));
        }
    }

    void ChangeAnimationState(string newState)
    {
        if (currentState == newState) return;
        animator.Play(newState);
        currentState = newState;
    }

    IEnumerator StopAttacking(float waitTime)
    {
        yield return new WaitForSeconds(waitTime);
        isAttacking = false;
        // 攻击结束后切回/idle或移动动画
        ChangeAnimationState(IDLE);
    }
}

额外优化建议

  • 可以在Animator中给攻击动画添加动画事件,在动画结束时直接调用StopAttacking方法,比用WaitForSeconds更精准(避免因动画速度缩放导致的时间偏差)。
  • 移除不必要的using引用,比如System.Linq、UnityEngine.AdaptivePerformance.Provider,减少资源占用。

内容的提问来源于stack exchange,提问作者James Jones

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 09:38:11