[Unity Demo]从零开始制作空洞骑士Hollow Knight第十集:制作后坐力系统Recoil和小骑士的生命系统和受伤系统

news2024/9/24 8:31:49

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、制作后坐力系统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,玩家在此等待重生,至此我们今天的目标均已实现。、

下一期我们来做下小骑士的灵魂系统和法术系统,喜欢的话就蹲下明天八点看我能不能搞出来吧。本文一共五万多字,看完记得做下眼保健操 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2159902.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Appium独立测试自动化初始化脚本

1、查看环境初始化参数 确保appium已经开起来了&#xff0c;设置ip ,并点击启动 打开夜神模拟器&#xff0c;点击工具--设置 最下面的版本说明&#xff0c;双击进去 版本号这里再去单击。 直到进入到开发者模式。 可能我们不是开发者模式打开的状态&#xff0c;所以软件访问模…

fo-dicom是如何实现DICOM 的网络通信功能

一、前言 前面的文章&#xff0c;我们介绍了fo-dicom是一个怎样的开源库等一些内容&#xff1a; fo-dicom&#xff0c;第一个基于.NET Standard 2.0 开发的DICOM开源库fo-dicom开源库是如何满足 DICOM标准的基本要求fo-dicom开发之DICOM数据解析&#xff1a;常见数据类型及处…

自学前端的正确姿势是...

师傅带进门&#xff0c;修行在个人。 在前端自学成才的道路上&#xff0c;有些人走的很快&#xff0c;有些人却举步维艰。 为什么会这样子呢&#xff1f;因为他们没有掌握自学前端的正确姿势。 在介绍应该要怎样自学前端之前&#xff0c;首先来看下&#xff0c;自学前端容易…

JavaWeb--小白笔记07:servlet对表单数据的简单处理

这里的servlet对表单数据的处理是指使用IDEA创建web工程&#xff0c;再创建html和class文件进行连接&#xff0c;实现html创建一个表单网页&#xff0c;我们对网页中的表单进行填充&#xff0c;可以通过class文件得到网页我们填充的内容进行打印到控制台。 一登录系统页面---h…

[vulnhub] w1r3s.v1.0

https://www.vulnhub.com/entry/w1r3s-101,220/ 思路&#xff1a;红队笔记 主机发现端口扫描 使用nmap扫描网段类存活主机 因为靶机是我最后添加的&#xff0c;所以靶机IP是133 nmap -sP 192.168.75.0/24 // Starting Nmap 7.93 ( https://nmap.org ) at 2024-09-20 09:09 CST…

MySQL 数据库安装(详细教程)

文章目录 一、前言二、下载 MySQL2.1 安装包方式2.2 压缩包方式&#xff08;推荐&#xff09; 三、安装 MySQL3.1 解压 MySQL 文件3.2 配置环境变量3.3 初始化 data 目录3.4 安装 MySQL 服务3.5 开启 MySQL 服务3.6 修改 MySQL 密码 四、卸载 MySQL4.1 停止 MySQL 服务4.2 删除…

MySQL record 08 part

数据库连接池&#xff1a; Java DataBase Connectivity&#xff08;Java语言连接数据库&#xff09; 答&#xff1a; 使用连接池能解决此问题&#xff0c; 连接池&#xff0c;自动分配连接对象&#xff0c;并对闲置的连接进行回收。 常用的数据库连接池&#xff1a; 建立数…

【WRF运行第三期】服务器上运行WRF模型(官网案例-Hurricane Matthew)

【WRF运行第三期】运行WRF模型&#xff08;官网案例-Hurricane Matthew&#xff09; 官网案例-Hurricane Matthew介绍0 创建DATA文件夹1 WPS预处理1.1 解压GRIB数据&#xff08;ungrib.exe&#xff09;1.1.1 解压GRIB数据---GFS&#xff08;Matthew案例研究数据&#xff09;1.1…

Bytebase 2.22.3 - 一键回滚 PostgreSQL DML 变更

&#x1f680; 新功能 支持一键回滚 PostgreSQL DML 变更。 &#x1f384; 改进 优化 DML 事前备份和回滚体验&#xff1a; 引导用户创建 bbdataarchive 数据库。如果没有 bbdataarchive 数据库&#xff0c;无法开启备份功。用户现在可以在创建工单之后开启或关闭备份功能&a…

PyCharm远程连接AutoDL服务器实现程序调试

本文详细介绍了如何在Pycharm中配置SSH和SFTP&#xff0c;以便于在AOTUDL服务器上进行代码修改、调试。步骤包括新建工程、配置SFTP连接、设置Rootpath和Mapping&#xff0c;以及实现自动上传和下载文件的功能。远程服务器编辑调试只是试用于专业版本的pycharm&#xff0c;我的…

【LLM学习之路】9月22日 第九天 自然语言处理

【LLM学习之路】9月22日 第九天 直接看Transformer 第一章 自然语言处理 自然语言处理发展史 只要看的足够多&#xff0c;未必需要理解语言 统计语言模型发展史 统计语言模型&#xff1a; 判断一个句子是否合理&#xff0c;就计算这个句子会出现的概率 缺点是句子越长越…

微软推迟在MDM设备上启用OOBE强制更新 因为IT管理员反馈称缺乏控制

微软很久之前就计划在 Windows 10/11 OOBE 期间强制下载更新&#xff0c;即若检测到系统本身属于旧版本例如并未安装最新累积更新&#xff0c;则在 OOBE 期间强制下载最新累积更新并自动安装。这种更新方式已经在面向消费者的设备上启用&#xff0c;而上周微软则是在适用于企业…

盘点那些功能强大的思维导图在线工具,你用过几个

如果我们日常遇到比较繁杂的信息需要梳理&#xff0c;那我比较推荐使用思维导图在线工具进行梳理。这些工具可以通过图形化的方式展示各种信息之间的关系。这篇文章我将要介绍几款好用的思维导图工具帮我们更好的组织思维。 1.福晰思维导图 链接一下&#xff1a;https://www.…

GPIO与MIO控制LED——ZYNQ学习笔记2

一、GPIO简介 ZYNQ 分为 PS 和 PL 两部分&#xff0c;那么器件的引脚&#xff08; Pin&#xff09;资源同样也分成了两部分。 ZYNQ PS 中的外设可以通过 MIO&#xff08; multiplexed I/O&#xff0c;多路复用 I/O&#xff09;模块连接到 PS 端的引脚上&#xff0c;也可以通过 …

HTML讲解(三)通用部分

目录 1.空格标记 2.特殊文字的标记 3.注释语句 4.对文字字体的设置 5.修改文字形态 6.换行标记 7.居中标记 8.水平线标记 9.设置滚动弹幕 1.空格标记 在HTML中&#xff0c;我们想打印空格并不能直接敲一个空格键&#xff0c;因为如果是敲空格键&#xff0c;那无论你敲…

【JUC并发编程系列】深入理解Java并发机制:Volatile从底层原理解析到高级应用技巧(六、Volatile关键字、JMM、重排序、双重检验锁)

文章目录 【JUC并发编程系列】深入理解Java并发机制&#xff1a;Volatile从底层原理解析到高级应用技巧(六、Volatile关键字、JMM、重排序、双重检验锁)1. Volatile的特性2. Volatile的用法3. CPU多核硬件架构剖析4. JMM内存模型4.1 主要特性4.2 JMM 的工作原理4.3 实现机制 5.…

Leetcode面试经典150题-39.组合总数进阶:40.组合总和II

本题是扩展题&#xff0c;真实考过&#xff0c;看这个题之前先看一下39题 Leetcode面试经典150题-39.组合总数-CSDN博客 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数…

Docker:解决开发运维问题的开源容器化平台

云计算de小白 Docker是一个开源的容器化平台&#xff0c;可以将应用程序及其依赖的环境打包成轻量级、可移植的容器。 Docker为什么这么受欢迎呢?原因很简单&#xff1a;Docker可以解决不同环境一致运行的问题&#xff0c;而且占用资源少&#xff0c;速度快。 所以好的东西…

链式队列操作

文章目录 &#x1f34a;自我介绍&#x1f34a;概述&#x1f34a;链式队列代码linkstack.clinkstack.hmain.c 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&…

OmniPeek 空口抓包软件安装指导

OmniPeek 空口抓包软件安装指导 1 双击omnp75安装包---Unzip解压缩 生成install包 2 进入install文件夹点击setup开始进入安装界面 3 点击install Omnipeek 4 点击next,勾选手动安装