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类银河恶魔城游戏学习笔记目录


文章目录

  • Unity开发2D类银河恶魔城游戏学习笔记
  • 前言
  • 一、概述
  • 二、骷髅的眩晕状态
    • (1)创建骷髅眩晕动画
    • (2)创建SkeletonStunnedState
    • (3)实现被击晕红色闪烁特效
  • 三、反击窗口的实现
    • (1)创建打开和关闭反击窗口函数
    • (2)设置动画事件
  • 四、玩家的反击状态
    • (1)完成反击窗口检查和反击成功的操作
    • (2)创建玩家反击动画
    • (3)创建PlayerCounterAttackState
    • (4)进入反击状态
    • (5)解决反击时能移动的问题(改不改都行)
  • 总结 完整代码
    • SkeletonStunnedState.cs
    • Enemy.cs
    • EnemySkeleton.cs
    • Enemy_SkeletonAnimationTriggers.cs
    • EntityFX.cs
    • PlayerCounterAttackState.cs
    • PlayerGroundedState
    • Player.cs


前言

本文为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);
        }
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/952141.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Garnet:微软官方基于.Net 8开源缓存系统,可无需任何改动直接替代Redis,而且还更高性能!

近日微软官方开源了一个开源缓存系统&#xff0c;可完全替代Redis。 01 项目简介 Garnet是微软官方基于.Net 8开发的、开源高性能远程缓存存储系统&#xff0c;支持单节点、集群。 Garnet使用的是当前流行的 RESP 协议&#xff0c;使得当前大多数编程语言现成的 Redis 客户端…

java项目之网上点餐系统源码(springboot+mysql+vue)

大家好我是风歌&#xff0c;曾担任某大厂java架构师&#xff0c;如今专注java毕设领域。今天要和大家聊的是一款基于springboot的网上点餐系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 网上点餐系统的主要使用者分为管理员登录…

深入Android架构(从线程到AIDL)_23 活用IBinder接口于近程通信01

1、 在同一进程里活用IBinder接口 议题 1. myActivity对象是谁创建的呢? 2. myService对象是谁创建的呢? 3. 当myService类里有个f1()函数&#xff0c;如何去调用它呢? 4. 必须先取得myService对象的指针&#xff0c;才能调用f1()函数去存取对象的属性(Attribute)值。 …

拥抱HarmonyOS之高效使用DevEco

拥抱HarmonyOS之高效使用DevEco 前半年对公司的IM SDK进行了鸿蒙化&#xff0c;半年过去系统已经伴随APP上线应用市场&#xff0c;总结一些适配过程中的经验技巧&#xff0c;希望对大家有用。 1.应用AI翻译代码 IM SDK和普通应用的最大差异是逻辑性代码比较多&#xff0c;使…

RT-DETR代码详解(官方pytorch版)——参数配置(1)

前言 RT-DETR虽然是DETR系列&#xff0c;但是它的代码结构和之前的DETR系列代码不一样。 它是通过很多的yaml文件进行参数配置&#xff0c;和之前在train.py的parser argparse.ArgumentParser()去配置所有参数不同&#xff0c;所以刚开始不熟悉代码的时候可能不知道在哪儿修…

Halcon在linux及ARM上的安装及c++工程化

一、HALCON下载 建议到HALCON官方下载页选择linux版本下载,压缩包名为MVTec_HALCON_Progress-18.11.0.1-linux(x64-aarch64-armv7a)-FullVersion.tar.gz。下载前需要登录HALCON帐号,如果没有请自行注册,填写一些基本信息然后激活邮件,操作方便简易。 下载许可证文件 该许…

单通道串口服务器(三格电子)

一、产品介绍 1.1 功能简介 SG-TCP232-110 是一款用来进行串口数据和网口数据转换的设备。解决普通 串口设备在 Internet 上的联网问题。 设备的串口部分提供一个 232 接口和一个 485 接口&#xff0c;两个接口内部连接&#xff0c;同 时只能使用一个口工作。 设 备 的网 口…

Figma如何装中文字体-PingFang苹方字体、Alibaba PuHuiTi阿里普惠

**写在前面&#xff1a; 工具类软件更新迭代如此快的世界&#xff0c;不能靠历史知识来做操作反应。需要着眼于当下工具的形态来思考用法。另外&#xff0c;有人说&#xff0c;当前的用户越来越少发教程类的图文消息了&#xff08;转去了视频&#xff09;&#xff0c;现在很多…

JVM实战—13.OOM的生产案例

大纲 1.每秒仅上百请求的系统为何会OOM(RPC超时时间设置过长导致QPS翻几倍) 2.Jetty服务器的NIO机制如何导致堆外内存溢出(S区太小 禁NIO的显式GC) 3.一次微服务架构下的RPC调用引发的OOM故障排查实践(MAT案例) 4.一次没有WHERE条件的SQL语句引发的OOM问题排查实践(使用MA…

Photoshop PS批处理操作教程(批量修改图片尺寸、参数等)

前言 ‌Photoshop批处理的主要作用‌是通过自动化处理一系列相似的操作来同时应用于多张图片&#xff0c;从而节省时间和精力&#xff0c;提高工作效率。批处理功能特别适用于需要批量处理的任务&#xff0c;如图像尺寸调整、颜色校正、水印添加等‌。 操作步骤 1.创建动作 …

【物联网原理与运用】知识点总结(上)

目录 名词解释汇总 第一章 物联网概述 1.1物联网的基本概念及演进 1.2 物联网的内涵 1.3 物联网的特性——泛在性 1.4 物联网的基本特征与属性&#xff08;五大功能域&#xff09; 1.5 物联网的体系结构 1.6 物联网的关键技术 1.7 物联网的应用领域 第二章 感知与识别技术 2.1 …

新车月交付突破2万辆!小鹏汽车“激活”智驾之困待解

首次突破月交付2万辆规模的小鹏汽车&#xff0c;稳吗&#xff1f; 本周&#xff0c;高工智能汽车研究院发布的最新监测数据显示&#xff0c;2024年11月&#xff0c;小鹏汽车在国内市场&#xff08;不含出口&#xff09;交付量&#xff08;上险口径&#xff0c;下同&#xff09…

基于Springboot+Vue的仓库管理系统

开发一个基于Spring Boot和Vue的仓库管理系统涉及到前端和后端的开发。本文呢&#xff0c;给出一个简单的开发步骤指南&#xff0c;用于指导初入的新手小白如何开始构建这样一个系统&#xff0c;如果**你想直接学习全部内容&#xff0c;可以直接拉到文末哦。** 开始之前呢给小…

java项目之ONLY在线商城系统设计与实现源码(springboot+vue+mysql)

大家好我是风歌&#xff0c;曾担任某大厂java架构师&#xff0c;如今专注java毕设领域。今天要和大家聊的是一款基于springboot的ONLY在线商城系统设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; ONLY在线商城系统设计与实…

java后端对接飞书登陆

java后端对接飞书登陆 项目要求对接第三方登陆&#xff0c;飞书登陆&#xff0c;次笔记仅针对java后端&#xff0c;在看本笔记前&#xff0c;默认已在飞书开发方已建立了应用&#xff0c;并获取到了appid和appsecret。后端要做的其实很简单&#xff0c;基本都是前端做的&…

【2025最新计算机毕业设计】基于SpringBoot+Vue奶茶点单系统(高质量源码,提供文档,免费部署到本地)

作者简介&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流。✌ 主要内容&#xff1a;&#x1f31f;Java项目、Python项目、前端项目、PHP、ASP.NET、人工智能…

import语句详解

在 Java 中&#xff0c;import 语句用于引入其他包中的类、接口或静态成员&#xff0c;以便在当前源文件中直接使用它们&#xff0c;而不需要写完整的类名&#xff08;包括包名&#xff09;。以下是 import 语句的详细解释和使用方法&#xff1a; 一、import语句的基本概念 定…

android刷机

android ota和img包下载地址&#xff1a; https://developers.google.com/android/images?hlzh-cn android启动过程 线刷 格式&#xff1a;ota格式 模式&#xff1a;recovery 优点&#xff1a;方便、简单&#xff0c;刷机方法通用&#xff0c;不会破坏手机底层数据&#xff0…

Wi-Fi Direct (P2P)原理及功能介绍

目录 Wi-Fi Direct &#xff08;P2P&#xff09;介绍Wi-Fi Direct P2P 概述P2P-GO&#xff08;P2P Group Owner&#xff09;工作流程 wifi-Direct使用windows11 wifi-directOpenwrtwifi的concurrent mode Linux环境下的配置工具必联wifi芯片P2P支持REF Wi-Fi Direct &#xff…

scrapy爬取图片

scrapy 爬取图片 环境准备 python3.10scrapy pillowpycharm 简要介绍scrapy Scrapy 是一个开源的 Python 爬虫框架&#xff0c;专为爬取网页数据和进行 Web 抓取而设计。它的主要特点包括&#xff1a; 高效的抓取性能&#xff1a;Scrapy 采用了异步机制&#xff0c;能够高效…