Bootstrap

Unity教程(二十)战斗系统 角色反击

Unity开发2D类银河恶魔城游戏学习笔记

Unity教程(零)Unity和VS的使用相关内容
Unity教程(一)开始学习状态机
Unity教程(二)角色移动的实现
Unity教程(三)角色跳跃的实现
Unity教程(四)碰撞检测
Unity教程(五)角色冲刺的实现
Unity教程(六)角色滑墙的实现
Unity教程(七)角色蹬墙跳的实现
Unity教程(八)角色攻击的基本实现
Unity教程(九)角色攻击的改进

Unity教程(十)Tile Palette搭建平台关卡
Unity教程(十一)相机
Unity教程(十二)视差背景

Unity教程(十三)敌人状态机
Unity教程(十四)敌人空闲和移动的实现
Unity教程(十五)敌人战斗状态的实现
Unity教程(十六)敌人攻击状态的实现
Unity教程(十七)敌人战斗状态的完善

Unity教程(十八)战斗系统 攻击逻辑
Unity教程(十九)战斗系统 受击反馈
Unity教程(二十)战斗系统 角色反击

Unity教程(二十一)技能系统


如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录



前言

本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。

本节实现战斗系统的角色反击部分。

Udemy课程地址

对应视频:
Counter attack - Enemy’s Stun State
Counter 's attack window
Player’s Counter Attack


一、概述

本节实现角色反击,包括骷髅被反击后的眩晕状态和角色反击状态。

骷髅眩晕状态持续期间,会播放眩晕动画并闪烁红光。
将骷髅攻击时的一段时间设置为反击窗口,并且实现了反击窗口的显示,在反击窗口内骷髅才能被反击。
玩家反击时和攻击实现一样,记录攻击范围内的敌人,并判断是否处于反击窗口中,处于则反击成功。

玩家状态转换如下:
在这里插入图片描述
骷髅状态转换如下:
在这里插入图片描述
具体如图:

在这里插入图片描述
在这里插入图片描述

二、骷髅的眩晕状态

(1)创建骷髅眩晕动画

我们创建动画skeletonStunned
层次面板中选中Enemy_skeleton下的Animator,在Animation面板中创建动画
将精灵表SkeletonHit标号3的帧拖入
动画创建的更详细讲解见Unity教程(零)Unity和VS的使用相关内容
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

连接状态机,并添加过渡条件Stunned,并修改过渡设置
添加bool型条件变量Stunned,并连接过渡
在这里插入图片描述
Entry->skeletonStunned的过渡,加条件变量
在这里插入图片描述

skeletonStunned->Exit的过渡,加条件变量,并更改设置

在这里插入图片描述

(2)创建SkeletonStunnedState

首先创建SkeletonStunnedState,它继承自EnemyState,通过菜单生成构造函数和重写。
在这里插入图片描述
添加Enemy_Skeleton变量,并修改构造函数中传入值

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

public class SkeletonStunnedState : EnemyState
{
    private Enemy_Skeleton enemy;

    public SkeletonStunnedState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy,string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
    {
        this.enemy = _enemy;
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
    }
}

在Enemy中添加敌人被击晕的相关变量,包括被击晕的时长和方向。

    [Header("Stunned Info")]
    public float stunDuration = 1.0f;
    public Vector2 stunDirection;

在SkeletonStunnedState的Enter函数中给计时器赋初值并设置被击晕的速度,击晕方向与骷髅面向的方向相反。
当计时器归零时,切换到空闲状态。

//SkeletonStunnedState: 眩晕状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonStunnedState : EnemyState
{
    private Enemy_Skeleton enemy;

    public SkeletonStunnedState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy,string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
    {
        this.enemy = _enemy;
    }

    public override void Enter()
    {
        base.Enter();

        stateTimer = enemy.stunDuration;

        rb.velocity = new Vector2( -enemy.facingDir * enemy.stunDirection.x, enemy.stunDirection.y);
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();

        if(stateTimer < 0)
            stateMachine.ChangeState(enemy.idleState);
    }
}

在Enemy_Skeleton中创建骷髅眩晕状态。

    public SkeletonStunnedState stunnedState { get; private set; }

    protected override void Awake()
    {
        base.Awake();

        idleState = new SkeletonIdleState(stateMachine,this,this,"Idle");
        moveState = new SkeletonMoveState(stateMachine, this,this, "Move");
        battleState = new SkeletonBattleState(stateMachine, this, this, "Move");
        attackState = new SkeletonAttackState(stateMachine, this, this, "Attack");
        stunnedState = new SkeletonStunnedState(stateMachine, this, this, "Stunned");
    }

然后我们先设置一个按键,按下骷髅转换到被击晕状态看一下效果。这里只是看一下效果,具体状态转换条件会在后文实现。
再Enemy_Skeleton中添加代码:

    protected override void Update()
    {
        base.Update();

        if(Input.GetKeyDown(KeyCode.U))
        {
            stateMachine.ChangeState(stunnedState);
        }
    }

在层次面板中选中Enemy_Skeleton,设置合适的被击晕时间和方向。
在这里插入图片描述

效果如下:
在这里插入图片描述

(3)实现被击晕红色闪烁特效

在实体特效类EntityFX中添加一个红色的闪烁特效,在白色与红色之间切换。
实现这个效果,改变骷髅Sprite Renderer的Color属性即可。
在这里插入图片描述
在这里插入图片描述
改变颜色为红色时效果如下:
在这里插入图片描述
在这里插入图片描述
在EntityFX中添加颜色切换的函数:

    //红色闪烁特效
    private void RedColorBlink()
    {
        if (sr.color != Color.white)
            sr.color = Color.white;
        sr.color = Color.red;
    }

上面的函数实现了一次颜色切换,要实现闪烁效果要不断重复这个函数,我们用Invoke实现。
这里的Invoke是Unity的MonoBehaviour类中的延迟执行方法。
可参照Unity官方手册

函数作用
Invoke延迟指定时间后,调用指定函数
InvokeRepeating延迟指定时间后,调用指定函数,每隔一定时间重复执行一次
IsInvoking判断指定函数是否正被调用
CancelInvoke取消调用函数

几个函数调用如下:

Invoke(string methodName, float time)

InvokeRepeating(string methodName, float time, float repeatRate)

IsInvoking(string methodName)

//取消全部调用
CancelInvoke()
//取消指定方法调用
CancelInvoke(string methodName)

我们在SkeletonStunned类进入状态时使用InvokeRepeating重复调用红色闪烁特效:

    public override void Enter()
    {
        base.Enter();

        enemy.fx.InvokeRepeating("RedColorBlink", 0, 0.1f);

        stateTimer = enemy.stunDuration;

        rb.velocity = new Vector2( -enemy.facingDir * enemy.stunDirection.x, enemy.stunDirection.y);
    }

在眩晕结束后,要取消闪烁的调用。
在EntityFX中添加取消闪烁的函数

    private void CancelRedBlink()
    {
        CancelInvoke();
        sr.color = Color.white;
    }

在SkeletonStunned类退出状态时使用Invoke调用取消闪烁

    public override void Exit()
    {
        base.Exit();

        enemy.fx.Invoke("CancelRedBlink", 0);
    }

效果如下:
在这里插入图片描述

三、反击窗口的实现

在可以进行反击时,显示一个反击窗口,在动画播放到合适的时机时触发。

(1)创建打开和关闭反击窗口函数

在Enemy的被击晕信息中,添加表示能被击晕的标志变量,和反击窗口的图像。

    [Header("Stunned Info")]
    public float stunDuration = 1.0f;
    public Vector2 stunDirection;
    protected bool canBeStunned;
    [SerializeField] protected GameObject counterImage;

在Enemy中写两个函数,控制反击窗口的开启和关闭,为canBeStunned赋值和控制反击窗口图像的显示。

    //打开反击窗口
    protected virtual void openCounterAttackWindow()
    {
        canBeStunned = true;
        counterImage.SetActive(true);
    }

    //关闭反击窗口
    protected virtual void closeCounterAttackWindow()
    {
        canBeStunned = false;
        counterImage.SetActive(false);
    }

由于动画师Animator组件挂在骷髅之下,要在动画播放中设置触发事件,需要把事件写在Animator组件的脚本Enemy_SkeletonAnimationTriggers中。再由触发器调用Enemy类中的函数实现。
Enemy_SkeletonAnimationTriggers中添加代码实现:

    private void openCounterWindow() => enemy.OpenCounterAttackWindow();
    
    private void closeCounterWindow() => enemy.CloseCounterAttackWindow();

在层次面板中右击Enemy_Skeleton创建反击窗口图像
右击Enemy_Skeleton -> 2D Object -> Sprites -> Square -> 重命名为CounterImage

在这里插入图片描述
可以根据喜好选取攻击窗口颜色,这里我依照教程选了略透明的红色
在这里插入图片描述
在这里插入图片描述
把它拖到Stunned信息,CounterImage的位置。
在这里插入图片描述

(2)设置动画事件

选取骷髅拿起武器时打开反击窗口,攻击落下时关闭反击窗口。
选择动画skeletonAttack,在第4帧,骷髅拿起武器时,添加触发事件
在这里插入图片描述

在这里插入图片描述
选择触发的函数打开反击窗口
Enemy_SkeletonAnimationTriggers -> Methods -> openCounterWindow()
在这里插入图片描述
在第8帧,骷髅完成攻击时,添加触发事件
在这里插入图片描述
选择触发的函数关闭反击窗口
Enemy_SkeletonAnimationTriggers -> Methods -> closeCounterWindow()
在这里插入图片描述
调整CounterImage层次
在这里插入图片描述
调整反击窗口到合适的大小和位置
在这里插入图片描述
反击窗口初始状态设置为不显示,让它只在骷髅攻击时出现。
在这里插入图片描述
效果如下:
在这里插入图片描述

四、玩家的反击状态

玩家进行反击,并且反击的敌人处于可反击状态则反击成功。
反击的整体过程如下:
在这里插入图片描述
canBeStunned变量就代表了敌人是否处于可被反击的时间段,所以上面一系列调用本质上就是在反击时判断canBeStunned是否为真。中间这些函数只是在反击成功时,顺便完成了一些反击成功时应该进行的操作。

(1)完成反击窗口检查和反击成功的操作

在Enemy中创建虚函数CheckCanBeStunned,检查canBeStunned变量并返回。此时攻击窗口是打开的,打开和关闭攻击窗口的函数就在Enemy中,因此在canBeStunned为真时,反击成功,在其中书写关闭攻击窗口。

    //检查击晕条件,关闭反击窗口
    public virtual bool CheckCanBeStunned()
    {
        if(canBeStunned)
        {
            CloseCounterAttackWindow();

            return true;
        }
        return false;
    }

接着在Enemy_Skeleton中重载函数CheckCanBeStunned,调用基类函数得到canBeStunned的检查结果并返回。当检查结果为真时,添加反击成功后骷髅被击晕的状态转换。

    //检查击晕条件,转到眩晕状态
    public override bool CheckCanBeStunned()
    {
        if(base.CheckCanBeStunned())
        {
            stateMachine.ChangeState(stunnedState);

            return true;
        }

        return false;

    }

(2)创建玩家反击动画

我们创建动画playerCounterAttack
层次面板中选中Player下的Animator,在Animation面板中创建动画
将精灵表标号为22的帧拖入

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


创建动画playerSuccessfulCounterAttack
选择精灵表标号为23、24、25、18的帧拖入
这个动作稍快一点,采样率改为20

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

连接状态机,并添加过渡条件CounterAttack,并修改过渡设置
添加bool型条件变量CounterAttack,并连接过渡
在这里插入图片描述

Entry->playerCounterAttack的过渡,加条件变量
在这里插入图片描述
playerCounterAttack->Exit的过渡,加条件变量,并更改设置
在这里插入图片描述
连接状态机,并添加过渡条件SuccessfulCounterAttack,并修改过渡设置
添加bool型条件变量SuccessfulCounterAttack,并连接过渡。反击成功的动画连接与其他不同,它是由反击状态过渡过来的。
在这里插入图片描述

playerCounterAttack->playerSuccessfulCounterAttack的过渡,加条件变量,并更改设置

在这里插入图片描述
playerSuccessfulCounterAttack->Exit的过渡,加条件变量,并更改设置
注意:这里退出条件也是CounterAttack为false

在这里插入图片描述
反击成功的动画播放完后要退出反击状态,所以还要在反击成功最后一帧添加事件调用AnimationTrigger()
在这里插入图片描述
在这里插入图片描述

(3)创建PlayerCounterAttackState

首先创建PlayerCounterAttackState,它继承自PlayerState,通过菜单生成构造函数和重写。
在这里插入图片描述
在Player的攻击信息中添加变量,反击持续时间counterAttackDuration。这个值设置时建议小一点,不然会出现角色摆好反击姿势不动等着骷髅攻击就弹反的情况。

    [Header("Attack details")]
    public Vector2[] attackMovement;
    public float counterAttackDuration = 0.2f;
    
    public bool isBusy { get; private set; }

在玩家进入反击状态时,将计时器重置为反击持续时间,并且将反击成功的标志变量SuccessfulCounterAttack先置为false。

    public override void Enter()
    {
        base.Enter();

        stateTimer = player.counterAttackDuration;
        player.anim.SetBool("SuccessfulCounterAttack", false);
    }

反击过程中,要像攻击时一样,记录在攻击范围内的敌人,并检查敌人此时是否处于可被击晕的窗口期,如果处于则播放反击成功动画。
同时还要注意,stateTimer时间较短,反击成功时会播不完动画。因此在反击成功时,需要先将stateTimer改为一个较大的数值。

    public override void Update()
    {
        base.Update();

        //记录攻击范围内的敌人
        Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);

        foreach (var hit in colliders)
        {
            if (hit.GetComponent<Enemy>() != null)
            {
                if(hit.GetComponent<Enemy>().CheckCanBeStunned())
                {
                    stateTimer = 10;

                    player.anim.SetBool("SuccessfulCounterAttack", true);
                }
            }
        }
    }

玩家退出反击状态有两种情况。

1.在反击持续时间里,反击不成功,计时器stateTimer小于0反击结束。
2.反击成功,在成功反击的动画运行到最后一帧,触发动画事件,triggerCalled为true,退出反击状态。

因此退出反击状态的条件为stateTimer<0 或 triggerCalled = true
接着在类中添加这部分代码

    public override void Update()
    {
        base.Update();

        //记录攻击范围内的敌人
        Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);

        foreach (var hit in colliders)
        {
            if (hit.GetComponent<Enemy>() != null)
            {
                if(hit.GetComponent<Enemy>().CheckCanBeStunned())
                {
                    stateTimer = 10;

                    player.anim.SetBool("SuccessfulCounterAttack", true);
                }
            }
        }

        if(stateTimer<0 || triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }

在Player中创建反击状态

    public PlayerCounterAttackState counterAttack { get; private set; }

    //创建对象
    protected override void Awake()
    {
        base.Awake();

        StateMachine = new PlayerStateMachine();

        idleState = new PlayerIdleState(StateMachine, this, "Idle");
        moveState = new PlayerMoveState(StateMachine, this, "Move");
        jumpState = new PlayerJumpState(StateMachine, this, "Jump");
        airState = new PlayerAirState(StateMachine, this, "Jump");
        dashState = new PlayerDashState(StateMachine, this, "Dash");
        wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");
        wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");
        primaryAttack = new PlayerPrimaryAttackState(StateMachine, this, "Attack");
        counterAttack = new PlayerCounterAttackState(StateMachine, this, "CounterAttack");
    }

(4)进入反击状态

在PlayerGroundedState中设置按键,切换到反击状态

    //更新
    public override void Update()
    {
        base.Update();

        if (Input.GetKeyDown(KeyCode.Q))
            stateMachine.ChangeState(player.counterAttack);

        if (Input.GetKeyDown(KeyCode.Mouse0))
            stateMachine.ChangeState(player.primaryAttack);

        if(!player.isGroundDetected())
            stateMachine.ChangeState(player.airState);

        if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())
            stateMachine.ChangeState(player.jumpState);
    }

反击效果如下:
不在反击窗口时反击:
在这里插入图片描述

处于反击窗口时反击:
在这里插入图片描述

(5)解决反击时能移动的问题(改不改都行)

现在反击时玩家可以移动,可以看情况修改。
只需在PlayerCounterAttackState的Update一开始设置速度为零即可。

    public override void Update()
    {
        base.Update();

        player.ZeroVelocity();

        //记录攻击范围内的敌人
        Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);

        foreach (var hit in colliders)
        {
            if (hit.GetComponent<Enemy>() != null)
            {
                if(hit.GetComponent<Enemy>().CheckCanBeStunned())
                {
                    stateTimer = 10;

                    player.anim.SetBool("SuccessfulCounterAttack", true);
                }
            }
        }

        if(stateTimer<0 || triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }

总结 完整代码

SkeletonStunnedState.cs

设置骷髅眩晕时长和速度。设置被击晕时红色闪烁特效。
实现切换到空闲状态。

//SkeletonStunnedState: 眩晕状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonStunnedState : EnemyState
{
    private Enemy_Skeleton enemy;

    public SkeletonStunnedState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton _enemy,string _animBoolName) : base(_stateMachine, _enemyBase, _animBoolName)
    {
        this.enemy = _enemy;
    }

    public override void Enter()
    {
        base.Enter();

        enemy.fx.InvokeRepeating("RedColorBlink", 0, 0.1f);

        stateTimer = enemy.stunDuration;

        rb.velocity = new Vector2( -enemy.facingDir * enemy.stunDirection.x, enemy.stunDirection.y);
    }

    public override void Exit()
    {
        base.Exit();

        enemy.fx.Invoke("CancelRedBlink", 0);
    }

    public override void Update()
    {
        base.Update();

        if(stateTimer < 0)
            stateMachine.ChangeState(enemy.idleState);
    }
}

Enemy.cs

添加骷髅被击晕相关变量。
创建函数控制反击窗口开关。
创建函数检查时候处于反击窗口,并在玩家反击成功时关闭反击窗口。

//Enemy:敌人基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : Entity
{
    [SerializeField] protected LayerMask WhatIsPlayer;

    [Header("Stunned Info")]
    public float stunDuration = 1.0f;
    public Vector2 stunDirection;
    protected bool canBeStunned;
    [SerializeField] protected GameObject counterImage;

    [Header("Move Info")]
    public float moveSpeed = 1.5f;
    public float idleTime = 2.0f;
    public float battleTime = 4.0f;

    [Header("Attack Info")]
    public float attackDistance;
    public float attackCoolDown;
    [HideInInspector] public float lastTimeAttacked;


    public EnemyStateMachine stateMachine;

    protected override void Awake()
    {
        base.Awake();
        stateMachine = new EnemyStateMachine();
    }


    protected override void Update()
    {
        base.Update();
        stateMachine.currentState.Update();
    }

    //打开反击窗口
    public virtual void OpenCounterAttackWindow()
    {
        canBeStunned = true;
        counterImage.SetActive(true);
    }

    //关闭反击窗口
    public virtual void CloseCounterAttackWindow()
    {
        canBeStunned = false;
        counterImage.SetActive(false);
    }

    //检查击晕条件,关闭反击窗口
    public virtual bool CheckCanBeStunned()
    {
        if(canBeStunned)
        {
            CloseCounterAttackWindow();

            return true;
        }
        return false;
    }

    //设置触发器
    public virtual void AnimationTrigger() => stateMachine.currentState.AnimationFinishTrigger();

    public virtual RaycastHit2D IsPlayerDetected()=>Physics2D.Raycast(transform.position, Vector2.right * facingDir, 50 ,WhatIsPlayer);

    protected override void OnDrawGizmos()
    {
        base.OnDrawGizmos();

        Gizmos.color = Color.yellow;
        Gizmos.DrawLine(transform.position, new Vector3(transform.position.x + attackDistance * facingDir, transform.position.y));

    }
}

EnemySkeleton.cs

创建眩晕状态并赋值。
调用函数检查是否处于反击窗口,并在玩家反击成功时转到眩晕状态。

//Enemy_Skeleton:骷髅敌人
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy_Skeleton : Enemy
{
    #region 状态
    public SkeletonIdleState idleState { get; private set; }
    public SkeletonMoveState moveState { get; private set; }
    public SkeletonBattleState battleState { get; private set; }
    public SkeletonAttackState attackState { get; private set; }

    public SkeletonStunnedState stunnedState { get; private set; }
    #endregion

    protected override void Awake()
    {
        base.Awake();

        idleState = new SkeletonIdleState(stateMachine,this,this,"Idle");
        moveState = new SkeletonMoveState(stateMachine, this,this, "Move");
        battleState = new SkeletonBattleState(stateMachine, this, this, "Move");
        attackState = new SkeletonAttackState(stateMachine, this, this, "Attack");
        stunnedState = new SkeletonStunnedState(stateMachine, this, this, "Stunned");
    }

    protected override void Start()
    {
        base.Start();

        stateMachine.Initialize(idleState);
    }

    protected override void Update()
    {
        base.Update();

        if(Input.GetKeyDown(KeyCode.U))
        {
            stateMachine.ChangeState(stunnedState);
        }
    }


    //检查击晕条件,转到眩晕状态
    public override bool CheckCanBeStunned()
    {
        if(base.CheckCanBeStunned())
        {
            stateMachine.ChangeState(stunnedState);

            return true;
        }

        return false;

    }

}

Enemy_SkeletonAnimationTriggers.cs

调用函数控制反击窗口开关。

//Enemy_SkeletonAnimationTriggers:触发器组件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy_SkeletonAnimationTriggers : MonoBehaviour
{

    private Enemy_Skeleton enemy => GetComponentInParent<Enemy_Skeleton>();

    private void AnimationTrigger()
    {
        enemy.AnimationTrigger();
    }

    private void AttackTrigger()
    {
        Collider2D[] colliders = Physics2D.OverlapCircleAll(enemy.attackCheck.position, enemy.attackCheckRadius);

        foreach (var hit in colliders)
        {
            if (hit.GetComponent<Player>() != null)
                hit.GetComponent<Player>().Damage();
        }
    }

    private void openCounterWindow() => enemy.OpenCounterAttackWindow();

    private void closeCounterWindow() => enemy.CloseCounterAttackWindow();
}

EntityFX.cs

实现红色闪烁特效。

//EntityFX:实体特效
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class EntityFX : MonoBehaviour
{
    private SpriteRenderer sr;

    [Header("Flash FX")]
    [SerializeField] private Material hitMat;
    private Material originalMat;
    [SerializeField] private float flashDuration;


    private void Start()
    {
        sr = GetComponentInChildren<SpriteRenderer>();
        originalMat = sr.material;
    }

    //闪烁特效
    private IEnumerator FlashFX()
    {
        sr.material = hitMat;

        yield return new WaitForSeconds(flashDuration);

        sr.material = originalMat;
    }

    //红色闪烁特效
    private void RedColorBlink()
    {
        if (sr.color != Color.white)
            sr.color = Color.white;
        else
            sr.color = Color.red;
    }

    private void CancelRedBlink()
    {
        CancelInvoke();
        sr.color = Color.white;
    }
}

PlayerCounterAttackState.cs

实现玩家反击状态。

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

public class PlayerCounterAttackState : PlayerState
{
    public PlayerCounterAttackState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();

        stateTimer = player.counterAttackDuration;
        player.anim.SetBool("SuccessfulCounterAttack", false);
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();

        player.ZeroVelocity();

        //记录攻击范围内的敌人
        Collider2D[] colliders = Physics2D.OverlapCircleAll(player.attackCheck.position, player.attackCheckRadius);

        foreach (var hit in colliders)
        {
            if (hit.GetComponent<Enemy>() != null)
            {
                if(hit.GetComponent<Enemy>().CheckCanBeStunned())
                {
                    stateTimer = 10;

                    player.anim.SetBool("SuccessfulCounterAttack", true);
                }
            }
        }

        if(stateTimer<0 || triggerCalled)
            stateMachine.ChangeState(player.idleState);
    }
}

PlayerGroundedState

实现按Q键切换到反击状态。

//超级状态PlayerGroundedState:接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerGroundedState : PlayerState
{
    //构造函数
    public PlayerGroundedState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
    {
    }

    //进入
    public override void Enter()
    {
        base.Enter();
    }

    //退出
    public override void Exit()
    {
        base.Exit();
    }

    //更新
    public override void Update()
    {
        base.Update();

        if (Input.GetKeyDown(KeyCode.Q))
            stateMachine.ChangeState(player.counterAttack);

        if (Input.GetKeyDown(KeyCode.Mouse0))
            stateMachine.ChangeState(player.primaryAttack);

        if(!player.isGroundDetected())
            stateMachine.ChangeState(player.airState);

        if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())
            stateMachine.ChangeState(player.jumpState);
    }
}

Player.cs

创建反击状态并赋值。

//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : Entity
{
    [Header("Attack details")]
    public Vector2[] attackMovement;
    public float counterAttackDuration = 0.2f;

    public bool isBusy { get; private set; }

    [Header("Move Info")]
    public float moveSpeed = 8f;
    public float jumpForce = 12f;


    [Header("Dash Info")]
    [SerializeField] private float dashCoolDown;
    private float dashUsageTimer;
    public float dashSpeed=25f;
    public float dashDuration=0.2f;
    public float dashDir { get; private set; }


    #region 状态
    public PlayerStateMachine StateMachine { get; private set; }
    public PlayerIdleState idleState { get; private set; }
    public PlayerMoveState moveState { get; private set; }
    public PlayerJumpState jumpState { get; private set; }
    public PlayerAirState airState { get; private set; }
    public PlayerDashState dashState { get; private set; }
    public PlayerWallSlideState wallSlideState { get; private set; }
    public PlayerWallJumpState wallJumpState { get; private set; }
    public PlayerPrimaryAttackState primaryAttack { get; private set; }
    public PlayerCounterAttackState counterAttack { get; private set; }

    #endregion

    //创建对象
    protected override void Awake()
    {
        base.Awake();

        StateMachine = new PlayerStateMachine();

        idleState = new PlayerIdleState(StateMachine, this, "Idle");
        moveState = new PlayerMoveState(StateMachine, this, "Move");
        jumpState = new PlayerJumpState(StateMachine, this, "Jump");
        airState = new PlayerAirState(StateMachine, this, "Jump");
        dashState = new PlayerDashState(StateMachine, this, "Dash");
        wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");
        wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");
        primaryAttack = new PlayerPrimaryAttackState(StateMachine, this, "Attack");
        counterAttack = new PlayerCounterAttackState(StateMachine, this, "CounterAttack");
    }

    // 设置初始状态
    protected override void Start()
    {
        base.Start();

        StateMachine.Initialize(idleState);
    }

    // 更新
    protected override void Update()
    {
        base.Update();

        StateMachine.currentState.Update();

        CheckForDashInput();
    }

    public IEnumerator BusyFor(float _seconds)
    {
        isBusy = true;

        yield return new WaitForSeconds(_seconds);

        isBusy = false;
    }

    //设置触发器
    public void AnimationTrigger() => StateMachine.currentState.AnimationFinishTrigger();

    //检查冲刺输入
    public void CheckForDashInput()
    {

        dashUsageTimer -= Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer<0)
        {
            dashUsageTimer = dashCoolDown;
            dashDir = Input.GetAxisRaw("Horizontal");

            if (dashDir == 0)
                dashDir = facingDir;

            StateMachine.ChangeState(dashState);
        }
    }

}

;