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开发2D类银河恶魔城游戏学习笔记目录
文章目录
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节进行角色冲刺的实现。
对应视频:
Creating Dash State
Improving Dash State
一、概述
本节主要进行冲刺的实现和优化。
冲刺的切换条件如下:
我们还要对冲刺进行优化。有这几个小问题:
(1)解决空中无法进行冲刺的问题。我们改为在Player的Update函数中调用冲刺,这样我们可以在任何状态进行冲刺。
(2)解决空中冲刺过程中运动轨迹的问题。
(3)解决空中冲刺后没有落地就开始行走的问题。
(4)解决冲刺方向的问题,添加冲刺方向。
(5)解决连续冲刺冲出平台问题,为冲刺添加冷却,冷却未结束时无法进行冲刺。
切换条件如下:
具体类的实现如图:
二、冲刺的实现
(1)创建冲刺动画
层次面板中选中Animator,在Animation面板中创建动画playerDash
playerDash精灵表标号69-72,采样率改为10
具体讲解见Unity教程(零)Unity和VS的使用相关内容
连接状态机,并添加过渡条件Dash,并修改过渡设置
添加条件变量Dash,并连接过渡
Entry->playerDash的过渡,加条件变量
playerDash -> Exit的过渡,如图更改条件
(2)创建计时器
计时器在许多状态中都会用到,因此我们将它创建在状态基类playerState中,并在其中每帧更新。
protected float stateTimer;
public virtual void Update()
{
xInput = Input.GetAxisRaw("Horizontal");
player.anim.SetFloat("yVelocity", rb.velocity .y);
stateTimer -= Time.deltaTime;
}
DeltaTime 是连续帧之间的时间差,准确说是完成上一帧所用的时间,增量时间是实时变动的。举个例子:假如帧率是1秒30帧,那增量时间就是 1/30秒。帧率是1秒60帧,那增量时间就是 1/60 秒。
(3)冲刺状态切换的完成
首先创建playerDashSate,通过菜单生成构造函数和重写。
在Player中创建状态。
添加变量冲刺速度dashSpeed和冲刺持续时间dashDuration,下面是要添加的代码
[Header("Dash Info")]
public float dashSpeed=25f;
public float dashDuration=0.2f;
public PlayerDashState dashState { get; private set; }
//创建对象
private void Awake()
{
StateMachine = new PlayerStateMachine();
idleState = new PlayerIdleState(StateMachine, this, "Idle");
moveState = new PlayerMoveState(StateMachine, this, "Move");
jumpState = new PlayerJumpState(StateMachine, this, "Jump");
airState = new PlayerAirState(StateMachine, this, "Jump");
dashState = new PlayerDashState(StateMachine, this, "Dash");
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
}
首先我们在playerDashSate里给计时器赋值,确定冲刺的持续时间。要写在Enter函数中,因为每次进入冲刺状态都要有一个冲刺持续时间。
public override void Enter()
{
base.Enter();
stateTimer = player.dashDuration;
}
在playerGroudedState中写切换到冲刺状态
//更新
public override void Update()
{
base.Update();
if (Input.GetKeyDown(KeyCode.LeftShift))
stateMachine.ChangeState(player.dashState);
if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())
stateMachine.ChangeState(player.jumpState);
}
在playerDashState中写结束冲刺切换到空闲状态
public override void Update()
{
base.Update();
if (stateTimer < 0)
stateMachine.ChangeState(player.idleState);
}
完成状态切换后运行,会发现状态动画可以顺利播放,但角色仍保持在原地,像这样
(4)设置冲刺速度
我们要设置冲刺速度,之后角色才能动起来。这里设置速度基本与移动一致
public override void Update()
{
base.Update();
//设置冲刺速度
player.SetVelocity(player.facingDir * player.dashSpeed, rb.velocity.y);
//切换到空闲状态
if (stateTimer < 0)
stateMachine.ChangeState(player.idleState);
}
这时角色冲刺后会不停往前冲刺直至冲出平台,因为我们冲刺后就一直保持着冲刺速度。
我们只需要在冲刺结束时将x方向速度设置为0就可以解决了。由于冲刺结束后会切换空闲状态,我决定在进入空闲状态时将角色x速度设置为0。
//进入
public override void Enter()
{
base.Enter();
player.SetVelocity(0, 0);
}
结果如下:
二、冲刺的完善
(1)解决空中无法冲刺的问题
在游戏中,我们需要灵活的操作去躲避攻击或者平台跳跃,这就要求我们在几乎任何状态下都能进行冲刺去进行一些操作。
原来我们切换冲刺状态是在接地状态中进行,这就导致我们跳跃起来处于跳跃状态和空中状态时无法进行冲刺。所以我们改为直接在Player中进行冲刺。
// 更新
private void Update()
{
StateMachine.currentState.Update();
CheckForDashInput();
}
//检查冲刺输入
public void CheckForDashInput()
{
if (Input.GetKeyDown(KeyCode.LeftShift))
StateMachine.ChangeState(dashState);
}
现在我们可以在空中冲刺了。
(2)解决空中冲刺的速度问题
上面我们可以看到角色在冲刺时掉落的运动轨迹有些奇怪。这是因为我们在跳跃时切换冲刺,跳跃状态y方向的速度不会马上消失而是继续保持,这时角色的速度是斜向上的,然后y方向速度会继续保持跳跃时的状态,角色接着会在冲刺过程中下落,这样就导致了角色轨迹是一条弧线。这不方便我们做一些操作。
要改变这一点我们只需在冲刺时不再保持y方向速度,而是直接设置为0即可。在playerDashState中更改:
//更新
public override void Update()
{
base.Update();
//设置冲刺速度
player.SetVelocity(player.facingDir * player.dashSpeed, 0);
//切换到空闲状态
if (stateTimer < 0)
stateMachine.ChangeState(player.idleState);
}
这样我们的角色就会基本直着冲刺,不会在冲刺过程中往下掉啦
(3)解决空中冲刺后没有落地就开始行走的问题
为了看的更明显一点我调小重力,调整跳跃高度,设置了一个比较极端的数据。你会看到角色冲刺结束后开始“空中漫步”。
产生这个问题是因为角色在冲刺结束后还没有落地,于是直接在空中转换为空闲状态也就相当于没有判定是否接地直接转换为了接地状态,这时按方向键角色会转换为移动状态直接播放移动动画。
要解决这个问题很简单,只需要在接地状态时添加一个地面检测,如果没有检测到地面则转换为空中状态
//更新
public override void Update()
{
base.Update();
if(!player.isGroundDetected())
stateMachine.ChangeState(player.airState);
if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())
stateMachine.ChangeState(player.jumpState);
}
现在在冲刺后再按左右移动角色不会再有变化
但这样又产生一个问题角色在下落过程中没法左右移动,这样在游戏中我们就没法在下落时进行一些移动和躲避,这不符合我们的控制习惯,所以我们在空中状态添加一个角色速度设置接收输入。
我们在进行移动状态的实现时提到,在水平方向的输入有许多状态都会遇到,于是我们把水平收入的获取放在了为了便于在空中调整我们让方向速度比在地面上稍小一点。
//更新
public override void Update()
{
base.Update();
//落地切换到空闲状态
if(player.isGroundDetected())
stateMachine.ChangeState(player.idleState);
//设置下落速度
if(xInput!=0)
player.SetVelocity(0.8f * xInput * player.moveSpeed, rb.velocity.y);
}
角色在下落过程中可以正常移动
正常速度和重力下是这样
(4)解决冲刺方向的问题
在运行时你会发现这样一个问题,在角色移动时同时按下冲刺键和方向键,无论你按的方向是什么,你只能向角色面向的方向冲刺。在这里我向右移动时,同时按下了冲刺键和向左,但角色先向右冲刺一段距离后再改为面向左。
为了解决这个问题,我们添加一个冲刺方向,在冲刺过程中先接收一个冲刺方向的输入再进行冲刺,当没有输入时我们以面向方向为冲刺方向。
注意:要先获取冲刺方向再改变状态
Player中的代码添加
[Header("Dash Info")]
public float dashSpeed=25f;
public float dashDuration=0.2f;
public float dashDir { get; private set; }
// 更新
private void Update()
{
StateMachine.currentState.Update();
CheckForDashInput();
}
//检查冲刺输入
public void CheckForDashInput()
{
if (Input.GetKeyDown(KeyCode.LeftShift))
{
dashDir = Input.GetAxisRaw("Horizontal");
if (dashDir == 0)
dashDir = facingDir;
StateMachine.ChangeState(dashState);
}
}
加上之后反应灵敏了一点,但上面的情况并没有完全避免,还要以后想办法解决。
(5)解决冲刺冷却的问题
我们的冲刺现在写在player中,无论什么状态都可以切换冲刺,这就导致了一个问题。当我们连续不断的按冲刺,角色就会直接冲出去。
为了解决这个问题,我们需要给冲刺添加冷却。
在Player中添加冷却时间和计时器。在面板中设置dashCoolDown的值。
[Header("Dash Info")]
[SerializeField] private float dashCoolDown=1.0f;
private float dashUsageTimer;
public float dashSpeed=25f;
public float dashDuration=0.2f;
public float dashDir { get; private set; }
//检查冲刺输入
public void CheckForDashInput()
{
dashUsageTimer -= Time.deltaTime;
if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer<0)
{
dashUsageTimer = dashCoolDown;
dashDir = Input.GetAxisRaw("Horizontal");
if (dashDir == 0)
dashDir = facingDir;
StateMachine.ChangeState(dashState);
}
}
连续冲刺的问题解决,冲刺功能完成。
总结 完整代码
PlayerState.cs
添加状态计时器
//PlayerState:状态基类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerState
{
protected PlayerStateMachine stateMachine;
protected Player player;
protected Rigidbody2D rb;
protected float xInput;
private string animBoolName;
protected float stateTimer;
//构造函数
public PlayerState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName)
{
this.stateMachine = _stateMachine;
this.player = _player;
this.animBoolName = _animBoolName;
}
//进入状态
public virtual void Enter()
{
//进入动画播放条件变量设置
player.anim.SetBool(animBoolName, true);
//获取刚体
rb = player.rb;
}
//更新状态
public virtual void Update()
{
xInput = Input.GetAxisRaw("Horizontal");
player.anim.SetFloat("yVelocity", rb.velocity .y);
stateTimer -= Time.deltaTime;
}
//退出状态
public virtual void Exit()
{
//退出动画播放条件变量设置
player.anim.SetBool(animBoolName, false);
}
}
PlayerDashState.cs
重置计时器、设置冲刺速度以及切换到空闲状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerDashState : PlayerState
{
//构造函数
public PlayerDashState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
{
}
//进入状态
public override void Enter()
{
base.Enter();
//设置冲刺持续时间
stateTimer = player.dashDuration;
}
//退出状态
public override void Exit()
{
base.Exit();
}
//更新
public override void Update()
{
base.Update();
//设置冲刺速度
player.SetVelocity(player.dashDir * player.dashSpeed, 0);
//切换到空闲状态
if (stateTimer < 0)
stateMachine.ChangeState(player.idleState);
}
}
Player.cs
冲刺功能实现转移到这里,添加冲刺冷却,冲刺方向
//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[Header("Move Info")]
public float moveSpeed = 8f;
public int facingDir { get; private set; } = 1;
private bool facingRight = true;
public float jumpForce = 12f;
[Header("Dash Info")]
[SerializeField] private float dashCoolDown;
private float dashUsageTimer;
public float dashSpeed=25f;
public float dashDuration=0.2f;
public float dashDir { get; private set; }
[Header("Collision Info")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckDistance;
[SerializeField] private Transform wallCheck;
[SerializeField] private float wallCheckDistance;
[SerializeField] private LayerMask whatIsGround;
#region 组件
public Animator anim { get; private set; }
public Rigidbody2D rb { get; private set; }
#endregion
#region 状态
public PlayerStateMachine StateMachine { get; private set; }
public PlayerIdleState idleState { get; private set; }
public PlayerMoveState moveState { get; private set; }
public PlayerJumpState jumpState { get; private set; }
public PlayerAirState airState { get; private set; }
public PlayerDashState dashState { get; private set; }
#endregion
//创建对象
private void Awake()
{
StateMachine = new PlayerStateMachine();
idleState = new PlayerIdleState(StateMachine, this, "Idle");
moveState = new PlayerMoveState(StateMachine, this, "Move");
jumpState = new PlayerJumpState(StateMachine, this, "Jump");
airState = new PlayerAirState(StateMachine, this, "Jump");
dashState = new PlayerDashState(StateMachine, this, "Dash");
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
}
// 设置初始状态
private void Start()
{
StateMachine.Initialize(idleState);
}
// 更新
private void Update()
{
StateMachine.currentState.Update();
CheckForDashInput();
}
//检查冲刺输入
public void CheckForDashInput()
{
dashUsageTimer -= Time.deltaTime;
if (Input.GetKeyDown(KeyCode.LeftShift) && dashUsageTimer<0)
{
dashUsageTimer = dashCoolDown;
dashDir = Input.GetAxisRaw("Horizontal");
if (dashDir == 0)
dashDir = facingDir;
StateMachine.ChangeState(dashState);
}
}
//设置速度
public void SetVelocity(float _xVelocity, float _yVelocity)
{
rb.velocity = new Vector2(_xVelocity, _yVelocity);
FlipController(_xVelocity);
}
//翻转实现
public void Flip()
{
facingDir = -1 * facingDir;
facingRight = !facingRight;
transform.Rotate(0, 180, 0);
}
//翻转控制
public void FlipController(float _x)
{
if (_x > 0 && !facingRight)
Flip();
else if(_x < 0 && facingRight)
Flip();
}
//碰撞检测
public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
//绘制碰撞检测
private void OnDrawGizmos()
{
Gizmos.DrawLine(groundCheck.position, new Vector3(groundCheck.position.x, groundCheck.position.y - groundCheckDistance));
Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x+ wallCheckDistance, wallCheck.position.y));
}
}
PlayerGroundedState.cs
添加切换到空中状态,解决空中漫步问题
//超级状态PlayerGroundedState:接地状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerGroundedState : PlayerState
{
//构造函数
public PlayerGroundedState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
{
}
//进入
public override void Enter()
{
base.Enter();
}
//退出
public override void Exit()
{
base.Exit();
}
//更新
public override void Update()
{
base.Update();
if(!player.isGroundDetected())
stateMachine.ChangeState(player.airState);
if (Input.GetKeyDown(KeyCode.Space)&& player.isGroundDetected())
stateMachine.ChangeState(player.jumpState);
}
}
PlayerAirState
解决下落过程中角色方向控制问题
//超级状态PlayerAirState:空中状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAirState : PlayerState
{
//构造函数
public PlayerAirState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
{
}
//进入
public override void Enter()
{
base.Enter();
}
//退出
public override void Exit()
{
base.Exit();
}
//更新
public override void Update()
{
base.Update();
//落地切换到空闲状态
if(player.isGroundDetected())
stateMachine.ChangeState(player.idleState);
//设置下落速度
if(xInput!=0)
player.SetVelocity(0.8f * xInput * player.moveSpeed, rb.velocity.y);
}
}