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类银河恶魔城游戏学习笔记目录
文章目录
- Unity开发2D类银河恶魔城游戏学习笔记
- 前言
- 一、概述
- 二、创建骷髅攻击动画
- 三、骷髅攻击的实现
- 四、骷髅攻击的冷却
- [总结 完整代码](#总结 完整代码)
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节实现敌人的攻击状态。
对应视频:
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);
}
}
}