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



前言

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

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

Udemy课程地址

对应视频:
Enemy’s Battle State


一、概述

本节中我们实现骷髅的战斗状态。
骷髅小怪在检测到玩家时,会向着玩家移动。在移动到一定距离时停止移动进入攻击状态。
由于我们想让骷髅在空闲和移动时均能转入战斗状态,我们创建超级状态接地状态,直接写接地状态到战斗状态的切换即可。
状态切换条件如下:
在这里插入图片描述

在这里插入图片描述

二、实现Player的检测

玩家的检测在Enemy基类中实现。
碰撞检测的详细讲解见Unity教程(四)碰撞检测

这里我们不再仅仅使用bool类型作为函数的返回值,而是使用RaycastHit2D返回更详细的信息。
RaycastHit2DUnity官方手册
Raycast包含的变量如下表:

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

我们要新建一个过滤器WhatIsPlayer进行检测。
这里我们以骷髅的位置为起点,向它面向的方向发射射线检测,检测的最大距离设置为50.

为了方便调试我们还要绘制出攻击检测的线条,在Enemy里对实体中的OnDrawGizmos()函数进行重写。
攻击检测的线条绘制:起点为Enemy_Skeleton的位置,向右attackDistance找到终点。
在Enemy中添加如下代码:

    [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,传递骷髅小怪独有的变量和函数,并修改构造函数。

//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,并修改构造函数。

    private Enemy_Skeleton enemy;

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

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

 #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()函数中添加

    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,玩家在骷髅左边,移动方向向左

//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中添加如下代码:

    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

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

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

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

//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

修改构造函数

//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

//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

创建战斗状态,实现切换到攻击状态,和随玩家移动改变方向。

//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

创建战斗状态

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

}

回合制自己写的 战斗DEMO 加动画状态 Q键移动并攻击。 代码很简单。不要抱有太大希望 。作为新手学习使用。 public enum HeroStatus { idle = 0, //空闲 attack, //攻击 other, //其他 hit, //受击 die, //死亡 defense, //防御 cast, //施法 miss, //闪避 seriousInjury, //重伤 move, //移动 exit, //获取下一个状态 MAX, } public class hero : MonoBehaviour { public UISpriteAnimation m_spriteAnimation = null; public HeroStatus m_staus = HeroStatus.exit; //身体碰撞盒 public BoxCollider m_bodyBC = null; public string m_name = ""; public AttackCrash m_attackCrash = null; public byte m_posIndex = 0; void Awake() { m_spriteAnimation = transform.GetComponent(); m_bodyBC = transform.GetComponent(); } // Use this for initialization void Start () { m_spriteAnimation.AddFrameCallBack("attack", 1, AttackCallBack); m_spriteAnimation.AddFrameCallBack("attack", 4, AttackCallBack); m_spriteAnimation.AddFrameCallBack("attack", 7, AttackCallBack); } public float m_speed = 1f; public float m_offset = 0.5f; public Vector3 m_destination = Vector3.zero; public List m_actionList = new List(); // Update is called once per frame void Update () { switch (m_staus) { case HeroStatus.idle: //空闲状态 if (m_actionList.Count > 0) m_staus = HeroStatus.exit; break; case HeroStatus.attack: if (!m_spriteAnimation.isPlaying) { //攻击动画播放完毕 m_staus = HeroStatus.exit; } break; case HeroStatus.defense: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.hit: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.die: break; case HeroStatus.cast: break; case HeroStatus.miss: break; case HeroStatus.seriousInjury: break; case HeroStatus.move: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.exit: //获取下一个状态 if (m_actionList.Count > 0) { string str = "idle"; switch(m_actionList[0].status) { case HeroStatus.move: str = "idle"; break; default: str = Enum.GetName(typeof(HeroStatus), m_actionList[0].status); break; } m_spriteAnimation.namePrefix = str; m_spriteAnimation.loop = m_actionList[0].loop; m_destination = m_actionList[0].destinationMove; m_speed = m_actionList[0].speed; m_spriteAnimation.ResetToBeginning(); m_staus = m_actionList[0].status; m_actionList.RemoveAt(0); } else { m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); m_spriteAnimation.loop = true; m_spriteAnimation.ResetToBeginning(); m_staus = HeroStatus.idle; } break; // case HeroStatus.moveback: // //transform.position = Vector3.SmoothDamp(transform.position, destinationMove, ref cameraVelocity, smoothTime); // transform.position = Vector3.MoveTowards(transform.position, destinationMove, m_speed * Time.deltaTime); // if (transform.position == destinationMove) // m_staus = HeroStatus.idle; // break; } } public void SetPosition(byte pos, float x, float y) { m_posIndex = pos; transform.localPosition = new Vector3(x, y); } public void AttackCallBack() { //创建攻击特效 A攻击B B掉血222 B反击A闪避 UnityEngine.Object sourceObj = Resources.Load("AttackCrash"); GameObject go = UnityEngine.Object.Instantiate(sourceObj) as GameObject; go.transform.parent = transform; go.transform.localScale = Vector3.one; go.transform.localPosition = new Vector3(-70, 0, 0); } private void OnCollisionEnter(Collision co) { //进入碰撞 Debug.Log("进入碰撞!"); UnityEngine.Object sourceObj = Resources.Load("Effect"); GameObject go = UnityEngine.Object.Instantiate(sourceObj) as GameObject; go.transform.parent = transform; go.transform.localScale = Vector3.one; go.transform.localPosition = new Vector3(0, 0, 0); //Defense(); Hit(); } public void Attack(GameObject aims) { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.move; tmpAD.loop = true; UISprite tmpS = transform.GetComponent(); tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(aims.transform.localPosition.x + (tmpS.width/2), aims.transform.localPosition.y)); tmpAD.speed = Vector3.Distance(transform.position, tmpAD.destinationMove) * 4; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.attack; tmpAD1.loop = false; tmpAD1.destinationMove = Vector3.zero; tmpAD1.speed = 0; m_actionList.Add(tmpAD1); ActionData tmpAD2 = new ActionData(); tmpAD2.status = HeroStatus.move; tmpAD2.loop = true; tmpAD2.destinationMove = transform.position; tmpAD2.speed = Vector3.Distance(tmpAD.destinationMove, tmpAD2.destinationMove) * 4; //4/1秒到达目的地 m_actionList.Add(tmpAD2); } public void Defense() { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.defense; tmpAD.loop = false; tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(transform.localPosition.x - 25, transform.localPosition.y)); tmpAD.speed = 0.25f; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.move; tmpAD1.loop = false; tmpAD1.destinationMove = transform.position; tmpAD1.speed = 0.8f; //4/1秒到达目的地 m_actionList.Add(tmpAD1); } public void Hit() { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.hit; tmpAD.loop = false; tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(transform.localPosition.x - 25, transform.localPosition.y)); tmpAD.speed = 0.3f; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.move; tmpAD1.loop = false; tmpAD1.destinationMove = transform.position; tmpAD1.speed = 0.8f; //4/1秒到达目的地 m_actionList.Add(tmpAD1); } public void Idle() { m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); m_spriteAnimation.loop = true; m_spriteAnimation.ResetToBeginning(); m_staus = HeroStatus.idle; } public void Move() { // //transform.GetComponent().depth = 99; // GameObject go = GameObject.Find("enemy").gameObject; // destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(go.transform.localPosition.x + 98,go.transform.localPosition.y,go.transform.localPosition.z)); // m_speed = Vector3.Distance(transform.position, destinationMove) * 4; //4/1秒到达目的地 // m_staus = HeroStatus.moveto; // m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); // m_spriteAnimation.loop = true; // m_spriteAnimation.ResetToBeginning(); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值