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教程(二十)战斗系统 角色反击
如果你更习惯用知乎
Unity开发2D类银河恶魔城游戏学习笔记目录
文章目录
- Unity开发2D类银河恶魔城游戏学习笔记
- 前言
- 一、概述
- 二、闪烁特效的实现
- 三、击退效果的实现
- 四、攻击方向问题的修复
- [总结 完整代码](#总结 完整代码)
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记,如有错误,欢迎指正。
本节实现战斗系统的受击反馈部分。
对应视频:
On Hit Fx
On Hit Impact
Attack's direction hot fix
一、概述
本节我们实现角色的受击反馈,包括闪烁特效,击退效果两部分。
为实现特效,创建了实体特效组件,用于存放作用于实体的特效。闪烁特效用两种材质交替实现。击退效果设置受击角色速度实现。
除此之外,还修复了学习过程中发现的攻击方向bug。
二、闪烁特效的实现
在开始之前我们先整理一下脚本文件,把相应脚本放入创建的文件夹中,文件整理大致如下:
(1)创建闪烁特效材质
首先我们在Materials文件夹中创建闪烁特效FlashFXMaterial。
在Project面板中
右键->Create->Material->重命名为FlashFXMaterial
Shader选择GUI->Text Shader ,Text Color选择白色
更换Playerd Animator的材质看一下效果
将原始材质与这个纯白的来回交替就可以产生闪烁的特效。
(2)实现闪烁特效脚本
首先我们创建一个特效组件EntityFX,用来实现实体的特效。
特效由更换精灵的材质实现,即要调用接口,修改对应实体下Animator中SpriteRenderer组件的Material属性。
首先要在EntityFX中获取相应SpriteRenderer组件,并设置两个变量分别存储原始材质和受击后的材质。还要创建变量flashDuration,定义受击后闪烁的时长。
csharp
//EntityFX:实体特效
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EntityFX : MonoBehaviour
{
private SpriteRenderer sr;
[Header("Flash FX")]
[SerializeField] private Material hitMat;
private Material originalMat;
[SerializeField] private float flashDuration;
private void Start()
{
sr = GetComponentInChildren<SpriteRenderer>();
originalMat = sr.material;
}
}
接着使用协程实现材质每隔一小段时间更替一次材质。
csharp
//闪烁特性
private IEnumerator FlashFX()
{
sr.material = hitMat;
yield return new WaitForSeconds(flashDuration);
sr.material = originalMat;
}
在Entity中创建特效组件EntityFX并赋值。
csharp
#region 组件
public EntityFX fx { get; private set; }
public Rigidbody2D rb { get; private set; }
public Animator anim { get; private set; }
#endregion
//获取组件
protected virtual void Start()
{
fx= GetComponent<EntityFX>();
rb= GetComponent<Rigidbody2D>();
anim= GetComponentInChildren<Animator>();
}
在Entity的Damage函数中调用闪烁特效。
csharp
public virtual void Damage()
{
fx.StartCoroutine("FlashFX");
Debug.Log(gameObject.name + " was damaged");
}
将EntityFX拖到Player上作为组件,拖入闪烁特效作为受击材质,并给闪烁持续时间赋一个合适的值。
骷髅小怪Enemy_Skeleton同理,最终效果如下:
三、击退效果的实现
击退效果我们依然用协程实现。在实体受到攻击时,设置实体后退速度,等待一小段时间后恢复原来速度设置。
在Entity中创建击退效果相关的变量,分别为击退方向,是否被击退,击退持续的时间。
cpp
[Header("Knockback Info")]
[SerializeField] protected Vector2 knockbackDirection;
[SerializeField] protected float knockbackDuration;
protected bool isKnocked;
创建一个协程HitKnockback设置击退速度。一个实体受到攻击后,后退方向与它面向的方向相反。这里存在一个问题,在玩家从背后攻击骷髅时,一般骷髅会在检测后迅速转身,但个别时候会发生它没有转身就受到攻击的情况,这时骷髅会向它朝向的方向跌,这个问题本节暂不解决。
isKnocked用来记录实体是否处于被击退的状态,在击退效果持续期间我们需要让其他涉及设置速度的操作都不生效,在击退结束后再恢复原来状态里的速度设置。
csharp
protected virtual IEnumerator HitKnockback()
{
isKnocked = true;
rb.velocity = new Vector2(knockbackDirection.x * -facingDir, knockbackDirection.y);
yield return new WaitForSeconds(knockbackDuration);
isKnocked= false;
}
在速度设置和速度置零函数中添加一个判定,当处于击退状态时直接return,不执行后面设置速度的部分,直到击退结束。
csharp
#region 速度设置
//速度置零
public void ZeroVelocity()
{
if (isKnocked)
return;
rb.velocity = new Vector2(0, 0);
}
//设置速度
public void SetVelocity(float _xVelocity, float _yVelocity)
{
if (isKnocked)
return;
rb.velocity = new Vector2(_xVelocity, _yVelocity);
FlipController(_xVelocity);
}
#endregion
在Damage函数中调用击退效果。
csharp
public virtual void Damage()
{
fx.StartCoroutine("FlashFX");
StartCoroutine("HitKnockback");
Debug.Log(gameObject.name + " was damaged");
}
骷髅受到攻击后直接飘出去了。加大重力就可以解决这一问题。但需要注意加大重力后,骷髅的移速也会变迟缓,可以根据需要设定。
四、攻击方向问题的修复
在学习过程中,发现了一个问题,在个别先向左后向右跑的时候,角色面向右进行攻击时,攻击方向却是向左。
问题出在下图位置。在基本攻击状态PlayerPrimaryAttackState中,我们原本设置的是攻击时如果有输入,攻击方向为输入方向,但在进入攻击状态时xInput并不是最新的。
xInput的定义在状态基类PlayerState中,它的赋值在Update函数中进行,PlayerPrimaryAttackState继承自状态基类,所以每次进入攻击状态获取的xInput都是上次攻击Update中获取的。
教程里的改法是在进入状态时,xInput赋0。我选择了在进入时,重新获取一次xInput。
csharp
//进入
public override void Enter()
{
base.Enter();
xInput = Input.GetAxisRaw("Horizontal");
//攻击方向问题教程改法
//xInput = 0;
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;
}
总结 完整代码
EntityFX.cs
实体特效组件。实现闪烁特效。
csharp
//EntityFX:实体特效
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EntityFX : MonoBehaviour
{
private SpriteRenderer sr;
[Header("Flash FX")]
[SerializeField] private Material hitMat;
private Material originalMat;
[SerializeField] private float flashDuration;
private void Start()
{
sr = GetComponentInChildren<SpriteRenderer>();
originalMat = sr.material;
}
//闪烁特效
private IEnumerator FlashFX()
{
sr.material = hitMat;
yield return new WaitForSeconds(flashDuration);
sr.material = originalMat;
}
}
Entity.cs
获取实体特效组件,调用闪烁特效。实现并调用击退效果,修改速度设置。
csharp
//Entity:实体类
using System.Collections;
using System.Collections.Generic;
using Unity.IO.LowLevel.Unsafe;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class Entity : MonoBehaviour
{
[Header("Knockback Info")]
[SerializeField] protected Vector2 knockbackDirection;
[SerializeField] protected float knockbackDuration;
protected bool isKnocked;
[Header("Flip Info")]
protected bool facingRight = true;
public int facingDir { get; private set; } = 1;
[Header("Collision Info")]
public Transform attackCheck;
public float attackCheckRadius;
[SerializeField] protected Transform groundCheck;
[SerializeField] protected float groundCheckDistance;
[SerializeField] protected Transform wallCheck;
[SerializeField] protected float wallCheckDistance;
[SerializeField] protected LayerMask whatIsGround;
#region 组件
public EntityFX fx { get; private set; }
public Rigidbody2D rb { get; private set; }
public Animator anim { get; private set; }
#endregion
protected virtual void Awake()
{
}
//获取组件
protected virtual void Start()
{
fx= GetComponent<EntityFX>();
rb= GetComponent<Rigidbody2D>();
anim= GetComponentInChildren<Animator>();
}
// 更新
protected virtual void Update()
{
}
public virtual void Damage()
{
fx.StartCoroutine("FlashFX");
StartCoroutine("HitKnockback");
Debug.Log(gameObject.name + " was damaged");
}
protected virtual IEnumerator HitKnockback()
{
isKnocked = true;
rb.velocity = new Vector2(knockbackDirection.x * -facingDir, knockbackDirection.y);
yield return new WaitForSeconds(knockbackDuration);
isKnocked= false;
}
#region 速度设置
//速度置零
public void ZeroVelocity()
{
if (isKnocked)
return;
rb.velocity = new Vector2(0, 0);
}
//设置速度
public void SetVelocity(float _xVelocity, float _yVelocity)
{
if (isKnocked)
return;
rb.velocity = new Vector2(_xVelocity, _yVelocity);
FlipController(_xVelocity);
}
#endregion
#region 翻转
//翻转实现
public virtual void Flip()
{
facingDir = -1 * facingDir;
facingRight = !facingRight;
transform.Rotate(0, 180, 0);
}
//翻转控制
public virtual void FlipController(float _x)
{
if (_x > 0 && !facingRight)
Flip();
else if (_x < 0 && facingRight)
Flip();
}
#endregion
#region 碰撞
//碰撞检测
public virtual bool isGroundDetected() => Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, whatIsGround);
public virtual bool isWallDetected() => Physics2D.Raycast(wallCheck.position, Vector2.right * facingDir, wallCheckDistance, whatIsGround);
//绘制碰撞检测
protected virtual 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));
Gizmos.DrawWireSphere(attackCheck.position, attackCheckRadius);
}
#endregion
}
PlayerPrimaryAttackState.cs
修复攻击方向问题。
csharp
//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();
xInput = Input.GetAxisRaw("Horizontal");
//攻击方向问题教程改法
//xInput = 0;
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);
}
}