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


文章目录


前言

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

本节实现敌人的战斗状态。

对应b站视频:
【Unity教程】从0编程制作类银河恶魔城游戏P50

一、概述

本节中我们实现骷髅的战斗状态。

骷髅小怪在检测到玩家时,会向着玩家移动。在移动到一定距离时停止移动进入攻击状态。

由于我们想让骷髅在空闲和移动时均能转入战斗状态,我们创建超级状态接地状态,直接写接地状态到战斗状态的切换即可。

状态切换条件如下:

二、实现Player的检测

玩家的检测在Enemy基类中实现。

碰撞检测的详细讲解见Unity教程(四)碰撞检测

这里我们不再仅仅使用bool类型作为函数的返回值,而是使用RaycastHit2D返回更详细的信息。
RaycastHit2DUnity官方手册

Raycast包含的变量如下表:

变量 介绍
centroid 用于执行投射的图元的质心
collider 射线命中的碰撞体
distance 从射线原点到撞击点的距离
fraction 射线上发生命中的距离的分数
normal 射线命中的表面的法线矢量
point 世界空间中射线命中碰撞体表面的点
rigidbody 附加到命中的对象的 Rigidbody2D
transform 命中的对象的变换

我们要新建一个过滤器WhatIsPlayer进行检测。

这里我们以骷髅的位置为起点,向它面向的方向发射射线检测,检测的最大距离设置为50.

为了方便调试我们还要绘制出攻击检测的线条,在Enemy里对实体中的OnDrawGizmos()函数进行重写。

攻击检测的线条绘制:起点为Enemy_Skeleton的位置,向右attackDistance找到终点。

在Enemy中添加如下代码:

csharp 复制代码
    [SerializeField] protected LayerMask WhatIsPlayer;

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

    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_Skeleton的大小调大一点

创建一个新的WallCheck,并拖到Enemy_Skeleton中

将Wallcheck往下拖一些,以免与攻击检测重合。

在Enemy中重写OnDrawGizmos(),换一个颜色绘制出攻击检测。

给attackDistancce赋一个恰当的值。

将Enemy_Skeleton中WhatIsPlayer改为Player

新建Player和Enemy层。

Player改为Player层,Enemy_Skeleton改为Enemy层,修改时选择将它们所有子物体也改为对应层次。


三、创建接地状态

创建接地状态SkeletonGroundedState,它继承自EnemyState。

在子菜单中创建构造函数,生成重写。

添加Enemy_Skeleton enermy,传递骷髅小怪独有的变量和函数,并修改构造函数。

csharp 复制代码
//SkeletonGroundedState:骷髅接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonGroundedState : EnemyState
{

    protected Enemy_Skeleton enemy;

    public SkeletonGroundedState(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();
    }

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

将SkeletonIdleState和SkeletonMoveState的父类改为SkeletonGroundedState,这时两个子类构造函数参数与父类相同,可以删除原有构造函数和Enemy_Skeleton enemy,直接在子菜单重新生成一个构造函数了。

三、实现战斗状态

(1)创建战斗状态

创建战斗状态SkeletonBattleState,它继承自EnemyState。

在子菜单中创建构造函数,生成重写。添加Enemy_Skeleton enermy,并修改构造函数。

csharp 复制代码
    private Enemy_Skeleton enemy;

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

在EnemySkeleton类中创建battleState。这里条件变量仍然选择"Move",因为战斗状态仍然播放移动动画。

csharp 复制代码
 #region 状态
 public SkeletonIdleState idleState { get; private set; }
 public SkeletonMoveState moveState { get; private set; }

 public SkeletonBattleState battleState { 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");
 }

(2)状态切换

当检测到玩家时,骷髅由接地状态转换为战斗状态。

在SkeletonGroundedState的Update()函数中添加

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

        if(enemy.IsPlayerDetected())
            stateMachine.ChangeState(enemy.battleState);
    }

骷髅进入战斗状态时,要随玩家位置的变化改变移动方向。因此我们引入player的位置这个变量,通过它与骷髅位置的对比确定战斗状态骷髅移动的方向。

player.position.x > enemy.transform.position.x,玩家在骷髅右边,移动方向向右

player.position.x < enemy.transform.position.x,玩家在骷髅左边,移动方向向左

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(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);
    }
}

效果如下:

而当玩家进入骷髅的攻击距离时,骷髅停止移动切换为攻击状态。根据上述内容所讲,骷髅与玩家的距离可由RaycastHit2D里的distance获得。攻击状态我们先控制输出代替,具体攻击状态内容我们下一节再进行实现。

Update中添加如下代码:

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

        if (enemy.IsPlayerDetected())
        {
            if (enemy.IsPlayerDetected().distance < enemy.attackDistance)
            {
                Debug.Log("attack");
                enemy.ZeroVelocity();
                return;
            }
        }


        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);
    }

总结 完整代码

Enemy.cs

添加碰撞检测和攻击距离变量,实现攻击检测的函数和攻击检测的绘制

csharp 复制代码
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 EnemyStateMachine stateMachine;

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


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

    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));

    }
}

SkeletonGroundedState.cs

创建接地状态,由移动和空闲状态继承。实现切换到战斗状态。

csharp 复制代码
//SkeletonGroundedState:骷髅接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkeletonGroundedState : EnemyState
{

    protected Enemy_Skeleton enemy;

    public SkeletonGroundedState(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();
    }

    public override void Update()
    {
        base.Update();
        if (enemy.IsPlayerDetected())
            stateMachine.ChangeState(enemy.battleState);
    }
}

SkeletonIdleState.cs

修改构造函数

csharp 复制代码
//SkeletonIdleState:骷髅空闲状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    public override void Enter()
    {
        base.Enter();
        stateTimer = enemy.idleTime;
    }

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

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

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

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,enemy.rb.velocity.y);

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

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)
            {
                Debug.Log("attack");
                enemy.ZeroVelocity();
                return;
            }
        }


        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);
    }
}

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; }
    #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");
    }

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

        stateMachine.Initialize(idleState);
    }

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

}
相关推荐
咩咩觉主4 小时前
Unity实战案例全解析 :PVZ 植物脚本分析
unity·游戏引擎
拾忆丶夜4 小时前
Unity3d 以鼠标位置点为中心缩放视角(正交模式下)
unity
DA树聚6 小时前
大语言模型之ICL(上下文学习) - In-Context Learning Creates Task Vectors
人工智能·学习·程序人生·ai·语言模型·自然语言处理·easyui
神一样的老师8 小时前
使用卷积神经网络进行人类活动识别的特征学习:惯性测量单元和音频数据的案例研究
学习·cnn·音视频
小蒜学长9 小时前
集团门户网站设计与实现
java·开发语言·数据库·学习·tomcat·maven
LeeYLong10 小时前
I2C/IIC学习笔记
笔记·学习·通信协议·i2c
9毫米的幻想10 小时前
【C++】—— list 的了解与使用
开发语言·c++·windows·学习·list
JT灬新一10 小时前
ARM驱动学习之9注册字符类设备
arm开发·学习
JT灬新一10 小时前
ARM驱动学习之 IOremap实现GPIO 读
arm开发·单片机·学习