Unity开发2D类银河恶魔城游戏学习笔记
Unity教程(零)Unity和VS的使用相关内容
Unity教程(一)开始学习状态机
Unity教程(二)角色移动的实现
Unity教程(三)角色跳跃的实现
Unity教程(四)碰撞检测
Unity教程(五)角色冲刺的实现
Unity教程(六)角色滑墙的实现
Unity教程(七)角色蹬墙跳的实现
Unity教程(八)角色攻击的基本实现
Unity教程(九)角色攻击的改进
Unity教程(十)Tile Palette搭建平台关卡
如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录
文章目录
- Unity开发2D类银河恶魔城游戏学习笔记
- 前言
- 一、概述
- 二、基础攻击的改进
- (1)调整动画提升流畅度
- (2)解决攻击时滑动的问题
- (3)解决两次攻击间角色移动的问题
- (4)攻击间移动
- (5)添加攻击方向
- (6)加速操作(不需要做)
- 三、代码整理
- 总结 完整代码
- Player.cs
- PlayerPrimaryAttackState.cs
- PlayerIdleState.cs
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节进行角色基本攻击的改进。
对应b站视频:
【Unity教程】从0编程制作类银河恶魔城游戏P40
【Unity教程】从0编程制作类银河恶魔城游戏P41
一、概述
本节主要进行角色基本攻击的改进。
空闲状态到移动状态之间的转换添加条件 ! isBusy
本节改进了基本攻击的小问题,添加了攻击移动和攻击方向,提升了动画流畅度。在最后对代码进行了一些整理。整体结构如下:
二、基础攻击的改进
(1)调整动画提升流畅度
调整三个攻击动画的采样率,并且将最后一个攻击动画事件提前。
(2)解决攻击时滑动的问题
现在我们攻击时角色会来回滑动,因为我们的角色还保持着移动的速度但已经播放攻击动画了。
这里教程中的解决方式是在攻击时将角色速度置为0。
但我个人感觉这种处理方式会让攻击缺少一些灵活性。但如果想实现边移动边攻击就需要有另外的动画和另外的逻辑来实现,需要另行设计,所以在此先按教程中的实现。
此外,我们可以在进入时给stateTimer赋一个值,让角色停顿一下再攻击,做出惯性的效果。
在PlayerPrimaryAttack中修改:
//进入
public override void Enter()
{
base.Enter();
if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
comboCounter = 0;
player.anim.SetInteger("comboCounter",comboCounter);
stateTimer = 0.1f;
}
// 更新
public override void Update()
{
base.Update();
if (stateTimer < 0)
rb.velocity = new Vector2(0, 0);
if(triggerCalled)
stateMachine.ChangeState(player.idleState);
}
(3)解决两次攻击间角色移动的问题
我依然认为这种处理方法缺乏灵活性,但可以先学习一下他的处理方式。
如果需要解决这个问题,我们可以使用协程。我参照了以下文章的讲解:
进程、线程和协程之间的区别和联系
Unity 协程(Coroutine)原理与用法详解
Unity官方手册
协程是通过迭代器来实现功能的,通过关键字IEnumerator来定义。
启动协程:
StartCoroutine(IEnumerator routine:通过方法形式调用
StartCoroutine(string methodName,object values):带参数的通过方法名进行调用
停止携程:
StopCoroutine(string methodName:通过方法名(字符串)来进行
StopCoroutine(IEnumerator routine:通过方法形式来调用
StopCoroutine(Coroutine routine):通过指定的协程来关闭
yield方法:
yield return null; 暂停协程等待下一帧继续执行
yield return 0或其他数字; 暂停协程等待下一帧继续执行
yield return new WairForSeconds(时间); 等待规定时间后继续执行
yield return StartCoroutine(“协程方法名”); 开启一个协程(嵌套协程)
在Player中创建一个参数isBusy,并定义BusyFor函数
public bool isBusy { get; private set; }
public IEnumerator BusyFor(float _seconds)
{
isBusy = true;
yield return new WaitForSeconds(_seconds);
isBusy = false;
}
在每次攻击结束时调用,在PlayerPriamaryAttack
//退出
public override void Exit()
{
base.Exit();
player.StartCoroutine("BusyFor", 0.15f);
comboCounter++;
lastTimeAttacked = Time.time;
}
然后给空闲状态转到移动状态添加一个 ! Busy 的条件:
//更新
public override void Update()
{
base.Update();
//切换到移动状态
if(xInput!=0 && !player.isBusy)
stateMachine.ChangeState(player.moveState);
}
在攻击期间即使我一直按着移动键,角色也不能移动了,效果如下:
(4)攻击间移动
给连击中的每一段设置一个位移。
在Player中添加变量攻击位移数组
[Header("Attack details")]
public float[] attackMovement;
在开始攻击时设置位移
//进入
public override void Enter()
{
base.Enter();
if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
comboCounter = 0;
player.anim.SetInteger("comboCounter",comboCounter);
player.SetVelocity(player.attackMovement[comboCounter] * player.facingDir, rb.velocity.y);
stateTimer = 0.1f;
}
给数列赋值,调整数值直到你想要的效果
(5)添加攻击方向
现在的攻击还有一个小问题,在攻击后马上按相反方向键向角色身后攻击,我们会发现角色完全没有转向。
因为我们在攻击时始终是向着角色面向的方向,没有翻转。
我们在PlayerPrimaryAttack中添加这个功能:
//进入
public override void Enter()
{
base.Enter();
if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
comboCounter = 0;
player.anim.SetInteger("comboCounter",comboCounter);
float attackDir = player.facingDir;
if (xInput != 0)
attackDir = xInput;
player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);
stateTimer = 0.1f;
}
当没有输入时,向角色面向方向攻击;当有输入时,向输入方向攻击。现在角色可以迅速回身攻击了。
(6)加速操作(不需要做)
教程中顺便讲到了使所有动画加速的操作。这个操作可以实现不同武器不同攻速等操作,或者实现整体加速。
用player.anim.speed 进行实现,在进入攻击状态时加速,在退出时恢复原速。
//进入
public override void Enter()
{
base.Enter();
if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
comboCounter = 0;
player.anim.SetInteger("comboCounter",comboCounter);
player.anim.speed = 3.0f;
player.SetVelocity(player.attackMovement[comboCounter].x * player.facingDir, player.attackMovement[comboCounter].y);
stateTimer = 0.1f;
}
//退出
public override void Exit()
{
base.Exit();
player.StartCoroutine("BusyFor", 0.15f);
player.anim.speed = 1.0f;
comboCounter++;
lastTimeAttacked = Time.time;
}
为了效果明显,我调了三倍速。
三、代码整理
速度置零的操作,我们在Player中写一个函数ZeroVelocity()用来调用
//速度置零
public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);
在PlayerPrimaryAttack中改为调用函数
//PlayerPrimaryAttackState:基本攻击状态
// 更新
public override void Update()
{
base.Update();
if (stateTimer < 0)
player.ZeroVelocity();
if(triggerCalled)
stateMachine.ChangeState(player.idleState);
}
Player中代码划分区域
速度设置
#region 速度设置
//速度置零
public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);
//设置速度
public void SetVelocity(float _xVelocity, float _yVelocity)
{
rb.velocity = new Vector2(_xVelocity, _yVelocity);
FlipController(_xVelocity);
}
#endregion
翻转
#region 翻转
//翻转实现
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();
}
#endregion
碰撞
#region 碰撞
//碰撞检测
public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
public bool isWallDetected() => Physics2D.Raycast(wallCheck.position,Vector2.right * facingDir,wallCheckDistance,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));
}
#endregion
给PlayerPrimaryAttack改名为PlayerPrimaryAttackState
右键状态名->重命名->Enter
总结 完整代码
Player.cs
添加攻击位移变量
添加isBusy和协程
//Player:玩家
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[Header("Attack details")]
public Vector2[] attackMovement;
public bool isBusy { get; private set; }
[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; }
public PlayerWallSlideState wallSlideState { get; private set; }
public PlayerWallJumpState wallJumpState { get; private set; }
public PlayerPrimaryAttack primaryAttack { 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");
wallSlideState = new PlayerWallSlideState(StateMachine, this, "WallSlide");
wallJumpState = new PlayerWallJumpState(StateMachine, this, "Jump");
primaryAttack = new PlayerPrimaryAttack(StateMachine, this, "Attack");
anim = GetComponentInChildren<Animator>();
rb = GetComponent<Rigidbody2D>();
}
// 设置初始状态
private void Start()
{
StateMachine.Initialize(idleState);
}
// 更新
private void Update()
{
StateMachine.currentState.Update();
CheckForDashInput();
}
public IEnumerator BusyFor(float _seconds)
{
isBusy = true;
yield return new WaitForSeconds(_seconds);
isBusy = false;
}
//设置触发器
public void AnimationTrigger() => StateMachine.currentState.AnimationFinishTrigger();
//检查冲刺输入
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);
}
}
#region 速度设置
//速度置零
public void ZeroVelocity() => rb.velocity = new Vector2(0, 0);
//设置速度
public void SetVelocity(float _xVelocity, float _yVelocity)
{
rb.velocity = new Vector2(_xVelocity, _yVelocity);
FlipController(_xVelocity);
}
#endregion
#region 翻转
//翻转实现
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();
}
#endregion
#region 碰撞
//碰撞检测
public bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
public bool isWallDetected() => Physics2D.Raycast(wallCheck.position,Vector2.right * facingDir,wallCheckDistance,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));
}
#endregion
}
PlayerPrimaryAttackState.cs
攻击时速度置零
添加协程
添加攻击位移
添加攻击方向
//PlayerPrimaryAttackState:基本攻击状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerPrimaryAttackState : PlayerState
{
private int comboCounter;
private float lastTimeAttacked;
private float comboWindow = 2
;
public PlayerPrimaryAttackState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
{
}
//进入
public override void Enter()
{
base.Enter();
if (comboCounter > 2 || Time.time > lastTimeAttacked + comboWindow)
comboCounter = 0;
player.anim.SetInteger("comboCounter",comboCounter);
float attackDir = player.facingDir;
if (xInput != 0)
attackDir = xInput;
player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);
stateTimer = 0.1f;
}
//退出
public override void Exit()
{
base.Exit();
player.StartCoroutine("BusyFor", 0.15f);
comboCounter++;
lastTimeAttacked = Time.time;
}
// 更新
public override void Update()
{
base.Update();
if (stateTimer < 0)
player.ZeroVelocity();
if(triggerCalled)
stateMachine.ChangeState(player.idleState);
}
}
PlayerIdleState.cs
修改转到移动状态的条件
//PlayerIdleState:空闲状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
public class PlayerIdleState : PlayerGroundedState
{
//构造函数
public PlayerIdleState(PlayerStateMachine _stateMachine, Player _player, string _animBoolName) : base(_stateMachine, _player, _animBoolName)
{
}
//进入
public override void Enter()
{
base.Enter();
player.SetVelocity(0, rb.velocity.y);
}
//退出
public override void Exit()
{
base.Exit();
}
//更新
public override void Update()
{
base.Update();
//切换到移动状态
if(xInput!=0 && !player.isBusy)
stateMachine.ChangeState(player.moveState);
}
}