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


文章目录


前言

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

本节实现敌人的攻击状态。

Udemy课程地址

对应视频:

Enemy's Attack State


一、概述

在骷髅进入战斗状态并走到距离玩家很近的距离时,就要进入攻击状态攻击玩家。本节中我们就实现骷髅的攻击状态。

这一部分状态转换条件较多,如下图:

二、创建骷髅攻击动画

我们创建动画skeletonAttack

层次面板中选中Enemy_skeleton下的Animator,在Animation面板中创建动画

将精灵表SkeletonAttack内容全部拖入,采样率改为15

动画创建的更详细讲解见Unity教程(零)Unity和VS的使用相关内容


连接状态机,并添加过渡条件Attack,并修改过渡设置

添加bool型条件变量Attack,并连接过渡

Entry->skeletonAttack的过渡,加条件变量

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

三、骷髅攻击的实现

(1)整理代码

上一节我们用到了很多次骷髅的速度,所以我们在敌人状态中创建刚体,使代码更简洁。

csharp 复制代码
    protected Rigidbody2D rb;
    
    public virtual void Enter()
    {
        rb = enemyBase.rb;
        triggerCalled = false;
        enemyBase.anim.SetBool(animBoolName, true);
    }

SkeletonBattleState状态Update函数中改为

csharp 复制代码
    public override void Update()
    {
        base.Update();

        if (enemy.IsPlayerDetected())
        {
            if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
            {
                stateMachine.ChangeState(enemy.attackState);
            }
        }


        if(player.position.x > enemy.transform.position.x)
            moveDir = 1;
        else if(player.position.x < enemy.transform.position.x)
            moveDir = -1;

        enemy.SetVelocity(enemy.moveSpeed * moveDir, rb.velocity.y);
    }

SkeletonMoveState状态Update函数中改为

csharp 复制代码
    public override void Update()
    {
        base.Update();

        enemy.SetVelocity(enemy.moveSpeed*enemy.facingDir,rb.velocity.y);

        if(!enemy.isGroundDetected() || enemy.isWallDetected())
        {
            enemy.Flip();
            stateMachine.ChangeState(enemy.idleState);
        }
    }

(2)创建SkeletonAttackState

首先创建SkeletonAttackState,它继承自EnemyState,通过菜单生成构造函数和重写。

添加Enemy_Skeleton变量,并修改构造函数中传入值

csharp 复制代码
    protected Enemy_Skeleton enemy;

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

(3)触发器的实现

和Player一样,敌人攻击的结束也要借助触发器来实现。

详细讲解请见Unity教程(八)角色基本攻击的实现

我们在状态基类EnemyState中添加改变参数的函数,若触发器被调用,则triggerCalled置为真。添加代码:

csharp 复制代码
    //修改触发器参数
    public virtual void AnimationFinishTrigger()
    {
        triggerCalled = true;
    }

在Enemy中进行调用

csharp 复制代码
    //设置触发器
    public virtual void AnimationTrigger() => stateMachine.currentState.AnimationFinishTrigger();

我们创建骷髅的触发器脚本Enemy_SkeletonAnimationTriggers。获取Animator的父组件Enemy_Skeleton,并调用其中的AnimationTrigger()

csharp 复制代码
//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();
    }
}

把触发器脚本挂在Enemy_Skeleton的Animator下面 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/41a27165de72476f966005e4d1ccdc39.png#pic_center) 我们在skeletonAttack的最后一帧设置事件帧,调用触发器函数设置triggerCalled参数。

先把白色竖线拉到最后一帧,点击动画面板左边钉子符号的标志添加事件。

点击选中事件帧,在右边面板选中函数AnimationTrigger
Fuction->Enemy_SkeletonAnimationTriggers->Methods->AnimationTrigger()

(4)攻击的实现

我们要在Enemy_Skeleton中创建骷髅攻击状态。

csharp 复制代码
public SkeletonAttackState attackState { 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");
}

在SkeletonBattleState中添加状态转换,当距离过近时转换到攻击状态。

csharp 复制代码
    public override void Update()
    {
        base.Update();

        if (enemy.IsPlayerDetected())
        {
            if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
            {
                stateMachine.ChangeState(enemy.attackState);
            }
        }


        if(player.position.x > enemy.transform.position.x)
            moveDir = 1;
        else if(player.position.x < enemy.transform.position.x)
            moveDir = -1;

        enemy.SetVelocity(enemy.moveSpeed * moveDir, enemy.rb.velocity.y);
    }

我们在SkeletonAttackState中实现攻击的具体内容。

首先我们希望骷髅靠近玩家时,先停止移动再攻击,所以我们先把骷髅速度赋零。

在攻击动画播放完触发器被触发时,攻击结束,转换到战斗状态。

点击Animator也能看到左侧Attack变量在转换(切换很快,不仔细看不清楚)。

四、骷髅攻击的冷却

现在骷髅的攻击是连续不断的,在游戏中通常小怪是按照一定的频率进行的,让玩家有一定的躲避时间。

我们给骷髅小怪加上攻击冷却时间,实现上可以参照Player的连击。

我们在Enemy中添加冷却时间attackCoolDown,记录上次攻击的时间lastTimeAttacked用于实现。

csharp 复制代码
    [Header("Attack Info")]
    public float attackDistance;
    public float attackCoolDown;
    [HideInInspector] public float lastTimeAttacked;

在SkeletonAttackState中,每次退出攻击状态时,更新lastTimeAttacked。

csharp 复制代码
    public override void Exit()
    {
        base.Exit();
        enemy.lastTimeAttacked=Time.time;
    }

在SkeletonBattleState中添加CanAttack函数,利用现在的时间与上次攻击时间加冷却时间比较,判断冷却时间是否已经过去,骷髅是否处于可攻击状态。

先判断是否能检测到玩家,再判断玩家与骷髅的距离,再做攻击冷却的判断,条件都成立时转到战斗状态。

csharp 复制代码
    public override void Update()
    {
        base.Update();

        if (enemy.IsPlayerDetected())
        {
            if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
            {
                if(CanAttack())
                    stateMachine.ChangeState(enemy.attackState);
            }
        }


        if(player.position.x > enemy.transform.position.x)
            moveDir = 1;
        else if(player.position.x < enemy.transform.position.x)
            moveDir = -1;

        enemy.SetVelocity(enemy.moveSpeed * moveDir, rb.velocity.y);
    }

    private bool CanAttack()
    {
        if (Time.time >= enemy.lastTimeAttacked + enemy.attackCoolDown)
            return true;
        return false;
    }

设置合适的冷却时间

效果如下

我们可以看到骷髅每隔一段时间进行攻击了。但是有个很明显的问题,在攻击的间隙骷髅不会停止移动,玩家会被推着走。这个问题下节我们再来解决。

总结 完整代码

EnemyState.cs

增加刚体和触发函数调用

csharp 复制代码
//EnemyState:敌人状态基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyState
{
    protected EnemyStateMachine stateMachine;
    protected Enemy enemyBase;
    protected Rigidbody2D rb;

    private string animBoolName;

    protected float stateTimer;
    protected bool triggerCalled;

    //构造函数
    public EnemyState(EnemyStateMachine _stateMachine, Enemy _enemyBase, string _animBoolName)
    {
        this.stateMachine = _stateMachine;
        this.enemyBase = _enemyBase;
        this.animBoolName = _animBoolName;
    }

    public virtual void Enter()
    {
        rb = enemyBase.rb;
        triggerCalled = false;
        enemyBase.anim.SetBool(animBoolName, true);
    }

    public virtual void Update()
    {
        stateTimer-= Time.deltaTime;
    }

    public virtual void Exit()
    {
        enemyBase.anim.SetBool(animBoolName, false);
    }

    //修改触发器参数
    public virtual void AnimationFinishTrigger()
    {
        triggerCalled = true;
    }
}

Enemy.cs

添加攻击相关变量,添加触发函数调用。

csharp 复制代码
//Enemy:敌人基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    [Header("Move Info")]
    public float moveSpeed = 1.5f;
    public float idleTime = 2.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 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));

    }
}

Enemy_SkeletonAnimationTriggers.cs

触发器组件

csharp 复制代码
//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();
    }
}

Enemy_Skeleton.cs

增加攻击状态创建

csharp 复制代码
//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; }
    #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");
    }

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

        stateMachine.Initialize(idleState);
    }

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

}

SkeletonBattleState.cs

添加攻击冷却判断条件,修改转到攻击状态的条件,修改刚体速度部分代码

csharp 复制代码
//SkeletonBattleState:骷髅战斗状态
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonBattleState : EnemyState
{
    private Transform player;
    private Enemy_Skeleton enemy;
    private int moveDir;

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

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

        player = GameObject.Find("Player").transform;
    }

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

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

        if (enemy.IsPlayerDetected())
        {
            if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
            {
                if(CanAttack())
                    stateMachine.ChangeState(enemy.attackState);
            }
        }


        if(player.position.x > enemy.transform.position.x)
            moveDir = 1;
        else if(player.position.x < enemy.transform.position.x)
            moveDir = -1;

        enemy.SetVelocity(enemy.moveSpeed * moveDir, rb.velocity.y);
    }

    private bool CanAttack()
    {
        if (Time.time >= enemy.lastTimeAttacked + enemy.attackCoolDown)
            return true;
        return false;
    }
}

SkeletonAttackState.cs

创建攻击状态,攻击时速度置零,触发器触发转回到战斗状态

csharp 复制代码
//SkeletonAttackState:骷髅攻击状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonAttackState : EnemyState
{

    protected Enemy_Skeleton enemy;

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

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

    public override void Exit()
    {
        base.Exit();
        enemy.lastTimeAttacked=Time.time;
    }

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

        enemy.ZeroVelocity();
        if (triggerCalled)
            stateMachine.ChangeState(enemy.battleState);
    }
}

SkeletonMoveState.cs

修改刚体速度部分代码

csharp 复制代码
//SkeletonMoveState:骷髅移动状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonMoveState : SkeletonGroundedState
{
    public SkeletonMoveState(EnemyStateMachine _stateMachine, Enemy _enemyBase, Enemy_Skeleton enemy, string _animBoolName) : base(_stateMachine, _enemyBase, enemy, _animBoolName)
    {
    }

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

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

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

        enemy.SetVelocity(enemy.moveSpeed*enemy.facingDir,rb.velocity.y);

        if(!enemy.isGroundDetected() || enemy.isWallDetected())
        {
            enemy.Flip();
            stateMachine.ChangeState(enemy.idleState);
        }
    }
}
相关推荐
知识分享小能手34 分钟前
mysql学习教程,从入门到精通,SQL DISTINCT 子句 (16)
大数据·开发语言·sql·学习·mysql·数据分析·数据库开发
qq 1778036222 小时前
智能新时代,游戏盾守护顺畅体验
运维·服务器·网络·游戏·云计算·ddos·ip
晓幂2 小时前
CTFShow-信息搜集
笔记·学习
Code哈哈笑2 小时前
【C++ 学习】多态的基础和原理(10)
java·c++·学习
nbsaas-boot3 小时前
架构卡牌游戏:通过互动与挑战学习系统设计的创新玩法
学习·游戏·架构
FairGuard手游加固3 小时前
游戏如何应对云手机刷量问题
游戏
QuantumYou3 小时前
计算机视觉 对比学习 串烧二
人工智能·学习·计算机视觉
拥抱AGI3 小时前
我说大模型微调没啥技术含量,谁赞成谁反对?
人工智能·学习·语言模型·大模型学习·大模型入门·大模型教程
哆木3 小时前
部署在线GBA游戏,并通过docker安装启动
游戏·html·gba