[Unity Demo]从零开始制作空洞骑士Hollow Knight第十集:制作后坐力系统Recoil和小骑士的生命系统和受伤系统
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作后坐力系统Recoil
- 1.使用代码实现扩展新的后坐力行为
- 2.为敌人脚本添加后坐力行为
- 3.为小骑士添加后坐力行为
- 二、制作小骑士的生命系统和受伤系统
- 1.制作动画以及使用UNITY编辑器编辑
- 2.使用代码制作生命系统和受伤系统
- 总结
前言
警告:此篇文章难度比上一篇还高,非常不适合刚刚入门的或者没看过我前几期的读者,我特么又做了一整天才把明显的bug给解决了,因此请读者如果在阅读后感到身体不适请立刻退出这篇文章。
本期主要涉及的内容是:制作后坐力系统Recoil和小骑士的生命系统和受伤系统,每一个大纲我都已经设置好分类,OK话不多说直接开Code!
一、制作后坐力系统Recoil
1.使用代码实现扩展新的后坐力行为
为每一个敌人添加一个新的脚本叫Recoil.cs:
在打代码之前我们先想想后坐力行为有几种状态:Ready准备进入状态,Frozen冻结状态,Recoiling正在后坐力状态
需要的变量:private float recoilDuration; //后坐力持续时间
private float recoilSpeedBase = 15f; //基本后坐力速度
public bool freezeInPlace; //是否会不动
以及取消后坐力后的事件,冻结时的事件:
public delegate void CancelRecoilEvent();
public event CancelRecoilEvent OnCancelRecoil;
public delegate void FreezeEvent();
public event FreezeEvent OnHandleFreeze;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Recoil : MonoBehaviour
{
private Rigidbody2D rb2d;
private Collider2D bodyCollider;
[SerializeField] private bool recoilUp; //是否有向上的后坐力
[SerializeField] private float recoilSpeedBase = 15f; //基本后坐力速度
[SerializeField] private float recoilDuration; //后坐力持续时间
[SerializeField] public bool freezeInPlace; //是否会不动
[SerializeField] private bool stopVelocityXWhenRecoilingUp; //当正在受上后坐力的时候停止X轴方向的速度
[SerializeField] private bool preventRecoilUp;
private bool skipFreezingByController;
[SerializeField]private States state;
private float recoilTimeRemaining; //后坐力持续时间
private float recoilSpeed;//最终后坐力的速度
private Sweep recoilSweep; //检测地形
private bool isRecoilSweeping; //是否
private const int SweepLayerMask = 256; //也就是Layer "Terrain"
public delegate void CancelRecoilEvent();
public event CancelRecoilEvent OnCancelRecoil;
public delegate void FreezeEvent();
public event FreezeEvent OnHandleFreeze;
public bool SkipFreezingByController
{
get
{
return skipFreezingByController;
}
set
{
skipFreezingByController = value;
}
}
public bool IsRecoiling
{
get
{
return state == States.Recoiling || state == States.Frozen;
}
}
protected void Reset()
{
freezeInPlace = false;
stopVelocityXWhenRecoilingUp = true;
recoilDuration = 0.5f;
recoilSpeedBase = 15f;
preventRecoilUp = false;
}
protected void Awake()
{
rb2d = GetComponent<Rigidbody2D>();
bodyCollider = GetComponent<BoxCollider2D>();
}
private void OnEnable()
{
CancelRecoil();
}
protected void FixedUpdate()
{
UpdatePhysics(Time.fixedDeltaTime);
}
/// <summary>
/// 更新游戏对象的物理行为
/// </summary>
/// <param name="deltaTime"></param>
private void UpdatePhysics(float deltaTime)
{
if(state == States.Frozen)
{
if(rb2d != null)
{
rb2d.velocity = Vector2.zero;
}
recoilTimeRemaining -= deltaTime;
if(recoilTimeRemaining <= 0f)
{
CancelRecoil();
return;
}
}
else if(state == States.Recoiling)
{
if (isRecoilSweeping)
{
float num;
if(recoilSweep.Check(transform.position,recoilSpeed * deltaTime, SweepLayerMask,out num))
{
isRecoilSweeping = false;
}
if(num > Mathf.Epsilon)
{
transform.Translate(recoilSweep.Direction * num, Space.World);
}
}
recoilTimeRemaining -= deltaTime;
if (recoilTimeRemaining <= 0f)
{
CancelRecoil();
}
}
}
/// <summary>
/// 在某个方向上受后坐力的行为
/// </summary>
/// <param name="attackDirection"></param>
/// <param name="attackMagnitude"></param>
public void RecoilByDirection(int attackDirection,float attackMagnitude)
{
if(state != States.Ready)
{
return;
}
if (freezeInPlace)
{
Freeze();
return;
}
if(attackDirection == 1&& preventRecoilUp)
{
return;
}
if (bodyCollider == null)
{
bodyCollider = GetComponent<Collider2D>();
}
state = States.Recoiling;
recoilSpeed = recoilSpeedBase * attackMagnitude;
recoilSweep = new Sweep(bodyCollider, attackDirection, 3, 0.1f);
isRecoilSweeping = true;
recoilTimeRemaining = recoilDuration;
switch (attackDirection)
{
case 0:
FSMUtility.SendEventToGameObject(gameObject, "RECOIL HORIZONTAL", false);
FSMUtility.SendEventToGameObject(gameObject, "HIT RIGHT", false);
break;
case 1:
FSMUtility.SendEventToGameObject(gameObject, "HIT UP", false);
break;
case 2:
FSMUtility.SendEventToGameObject(gameObject, "RECOIL HORIZONTAL", false);
FSMUtility.SendEventToGameObject(gameObject, "HIT LEFT", false);
break;
case 3:
FSMUtility.SendEventToGameObject(gameObject, "HIT DOWN", false);
break;
}
UpdatePhysics(0f);
}
/// <summary>
/// 冻结状态发生的行为
/// </summary>
private void Freeze()
{
if (skipFreezingByController)
{
if (OnHandleFreeze != null)
{
OnHandleFreeze();
}
state = States.Ready;
return;
}
state = States.Frozen;
if(rb2d != null)
{
rb2d.velocity = Vector2.zero;
}
PlayMakerFSM playMakerFSM = PlayMakerFSM.FindFsmOnGameObject(gameObject, "Climber Control");
if(playMakerFSM != null)
{
playMakerFSM.SendEvent("FREEZE IN PLACE");
}
recoilTimeRemaining = recoilDuration;
UpdatePhysics(0f);
}
public void CancelRecoil()
{
if(state != States.Ready)
{
state = States.Ready;
if (OnCancelRecoil != null)
{
OnCancelRecoil();
}
}
}
public void SetRecoilSpeed(float newSpeed)
{
recoilSpeedBase = newSpeed;
}
private enum States
{
Ready,
Frozen,
Recoiling
}
}
还记得我们制作敌人行为(我记得是Walker.cs)的时候用到的Sweep结构体,当时被用于与地面的碰撞检测,这一次我们还是要用到它,这次我再把代码贴出来吧:
using System;
using UnityEngine;
public struct Sweep
{
public int CardinalDirection;//基数(1-9的数字)的方向
public Vector2 Direction;
public Vector2 ColliderOffset;
public Vector2 ColliderExtents;
public float SkinThickness;
public int RayCount;
public const float DefaultSkinThickness = 0.1f;
public const int DefaultRayCount = 3;
public Sweep(Collider2D collider, int cardinalDirection, int rayCount, float skinThickness = DefaultSkinThickness)
{
CardinalDirection = cardinalDirection;
Direction = new Vector2(DirectionUtils.GetX(cardinalDirection), DirectionUtils.GetY(cardinalDirection));
ColliderOffset = collider.offset.MultiplyElements(collider.transform.localScale);
ColliderExtents = collider.bounds.extents;
RayCount = rayCount;
SkinThickness = skinThickness;
}
public bool Check(Vector2 offset, float distance, int layerMask)
{
float num;
return Check(offset, distance, layerMask, out num);
}
public bool Check(Vector2 offset, float distance, int layerMask, out float clippedDistance)
{
if (distance <= 0f)
{
clippedDistance = 0f;
return false;
}
Vector2 a = ColliderOffset + Vector2.Scale(ColliderExtents, Direction);
Vector2 a2 = Vector2.Scale(ColliderExtents, new Vector2(Mathf.Abs(Direction.y), Mathf.Abs(Direction.x)));
float num = distance;
for (int i = 0; i < RayCount; i++)
{
float d = 2f * ((float)i / (float)(RayCount - 1)) - 1f;
Vector2 b = a + a2 * d + Direction * -SkinThickness;
Vector2 vector = offset + b;
RaycastHit2D hit = Physics2D.Raycast(vector, Direction, num + SkinThickness, layerMask);
float num2 = hit.distance - SkinThickness;
if (hit && num2 < num)
{
num = num2;
Debug.DrawLine(vector, vector + Direction * hit.distance, Color.red);
}
else
{
Debug.DrawLine(vector, vector + Direction * (distance + SkinThickness), Color.green);
}
}
clippedDistance = num;
return distance - num > Mathf.Epsilon;
}
}
2.为敌人脚本添加后坐力行为
然后回到我们之间创作的代码,为敌人脚本添加上后坐力的系统,这里先从Climber.cs开始,在Start()函数中:我们订阅这个事件并实现这个事件Stun:速度设置为0,播放Stun动画后回到Walk()状态,
Recoil component = GetComponent<Recoil>();
if (component)
{
component.SkipFreezingByController = true;
component.OnHandleFreeze += Stun;
}
public void Stun()
{
if(turnRoutine == null)
{
StopAllCoroutines();
StartCoroutine(DoStun());
}
}
private IEnumerator DoStun()
{
body.velocity = Vector2.zero;
yield return StartCoroutine(anim.PlayAnimWait("Stun"));
StartCoroutine(Walk());
}
完整的代码如下:
using System;
using System.Collections;
using UnityEngine;
public class Climber : MonoBehaviour
{
private tk2dSpriteAnimator anim;
private Rigidbody2D body;
private BoxCollider2D col;
public bool startRight; //开始的方向是右边
private bool clockwise; //是否顺时针旋转
public float speed;//移动速度
public float spinTime; //旋转时间
[Space]
public float wallRayPadding; //墙壁射线检测距离
[Space]
public Vector2 constrain; //束缚
public float minTurnDistance; //最小转向距离
private Vector2 previousPos;
private Vector2 previousTurnPos;
[SerializeField]private Direction currentDirection; //Debug用,发现没问题可以删了[SerializeField]
private Coroutine turnRoutine; //给转向设置为协程,循序渐进的实现转身的效果
public Climber()
{
startRight = true;
clockwise = true;
speed = 2f;
spinTime = 0.25f;
wallRayPadding = 0.1f;
constrain = new Vector2(0.1f, 0.1f);
minTurnDistance = 0.25f;
}
private void Awake()
{
//公式化三件套
anim = GetComponent<tk2dSpriteAnimator>();
body = GetComponent<Rigidbody2D>();
col = GetComponent<BoxCollider2D>();
}
private void Start()
{
StickToGround();
float num = Mathf.Sign(transform.localScale.x);
if (!startRight)
{
num *= -1f;
}
clockwise = num > 0f; //判断是顺时针还是逆时针
float num2 = transform.eulerAngles.z % 360f;
//获取开始游戏时climber当前方向
if(num2 > 45f && num2 <= 135f)
{
currentDirection = clockwise ? Direction.Up : Direction.Down;
}
else if(num2 > 135f && num2 <= 225f)
{
currentDirection = clockwise ? Direction.Left : Direction.Right;
}
else if (num2 > 225f && num2 <= 315f)
{
currentDirection = clockwise ? Direction.Down : Direction.Up;
}
else
{
currentDirection = clockwise ? Direction.Right : Direction.Left;
}
Recoil component = GetComponent<Recoil>();
if (component)
{
component.SkipFreezingByController = true;
component.OnHandleFreeze += Stun;
}
previousPos = transform.position;
StartCoroutine(Walk());
}
private IEnumerator Walk()
{
anim.Play("Walk");
body.velocity = GetVelocity(currentDirection);
for(; ; )
{
Vector2 vector = transform.position;
bool flag = false;
if(Mathf.Abs(vector.x - previousPos.x) > constrain.x)
{
vector.x = previousPos.x;
flag = true;
}
if (Mathf.Abs(vector.y - previousPos.y) > constrain.y)
{
vector.y = previousPos.y;
flag = true;
}
if(flag)
{
transform.position = vector;
}
else
{
previousPos = transform.position;
}
if (Vector3.Distance(previousTurnPos, transform.position) >= minTurnDistance)
{
if (!CheckGround())
{
turnRoutine = StartCoroutine(Turn(clockwise, false));
yield return turnRoutine;
}
else if (CheckWall()) //当不在地面上以及碰到墙壁后挂机并执行Turn协程
{
turnRoutine = StartCoroutine(Turn(!clockwise, true));
yield return turnRoutine;
}
}
yield return null;
}
}
private IEnumerator Turn(bool turnClockwise, bool tweenPos = false)
{
body.velocity = Vector2.zero;
float currentRotation = transform.eulerAngles.z;
float targetRotation = currentRotation + (turnClockwise ? -90 : 90);
Vector3 currentPosition = transform.position;
Vector3 targetPosition = currentPosition + GetTweenPos(currentDirection);
for (float elapsed = 0f; elapsed < spinTime; elapsed += Time.deltaTime)
{
float t = elapsed / spinTime;
transform.SetRotation2D(Mathf.Lerp(currentRotation, targetRotation, t)); //更改rotation和position
if (tweenPos)
{
transform.position = Vector3.Lerp(currentPosition, targetPosition, t);
}
yield return null;
}
transform.SetRotation2D(targetRotation);
int num = (int)currentDirection;
num += (turnClockwise ? 1 : -1);
int num2 = Enum.GetNames(typeof(Direction)).Length; //4
//防止数字超出枚举长度或者小于0
if(num < 0)
{
num = num2 - 1;
}
else if(num >= num2)
{
num = 0;
}
currentDirection = (Direction)num;
body.velocity = GetVelocity(currentDirection);
previousPos = transform.position;
previousTurnPos = previousPos;
turnRoutine = null;
}
/// <summary>
/// 不同方向上赋值的速度不同
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
private Vector2 GetVelocity(Direction direction)
{
Vector2 zero = Vector2.zero;
switch (direction)
{
case Direction.Right:
zero = new Vector2(speed, 0f);
break;
case Direction.Down:
zero = new Vector2(0f, -speed);
break;
case Direction.Left:
zero = new Vector2(-speed, 0f);
break;
case Direction.Up:
zero = new Vector2(0f, speed);
break;
}
return zero;
}
private bool CheckGround()
{
return FireRayLocal(Vector2.down, 1f).collider != null;
}
private bool CheckWall()
{
return FireRayLocal(clockwise ? Vector2.right : Vector2.left, col.size.x / 2f + wallRayPadding).collider != null;
}
/// <summary>
/// 以后做到人物攻击时才要用到
/// </summary>
public void Stun()
{
if(turnRoutine == null)
{
StopAllCoroutines();
StartCoroutine(DoStun());
}
}
private IEnumerator DoStun()
{
body.velocity = Vector2.zero;
yield return StartCoroutine(anim.PlayAnimWait("Stun"));
StartCoroutine(Walk());
}
private RaycastHit2D FireRayLocal(Vector2 direction, float length)
{
Vector2 vector = transform.TransformPoint(col.offset);
Vector2 vector2 = transform.TransformDirection(direction);
RaycastHit2D result = Physics2D.Raycast(vector, vector2, length, LayerMask.GetMask("Terrain"));
Debug.DrawRay(vector, vector2);
return result;
}
private Vector3 GetTweenPos(Direction direction)
{
Vector2 result = Vector2.zero;
switch (direction)
{
case Direction.Right:
result = (clockwise ? new Vector2(col.size.x / 2f, col.size.y / 2f) : new Vector2(col.size.x / 2f, -(col.size.y / 2f)));
result.x += wallRayPadding;
break;
case Direction.Down:
result = (clockwise ? new Vector2(col.size.x / 2f, -(col.size.y / 2f)) : new Vector2(-(col.size.x / 2f), -(col.size.y / 2f)));
result.y -= wallRayPadding;
break;
case Direction.Left:
result = (clockwise ? new Vector2(-(col.size.x / 2f), -(col.size.y / 2f)) : new Vector2(-(col.size.x / 2f), col.size.y / 2f));
result.x -= wallRayPadding;
break;
case Direction.Up:
result = (clockwise ? new Vector2(-(col.size.x / 2f), col.size.y / 2f) : new Vector2(col.size.x / 2f, col.size.y / 2f));
result.y += wallRayPadding;
break;
}
return result;
}
/// <summary>
/// 在开始游戏时让它粘在离它向下射线2f最近的地面。
/// </summary>
private void StickToGround()
{
RaycastHit2D raycastHit2D = FireRayLocal(Vector2.down, 2f);
if(raycastHit2D.collider != null)
{
transform.position = raycastHit2D.point;
}
}
private enum Direction
{
Right,
Down,
Left,
Up
}
}
然后回到Crawler.cs代码中:
private Recoil recoil;
recoil = GetComponent<Recoil>();
在Start()函数中订阅OnCancelRecoil事件,当取消后坐力后让速度回到原始的速度,通过类型判断是否应该开启freezeInPlace:
recoil.SetRecoilSpeed(0f);
recoil.OnCancelRecoil += delegate()
{
body.velocity = velocity;
};
CrawlerType crawlerType = type;
if(crawlerType != CrawlerType.Floor)
{
if(crawlerType - CrawlerType.Roof <= 1)
{
body.gravityScale = 0;//如果在墙面面上rb2d的重力就设置为1
recoil.freezeInPlace = true;
}
}
else
{
body.gravityScale = 1; //如果在地面上rb2d的重力就设置为1
recoil.freezeInPlace = false;
}
完整的代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Crawler : MonoBehaviour
{
public float speed;
[Space]
public Transform wallCheck; //墙面检测的位置
public Transform groundCheck; //地面检测的位置
private Vector2 velocity; //记录速度
private CrawlerType type;
private Rigidbody2D body;
private Recoil recoil;
private tk2dSpriteAnimator anim;
private void Awake()
{
body = GetComponent<Rigidbody2D>();
recoil = GetComponent<Recoil>();
anim = GetComponent<tk2dSpriteAnimator>();
}
private void Start()
{
float z = transform.eulerAngles.z;
//通过transform.eulerAngles.z来判断哪种类型的Crawler
if (z >= 45f && z <= 135f)
{
type = CrawlerType.Wall;
velocity = new Vector2(0f, Mathf.Sign(-transform.localScale.x) * speed);
}
else if (z >= 135f && z <= 225f)
{
type = ((transform.localScale.y > 0f) ? CrawlerType.Roof : CrawlerType.Floor);
velocity = new Vector2(Mathf.Sign(transform.localScale.x) * speed, 0f);
}
else if (z >= 225f && z <= 315f)
{
type = CrawlerType.Wall;
velocity = new Vector2(0f, Mathf.Sign(transform.localScale.x) * speed);
}
else
{
type = ((transform.localScale.y > 0f) ? CrawlerType.Floor : CrawlerType.Roof);
velocity = new Vector2(Mathf.Sign(-transform.localScale.x) * speed, 0f);
}
recoil.SetRecoilSpeed(0f);
recoil.OnCancelRecoil += delegate()
{
body.velocity = velocity;
};
CrawlerType crawlerType = type;
if(crawlerType != CrawlerType.Floor)
{
if(crawlerType - CrawlerType.Roof <= 1)
{
body.gravityScale = 0;//如果在墙面面上rb2d的重力就设置为1
recoil.freezeInPlace = true;
}
}
else
{
body.gravityScale = 1; //如果在地面上rb2d的重力就设置为1
recoil.freezeInPlace = false;
}
StartCoroutine(nameof(Walk));
}
/// <summary>
/// 使用协程实现Walk函数,循环直至hit=true后挂起然后启用协程Turn()
/// </summary>
/// <returns></returns>
private IEnumerator Walk()
{
for(; ; )
{
anim.Play("Walk");
body.velocity = velocity;
bool hit = false;
while (!hit)
{
if(CheckRayLocal(wallCheck.localPosition,(transform.localScale.x > 0f )? Vector2.left : Vector2.right, 1f))
{
hit = true;
break;
}
if (CheckRayLocal(groundCheck.localPosition, (transform.localScale.y > 0f) ? Vector2.down : Vector2.up, 1f))
{
hit = true;
break;
}
yield return null;
}
yield return StartCoroutine(Turn());
yield return null;
}
}
/// <summary>
/// 使用协程实现转向函数
/// </summary>
/// <returns></returns>
private IEnumerator Turn()
{
body.velocity = Vector2.zero;
yield return StartCoroutine(anim.PlayAnimWait("Turn"));
transform.SetScaleX(transform.localScale.x * -1f);
velocity.x = velocity.x * -1f;
velocity.y = velocity.y * -1f;
}
/// <summary>
/// 发射射线,检测是否有LayerMask.GetMask("Terrain").collider
/// </summary>
/// <param name="originLocal"></param>
/// <param name="directionLocal"></param>
/// <param name="length"></param>
/// <returns></returns>
public bool CheckRayLocal(Vector3 originLocal, Vector2 directionLocal, float length)
{
Vector2 vector = transform.TransformPoint(originLocal);
Vector2 vector2 = transform.TransformDirection(directionLocal);
RaycastHit2D raycastHit2D = Physics2D.Raycast(vector, vector2, length, LayerMask.GetMask("Terrain"));
Debug.DrawLine(vector, vector + vector2 * length);
return raycastHit2D.collider != null;
}
private enum CrawlerType
{
Floor,
Roof,
Wall
}
}
3.为小骑士添加后坐力行为
private int recoilSteps;
private float recoilTimer; //后坐力计时器
private bool recoilLarge; //是否是更大的后坐力
private Vector2 recoilVector; //后坐力二维上的速度
public float RECOIL_HOR_VELOCITY; //后坐力X轴上的速度
public float RECOIL_HOR_VELOCITY_LONG; //后坐力X轴上更大的速度
public float RECOIL_DOWN_VELOCITY; //后坐力Y轴上的速度
public float RECOIL_HOR_STEPS; //后坐力X轴的步
public float RECOIL_DURATION; //后坐力持续时间
public float RECOIL_VELOCITY; //后坐力时的速度(是两个轴上都适用的)
在HeroControllerState.cs中创建新的状态:
public bool recoilFrozen;
public bool recoiling;
public bool recoilingLeft;
public bool recoilingRight;
[Serializable]
public class HeroControllerStates
{
public bool facingRight;
public bool onGround;
public bool wasOnGround;
public bool attacking;
public bool altAttack;
public bool upAttacking;
public bool downAttacking;
public bool inWalkZone;
public bool jumping;
public bool falling;
public bool dashing;
public bool backDashing;
public bool touchingWall;
public bool wallSliding;
public bool willHardLand;
public bool recoilFrozen;
public bool recoiling;
public bool recoilingLeft;
public bool recoilingRight;
public bool dead;
public bool hazardDeath;
public bool invulnerable;
public bool preventDash;
public bool preventBackDash;
public bool dashCooldown;
public bool backDashCooldown;
public bool isPaused;
public HeroControllerStates()
{
facingRight = false;
onGround = false;
wasOnGround = false;
attacking = false;
altAttack = false;
upAttacking = false;
downAttacking = false;
inWalkZone = false;
jumping = false;
falling = false;
dashing = false;
backDashing = false;
touchingWall = false;
wallSliding = false;
willHardLand = false;
recoilFrozen = false;
recoiling = false;
recoilingLeft = false;
recoilingRight = false;
dead = false;
hazardDeath = false;
invulnerable = false;
preventDash = false;
preventBackDash = false;
dashCooldown = false;
backDashCooldown = false;
isPaused = false;
}
}
在Update()函数中,如果hero_state == ActorStates.no_input,通过后坐力状态来切换是否要关闭后坐力行为,如果有输入了就取消进入recoiling状态
if (hero_state == ActorStates.no_input)
{
if (cState.recoiling)
{
if (recoilTimer < RECOIL_DURATION)
{
recoilTimer += Time.deltaTime;
}
else
{
CancelDamageRecoil();
if ((prev_hero_state == ActorStates.idle || prev_hero_state == ActorStates.running) && !CheckTouchingGround())
{
cState.onGround = false;
SetState(ActorStates.airborne);
}
else
{
SetState(ActorStates.previous);
}
}
}
}
else if (hero_state != ActorStates.no_input)
{
LookForInput();
if (cState.recoiling)
{
cState.recoiling = false;
AffectedByGravity(true);
}
if(cState.attacking && !cState.dashing)
{
attack_time += Time.deltaTime;
if(attack_time >= attackDuration)
{
ResetAttacks();
animCtrl.StopAttack();
}
}
}
在FixedUpdate()中如果recoil步骤到达后就取消X轴上的后坐力CancelRecoilHorizonal:
if(cState.recoilingLeft || cState.recoilingRight)
{
if(recoilSteps <= RECOIL_HOR_STEPS)
{
recoilSteps++;
}
else
{
CancelRecoilHorizonal();
}
}
还有判断后坐力时左边还是右边,以及是否开启recoilLarge来赋予 rb2d.velocity不同的速度:
if(cState.recoilingLeft)
{
float num;
if (recoilLarge)
{
num = RECOIL_HOR_VELOCITY_LONG;
}
else
{
num = RECOIL_HOR_VELOCITY;
}
if(rb2d.velocity.x > -num)
{
rb2d.velocity = new Vector2(-num, rb2d.velocity.y);
}
else
{
rb2d.velocity = new Vector2(rb2d.velocity.x - num, rb2d.velocity.y);
}
}
if (cState.recoilingRight)
{
float num2;
if(recoilLarge)
{
num2 = RECOIL_HOR_VELOCITY_LONG;
}
else
{
num2 = RECOIL_HOR_VELOCITY;
}
if (rb2d.velocity.x < num2)
{
rb2d.velocity = new Vector2(num2, rb2d.velocity.y);
}
else
{
rb2d.velocity = new Vector2(rb2d.velocity.x + num2, rb2d.velocity.y);
}
}
还需要添加几个新的后坐力函数:
public void RecoilLeft()
{
if(!cState.recoilingLeft && !cState.recoilingRight)
{
CancelDash();
recoilSteps = 0;
cState.recoilingLeft = true;
cState.recoilingRight = false;
recoilLarge = false;
rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY, rb2d.velocity.y);
}
}
public void RecoilRight()
{
if (!cState.recoilingLeft && !cState.recoilingRight)
{
CancelDash();
recoilSteps = 0;
cState.recoilingLeft = false;
cState.recoilingRight = true;
recoilLarge = false;
rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY, rb2d.velocity.y);
}
}
public void RecoilLeftLong()
{
if (!cState.recoilingLeft && !cState.recoilingRight)
{
CancelDash();
ResetAttacks();
recoilSteps = 0;
cState.recoilingLeft = true;
cState.recoilingRight = false;
recoilLarge = true;
rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);
}
}
public void RecoilRightLong()
{
if (!cState.recoilingLeft && !cState.recoilingRight)
{
CancelDash();
ResetAttacks();
recoilSteps = 0;
cState.recoilingLeft = false;
cState.recoilingRight = true;
recoilLarge = true;
rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);
}
}
public void RecoilDown()
{
CancelJump();
if(rb2d.velocity.y > RECOIL_DOWN_VELOCITY)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, RECOIL_DOWN_VELOCITY);
}
}
public void CancelRecoilHorizonal()
{
cState.recoilingLeft = false;
cState.recoilingRight = false;
recoilSteps = 0;
}
这里我为什么不贴完整的代码呢?因为其实小骑士的后坐力系统还和受伤系统有关系,所以我打算放到下面再完整的后坐力系统。
二、制作小骑士的生命系统和受伤系统
1.制作动画以及使用UNITY编辑器编辑
我们来个小骑士制作受伤动画和死亡动画和冻结动画吧。
老规矩,还是只添加偶数帧的sprite上去:这个是受伤Recoil动画
这个是死亡Death动画:
最后是冻结Stun动画,是的它就一帧:
2.使用代码制作生命系统和受伤系统
制作完动画后当然要来到我们最爱的HeroAnimationController.cs中:
if (actorStates == ActorStates.no_input)
{
if (cState.recoilFrozen)
{
Play("Stun");
}
else if (cState.recoiling)
{
Play("Recoil");
}
}
using System;
using GlobalEnums;
using UnityEngine;
public class HeroAnimationController : MonoBehaviour
{
private HeroController heroCtrl;
private HeroControllerStates cState;
private tk2dSpriteAnimator animator;
private PlayerData pd;
private bool wasFacingRight;
private bool playLanding;
private bool playRunToIdle;//播放"Run To Idle"动画片段
private bool playDashToIdle; //播放"Dash To Idle"动画片段
private bool playBackDashToIdleEnd; //播放"Back Dash To Idle"动画片段(其实并不会播放)
private bool changedClipFromLastFrame;
public ActorStates actorStates { get; private set; }
public ActorStates prevActorStates { get; private set; }
private void Awake()
{
heroCtrl = HeroController.instance;
cState = heroCtrl.cState;
animator = GetComponent<tk2dSpriteAnimator>();
}
private void Start()
{
pd = PlayerData.instance;
ResetAll();
actorStates = heroCtrl.hero_state;
if(heroCtrl.hero_state == ActorStates.airborne)
{
animator.PlayFromFrame("Airborne", 7);
return;
}
PlayIdle();
}
private void Update()
{
UpdateAnimation();
if (cState.facingRight)
{
wasFacingRight = true;
return;
}
wasFacingRight = false;
}
private void UpdateAnimation()
{
changedClipFromLastFrame = false;
if (playLanding)
{
Play("Land");
animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
playLanding = false;
}
if (playRunToIdle)
{
Play("Run To Idle");
animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
playRunToIdle = false;
}
if (playBackDashToIdleEnd)
{
Play("Backdash Land 2");
//处理animation播放完成后的事件(其实并不会播放)
animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
playDashToIdle = false;
}
if (playDashToIdle)
{
Play("Dash To Idle");
//处理animation播放完成后的事件
animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
playDashToIdle = false;
}
if (actorStates == ActorStates.no_input)
{
//TODO:
if (cState.recoilFrozen)
{
Play("Stun");
}
else if (cState.recoiling)
{
Play("Recoil");
}
}
else if (cState.dashing)
{
if (heroCtrl.dashingDown)
{
Play("Dash Down");
}
else
{
Play("Dash"); //通过cState.dashing判断是否播放Dash动画片段
}
}
else if (cState.backDashing)
{
Play("Back Dash");
}
else if(cState.attacking)
{
if (cState.upAttacking)
{
Play("UpSlash");
}
else if (cState.downAttacking)
{
Play("DownSlash");
}
else if (!cState.altAttack)
{
Play("Slash");
}
else
{
Play("SlashAlt");
}
}
else if (actorStates == ActorStates.idle)
{
//TODO:
if (CanPlayIdle())
{
PlayIdle();
}
}
else if (actorStates == ActorStates.running)
{
if (!animator.IsPlaying("Turn"))
{
if (cState.inWalkZone)
{
if (!animator.IsPlaying("Walk"))
{
Play("Walk");
}
}
else
{
PlayRun();
}
}
}
else if (actorStates == ActorStates.airborne)
{
if (cState.jumping)
{
if (!animator.IsPlaying("Airborne"))
{
animator.PlayFromFrame("Airborne", 0);
}
}
else if (cState.falling)
{
if (!animator.IsPlaying("Airborne"))
{
animator.PlayFromFrame("Airborne", 7);
}
}
else if (!animator.IsPlaying("Airborne"))
{
animator.PlayFromFrame("Airborne", 3);
}
}
//(其实并不会播放)
else if (actorStates == ActorStates.dash_landing)
{
animator.Play("Dash Down Land");
}
else if(actorStates == ActorStates.hard_landing)
{
animator.Play("HardLand");
}
if (cState.facingRight)
{
if(!wasFacingRight && cState.onGround && CanPlayTurn())
{
Play("Turn");
}
wasFacingRight = true;
}
else
{
if (wasFacingRight && cState.onGround && CanPlayTurn())
{
Play("Turn");
}
wasFacingRight = false;
}
ResetPlays();
}
private void AnimationCompleteDelegate(tk2dSpriteAnimator anim, tk2dSpriteAnimationClip clip)
{
if(clip.name == "Land")
{
PlayIdle();
}
if(clip.name == "Run To Idle")
{
PlayIdle();
}
if(clip.name == "Backdash To Idle")//(其实并不会播放)
{
PlayIdle();
}
if(clip.name == "Dash To Idle")
{
PlayIdle();
}
}
private void Play(string clipName)
{
if(clipName != animator.CurrentClip.name)
{
changedClipFromLastFrame = true;
}
animator.Play(clipName);
}
private void PlayRun()
{
animator.Play("Run");
}
public void PlayIdle()
{
animator.Play("Idle");
}
public void StopAttack()
{
if(animator.IsPlaying("UpSlash") || animator.IsPlaying("DownSlash"))
{
animator.Stop();
}
}
public void FinishedDash()
{
playDashToIdle = true;
}
private void ResetAll()
{
playLanding = false;
playRunToIdle = false;
playDashToIdle = false;
wasFacingRight = false;
}
private void ResetPlays()
{
playLanding = false;
playRunToIdle = false;
playDashToIdle = false;
}
public void UpdateState(ActorStates newState)
{
if(newState != actorStates)
{
if(actorStates == ActorStates.airborne && newState == ActorStates.idle && !playLanding)
{
playLanding = true;
}
if(actorStates == ActorStates.running && newState == ActorStates.idle && !playRunToIdle && !cState.inWalkZone)
{
playRunToIdle = true;
}
prevActorStates = actorStates;
actorStates = newState;
}
}
private bool CanPlayIdle()
{
return !animator.IsPlaying("Land") && !animator.IsPlaying("Run To Idle") && !animator.IsPlaying("Dash To Idle") && !animator.IsPlaying("Backdash Land") && !animator.IsPlaying("Backdash Land 2") && !animator.IsPlaying("LookUpEnd") && !animator.IsPlaying("LookDownEnd") && !animator.IsPlaying("Exit Door To Idle") && !animator.IsPlaying("Wake Up Ground") && !animator.IsPlaying("Hazard Respawn");
}
private bool CanPlayTurn()
{
return !animator.IsPlaying("Wake Up Ground") && !animator.IsPlaying("Hazard Respawn"); ;
}
}
然后到HeroAudioController.cs添加一个新的AudioSource:
public AudioSource takeHit;
case HeroSounds.TAKE_HIT:
takeHit.Play();
break;
using System.Collections;
using System.Collections.Generic;
using GlobalEnums;
using UnityEngine;
public class HeroAudioController : MonoBehaviour
{
private HeroController heroCtrl;
private void Awake()
{
heroCtrl = GetComponent<HeroController>();
}
[Header("Sound Effects")]
public AudioSource softLanding;
public AudioSource hardLanding;
public AudioSource jump;
public AudioSource footStepsRun;
public AudioSource footStepsWalk;
public AudioSource falling;
public AudioSource backDash;
public AudioSource dash;
public AudioSource takeHit;
private Coroutine fallingCo;
public void PlaySound(HeroSounds soundEffect)
{
if(!heroCtrl.cState.isPaused)
{
switch (soundEffect)
{
case HeroSounds.FOOTSETP_RUN:
if(!footStepsRun.isPlaying && !softLanding.isPlaying)
{
footStepsRun.Play();
return;
}
break;
case HeroSounds.FOOTSTEP_WALK:
if (!footStepsWalk.isPlaying && !softLanding.isPlaying)
{
footStepsWalk.Play();
return;
}
break;
case HeroSounds.SOFT_LANDING:
RandomizePitch(softLanding, 0.9f, 1.1f);
softLanding.Play();
break;
case HeroSounds.HARD_LANDING:
hardLanding.Play();
break;
case HeroSounds.JUMP:
RandomizePitch(jump, 0.9f, 1.1f);
jump.Play();
break;
case HeroSounds.BACK_DASH:
backDash.Play();
break;
case HeroSounds.DASH:
dash.Play();
break;
case HeroSounds.TAKE_HIT:
takeHit.Play();
break;
case HeroSounds.FALLING:
fallingCo = StartCoroutine(FadeInVolume(falling, 0.7f));
falling.Play();
break;
default:
break;
}
}
}
public void StopSound(HeroSounds soundEffect)
{
if(soundEffect == HeroSounds.FOOTSETP_RUN)
{
footStepsRun.Stop();
return;
}
if (soundEffect == HeroSounds.FOOTSTEP_WALK)
{
footStepsWalk.Stop();
return;
}
switch (soundEffect)
{
case HeroSounds.FALLING:
falling.Stop();
if(fallingCo != null)
{
StopCoroutine(fallingCo);
}
return;
default:
return;
}
}
public void StopAllSounds()
{
softLanding.Stop();
hardLanding.Stop();
jump.Stop();
falling.Stop();
backDash.Stop();
dash.Stop();
footStepsRun.Stop();
footStepsWalk.Stop();
}
public void PauseAllSounds()
{
softLanding.Pause();
hardLanding.Pause();
jump.Pause();
falling.Pause();
backDash.Pause();
dash.Pause();
footStepsRun.Pause();
footStepsWalk.Pause();
}
public void UnPauseAllSounds()
{
softLanding.UnPause();
hardLanding.UnPause();
jump.UnPause();
falling.UnPause();
backDash.UnPause();
dash.UnPause();
footStepsRun.UnPause();
footStepsWalk.UnPause();
}
/// <summary>
/// 音量淡入线性插值的从0到1
/// </summary>
/// <param name="src"></param>
/// <param name="duration"></param>
/// <returns></returns>
private IEnumerator FadeInVolume(AudioSource src, float duration)
{
float elapsedTime = 0f;
src.volume = 0f;
while (elapsedTime < duration)
{
elapsedTime += Time.deltaTime;
float t = elapsedTime / duration;
src.volume = Mathf.Lerp(0f, 1f, t);
yield return null;
}
}
/// <summary>
/// 随机旋转一个在和之间的pitch的值返回给audiosource
/// </summary>
/// <param name="src"></param>
/// <param name="minPitch"></param>
/// <param name="maxPitch"></param>
private void RandomizePitch(AudioSource src, float minPitch, float maxPitch)
{
float pitch = Random.Range(minPitch, maxPitch);
src.pitch = pitch;
}
/// <summary>
/// 重置audiosource的pitch
/// </summary>
/// <param name="src"></param>
private void ResetPitch(AudioSource src)
{
src.pitch = 1f;
}
}
如果报错了自然是在GlobalEnums中我们没有添加这个新的herosounds数组:
using System;
namespace GlobalEnums
{
public enum ActorStates
{
grounded,
idle,
running,
airborne,
wall_sliding,
hard_landing,
dash_landing,
no_input,
previous
}
public enum AttackDirection
{
normal,
upward,
downward
}
public enum CollisionSide
{
top,
left,
right,
bottom,
other
}
public enum DamageMode
{
FULL_DAMAGE,
HAZARD_ONLY,
NO_DAMAGE
}
public enum HazardTypes
{
NON_HAZARD,
SPIKES,
ACID,
LAVA,
PIT
}
public enum HeroSounds
{
FOOTSETP_RUN,
FOOTSTEP_WALK,
SOFT_LANDING,
HARD_LANDING,
JUMP,
BACK_DASH,
DASH,
FALLING,
TAKE_HIT
}
public enum PhysLayers
{
DEFAULT,
IGNORE_RAYCAST = 2,
WATER = 4,
UI,
TERRAIN = 8,
PLAYER,
TRANSITION_GATES,
ENEMIES,
PROJECTILES,
HERO_DETECTOR,
TERRAIN_DETECTOR,
ENEMY_DETECTOR,
ITEM,
HERO_ATTACK,
PARTICLE,
INTERACTIVE_OBJECT,
HERO_BOX,
BOUNCER = 24,
SOFT_TERRAIN = 25
}
}
回到Unity编辑器中,在小骑士对应的脚本添加完新的AudioSource后,我们还要创建一个新的Layer就叫Hero Box,它用于专门处理和可交互物体(特别是敌人Enemy)的碰撞检测。
为小骑士新建一个子对象就叫HeroBox,把刚刚创建的Layer放上去,然后设置你觉得合适的碰撞大小:
创建一个同名脚本HeroBox.cs:
using System;
using GlobalEnums;
using UnityEngine;
public class HeroBox : MonoBehaviour
{
public static bool inactive;
private HeroController heroCtrl;
private GameObject damagingObject;
private bool isHitBuffered;
private int damageDealt;
private int hazardType;
private CollisionSide collisionSide;
private void Start()
{
heroCtrl = HeroController.instance;
}
private void LateUpdate()
{
if (isHitBuffered)
{
ApplyBufferedHit();
}
}
private void OnTriggerEnter2D(Collider2D otherCollider)
{
if (!inactive)
{
CheckForDamage(otherCollider);
}
}
private void OnTriggerStay2D(Collider2D otherCollider)
{
if (!inactive)
{
CheckForDamage(otherCollider);
}
}
/// <summary>
/// 通过两种方法检测受到伤害的方法
/// 一种是通过otherCollider.gameObject中是否有一个名字叫"damages_hero"的playmakerFSM
/// 另一种是通过otherCollider.gameObject是否有个叫DamageHero的脚本
/// </summary>
/// <param name="otherCollider"></param>
private void CheckForDamage(Collider2D otherCollider)
{
if (!FSMUtility.ContainsFSM(otherCollider.gameObject, "damages_hero"))
{
DamageHero component = otherCollider.gameObject.GetComponent<DamageHero>();
if (component != null)
{
damageDealt = component.damageDealt;
hazardType = component.hazardType;
damagingObject = otherCollider.gameObject;
collisionSide = ((damagingObject.transform.position.x > transform.position.x) ? CollisionSide.right : CollisionSide.left);
if (!IsHitTypeBuffered(hazardType))
{
ApplyBufferedHit();
return;
}
isHitBuffered = true;
}
return;
}
PlayMakerFSM fsm = FSMUtility.LocateFSM(otherCollider.gameObject, "damages_hero");
int dealt = FSMUtility.GetInt(fsm, "damageDealt");
int type = FSMUtility.GetInt(fsm, "hazardType");
if (otherCollider.transform.position.x > transform.position.x)
{
heroCtrl.TakeDamage(otherCollider.gameObject, CollisionSide.right, dealt, type);
return;
}
heroCtrl.TakeDamage(otherCollider.gameObject, CollisionSide.left, dealt, type);
}
public static bool IsHitTypeBuffered(int hazardType)
{
return hazardType == 0;
}
/// <summary>
/// 应用缓冲后受击,就是执行HeroController的TakeDamage方法
/// </summary>
private void ApplyBufferedHit()
{
heroCtrl.TakeDamage(damagingObject, collisionSide, damageDealt, hazardType);
isHitBuffered = false;
}
}
我们先为每一个敌人添加一个新的脚本叫DamageHero.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DamageHero : MonoBehaviour
{
public int damageDealt = 1;
public int hazardType = 1;
public bool shadowDashHazard;
public bool resetOnEnable;
private int? initialValue;
private void OnEnable()
{
if (resetOnEnable)
{
if(initialValue == null)
{
initialValue = new int?(damageDealt);
return;
}
damageDealt = initialValue.Value;
}
}
}
回到HealthManager.cs中,如果执行Die()函数后就把damageHero里面的damageDealt设置为0
private DamageHero damageHero;
damageHero = GetComponent<DamageHero>();
if(damageHero != null)
{
damageHero.damageDealt = 0;
}
完整的代码如下:
using System;
using System.Collections;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;
public class HealthManager : MonoBehaviour, IHitResponder
{
private BoxCollider2D boxCollider;
private IHitEffectReciever hitEffectReceiver;
private Recoil recoil;
private tk2dSpriteAnimator animator;
private tk2dSprite sprite;
private DamageHero damageHero;
[Header("Asset")]
[SerializeField] private AudioSource audioPlayerPrefab; //声音播放器预制体
[Header("Body")]
[SerializeField] public int hp; //血量
[SerializeField] public int enemyType; //敌人类型
[SerializeField] private Vector3 effectOrigin; //生效偏移量
public bool isDead;
private int directionOfLastAttack; //最后一次受到攻击的方向
private float evasionByHitRemaining; //剩余攻击下的逃避时间
private const string CheckPersistenceKey = "CheckPersistence";
public delegate void DeathEvent();
public event DeathEvent OnDeath;
protected void Awake()
{
boxCollider = GetComponent<BoxCollider2D>();
hitEffectReceiver = GetComponent<IHitEffectReciever>();
recoil = GetComponent<Recoil>();
animator = GetComponent<tk2dSpriteAnimator>();
sprite = GetComponent<tk2dSprite>();
damageHero = GetComponent<DamageHero>();
}
protected void OnEnable()
{
StartCoroutine(CheckPersistenceKey);
}
protected void Start()
{
evasionByHitRemaining = -1f;
}
protected void Update()
{
evasionByHitRemaining -= Time.deltaTime;
}
public void Hit(HitInstance hitInstance)
{
if (isDead)
{
return;
}
if(evasionByHitRemaining > 0f)
{
return;
}
if(hitInstance.DamageDealt < 0f)
{
return;
}
FSMUtility.SendEventToGameObject(hitInstance.Source, "DEALT DAMAGE", false);
int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
if (IsBlockingByDirection(cardinalDirection, hitInstance.AttackType))
{
Invincible(hitInstance);
return;
}
TakeDamage(hitInstance);
}
private void Invincible(HitInstance hitInstance)
{
int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
directionOfLastAttack = cardinalDirection;
FSMUtility.SendEventToGameObject(gameObject, "BLOCKED HIT", false);
FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
if (!(GetComponent<DontClinkGates>() != null))
{
FSMUtility.SendEventToGameObject(gameObject, "HIT", false);
if(hitInstance.AttackType == AttackTypes.Nail)
{
if(cardinalDirection == 0)
{
HeroController.instance.RecoilLeft();
}
else if(cardinalDirection == 2)
{
HeroController.instance.RecoilRight();
}
}
Vector2 v;
Vector3 eulerAngles;
if (boxCollider != null)
{
switch (cardinalDirection)
{
case 0:
v = new Vector2(transform.GetPositionX() + boxCollider.offset.x - boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
eulerAngles = new Vector3(0f, 0f, 0f);
break;
case 1:
v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Max(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y - boxCollider.size.y * 0.5f));
eulerAngles = new Vector3(0f, 0f, 90f);
break;
case 2:
v = new Vector2(transform.GetPositionX() + boxCollider.offset.x + boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
eulerAngles = new Vector3(0f, 0f, 180f);
break;
case 3:
v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Min(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y + boxCollider.size.y * 0.5f));
eulerAngles = new Vector3(0f, 0f, 270f);
break;
default:
break;
}
}
else
{
v = transform.position;
eulerAngles = new Vector3(0f, 0f, 0f);
}
}
evasionByHitRemaining = 0.15f;
}
public void TakeDamage(HitInstance hitInstance)
{
Debug.LogFormat("Enemy Take Damage");
int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
directionOfLastAttack = cardinalDirection;
FSMUtility.SendEventToGameObject(gameObject, "HIT", false);
FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
FSMUtility.SendEventToGameObject(gameObject, "TOOK DAMAGE", false);
if(recoil != null)
{
recoil.RecoilByDirection(cardinalDirection,hitInstance.MagnitudeMultiplier);
}
switch (hitInstance.AttackType)
{
case AttackTypes.Nail:
if(hitInstance.AttackType == AttackTypes.Nail && enemyType !=3 && enemyType != 6)
{
}
Vector3 position = (hitInstance.Source.transform.position + transform.position) * 0.5f + effectOrigin;
break;
case AttackTypes.Generic:
break;
default:
break;
}
if(hitEffectReceiver != null)
{
hitEffectReceiver.ReceiverHitEffect(hitInstance.GetActualDirection(transform));
}
int num = Mathf.RoundToInt((float)hitInstance.DamageDealt * hitInstance.Multiplier);
hp = Mathf.Max(hp - num, -50);
if(hp > 0)
{
}
else
{
Die(new float?(hitInstance.GetActualDirection(transform)), hitInstance.AttackType, hitInstance.IgnoreInvulnerable);
}
}
public void Die(float? v, AttackTypes attackType, bool ignoreInvulnerable)
{
if (isDead)
{
return;
}
if (sprite)
{
sprite.color = Color.white;
}
FSMUtility.SendEventToGameObject(gameObject, "ZERO HP", false);
isDead = true;
if(damageHero != null)
{
damageHero.damageDealt = 0;
}
SendDeathEvent();
Destroy(gameObject); //TODO:
}
public void SendDeathEvent()
{
if (OnDeath != null)
{
OnDeath();
}
}
public bool IsBlockingByDirection(int cardinalDirection,AttackTypes attackType)
{
switch (cardinalDirection)
{
default:
return false;
}
}
protected IEnumerator CheckPersistence()
{
yield return null;
if (isDead)
{
gameObject.SetActive(false);
}
yield break;
}
}
然后为FSMUtility添加几个新的静态方法:
using System;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;
public static class FSMUtility
{
private static List<List<PlayMakerFSM>> fsmListPool;
private const int FsmListPoolSizeMax = 20;
static FSMUtility()
{
fsmListPool = new List<List<PlayMakerFSM>>();
}
public static PlayMakerFSM LocateFSM(GameObject go, string fsmName)
{
if (go == null)
{
return null;
}
List<PlayMakerFSM> list = ObtainFsmList();
go.GetComponents<PlayMakerFSM>(list);
PlayMakerFSM result = null;
for (int i = 0; i < list.Count; i++)
{
PlayMakerFSM playMakerFSM = list[i];
if (playMakerFSM.FsmName == fsmName)
{
result = playMakerFSM;
break;
}
}
ReleaseFsmList(list);
return result;
}
private static List<PlayMakerFSM> ObtainFsmList()
{
if (fsmListPool.Count > 0)
{
List<PlayMakerFSM> result = fsmListPool[fsmListPool.Count - 1];
fsmListPool.RemoveAt(fsmListPool.Count - 1);
return result;
}
return new List<PlayMakerFSM>();
}
public static bool ContainsFSM(GameObject go, string fsmName)
{
if (go == null)
{
return false;
}
List<PlayMakerFSM> list = FSMUtility.ObtainFsmList();
go.GetComponents<PlayMakerFSM>(list);
bool result = false;
for (int i = 0; i < list.Count; i++)
{
if (list[i].FsmName == fsmName)
{
result = true;
break;
}
}
FSMUtility.ReleaseFsmList(list);
return result;
}
public static int GetInt(PlayMakerFSM fsm, string variableName)
{
return fsm.FsmVariables.FindFsmInt(variableName).Value;
}
private static void ReleaseFsmList(List<PlayMakerFSM> fsmList)
{
fsmList.Clear();
if (fsmListPool.Count < FsmListPoolSizeMax)
{
fsmListPool.Add(fsmList);
}
}
public static PlayMakerFSM GetFSM(GameObject go)
{
return go.GetComponent<PlayMakerFSM>();
}
public static GameObject GetSafe(this FsmOwnerDefault ownerDefault, FsmStateAction stateAction)
{
if (ownerDefault.OwnerOption == OwnerDefaultOption.UseOwner)
{
return stateAction.Owner;
}
return ownerDefault.GameObject.Value;
}
public static void SendEventToGameObject(GameObject go, string eventName, bool isRecursive = false)
{
if (go != null)
{
SendEventToGameObject(go, FsmEvent.FindEvent(eventName), isRecursive);
}
}
public static void SendEventToGameObject(GameObject go, FsmEvent ev, bool isRecursive = false)
{
if (go != null)
{
List<PlayMakerFSM> list = ObtainFsmList();
go.GetComponents<PlayMakerFSM>(list);
for (int i = 0; i < list.Count; i++)
{
list[i].Fsm.Event(ev);
}
ReleaseFsmList(list);
if (isRecursive)
{
Transform transform = go.transform;
for (int j = 0; j < transform.childCount; j++)
{
SendEventToGameObject(transform.GetChild(j).gameObject, ev, isRecursive);
}
}
}
}
}
回到HeroController.cs处理报错的部分: 我们新生成一个方法就叫做TakeDamage():
private bool CanTakeDamage()
{
return damageMode != DamageMode.NO_DAMAGE && !cState.invulnerable && !cState.recoiling && !cState.dead;
}
public void TakeDamage(GameObject go,CollisionSide damageSide,int damageAmount,int hazardType)
{
bool spawnDamageEffect = true;
if (damageAmount > 0)
{
if (CanTakeDamage())
{
if (damageMode == DamageMode.HAZARD_ONLY && hazardType == 1)
{
return;
}
if (parryInvulnTimer > 0f && hazardType == 1)
{
return;
}
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
CancelAttack();
if (cState.touchingWall)
{
cState.touchingWall = false;
}
if (cState.recoilingLeft || cState.recoilingRight)
{
CancelRecoilHorizonal();
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
if (!takeNoDamage)
{
playerData.TakeHealth(damageAmount);
}
if (damageAmount > 0 && OnTakenDamage != null)
{
OnTakenDamage();
}
if (playerData.health == 0)
{
StartCoroutine(Die());
return;
}
if (hazardType == 2)
{
Debug.LogFormat("Die From Spikes");
return;
}
if (hazardType == 3)
{
Debug.LogFormat("Die From Acid");
return;
}
if (hazardType == 4)
{
Debug.LogFormat("Die From Lava");
return;
}
if (hazardType == 5)
{
Debug.LogFormat("Die From Pit");
return;
}
StartCoroutine(StartRecoil(damageSide, spawnDamageEffect, damageAmount));
return;
}
else if (cState.invulnerable && !cState.hazardDeath)
{
if(hazardType == 2)
{
if (!takeNoDamage)
{
playerData.TakeHealth(damageAmount);
}
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
if(playerData.health == 0)
{
StartCoroutine(Die());
return;
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
StartCoroutine(DieFromHazard(HazardTypes.SPIKES, (go != null) ? go.transform.rotation.z : 0f));
return;
}
else if (hazardType == 3)
{
playerData.TakeHealth(damageAmount);
proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
if (playerData.health == 0)
{
StartCoroutine(Die());
return;
}
audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
StartCoroutine(DieFromHazard(HazardTypes.ACID, 0f));
return;
}
else if(hazardType == 4)
{
Debug.LogFormat("Die From Lava");
}
}
}
}
我们还要一些新的受伤事件和变量要做:
public delegate void TakeDamageEvent();
public event TakeDamageEvent OnTakenDamage;
public delegate void OnDeathEvent();
public event OnDeathEvent OnDeath;
public bool takeNoDamage; //不受到伤害
public PlayMakerFSM damageEffectFSM; //负责的受伤效果playmakerFSM
public DamageMode damageMode; //受伤类型
private Coroutine takeDamageCoroutine; //受伤协程
private float parryInvulnTimer; //无敌时间
public float INVUL_TIME;//无敌时间
public float DAMAGE_FREEZE_DOWN; //受伤冻结的上半程时间
public float DAMAGE_FREEZE_WAIT; //受伤冻结切换的时间
public float DAMAGE_FREEZE_UP;//受伤冻结的下半程时间
来到PlayerData.cs中,我们需要创建几个新的变量,包括玩家的血量,是否阻止暂停(暂时用不上)public bool disablePause;
扣血函数:
public void TakeHealth(int amount)
{
if(amount > 0 && health == maxHealth && health != CurrentMaxHealth)
{
health = CurrentMaxHealth;
}
if(health - amount < 0)
{
health = 0;
return;
}
health -= amount;
}
using System;
using System.Collections.Generic;
using System.Reflection;
using GlobalEnums;
using UnityEngine;
[Serializable]
public class PlayerData
{
private static PlayerData _instance;
public static PlayerData instance
{
get
{
if(_instance == null)
{
_instance = new PlayerData();
}
return _instance;
}
set
{
_instance = value;
}
}
public bool disablePause;
public int health;
public int maxHealth;
public int maxHealthBase;
public int nailDamage;
public bool hasDash;
public bool canDash;
public bool hasBackDash;
public bool canBackDash;
public bool overcharmed;
public bool gotCharm_31;
public bool equippedCharm_31;
public int CurrentMaxHealth
{
get
{
return maxHealth;
}
}
protected PlayerData()
{
SetupNewPlayerData();
}
public void Reset()
{
SetupNewPlayerData();
}
private void SetupNewPlayerData()
{
disablePause = false;
health = 5;
maxHealth = 5;
maxHealthBase = 5;
nailDamage = 5;
hasDash = true; //测试阶段先设置为true方便测试
canDash = true;
hasBackDash = false;
canBackDash = false;
overcharmed = false;
gotCharm_31 = true;
equippedCharm_31 = true;
}
public void TakeHealth(int amount)
{
if(amount > 0 && health == maxHealth && health != CurrentMaxHealth)
{
health = CurrentMaxHealth;
}
if(health - amount < 0)
{
health = 0;
return;
}
health -= amount;
}
public int GetInt(string intName)
{
if (string.IsNullOrEmpty(intName))
{
Debug.LogError("PlayerData: Int with an EMPTY name requested.");
return -9999;
}
FieldInfo fieldInfo = GetType().GetField(intName);
if(fieldInfo != null)
{
return (int)fieldInfo.GetValue(instance);
}
Debug.LogError("PlayerData: Could not find int named " + intName + " in PlayerData");
return -9999;
}
public bool GetBool(string boolName)
{
if (string.IsNullOrEmpty(boolName))
{
return false;
}
FieldInfo field = GetType().GetField(boolName);
if (field != null)
{
return (bool)field.GetValue(instance);
}
Debug.Log("PlayerData: Could not find bool named " + boolName + " in PlayerData");
return false;
}
}
来到GameManager,我们创建一个新的协程FreezeMoment目的是设置新的TimeScale,而且它是分阶段的,从先到慢的 TimeScale再回到一般的TimeScale,功能很强大!
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
private int timeSlowedCount;
public bool TimeSlowed
{
get
{
return timeSlowedCount > 0;
}
}
[SerializeField] public PlayerData playerData;
private static GameManager _instance;
public static GameManager instance
{
get
{
if(_instance == null)
{
_instance = FindObjectOfType<GameManager>();
}
if (_instance == null)
{
Debug.LogError("Couldn't find a Game Manager, make sure one exists in the scene.");
}
else if (Application.isPlaying)
{
DontDestroyOnLoad(_instance.gameObject);
}
return _instance;
}
}
private void Awake()
{
if(_instance != this)
{
_instance = this;
DontDestroyOnLoad(this);
SetupGameRefs();
return;
}
if(this != _instance)
{
Destroy(gameObject);
return;
}
SetupGameRefs();
}
private void SetupGameRefs()
{
playerData = PlayerData.instance;
}
public int GetPlayerDataInt(string intName)
{
return playerData.GetInt(intName);
}
public bool GetPlayerDataBool(string boolName)
{
return playerData.GetBool(boolName);
}
private IEnumerator SetTimeScale(float newTimeScale,float duration)
{
float lastTimeScale = TimeController.GenericTimeScale;
for (float timer = 0f; timer < duration; timer += Time.unscaledDeltaTime)
{
float t = Mathf.Clamp01(timer / duration);
SetTimeScale(Mathf.Lerp(lastTimeScale, newTimeScale, t));
yield return null;
}
SetTimeScale(newTimeScale);
}
private void SetTimeScale(float newTimeScale)
{
if(timeSlowedCount > 1)
{
newTimeScale = Mathf.Min(newTimeScale, TimeController.GenericTimeScale);
}
TimeController.GenericTimeScale = ((newTimeScale > 0.01f) ? newTimeScale : 0f);
}
public IEnumerator FreezeMoment(float rampDownTime,float waitTime,float rampUpTime,float targetSpeed)
{
timeSlowedCount++;
yield return StartCoroutine(SetTimeScale(targetSpeed, rampDownTime));
for (float timer = 0f; timer < waitTime; timer += Time.unscaledDeltaTime)
{
yield return null;
}
yield return StartCoroutine(SetTimeScale(1f, rampUpTime));
timeSlowedCount--;
}
}
还需要新建一个新的静态类用过管理当前场景中的TimeScale,我们就叫他:TimeController:
using System;
using UnityEngine;
public static class TimeController
{
private static float slowMotionTimeScale = 1f;
private static float pauseTimeScale = 1f;
private static float platformBackgroundTimeScale = 1f;
private static float genericTimeScale = 1f;
public static float GenericTimeScale
{
get
{
return genericTimeScale;
}
set
{
SetTimeScaleFactor(ref genericTimeScale, value);
}
}
private static void SetTimeScaleFactor(ref float field, float val)
{
if (field != val)
{
field = val;
float num = slowMotionTimeScale * pauseTimeScale * platformBackgroundTimeScale * genericTimeScale;
if (num < 0.01f)
{
num = 0f;
}
Time.timeScale = num;
}
}
}
回到HeroController.cs中,我们来制作无敌状态的方法()
private IEnumerator Invulnerable(float duration)
{
cState.invulnerable = true;
yield return new WaitForSeconds(DAMAGE_FREEZE_DOWN);
invPulse.StartInvulnerablePulse();
yield return new WaitForSeconds(duration);
invPulse.StopInvulnerablePulse();
cState.invulnerable = false;
cState.recoiling = false;
}
这个invPulse用来制作无敌颜色和通常颜色的线性差值的变化
using System;
using UnityEngine;
public class InvulnerablePulse : MonoBehaviour
{
public Color invulColor;
public float pulseDuration;
private Color normalColor;
private tk2dSprite sprite;
private bool pulsing;
private bool reverse;
private float currentLerpTime;
private void Start()
{
sprite = GetComponent<tk2dSprite>();
normalColor = sprite.color;
pulsing = false;
currentLerpTime = 0f;
}
private void Update()
{
if (pulsing)
{
if (!reverse)
{
currentLerpTime += Time.deltaTime;
if(currentLerpTime > pulseDuration)
{
currentLerpTime = pulseDuration;
reverse = true;
}
}
else
{
currentLerpTime -= Time.deltaTime;
if(currentLerpTime < 0f)
{
currentLerpTime = 0f;
reverse = false;
}
}
float t = currentLerpTime / pulseDuration;
sprite.color = Color.Lerp(normalColor, invulColor, t);
}
}
public void StartInvulnerablePulse()
{
pulsing = true;
currentLerpTime = 0f;
}
public void StopInvulnerablePulse()
{
pulsing = false;
UpdateSpriteColor(normalColor);
currentLerpTime = 0f;
}
public void UpdateSpriteColor(Color color)
{
sprite.color = color;
}
}
而死亡函数和致命死亡函数仅仅是开始阶段,我们暂时只是将renderer.enabled = false;所以死亡动画什么的要到后面才能做到。
private IEnumerator Die()
{
if (OnDeath != null)
{
OnDeath();
}
if (!cState.dead)
{
playerData.disablePause = true;
rb2d.velocity = Vector2.zero;
CancelRecoilHorizonal();
AffectedByGravity(false);
HeroBox.inactive = true;
rb2d.isKinematic = true;
SetState(ActorStates.no_input);
cState.dead = true;
ResetMotion();
ResetHardLandingTimer();
renderer.enabled = false;
gameObject.layer = 2;
yield return null;
}
}
private IEnumerator DieFromHazard(HazardTypes hazardType,float angle)
{
if (!cState.hazardDeath)
{
playerData.disablePause = true;
SetHeroParent(null);
SetState(ActorStates.no_input);
cState.hazardDeath = true;
ResetMotion();
ResetHardLandingTimer();
AffectedByGravity(false);
renderer.enabled = false;
gameObject.layer = 2;
if(hazardType == HazardTypes.SPIKES)
{
}
else if(hazardType == HazardTypes.ACID)
{
}
yield return null;
}
}
最后我们来到Unity编辑器中,首先我们先来更改每一种Slash里面的PlayerMakerFSM的变量叫magnitudeMult的值都设置为1。
为Knight制作一个新的playmakerFSM叫proxyFSM:
这里有一个新的自定义行为叫SendEventToRegister.cs:
using System;
using HutongGames.PlayMaker;
[ActionCategory("Hollow Knight")]
public class SendEventToRegister : FsmStateAction
{
public FsmString eventName;
public override void Reset()
{
eventName = new FsmString();
}
public override void OnEnter()
{
if(eventName.Value != "")
{
EventRegister.SendEvent(eventName.Value);
}
base.Finish();
}
}
我们再给小骑士添加一个新的脚本叫EventRegister.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EventRegister : MonoBehaviour
{
public static Dictionary<string, List<EventRegister>> eventRegister = new Dictionary<string, List<EventRegister>>();
[SerializeField] public string subscribedEvent = "";
public delegate void RegisteredEvent();
public event RegisteredEvent OnReceivedEvent;
private void Awake()
{
SubscribeEvent(this);
}
private void OnDestroy()
{
UnsubscribeEvent(this);
}
public static void SendEvent(string eventName)
{
if (eventName == "")
{
return;
}
if (eventRegister.ContainsKey(eventName))
{
foreach (EventRegister eventRegister in eventRegister[eventName])
{
eventRegister.ReceiveEvent();
}
}
}
public void ReceiveEvent()
{
FSMUtility.SendEventToGameObject(gameObject, this.subscribedEvent, false);
if (this.OnReceivedEvent != null)
{
this.OnReceivedEvent();
}
}
public static void SubscribeEvent(EventRegister register)
{
string key = register.subscribedEvent;
List<EventRegister> list;
if (eventRegister.ContainsKey(key))
{
list = eventRegister[key];
}
else
{
list = new List<EventRegister>();
eventRegister.Add(key, list);
}
list.Add(register);
}
public static void UnsubscribeEvent(EventRegister register)
{
string key = register.subscribedEvent;
if (eventRegister.ContainsKey(key))
{
List<EventRegister> list = eventRegister[key];
if (list.Contains(register))
{
list.Remove(register);
}
if (list.Count <= 0)
{
eventRegister.Remove(key);
}
}
}
}
我们在小骑士的子对象Effects,给它添加一个新的子对象叫Damage Effect,用来制作受伤系统的效果:
首先从Leak开始介绍:
另一个粒子系统Hit Pt 1:
另一个粒子系统Hit Pt 2只需要改变Transform即可:
Hit Crack:这个tk2dSprite和tk2dSpriteAnimation我感觉你们都会做了就不细说了
然后是只剩一滴血的时候的Damage Effects:
这个low health hit我使用animator制作的,你也可以使用tk2dSpriteAnimator来制作:
然后是另一个粒子系统black particle burst:
为Hit Crack添加新的Playermaker FSM:简单,只需要把Wait的Time和动画的Clip Time的时间设置成相等即可
low health hit effect也同理:
然给Damage Effect添加一个名字叫Knight Damage 的playmakerFSM:
事件如上,变量如下,特殊处理的变量的图片我都放出来了:
每一种状态如下所示:
这里也需要一个新的自定义行为PlayerDataBoolTest .cs:
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.Logic)]
[Tooltip("Sends Events based on the value of a Boolean Variable.")]
public class PlayerDataBoolTest : FsmStateAction
{
[RequiredField]
[Tooltip("GameManager reference, set this to the global variable GameManager.")]
public FsmOwnerDefault gameObject;
[RequiredField]
public FsmString boolName;
[Tooltip("Event to send if the Bool variable is True.")]
public FsmEvent isTrue;
[Tooltip("Event to send if the Bool variable is False.")]
public FsmEvent isFalse;
private bool boolCheck;
public override void Reset()
{
gameObject = null;
boolName = null;
isTrue = null;
isFalse = null;
}
public override void OnEnter()
{
GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
if (ownerDefaultTarget == null)
{
return;
}
GameManager component = ownerDefaultTarget.GetComponent<GameManager>();
if (component == null)
{
return;
}
boolCheck = component.GetPlayerDataBool(boolName.Value);
if (boolCheck)
{
Fsm.Event(isTrue);
}
else
{
Fsm.Event(isFalse);
}
Finish();
}
}
}
总结
回到Unity编辑器中,我们来小骑士的面板给它设置好参数,仅供参考:
我们先来测试敌人的后坐力,这里以crawler为例子,我们给它的recoil行为就是收到攻击后会执行Stun函数也即是播放动画并保持速度为0.
可以看到动画播放完成
再以cralwer为例子,我们让它来测试我们的小骑士后坐力行为和受伤系统以及小部分死亡系统
敌人与HeroBox发生碰撞检测后,播放小骑士Recoil动画,玩家被后坐力影响了速度,播放音乐,DamageEffect发生效果
玩家在InvulnerablePulse无敌状态下不会再与敌人产生碰撞检测
当被撞到最后一滴血的时候,SetActive low health hit effect 设置为true,播放低血量状态下的hit effect,
到最后当hp=0的时候,renderer.enabled 设置为false,玩家在此等待重生,至此我们今天的目标均已实现。、
下一期我们来做下小骑士的灵魂系统和法术系统,喜欢的话就蹲下明天八点看我能不能搞出来吧。本文一共五万多字,看完记得做下眼保健操 。