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);
        }
    }
}
相关推荐
如生命般费解的谜团13 分钟前
LLM学习笔记(7)Scaled Dot-product Attention
人工智能·笔记·学习·语言模型·json
Mephisto.java2 小时前
【大数据学习 | Spark-Core】详解Spark的Shuffle阶段
大数据·学习·spark
南宫生2 小时前
力扣-位运算-3【算法学习day.43】
学习·算法·leetcode
xnuscd2 小时前
Milvus概念
数据库·学习·mysql
神秘的土鸡3 小时前
基于预测反馈的情感分析情境学习
学习
HABuo4 小时前
【数据结构与算法】合并链表、链表分割、链表回文结构
c语言·开发语言·数据结构·c++·学习·算法·链表
AI完全体4 小时前
【AI日记】24.11.25 学习谷歌数据分析初级课程-第6课
学习·数据分析
码到成龚5 小时前
《数字图像处理基础》学习06-图像几何变换之最邻近插值法缩小图像
图像处理·学习
fa_lsyk5 小时前
mysql window安装(学习使用)
学习·mysql·adb
熬夜的猪5 小时前
现代安全密码哈希算法
java·学习·算法·安全·哈希算法