[Unity Demo]从零开始制作空洞骑士Hollow Knight第九集:制作小骑士基本的攻击行为Attack以及为敌人制作生命系统和受伤系统

news2024/9/24 9:58:13

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

文章目录

  • 前言
  • 一、制作小骑士基本的攻击行为Attack
    • 1.制作动画以及使用UNITY编辑器编辑
    • 2.使用代码实现扩展新的落地行为和重落地行为
    • 3.使用状态机实现击中敌人造成伤害机制
  • 二、为敌人制作生命系统
    • 1.使用代码制作生命系统
  • 三、为敌人制作受伤系统
    • 1.使用代码制作受伤系统
    • 2.制作受伤特效
  • 总结


前言

        警告:此篇文章难度较高而且复杂繁重,非常不适合刚刚入门的或者没看过我前几期的读者,我做了一整天才把明显的bug给解决了真的快要睁不开眼了,因此请读者如果在阅读后感到身体不适请立刻退出这篇文章,本期主要涉及的内容是:制作小骑士基本的攻击行为Attack以及为敌人制作生命系统和受伤系统,我已经把内容上传到我的github空洞骑士demo资产中,欢迎大家下载后在Unity研究。

GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!


一、制作小骑士基本的攻击行为Attack

1.制作动画以及使用UNITY编辑器编辑 

我们来为小骑士添加几个新的tk2dSprite和tk2dSpriteAnimation:

我们把Knight文件夹中所有和Slash有关的文件夹里面的Sprite全部拖进去,然后点击Apply:

然后到Animation中,我们每一个种类的Slash都只要前六张图像,Clip Time统一设置成0.3

 

 

除此之外我们还做刀光效果也就是SlashEffect,为每一种 SlashEffect创建自己单独的tk2dSprite和tk2dSpriteAnimation:

同样,我们找到 Knight文件夹中所有和Slashffect有关的文件夹一个个拖上去:

在Animation中我们只要0,2,4三张图像,ClipTime设置成0.2:

万事准备OK后我们就给小骑士创建好这样的Attack子对象:

 一定要记得给Attacks下的每一个子对象设置Layer为HeroAttack:

选择好HeroAttack可以交互的层级:

 然后为每一种Slash Effect添加如下图所示的组件:

那些脚本你们先别管(比如里面的NailSlash.cs),我后续都会讲的。我们先设置好PolygonCollider2D的参数,这里有个小技巧,使用tk2dSprite的这个显示刀光的图片,然后对着这个大小调整好碰撞箱大小,注意要勾选isTrigger,调整好后

调整好后记得关上MeshRenderer和 PolygonCollider2D,我们只在需要的时候用到它们:

关于Slash的子对象Clash Tink目前还用不上,我们先把它放一边以后用的时候再扩展:

然后其它三个如上同理,最后打开Gizmos后效果如下所示:

2.使用代码实现扩展新的落地行为和重落地行为 

        我们每添加一个行为就要到HeroActions中添加一个PlayerAction,这次是attack

重大失误!!! 这个moveVector = CreateTwoAxisPlayerAction(left, right, down, up);它的顺序应该是-x,x-y,y。我之前倒数两个参数位置搞反了,所以检测的y轴输入时反的,现在已经更改过来了,只能说还好Debug发现的早。

using System;
using InControl;

public class HeroActions : PlayerActionSet
{
    public PlayerAction left;
    public PlayerAction right;
    public PlayerAction up;
    public PlayerAction down;
    public PlayerTwoAxisAction moveVector;
    public PlayerAction attack;
    public PlayerAction jump;
    public PlayerAction dash;

    public HeroActions()
    {
	left = CreatePlayerAction("Left");
	left.StateThreshold = 0.3f;
	right = CreatePlayerAction("Right");
	right.StateThreshold = 0.3f;
	up = CreatePlayerAction("Up");
	up.StateThreshold = 0.3f;
	down = CreatePlayerAction("Down");
	down.StateThreshold = 0.3f;
	moveVector = CreateTwoAxisPlayerAction(left, right, down, up); //重大失误!!!
	moveVector.LowerDeadZone = 0.15f;
	moveVector.UpperDeadZone = 0.95f;
	attack = CreatePlayerAction("Attack");
	jump = CreatePlayerAction("Jump");
	dash = CreatePlayerAction("Dash");
    }
}

然后就到InputHandler.cs中添加一行代码:    AddKeyBinding(inputActions.attack, "Z");

using System;
using System.Collections;
using System.Collections.Generic;
using GlobalEnums;
using InControl;
using UnityEngine;

public class InputHandler : MonoBehaviour
{
    public InputDevice gameController;
    public HeroActions inputActions;

    public void Awake()
    {
	inputActions = new HeroActions();

    }

    public void Start()
    {
	MapKeyboardLayoutFromGameSettings();
	if(InputManager.ActiveDevice != null && InputManager.ActiveDevice.IsAttached)
	{

	}
	else
	{
	    gameController = InputDevice.Null;
	}
	Debug.LogFormat("Input Device set to {0}.", new object[]
	{
	    gameController.Name
	});
    }

    private void MapKeyboardLayoutFromGameSettings()
    {
	AddKeyBinding(inputActions.up, "UpArrow");
	AddKeyBinding(inputActions.down, "DownArrow");
	AddKeyBinding(inputActions.left, "LeftArrow");
	AddKeyBinding(inputActions.right, "RightArrow");
	AddKeyBinding(inputActions.attack, "Z");
	AddKeyBinding(inputActions.jump, "X");
	AddKeyBinding(inputActions.dash, "D");
    }

    private static void AddKeyBinding(PlayerAction action, string savedBinding)
    {
	Mouse mouse = Mouse.None;
	Key key;
	if (!Enum.TryParse(savedBinding, out key) && !Enum.TryParse(savedBinding, out mouse))
	{
	    return;
	}
	if (mouse != Mouse.None)
	{
	    action.AddBinding(new MouseBindingSource(mouse));
	    return;
	}
	action.AddBinding(new KeyBindingSource(new Key[]
	{
	    key
	}));
    }

}

来到HeroControllerState部分,我们添加几个新的状态:

    public bool attacking;
    public bool altAttack;
    public bool upAttacking;
    public bool downAttacking; 

[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 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;
        preventDash = false;
        preventBackDash = false;
	dashCooldown = false;
        backDashCooldown = false;
	isPaused = false;
    }

回到HeroAnimationController.cs中,我们为attack攻击判断哪种攻击类型:

if(cState.attacking)
    {
        if (cState.upAttacking)
        {
                Play("UpSlash");
        }
        else if (cState.downAttacking)
        {
                Play("DownSlash");
        }
        else if (!cState.altAttack)
        {
                Play("Slash");
        }
        else
        {
                Play("SlashAlt");
        }
    }

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:
	}
	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"); ;
    }

}

回到HeroController.cs当中,我们来为攻击添加完整的行为状态控制机:

    [SerializeField] private NailSlash slashComponent; //决定使用哪种攻击的NailSlash
    [SerializeField] private PlayMakerFSM slashFsm;//决定使用哪种攻击的PlayMakerFSM

    public NailSlash normalSlash;
    public NailSlash altetnateSlash;
    public NailSlash upSlash;
    public NailSlash downSlash;

    public PlayMakerFSM normalSlashFsm; 
    public PlayMakerFSM altetnateSlashFsm;
    public PlayMakerFSM upSlashFsm;
    public PlayMakerFSM downSlashFsm;

    private bool attackQueuing; //是否开始攻击计数步骤
    private int attackQueueSteps; //攻击计数步骤

    private float attack_time;
    private float attackDuration; //攻击状态持续时间,根据有无护符来决定
    private float attack_cooldown;
    private float altAttackTime; //当时间超出可按二段攻击的时间后,cstate.altattack就会为false

    public float ATTACK_DURATION; //无护符时攻击状态持续时间
    public float ATTACK_COOLDOWN_TIME; //攻击后冷却时间
    public float ATTACK_RECOVERY_TIME; //攻击恢复时间,一旦超出这个时间就退出攻击状态
    public float ALT_ATTACK_RESET; //二段攻击重置时间

    private int ATTACK_QUEUE_STEPS = 5; //超过5步即可开始攻击

    private float NAIL_TERRAIN_CHECK_TIME = 0.12f;

在Update()中我们当攻击时间超过attackDuration后重置攻击,并开启冷却倒计时attack_cooldown :

  else if (hero_state != ActorStates.no_input)
        {
            LookForInput();

            if(cState.attacking && !cState.dashing)
           {
                attack_time += Time.deltaTime;
                if(attack_time >= attackDuration)
                    {
                    ResetAttacks();
                    animCtrl.StopAttack();
                   }
             }
        }

 if(attack_cooldown > 0f)
    {
            attack_cooldown -= Time.deltaTime;
    }

在方法LookForQueueInput()中我们判断是否按下攻击键:

 if(inputHandler.inputActions.attack.IsPressed && attackQueueSteps <= ATTACK_QUEUE_STEPS && CanAttack() && attackQueuing)
        {
                Debug.LogFormat("Start Do Attack");
                DoAttack();
        }

以及进入attackQueuing:

if(inputHandler.inputActions.attack.WasPressed)
        {
                if (CanAttack())
        {
                    DoAttack();
        }
        else
        {
                    attackQueueSteps = 0;
                    attackQueuing = true;
                }
        }

在Update()中我们直接++

    if(attackQueuing)
    {
            attackQueueSteps++;
    }

如果没按下攻击键就attackQueuing = false;

 if (!inputHandler.inputActions.attack.IsPressed)
        {
                attackQueuing = false;
        }

当然还有Attack(),DoAttack(),CanAttack(),CancelAttack()等等方法构成完整的攻击行为:

private bool CanAttack()
    {
        return hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing && attack_cooldown <= 0f && !cState.attacking && !cState.dashing;
    }

private void DoAttack()
    {


        attack_cooldown = ATTACK_COOLDOWN_TIME;
        if(vertical_input > Mathf.Epsilon)
    {
            Attack(AttackDirection.upward);
            StartCoroutine(CheckForTerrainThunk(AttackDirection.upward));
            return;
    }
        if(vertical_input >= -Mathf.Epsilon)
    {
            Attack(AttackDirection.normal);
            StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
            return;
        }
        if(hero_state != ActorStates.idle && hero_state != ActorStates.running)
    {
            Attack(AttackDirection.downward);
            StartCoroutine(CheckForTerrainThunk(AttackDirection.downward));
            return;
        }
        Attack(AttackDirection.normal);
        StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
    }

private void Attack(AttackDirection attackDir)
    {
        if(Time.timeSinceLevelLoad - altAttackTime > ALT_ATTACK_RESET)
    {
            cState.altAttack = false;
    }
        cState.attacking = true;
        attackDuration = ATTACK_DURATION;

        if (attackDir == AttackDirection.normal)
        {
            if (!cState.altAttack)
            {
                slashComponent = normalSlash;
                slashFsm = normalSlashFsm;
                cState.altAttack = true;

            }
            else
            {
                slashComponent = altetnateSlash;
                slashFsm = altetnateSlashFsm;
                cState.altAttack = false;
            }
        }
        else if (attackDir == AttackDirection.upward) 
        {
            slashComponent = upSlash;
            slashFsm = upSlashFsm;
            cState.upAttacking = true;

        }
        else if (attackDir == AttackDirection.downward)
        {
            slashComponent = downSlash;
            slashFsm = downSlashFsm;
            cState.downAttacking = true;

        }

        if(attackDir == AttackDirection.normal && cState.facingRight)
    {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 0f;
    }
        else if (attackDir == AttackDirection.normal && !cState.facingRight)
        {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 180f;
        }
        else if (attackDir == AttackDirection.upward)
        {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 90f;
        }
        else if (attackDir == AttackDirection.downward)
        {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 270f;
        }
        altAttackTime = Time.timeSinceLevelLoad;
        slashComponent.StartSlash();

    }

 完整的代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using GlobalEnums;
using UnityEngine;

public class HeroController : MonoBehaviour
{
    public ActorStates hero_state;
    public ActorStates prev_hero_state;

    public bool acceptingInput = true;

    public float move_input;
    public float vertical_input;

    private Vector2 current_velocity;

    public float WALK_SPEED = 3.1f;//走路速度
    public float RUN_SPEED = 5f;//跑步速度
    public float JUMP_SPEED = 5f;//跳跃的食欲

    [SerializeField] private NailSlash slashComponent; //决定使用哪种攻击的NailSlash
    [SerializeField] private PlayMakerFSM slashFsm;//决定使用哪种攻击的PlayMakerFSM

    public NailSlash normalSlash;
    public NailSlash altetnateSlash;
    public NailSlash upSlash;
    public NailSlash downSlash;

    public PlayMakerFSM normalSlashFsm; 
    public PlayMakerFSM altetnateSlashFsm;
    public PlayMakerFSM upSlashFsm;
    public PlayMakerFSM downSlashFsm;

    private bool attackQueuing; //是否开始攻击计数步骤
    private int attackQueueSteps; //攻击计数步骤

    private float attack_time;
    private float attackDuration; //攻击状态持续时间,根据有无护符来决定
    private float attack_cooldown;
    private float altAttackTime; //当时间超出可按二段攻击的时间后,cstate.altattack就会为false

    public float ATTACK_DURATION; //无护符时攻击状态持续时间
    public float ATTACK_COOLDOWN_TIME; //攻击后冷却时间
    public float ATTACK_RECOVERY_TIME; //攻击恢复时间,一旦超出这个时间就退出攻击状态
    public float ALT_ATTACK_RESET; //二段攻击重置时间

    private int ATTACK_QUEUE_STEPS = 5; //超过5步即可开始攻击

    private float NAIL_TERRAIN_CHECK_TIME = 0.12f;

    private int jump_steps; //跳跃的步
    private int jumped_steps; //已经跳跃的步
    private int jumpQueueSteps; //跳跃队列的步
    private bool jumpQueuing; //是否进入跳跃队列中

    private int jumpReleaseQueueSteps; //释放跳跃后的步
    private bool jumpReleaseQueuing; //是否进入释放跳跃队列中
    private bool jumpReleaseQueueingEnabled; //是否允许进入释放跳跃队列中

    public float MAX_FALL_VELOCITY; //最大下落速度(防止速度太快了)
    public int JUMP_STEPS; //最大跳跃的步
    public int JUMP_STEPS_MIN; //最小跳跃的步
    private int JUMP_QUEUE_STEPS; //最大跳跃队列的步
    private int JUMP_RELEASE_QUEUE_STEPS;//最大跳跃释放队列的步

    private int dashQueueSteps;
    private bool dashQueuing;

    private float dashCooldownTimer; //冲刺冷却时间
    private float dash_timer; //正在冲刺计数器
    private float back_dash_timer; 正在后撤冲刺计数器 (标注:此行代码无用待后续开发)
    private float dashLandingTimer;
    private bool airDashed;//是否是在空中冲刺
    public bool dashingDown;//是否正在执行向下冲刺
    public PlayMakerFSM dashBurst;
    public GameObject dashParticlesPrefab;//冲刺粒子效果预制体
    public GameObject backDashPrefab; //后撤冲刺特效预制体 标注:此行代码无用待后续开发
    private GameObject backDash;//后撤冲刺 (标注:此行代码无用待后续开发)
    private GameObject dashEffect;//后撤冲刺特效生成 (标注:此行代码无用待后续开发)

    public float DASH_SPEED; //冲刺时的速度
    public float DASH_TIME; //冲刺时间
    public float DASH_COOLDOWN; //冲刺冷却时间
    public float BACK_DASH_SPEED;//后撤冲刺时的速度 (标注:此行代码无用待后续开发)
    public float BACK_DASH_TIME;//后撤冲刺时间 (标注:此行代码无用待后续开发)
    public float BACK_DASH_COOLDOWN; //后撤冲刺冷却时间 (标注:此行代码无用待后续开发)
    public float DASH_LANDING_TIME;
    public int DASH_QUEUE_STEPS; //最大冲刺队列的步

    public float fallTimer { get; private set; }

    private float hardLandingTimer; //正在hardLanding的计时器,大于就将状态改为grounded并BackOnGround()
    private float hardLandFailSafeTimer; //进入hardLand后玩家失去输入的一段时间
    private bool hardLanded; //是否已经hardLand了

    public float HARD_LANDING_TIME; //正在hardLanding花费的时间。
    public float BIG_FALL_TIME;  //判断是否是hardLanding所需要的事件,大于它就是

    public GameObject hardLandingEffectPrefab;

    private float prevGravityScale;

    private int landingBufferSteps;
    private int LANDING_BUFFER_STEPS = 5;
    private bool fallRumble; //是否开启掉落时相机抖动

    public GameObject softLandingEffectPrefab;

    public bool touchingWall; //是否接触到墙
    public bool touchingWallL; //是否接触到的墙左边
    public bool touchingWallR; //是否接触到的墙右边

    private Rigidbody2D rb2d;
    private BoxCollider2D col2d;
    private GameManager gm;
    public PlayerData playerData;
    private InputHandler inputHandler;
    public HeroControllerStates cState;
    private HeroAnimationController animCtrl;
    private HeroAudioController audioCtrl; 

    private static HeroController _instance;

    public static HeroController instance
    {
	get
	{
            if (_instance == null)
                _instance = FindObjectOfType<HeroController>();
            if(_instance && Application.isPlaying)
	    {
                DontDestroyOnLoad(_instance.gameObject);
	    }
            return _instance;
	}
    }

    public HeroController()
    {
        ATTACK_QUEUE_STEPS = 5;
        NAIL_TERRAIN_CHECK_TIME = 0.12f;
        JUMP_QUEUE_STEPS = 2;
        JUMP_RELEASE_QUEUE_STEPS = 2;

        LANDING_BUFFER_STEPS = 5;
    }

    private void Awake()
    {
        if(_instance == null)
	{
            _instance = this;
            DontDestroyOnLoad(this);
	}
        else if(this != _instance)
	{
            Destroy(gameObject);
            return;
	}
        SetupGameRefs();
    }

    private void SetupGameRefs()
    {
        if (cState == null)
            cState = new HeroControllerStates();
        rb2d = GetComponent<Rigidbody2D>();
        col2d = GetComponent<BoxCollider2D>();
        animCtrl = GetComponent<HeroAnimationController>();
        audioCtrl = GetComponent<HeroAudioController>();
        gm = GameManager.instance;
        playerData = PlayerData.instance;
        inputHandler = gm.GetComponent<InputHandler>();
    }

    private void Start()
    {
        playerData = PlayerData.instance;
        if (dashBurst == null)
	{
            Debug.Log("DashBurst came up null, locating manually");
            dashBurst = FSMUtility.GetFSM(transform.Find("Effects").Find("Dash Burst").gameObject);
	}
    }

    private void Update()
    {
        current_velocity = rb2d.velocity;
        FallCheck();
        FailSafeCheck();
        if (hero_state == ActorStates.running && !cState.dashing && !cState.backDashing)
        {
            if (cState.inWalkZone)
            {
                audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
                audioCtrl.PlaySound(HeroSounds.FOOTSTEP_WALK);
            }
            else
            {
                audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
                audioCtrl.PlaySound(HeroSounds.FOOTSETP_RUN);
            }
        }
        else
        {
            audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
            audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
        }
        if(hero_state == ActorStates.dash_landing)
	{
	    dashLandingTimer += Time.deltaTime;
            if(dashLandingTimer > DASH_LANDING_TIME)
	    {
                BackOnGround();
	    }
	}
	if (hero_state == ActorStates.hard_landing)
        {
            hardLandingTimer += Time.deltaTime;
            if (hardLandingTimer > HARD_LANDING_TIME)
            {
                SetState(ActorStates.grounded);
                BackOnGround();
            }
        }
        if (hero_state == ActorStates.no_input)
        {

        }
        else if (hero_state != ActorStates.no_input)
        {
            LookForInput();

            if(cState.attacking && !cState.dashing)
	    {
                attack_time += Time.deltaTime;
                if(attack_time >= attackDuration)
		{
                    ResetAttacks();
                    animCtrl.StopAttack();
		}
	    }
        }
        LookForQueueInput();
        if(attack_cooldown > 0f)
	{
            attack_cooldown -= Time.deltaTime;
	}
        if (dashCooldownTimer > 0f) //计时器在Update中-= Time.deltaTime
        {
            dashCooldownTimer -= Time.deltaTime;
        }
    }

    private void FixedUpdate()
    {
        if(hero_state == ActorStates.hard_landing || hero_state == ActorStates.dash_landing)
	{
            ResetMotion();
	}
        else if(hero_state == ActorStates.no_input)
	{

	}
        else if (hero_state != ActorStates.no_input)
	{
            if(hero_state == ActorStates.running)
	    {
                if(move_input > 0f)
		{
		    if (CheckForBump(CollisionSide.right))
		    {
                        //rb2d.velocity = new Vector2(rb2d.velocity.x, BUMP_VELOCITY);
		    }
		}
                else if (CheckForBump(CollisionSide.left))
		{
                    //rb2d.velocity = new Vector2(rb2d.velocity.x, -BUMP_VELOCITY);
                }
            }
            if (!cState.dashing && !cState.backDashing)
            {
                Move(move_input);
                if (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME)
                {
                    if (move_input > 0f && !cState.facingRight)
                    {
                        FlipSprite();
                    }
                    else if (move_input < 0f && cState.facingRight)
                    {
                        FlipSprite();
                    }
                }
            }
	}

	if (cState.jumping) //如果cState.jumping就Jump
        {
            Jump();
	}
	if (cState.dashing)//如果cState.dashing就Dash
        {
            Dash();
	}
        //限制速度
        if(rb2d.velocity.y < -MAX_FALL_VELOCITY)
	{
            rb2d.velocity = new Vector2(rb2d.velocity.x, -MAX_FALL_VELOCITY);
	}
	if (jumpQueuing)
	{
            jumpQueueSteps++;
	}

	if (dashQueuing) //跳跃队列开始
	{
            dashQueueSteps++;
	}
        if(attackQueuing)
	{
            attackQueueSteps++;
	}
        if (landingBufferSteps > 0)
        {
            landingBufferSteps--;
        }
        if (jumpReleaseQueueSteps > 0)
	{
            jumpReleaseQueueSteps--;
	}

        cState.wasOnGround = cState.onGround;
    }

    /// <summary>
    /// 小骑士移动的函数
    /// </summary>
    /// <param name="move_direction"></param>
    private void Move(float move_direction)
    {
        if (cState.onGround)
        {
            SetState(ActorStates.grounded);
        }
        if(acceptingInput)
	{
            if (cState.inWalkZone)
            {
                rb2d.velocity = new Vector2(move_direction * WALK_SPEED, rb2d.velocity.y);
                return;
            }
            rb2d.velocity = new Vector2(move_direction * RUN_SPEED, rb2d.velocity.y);
	}
    }

    private void Attack(AttackDirection attackDir)
    {
        if(Time.timeSinceLevelLoad - altAttackTime > ALT_ATTACK_RESET)
	{
            cState.altAttack = false;
	}
        cState.attacking = true;
        attackDuration = ATTACK_DURATION;

        if (attackDir == AttackDirection.normal)
        {
            if (!cState.altAttack)
            {
                slashComponent = normalSlash;
                slashFsm = normalSlashFsm;
                cState.altAttack = true;

            }
            else
            {
                slashComponent = altetnateSlash;
                slashFsm = altetnateSlashFsm;
                cState.altAttack = false;
            }
        }
        else if (attackDir == AttackDirection.upward) 
        {
            slashComponent = upSlash;
            slashFsm = upSlashFsm;
            cState.upAttacking = true;

        }
        else if (attackDir == AttackDirection.downward)
        {
            slashComponent = downSlash;
            slashFsm = downSlashFsm;
            cState.downAttacking = true;

        }

        if(attackDir == AttackDirection.normal && cState.facingRight)
	{
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 0f;
	}
        else if (attackDir == AttackDirection.normal && !cState.facingRight)
        {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 180f;
        }
        else if (attackDir == AttackDirection.upward)
        {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 90f;
        }
        else if (attackDir == AttackDirection.downward)
        {
            slashFsm.FsmVariables.GetFsmFloat("direction").Value = 270f;
        }
        altAttackTime = Time.timeSinceLevelLoad;
        slashComponent.StartSlash();

    }

    private void DoAttack()
    {


        attack_cooldown = ATTACK_COOLDOWN_TIME;
        if(vertical_input > Mathf.Epsilon)
	{
            Attack(AttackDirection.upward);
            StartCoroutine(CheckForTerrainThunk(AttackDirection.upward));
            return;
	}
        if(vertical_input >= -Mathf.Epsilon)
	{
            Attack(AttackDirection.normal);
            StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
            return;
        }
        if(hero_state != ActorStates.idle && hero_state != ActorStates.running)
	{
            Attack(AttackDirection.downward);
            StartCoroutine(CheckForTerrainThunk(AttackDirection.downward));
            return;
        }
        Attack(AttackDirection.normal);
        StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
    }

    private bool CanAttack()
    {
        return hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing && attack_cooldown <= 0f && !cState.attacking && !cState.dashing;
    }

    //TODO:
    private void CancelAttack()
    {
        
    }


    private void ResetAttacks()
    {
        cState.attacking = false;
	cState.upAttacking = false;
        cState.downAttacking = false;
        attack_time = 0f;
    }

    /// <summary>
    /// 小骑士跳跃的函数
    /// </summary>
    private void Jump()
    {
	if (jump_steps <= JUMP_STEPS)
	{
	    rb2d.velocity = new Vector2(rb2d.velocity.x, JUMP_SPEED);
            jump_steps++;
            jumped_steps++;
            return;
        }
        CancelJump();
    }

    /// <summary>
    /// 取消跳跃,这个在释放跳跃键时有用
    /// </summary>
    private void CancelJump()
    {
        cState.jumping = false;
        jumpReleaseQueuing = false;
        jump_steps = 0;
    }

    /// <summary>
    /// 标注:此函数暂且不具备任何内容待后续开发
    /// </summary>
    private void BackDash()
    {

    }

    /// <summary>
    /// 冲刺时执行的函数
    /// </summary>
    private void Dash()
    {
        AffectedByGravity(false); //不受到重力影响
        ResetHardLandingTimer();
        if(dash_timer > DASH_TIME)
	{
            FinishedDashing();//大于则结束冲刺
            return;
	}
        float num;
	num = DASH_SPEED;
	if (dashingDown)
	{
            rb2d.velocity = new Vector2(0f, -num);
        }
	else if (cState.facingRight)
	{
	    if (CheckForBump(CollisionSide.right))
	    {
                //rb2d.velocity = new Vector2(num, cState.onGround ? BUMP_VELOCITY : BUMP_VELOCITY_DASH);
	    }
	    else
	    {
                rb2d.velocity = new Vector2(num, 0f); //为人物的velocity赋值DASH_SPEED
	    }
	}
        else if (CheckForBump(CollisionSide.left))
	{
            //rb2d.velocity = new Vector2(-num, cState.onGround ? BUMP_VELOCITY : BUMP_VELOCITY_DASH);
        }
	else
	{
            rb2d.velocity = new Vector2(-num, 0f);
	}
        dash_timer += Time.deltaTime;
    }

    private void HeroDash()
    {
	if (!cState.onGround)
	{
            airDashed = true;
	}

        audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
        audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
        audioCtrl.PlaySound(HeroSounds.DASH);


	if (inputHandler.inputActions.right.IsPressed)
	{
            FaceRight();
	}
        else if (inputHandler.inputActions.left.IsPressed)
        {
            FaceLeft();
        }
        cState.dashing = true;
        dashQueueSteps = 0;
        HeroActions inputActions = inputHandler.inputActions;
        if(inputActions.down.IsPressed && !cState.onGround && playerData.equippedCharm_31 && !inputActions.left.IsPressed && !inputActions.right.IsPressed)
        {
            dashBurst.transform.localPosition = new Vector3(-0.07f, 3.74f, 0.01f); //生成dashBurst后设置位置和旋转角
            dashBurst.transform.localEulerAngles = new Vector3(0f, 0f, 90f);
            dashingDown = true;
        }
	else
	{
            dashBurst.transform.localPosition = new Vector3(4.11f, -0.55f, 0.001f); //生成dashBurst后设置位置和旋转角
            dashBurst.transform.localEulerAngles = new Vector3(0f, 0f, 0f);
            dashingDown = false;
        }


        dashCooldownTimer = DASH_COOLDOWN;

        dashBurst.SendEvent("PLAY"); //发送dashBurst的FSM的事件PLAY
        dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission = true;

	if (cState.onGround)
	{
            dashEffect = Instantiate(backDashPrefab, transform.position, Quaternion.identity);
            dashEffect.transform.localScale = new Vector3(transform.localScale.x * -1f, transform.localScale.y, transform.localScale.z);
	}
    }

    /// <summary>
    /// 判断是否可以后撤冲刺
    /// </summary>
    /// <returns></returns>
    public bool CanBackDash()
    {
        return !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && !cState.preventBackDash && !cState.backDashCooldown && cState.onGround && playerData.canBackDash;
    } 

    /// <summary>
    /// 判断是否可以冲刺
    /// </summary>
    /// <returns></returns>
    public bool CanDash()
    {
        return hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing &&
           dashCooldownTimer <= 0f && !cState.dashing && !cState.backDashing && !cState.preventDash && (cState.onGround || !airDashed)  && playerData.canDash;
    }

    /// <summary>
    /// 结束冲刺
    /// </summary>
    private void FinishedDashing()
    {
        CancelDash();
        AffectedByGravity(true);//物体重新受到重力的影响
        animCtrl.FinishedDash(); //该播放Dash To Idle动画片段了

        if (cState.touchingWall && !cState.onGround)
	{
	    if (touchingWallL)
	    {

	    }
	    if (touchingWallR)
	    {

	    }
	}
    }

    /// <summary>
    /// 取消冲刺,将cState.dashing设置为false后动画将不再播放
    /// </summary>
    public void CancelDash()
    {

        cState.dashing = false;
        dash_timer = 0f; //重置冲刺时的计时器
        AffectedByGravity(true); //物体重新受到重力的影响

        if (dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission)
	{
            dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission = false;
        }
    }

    private void CancelBackDash()
    {
        cState.backDashing = false;
        back_dash_timer = 0f;
    }

    /// <summary>
    /// 物体是否受到重力的影响
    /// </summary>
    /// <param name="gravityApplies"></param>
    private void AffectedByGravity(bool gravityApplies)
    {
        float gravityScale = rb2d.gravityScale;
        if(rb2d.gravityScale > Mathf.Epsilon && !gravityApplies)
	{
            prevGravityScale = rb2d.gravityScale;
            rb2d.gravityScale = 0f;
            return;
	}
        if(rb2d.gravityScale <= Mathf.Epsilon && gravityApplies)
	{
            rb2d.gravityScale = prevGravityScale;
            prevGravityScale = 0f;
	}
    }

    private void FailSafeCheck()
    {
        if(hero_state == ActorStates.hard_landing)
	{
            hardLandFailSafeTimer += Time.deltaTime;
            if(hardLandFailSafeTimer > HARD_LANDING_TIME + 0.3f)
	    {
                SetState(ActorStates.grounded);
                BackOnGround();
                hardLandFailSafeTimer = 0f;
	    }
	}
	else
	{
            hardLandFailSafeTimer = 0f;
	}
    }

    /// <summary>
    /// 进入降落状态的检查
    /// </summary>
    private void FallCheck()
    {
        //如果y轴上的速度小于-1E-06F判断是否到地面上了
        if (rb2d.velocity.y < -1E-06F)
	{
	    if (!CheckTouchingGround())
	    {
                cState.falling = true;
                cState.onGround = false;

                if(hero_state != ActorStates.no_input)
		{
                    SetState(ActorStates.airborne);
		}
                fallTimer += Time.deltaTime;
                if(fallTimer > BIG_FALL_TIME)
		{
		    if (!cState.willHardLand)
		    {
                        cState.willHardLand = true;
		    }
		    if (!fallRumble)
		    {
                        StartFallRumble();
		    }
		}
	    }
	}
	else
	{
            cState.falling = false;
            fallTimer = 0f;

	    if (fallRumble)
	    {
                CancelFallEffects();
	    }
	}
    }

    private void DoHardLanding()
    {
        AffectedByGravity(true);
        ResetInput();
        SetState(ActorStates.hard_landing);

        hardLanded = true;
        audioCtrl.PlaySound(HeroSounds.HARD_LANDING);
        Instantiate(hardLandingEffectPrefab, transform.position,Quaternion.identity);
    }

    public void ResetHardLandingTimer()
    {
        cState.willHardLand = false;
        hardLandingTimer = 0f;
        fallTimer = 0f;
        hardLanded = false;
    }

    private bool ShouldHardLand(Collision2D collision)
    {
        return !collision.gameObject.GetComponent<NoHardLanding>() && cState.willHardLand && hero_state != ActorStates.hard_landing;
    }

    private void ResetInput()
    {
        move_input = 0f;
        vertical_input = 0f;
    }

    private void ResetMotion()
    {
        CancelJump();
        CancelDash();
        CancelBackDash();

        rb2d.velocity = Vector2.zero;

    }

    /// <summary>
    /// 翻转小骑士的localScale.x
    /// </summary>
    public void FlipSprite()
    {
        cState.facingRight = !cState.facingRight;
        Vector3 localScale = transform.localScale;
        localScale.x *= -1f;
        transform.localScale = localScale;
    }

    public void FaceRight()
    {
        cState.facingRight = true;
        Vector3 localScale = transform.localScale;
        localScale.x = -1f;
        transform.localScale = localScale;
    }

    public void FaceLeft()
    {
        cState.facingRight = false;
        Vector3 localScale = transform.localScale;
        localScale.x = 1f;
        transform.localScale = localScale;
    }

    private void LookForInput()
    {
        if (acceptingInput)
        {
            move_input = inputHandler.inputActions.moveVector.Vector.x; //获取X方向的键盘输入
            vertical_input = inputHandler.inputActions.moveVector.Vector.y;//获取Y方向的键盘输入
            FilterInput();//规整化


            if (inputHandler.inputActions.jump.WasReleased && jumpReleaseQueueingEnabled)
            {
                jumpReleaseQueueSteps = JUMP_RELEASE_QUEUE_STEPS;
                jumpReleaseQueuing = true;
            }
            if (!inputHandler.inputActions.jump.IsPressed)
            {
                JumpReleased();
            }
	    if (!inputHandler.inputActions.dash.IsPressed)
	    {
                if(cState.preventDash && !cState.dashCooldown)
		{
                    cState.preventDash = false;
		}
                dashQueuing = false;
	    }
	    if (!inputHandler.inputActions.attack.IsPressed)
	    {
                attackQueuing = false;
	    }
        }
    }

    private void LookForQueueInput()
    {
	if (acceptingInput)
	{
	    if (inputHandler.inputActions.jump.WasPressed)
	    {
                if (CanJump())
		{
                    HeroJump();
		}
		else
		{
                    jumpQueueSteps = 0;
                    jumpQueuing = true;

		}
	    }
	    if (inputHandler.inputActions.dash.WasPressed)
	    {
		if (CanDash())
		{
                    HeroDash();
		}
		else
		{
                    dashQueueSteps = 0;
                    dashQueuing = true;
		}
	    }
            if(inputHandler.inputActions.attack.WasPressed)
	    {
                if (CanAttack())
		{
                    DoAttack();
		}
		else
		{
                    attackQueueSteps = 0;
                    attackQueuing = true;
                }
	    }
	    if (inputHandler.inputActions.jump.IsPressed)
	    {
                if(jumpQueueSteps <= JUMP_QUEUE_STEPS && CanJump() && jumpQueuing)
		{
                    Debug.LogFormat("Execute Hero Jump");
                    HeroJump();
		}
	    }
            if(inputHandler.inputActions.dash.IsPressed && dashQueueSteps <= DASH_QUEUE_STEPS && CanDash() && dashQueuing)
	    {
                Debug.LogFormat("Start Hero Dash");
                HeroDash();
	    }
            if(inputHandler.inputActions.attack.IsPressed && attackQueueSteps <= ATTACK_QUEUE_STEPS && CanAttack() && attackQueuing)
	    {
                Debug.LogFormat("Start Do Attack");
                DoAttack();
	    }
	}
    }

    /// <summary>
    /// 可以跳跃吗
    /// </summary>
    /// <returns></returns>
    private bool CanJump()
    {
	if(hero_state == ActorStates.no_input || hero_state == ActorStates.hard_landing || hero_state == ActorStates.dash_landing || cState.dashing || cState.backDashing ||  cState.jumping)
	{
            return false;
	}
	if (cState.onGround)
	{
            return true; //如果在地面上就return true
	}
        return false;
    }

    /// <summary>
    /// 小骑士跳跃行为播放声音以及设置cstate.jumping
    /// </summary>
    private void HeroJump()
    {

        audioCtrl.PlaySound(HeroSounds.JUMP);

        cState.jumping = true;
        jumpQueueSteps = 0;
        jumped_steps = 0;
    }

    private void HeroJumpNoEffect()
    {

        audioCtrl.PlaySound(HeroSounds.JUMP);

        cState.jumping = true;
        jumpQueueSteps = 0;
        jumped_steps = 0;
    }

    /// <summary>
    /// 取消跳跃
    /// </summary>
    public void CancelHeroJump()
    {
	if (cState.jumping)
	{
            CancelJump();
            
            if(rb2d.velocity.y > 0f)
	    {
                rb2d.velocity = new Vector2(rb2d.velocity.x, 0f);
	    }
	}
    }

    private void JumpReleased()
    {
        if(rb2d.velocity.y > 0f &&jumped_steps >= JUMP_STEPS_MIN)
	{
	    if (jumpReleaseQueueingEnabled)
	    {
                if(jumpReleaseQueuing && jumpReleaseQueueSteps <= 0)
		{
                    rb2d.velocity = new Vector2(rb2d.velocity.x, 0f); //取消跳跃并且设置y轴速度为0
                    CancelJump();
		}
	    }
	    else
	    {
                rb2d.velocity = new Vector2(rb2d.velocity.x, 0f);
                CancelJump();
	    }
	}
        jumpQueuing = false;


    }

    /// <summary>
    /// 设置玩家的ActorState的新类型
    /// </summary>
    /// <param name="newState"></param>
    private void SetState(ActorStates newState)
    {
        if(newState == ActorStates.grounded)
	{
            if(Mathf.Abs(move_input) > Mathf.Epsilon)
	    {
                newState  = ActorStates.running;
	    }
	    else
	    {
                newState = ActorStates.idle;
            }
	}
        else if(newState == ActorStates.previous)
	{
            newState = prev_hero_state;
	}
        if(newState != hero_state)
	{
            prev_hero_state = hero_state;
            hero_state = newState;
            animCtrl.UpdateState(newState);
        }
    }

    /// <summary>
    /// 回到地面上时执行的函数
    /// </summary>
    public void BackOnGround()
    {
        if(landingBufferSteps <= 0)
	{
            landingBufferSteps = LANDING_BUFFER_STEPS;
            if(!cState.onGround && !hardLanded)
	    {
                Instantiate(softLandingEffectPrefab, transform.position,Quaternion.identity); //TODO:

            }
        }
        cState.falling = false;
        fallTimer = 0f;
        dashLandingTimer = 0f;
        cState.willHardLand = false;
        hardLandingTimer = 0f;
        hardLanded = false;
        jump_steps = 0;
        SetState(ActorStates.grounded);
	cState.onGround = true;
        airDashed = false;
    }

    /// <summary>
    /// 开启在下落时晃动
    /// </summary>
    public void StartFallRumble()
    {
        fallRumble = true;
        audioCtrl.PlaySound(HeroSounds.FALLING);
    }

    public void CancelFallEffects()
    {
        fallRumble = false;
        audioCtrl.StopSound(HeroSounds.FALLING);
    }

    /// <summary>
    /// 规整化输入
    /// </summary>
    private void FilterInput()
    {
        if (move_input > 0.3f)
        {
            move_input = 1f;
        }
        else if (move_input < -0.3f)
        {
            move_input = -1f;
        }
        else
        {
            move_input = 0f;
        }
        if (vertical_input > 0.5f)
        {
            vertical_input = 1f;
            return;
        }
        if (vertical_input < -0.5f)
        {
            vertical_input = -1f;
            return;
        }
        vertical_input = 0f;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {


        if(collision.gameObject.layer == LayerMask.NameToLayer("Terrain") && collision.gameObject.CompareTag("HeroWalkable") && CheckTouchingGround())
	{

	}
        if(hero_state != ActorStates.no_input)
	{

            if(collision.gameObject.layer == LayerMask.NameToLayer("Terrain") || collision.gameObject.CompareTag("HeroWalkable"))
	    {
                CollisionSide collisionSide = FindCollisionSide(collision);
                //如果头顶顶到了
                if (collisionSide == CollisionSide.top)
		{
		    if (cState.jumping)
		    {
                        CancelJump();

		    }


		}

                //如果底下碰到了
                if (collisionSide == CollisionSide.bottom)
		{
                    if(ShouldHardLand(collision))
		    {
                        DoHardLanding();
		    }
                    else if(collision.gameObject.GetComponent<SteepSlope>() == null && hero_state != ActorStates.hard_landing)
		    {
                        BackOnGround();
		    }
                    if(cState.dashing && dashingDown)
		    {
                        AffectedByGravity(true);
                        SetState(ActorStates.dash_landing);
                        hardLanded = true;
                        return;
		    }
		}
	    }
	}
        else if(hero_state == ActorStates.no_input)
	{

	}
    }

    private void OnCollisionStay2D(Collision2D collision)
    {
        if(hero_state != ActorStates.no_input && collision.gameObject.layer == LayerMask.NameToLayer("Terrain"))
	{
	    if (collision.gameObject.GetComponent<NonSlider>() == null)
	    {
		if (CheckStillTouchingWall(CollisionSide.left, false))
		{
                    cState.touchingWall = true;
                    touchingWallL = true;
                    touchingWallR = false;
		}
                else if (CheckStillTouchingWall(CollisionSide.right, false))
                {
                    cState.touchingWall = true;
                    touchingWallL = false;
                    touchingWallR = true;
                }
		else
		{
                    cState.touchingWall = false;
                    touchingWallL = false;
                    touchingWallR = false;
                }
		if (CheckTouchingGround())
		{
		    if (ShouldHardLand(collision))
		    {
                        DoHardLanding();
		    }
                    if(hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing && cState.falling)
		    {
                        BackOnGround();
                        return;
		    }
		}
                else if(cState.jumping || cState.falling)
		{
                    cState.onGround = false;

                    SetState(ActorStates.airborne);
                    return;
		}
            }
	    else
	    {

	    }
	}
    }

    private void OnCollisionExit2D(Collision2D collision)
    {
        if(touchingWallL && !CheckStillTouchingWall(CollisionSide.left, false))
	{
            cState.touchingWall = false;
            touchingWallL = false;
	}
        if (touchingWallR && !CheckStillTouchingWall(CollisionSide.left, false))
        {
            cState.touchingWall = false;
            touchingWallR = false;
        }
        if(hero_state != ActorStates.no_input && collision.gameObject.layer == LayerMask.NameToLayer("Terrain") && !CheckTouchingGround())
	{

            cState.onGround = false;

            SetState(ActorStates.airborne);
            
	}
    }

    /// <summary>
    /// 检查是否接触到地面
    /// </summary>
    /// <returns></returns>
    public bool CheckTouchingGround()
    {
        Vector2 vector = new Vector2(col2d.bounds.min.x, col2d.bounds.center.y);
        Vector2 vector2 = col2d.bounds.center;
	Vector2 vector3 = new Vector2(col2d.bounds.max.x, col2d.bounds.center.y);
        float distance = col2d.bounds.extents.y + 0.16f;
        Debug.DrawRay(vector, Vector2.down, Color.yellow);
        Debug.DrawRay(vector2, Vector2.down, Color.yellow);
        Debug.DrawRay(vector3, Vector2.down, Color.yellow);
        RaycastHit2D raycastHit2D = Physics2D.Raycast(vector, Vector2.down, distance, LayerMask.GetMask("Terrain"));
        RaycastHit2D raycastHit2D2 = Physics2D.Raycast(vector2, Vector2.down, distance, LayerMask.GetMask("Terrain"));
        RaycastHit2D raycastHit2D3 = Physics2D.Raycast(vector3, Vector2.down, distance, LayerMask.GetMask("Terrain"));
        return raycastHit2D.collider != null || raycastHit2D2.collider != null || raycastHit2D3.collider != null;
    }

    /// <summary>
    /// 检查是否保持着接触着墙
    /// </summary>
    /// <param name="side"></param>
    /// <param name="checkTop"></param>
    /// <returns></returns>
    private bool CheckStillTouchingWall(CollisionSide side,bool checkTop = false)
    {
        Vector2 origin = new Vector2(col2d.bounds.min.x, col2d.bounds.max.y);
        Vector2 origin2 = new Vector2(col2d.bounds.min.x, col2d.bounds.center.y);
        Vector2 origin3 = new Vector2(col2d.bounds.min.x, col2d.bounds.min.y);
        Vector2 origin4 = new Vector2(col2d.bounds.max.x, col2d.bounds.max.y);
        Vector2 origin5 = new Vector2(col2d.bounds.max.x, col2d.bounds.center.y);
        Vector2 origin6 = new Vector2(col2d.bounds.max.x, col2d.bounds.min.y);
        float distance = 0.1f;
        RaycastHit2D raycastHit2D = default(RaycastHit2D);
        RaycastHit2D raycastHit2D2 = default(RaycastHit2D);
        RaycastHit2D raycastHit2D3 = default(RaycastHit2D);
        if(side == CollisionSide.left)
	{
	    if (checkTop)
	    {
                raycastHit2D = Physics2D.Raycast(origin, Vector2.left, distance, LayerMask.GetMask("Terrain"));
	    }
            raycastHit2D2 = Physics2D.Raycast(origin2, Vector2.left, distance, LayerMask.GetMask("Terrain"));
            raycastHit2D3 = Physics2D.Raycast(origin3, Vector2.left, distance, LayerMask.GetMask("Terrain"));
        }
	else
	{
            if(side != CollisionSide.right)
	    {
                Debug.LogError("Invalid CollisionSide specified.");
                return false;
            }
            if (checkTop)
            {
                raycastHit2D = Physics2D.Raycast(origin4, Vector2.right, distance, LayerMask.GetMask("Terrain"));
            }
            raycastHit2D2 = Physics2D.Raycast(origin5, Vector2.right, distance, LayerMask.GetMask("Terrain"));
            raycastHit2D3 = Physics2D.Raycast(origin6, Vector2.right, distance, LayerMask.GetMask("Terrain"));
        }
        if(raycastHit2D2.collider != null)
	{
            bool flag = true;
	    if (raycastHit2D2.collider.isTrigger)
	    {
                flag = false;
	    }
            if(raycastHit2D2.collider.GetComponent<SteepSlope>() != null)
	    {
                flag = false;
	    }
            if (raycastHit2D2.collider.GetComponent<NonSlider>() != null)
            {
                flag = false;
            }
	    if (flag)
	    {
                return true;
	    }
        }
        if (raycastHit2D3.collider != null)
        {
            bool flag2 = true;
            if (raycastHit2D3.collider.isTrigger)
            {
                flag2 = false;
            }
            if (raycastHit2D3.collider.GetComponent<SteepSlope>() != null)
            {
                flag2 = false;
            }
            if (raycastHit2D3.collider.GetComponent<NonSlider>() != null)
            {
                flag2 = false;
            }
            if (flag2)
            {
                return true;
            }
        }
        if (checkTop && raycastHit2D.collider != null)
        {
            bool flag3 = true;
            if (raycastHit2D.collider.isTrigger)
            {
                flag3 = false;
            }
            if (raycastHit2D.collider.GetComponent<SteepSlope>() != null)
            {
                flag3 = false;
            }
            if (raycastHit2D.collider.GetComponent<NonSlider>() != null)
            {
                flag3 = false;
            }
            if (flag3)
            {
                return true;
            }
        }
        return false;
    }

    public IEnumerator CheckForTerrainThunk(AttackDirection attackDir)
    {
        bool terrainHit = false;
        float thunkTimer = NAIL_TERRAIN_CHECK_TIME;
	while (thunkTimer > 0.12f)
	{
	    if (!terrainHit)
	    {
		float num = 0.25f;
		float num2;
		if (attackDir == AttackDirection.normal)
		{
		    num2 = 2f;
		}
		else
		{
		    num2 = 1.5f;
		}
		float num3 = 1f;
		//TODO:
		num2 *= num3;
		Vector2 size = new Vector2(0.45f, 0.45f);
		Vector2 origin = new Vector2(col2d.bounds.center.x, col2d.bounds.center.y + num);
		Vector2 origin2 = new Vector2(col2d.bounds.center.x, col2d.bounds.max.y);
		Vector2 origin3 = new Vector2(col2d.bounds.center.x, col2d.bounds.min.y);
		int layerMask = 33554432; //2的25次方,也就是Layer Soft Terrain;
		RaycastHit2D raycastHit2D = default(RaycastHit2D);
		if (attackDir == AttackDirection.normal)
		{
		    if ((cState.facingRight && !cState.wallSliding) || (!cState.facingRight && !cState.wallSliding))
		    {
			raycastHit2D = Physics2D.BoxCast(origin, size, 0f, Vector2.right, num2, layerMask);
		    }
		    else
		    {
			raycastHit2D = Physics2D.BoxCast(origin, size, 0f, Vector2.right, num3, layerMask);
		    }
		}
		else if (attackDir == AttackDirection.upward)
		{
		    raycastHit2D = Physics2D.BoxCast(origin2, size, 0f, Vector2.up, num2, layerMask);
		}
		else if (attackDir == AttackDirection.downward)
		{
		    raycastHit2D = Physics2D.BoxCast(origin3, size, 0f, Vector2.down, num2, layerMask);
		}
		if (raycastHit2D.collider != null && !raycastHit2D.collider.isTrigger)
		{
		    NonThunker component = raycastHit2D.collider.GetComponent<NonThunker>();
		    bool flag = !(component != null) || !component.active;
		    if (flag)
		    {
			terrainHit = true;

			if (attackDir == AttackDirection.normal)
			{
			    if (cState.facingRight)
			    {

			    }
			    else
			    {

			    }
			}
			else if (attackDir == AttackDirection.upward)
			{

			}
		    }
		}
		thunkTimer -= Time.deltaTime;
	    }
            yield return null;
	}
    }

    public bool CheckForBump(CollisionSide side)
    {
        float num = 0.025f;
        float num2 = 0.2f;
        Vector2 vector = new Vector2(col2d.bounds.min.x + num2, col2d.bounds.min.y + 0.2f);
        Vector2 vector2 = new Vector2(col2d.bounds.min.x + num2, col2d.bounds.min.y - num);
        Vector2 vector3 = new Vector2(col2d.bounds.max.x - num2, col2d.bounds.min.y + 0.2f);
        Vector2 vector4 = new Vector2(col2d.bounds.max.x - num2, col2d.bounds.min.y - num);
        float num3 = 0.32f + num2;
        RaycastHit2D raycastHit2D = default(RaycastHit2D);
        RaycastHit2D raycastHit2D2 = default(RaycastHit2D);
        if(side == CollisionSide.left)
	{
            Debug.DrawLine(vector2, vector2 + Vector2.left * num3, Color.cyan, 0.15f);
            Debug.DrawLine(vector, vector + Vector2.left * num3, Color.cyan, 0.15f);
            raycastHit2D = Physics2D.Raycast(vector2, Vector2.left, num3, LayerMask.GetMask("Terrain"));
            raycastHit2D2 = Physics2D.Raycast(vector, Vector2.left, num3, LayerMask.GetMask("Terrain"));
        }
        else if (side == CollisionSide.right)
        {
            Debug.DrawLine(vector4, vector4 + Vector2.right * num3, Color.cyan, 0.15f);
            Debug.DrawLine(vector3, vector3 + Vector2.right * num3, Color.cyan, 0.15f);
            raycastHit2D = Physics2D.Raycast(vector4, Vector2.right, num3, LayerMask.GetMask("Terrain"));
            raycastHit2D2 = Physics2D.Raycast(vector3, Vector2.right, num3, LayerMask.GetMask("Terrain"));
	}
	else
	{
            Debug.LogError("Invalid CollisionSide specified.");
        }
        if(raycastHit2D2.collider != null && raycastHit2D.collider == null)
	{
            Vector2 vector5 = raycastHit2D2.point + new Vector2((side == CollisionSide.right) ? 0.1f : -0.1f, 1f);
            RaycastHit2D raycastHit2D3 = Physics2D.Raycast(vector5, Vector2.down, 1.5f, LayerMask.GetMask("Terrain"));
            Vector2 vector6 = raycastHit2D2.point + new Vector2((side == CollisionSide.right) ? -0.1f : 0.1f, 1f);
	    RaycastHit2D raycastHit2D4 = Physics2D.Raycast(vector6, Vector2.down, 1.5f, LayerMask.GetMask("Terrain"));
            if(raycastHit2D3.collider != null)
	    {
		Debug.DrawLine(vector5, raycastHit2D3.point, Color.cyan, 0.15f);
                if (!(raycastHit2D4.collider != null))
                {
                    return true;
		}
		Debug.DrawLine(vector6, raycastHit2D4.point, Color.cyan, 0.15f);
                float num4 = raycastHit2D3.point.y - raycastHit2D4.point.y;
                if(num4 > 0f)
		{
                    Debug.Log("Bump Height: " + num4.ToString());
                    return true;
                }
	    }
	}
        return false;
    }

    /// <summary>
    /// 找到碰撞点的方向也就是上下左右
    /// </summary>
    /// <param name="collision"></param>
    /// <returns></returns>
    private CollisionSide FindCollisionSide(Collision2D collision)
    {
        Vector2 normal = collision.GetSafeContact().Normal ;
        float x = normal.x;
        float y = normal.y;
        if(y >= 0.5f)
	{
            return CollisionSide.bottom; 
	}
        if (y <= -0.5f)
        {
            return CollisionSide.top;
        }
        if (x < 0)
        {
            return CollisionSide.right;
        }
        if (x > 0)
        {
            return CollisionSide.left;
        }
        Debug.LogError(string.Concat(new string[]
        {
            "ERROR: unable to determine direction of collision - contact points at (",
            normal.x.ToString(),
            ",",
            normal.y.ToString(),
            ")"
        }));
        return CollisionSide.bottom;
    }


}

[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 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;
        preventDash = false;
        preventBackDash = false;
	dashCooldown = false;
        backDashCooldown = false;
	isPaused = false;
    }
}

有一些地形检测函数式后续要用的,所以我先创建的,内容也很简单,其实就是一个判断类来用的,等后续某些地形是要用到的。 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NonThunker : MonoBehaviour
{
    public bool active = true;
    public void SetActive(bool active)
    {
	this.active = active;
    }
}
using System;
using UnityEngine;

public class NonBouncer : MonoBehaviour
{
    public bool active = true;
    public void SetActive(bool active)
    {
	this.active = active;
    }
}

然后就到了介绍NailSlash.cs的时候了,其实这就是一个控制slash effect的动画和音效之类的,还有就是开关PolygonCollider2D和MeshRenderer,我直接上代码吧你们看看也就懂了:

using System;
using GlobalEnums;
using UnityEngine;

public class NailSlash : MonoBehaviour
{
    public string animName;
    public Vector3 scale;
    private HeroController heroCtrl;
    private PlayMakerFSM slashFsm;
    private tk2dSpriteAnimator anim;
    private MeshRenderer mesh;
    private AudioSource audioSource;
    private PolygonCollider2D poly;
    private PolygonCollider2D clashTinkpoly;

    private float slashAngle;
    private bool slashing;
    private bool animCompleted;
    private int stepCounter;
    private int polyCounter;

    private void Awake()
    {
	try
	{
	    heroCtrl = transform.root.GetComponent<HeroController>();
	}
	catch(NullReferenceException ex)
	{
	    string str = "NailSlash: could not find HeroController on parent: ";
	    string name = transform.root.name;
	    string str2 = " ";
	    NullReferenceException ex2 = ex;
	    Debug.LogError(str + name + str2 + ((ex2 != null) ? ex2.ToString() : null));
	}
	slashFsm = GetComponent<PlayMakerFSM>();
	audioSource = GetComponent<AudioSource>();
	anim = GetComponent<tk2dSpriteAnimator>();
	mesh = GetComponent<MeshRenderer>();
	poly = GetComponent<PolygonCollider2D>();
	clashTinkpoly = transform.Find("Clash Tink").GetComponent<PolygonCollider2D>();
	poly.enabled = false;
	mesh.enabled = false;
    }

    private void FixedUpdate()
    {
	if (slashing)
	{
	    if(stepCounter == 1)
	    {
		poly.enabled = true;
		clashTinkpoly.enabled = true;
	    }
	    if(stepCounter >= 5 && polyCounter > 0f)
	    {
		poly.enabled = false;
		clashTinkpoly.enabled = false;
	    }
	    if(animCompleted && polyCounter > 1)
	    {
		CancelAttack();
	    }
	    if (poly.enabled)
	    {
		polyCounter++;
	    }
	    stepCounter++;
	}
    }

    public void StartSlash()
    {
	audioSource.Play();
	slashAngle = slashFsm.FsmVariables.GetFsmFloat("direction").Value;

	transform.localScale = scale;
	anim.Play(animName);
	anim.PlayFromFrame(0);
	stepCounter = 0;
	polyCounter = 0;
	poly.enabled = false;
	clashTinkpoly.enabled = false;
	animCompleted = false;
	anim.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(Disable);
	slashing = true;
	mesh.enabled = true;
    }

    private void Disable(tk2dSpriteAnimator sprite, tk2dSpriteAnimationClip clip)
    {
	animCompleted = true;
    }

    private void OnTriggerEnter2D(Collider2D otherCollider)
    {
	if(otherCollider != null)
	{
	    if(slashAngle == 0f)
	    {
		int layer = otherCollider.gameObject.layer;
		if(layer == 11 && (otherCollider.gameObject.GetComponent<NonBouncer>() == null || !otherCollider.gameObject.GetComponent<NonBouncer>().active))
		{
		    
		}
		
	    }
	    else if(slashAngle == 180f)
	    {
		int layer2 = otherCollider.gameObject.layer;
		if (layer2 == 11 && (otherCollider.gameObject.GetComponent<NonBouncer>() == null || !otherCollider.gameObject.GetComponent<NonBouncer>().active))
		{

		}
	    }
	    else if (slashAngle == 90f)
	    {
		int layer3 = otherCollider.gameObject.layer;
		if (layer3 == 11 && (otherCollider.gameObject.GetComponent<NonBouncer>() == null || !otherCollider.gameObject.GetComponent<NonBouncer>().active))
		{

		}
	    }
	    else if(slashAngle == 270f)
	    {
		PhysLayers layer4 = (PhysLayers)otherCollider.gameObject.layer;
		if((layer4 == PhysLayers.ENEMIES || layer4 == PhysLayers.INTERACTIVE_OBJECT || layer4 == PhysLayers.HERO_ATTACK) && (otherCollider.gameObject.GetComponent<NonBouncer>() == null || !otherCollider.gameObject.GetComponent<NonBouncer>().active))
		{

		}
	    }
	}
    }

    private void OnTriggerStay2D(Collider2D otherCollision)
    {
	OnTriggerEnter2D(otherCollision);
    }

    public void CancelAttack()
    {
	slashing = false;
	poly.enabled = false;
	clashTinkpoly.enabled = false;
	mesh.enabled = false;
    }

}

3.使用状态机实现击中敌人造成伤害机制

        然后就到了制作PlaymakerFSM之slashFSM,我们先给Attacks添加一个FSM设置nailDamage也就是骨钉伤害

首先自定义一个Action脚本叫GetNailDamage:

using System;
using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class GetNailDamage : FsmStateAction
{
    [UIHint(UIHint.Variable)]
    public FsmInt storeValue;

    public override void Reset()
    {
	storeValue = null;
    }

    public override void OnEnter()
    {
	//TODO:
	if(!storeValue.IsNone)
	{
	    storeValue.Value = GameManager.instance.playerData.nailDamage;
	}
	base.Finish();
    }

}

在PlayerData.cs中我们添加上     public int nailDamage;

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 int nailDamage;

    public bool hasDash;
    public bool canDash;
    public bool hasBackDash;
    public bool canBackDash;

    public bool gotCharm_31;
    public bool equippedCharm_31;


    protected PlayerData()
    {
	SetupNewPlayerData();
    }

    public void Reset()
    {
	SetupNewPlayerData();
    }

    private void SetupNewPlayerData()
    {
	nailDamage = 5;

	hasDash = true; //测试阶段先设置为true方便测试
	canDash = true;
	hasBackDash = false;
	canBackDash = false;
	gotCharm_31 = true;
	equippedCharm_31 = true;
    }


    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;
    }

}

获取PlayerData中int类型的变量的自定义行为: 

using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("PlayerData")]
    [Tooltip("Sends a Message to PlayerData to send and receive data.")]
    public class GetPlayerDataInt : FsmStateAction
    {
	[RequiredField]
	[Tooltip("GameManager reference, set this to the global variable GameManager.")]
	public FsmOwnerDefault gameObject;

	[RequiredField]
	public FsmString intName;

	[RequiredField]
	[UIHint(UIHint.Variable)]
	public FsmInt storeValue;

	public override void Reset()
	{
	    gameObject = null;
	    intName = null;
	    storeValue = null;
	}

	public override void OnEnter()
	{
	    GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
	    if(ownerDefaultTarget == null)
	    {
		return;
	    }
	    GameManager gameManager = ownerDefaultTarget.GetComponent<GameManager>();
	    if(gameManager == null)
	    {
		Debug.Log("GetPlayerDataInt: could not find a GameManager on this object, please refere to the GameManager global variable");
		return;
	    }
	    storeValue.Value = gameManager.GetPlayerDataInt(intName.Value);
	    Finish();
	}
    }

}

 对比String类型名字的自定义行为CompareNames .cs

using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class CompareNames : FsmStateAction
{
    public FsmString name;
    [ArrayEditor(VariableType.String, "", 0, 0, 65536)]
    public FsmArray strings;
    public FsmEventTarget target;
    public FsmEvent trueEvent;
    public FsmEvent falseEvent;

    public override void Reset()
    {
	name = new FsmString();
	target = new FsmEventTarget();
	strings = new FsmArray();
	trueEvent = null;
	falseEvent = null;
    }

    public override void OnEnter()
    {
	if(!name.IsNone && name.Value != "")
	{
	    foreach (string value in strings.stringValues)
	    {
		if(name.Value.Contains(value))
		{
		    Fsm.Event(target, trueEvent);
		    base.Finish();
		    return;
		}
	    }
	    Fsm.Event(target, falseEvent);
	}
	base.Finish();
    }

}

回到Playermaker面板中: 

 

然后就给每一个Slash先添加一个简单的nail_cancel_attack的FSM,这个是下一期要用到的,是取消攻击的状态:

 

 

这个CancelAttack()方法下一期再来做。

然后到了重点再添加一个新的playmakerFSM:damages_enemy给每一个Slash:

我们先添加事件和变量:

 

然后就到了我们最喜欢的自定义playmaker Action脚本 :

2d碰撞检测trigger2d事件:

using System;
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
	[ActionCategory("Physics 2d")]
	[Tooltip("Detect 2D trigger collisions between the Owner of this FSM and other Game Objects that have RigidBody2D components.\nNOTE: The system events, TRIGGER ENTER 2D, TRIGGER STAY 2D, and TRIGGER EXIT 2D are sent automatically on collisions triggers with any object. Use this action to filter collision triggers by Tag.")]
	public class Trigger2dEvent : FsmStateAction
	{
		
		[Tooltip("The type of trigger to detect.")]
		public PlayMakerUnity2d.Trigger2DType trigger;
		
		[UIHint(UIHint.Tag)]
		[Tooltip("Filter by Tag.")]
		public FsmString collideTag;

		[UIHint(UIHint.Layer)]
		[Tooltip("Filter by Layer.")]
		public FsmString collideLayer;

		[RequiredField]
		[Tooltip("Event to send if a collision is detected.")]
		public FsmEvent sendEvent;
		
		[UIHint(UIHint.Variable)]
		[Tooltip("Store the GameObject that collided with the Owner of this FSM.")]
		public FsmGameObject storeCollider;

		
		
		private PlayMakerUnity2DProxy _proxy;
		
		public override void Reset()
		{
			trigger =  PlayMakerUnity2d.Trigger2DType.OnTriggerEnter2D;
			collideTag = new FsmString(){UseVariable=true};
			sendEvent = null;
			storeCollider = null;
		}
		
		public override void OnEnter()
		{
			_proxy = (PlayMakerUnity2DProxy) this.Owner.GetComponent<PlayMakerUnity2DProxy>();
			
			if (_proxy == null)
			{
				_proxy = this.Owner.AddComponent<PlayMakerUnity2DProxy>();
			}
			
			switch (trigger)
			{
			case PlayMakerUnity2d.Trigger2DType.OnTriggerEnter2D:
				_proxy.AddOnTriggerEnter2dDelegate(this.DoTriggerEnter2D);
				break;
			case PlayMakerUnity2d.Trigger2DType.OnTriggerStay2D:
				_proxy.AddOnTriggerStay2dDelegate(this.DoTriggerStay2D);
				break;
			case PlayMakerUnity2d.Trigger2DType.OnTriggerExit2D:
				_proxy.AddOnTriggerExit2dDelegate(this.DoTriggerExit2D);
				break;
			}
		}
		
		public override void OnExit()
		{
			if (_proxy==null)
			{
				return;
			}
			
			switch (trigger)
			{
			case PlayMakerUnity2d.Trigger2DType.OnTriggerEnter2D:
				_proxy.RemoveOnTriggerEnter2dDelegate(this.DoTriggerEnter2D);
				break;
			case PlayMakerUnity2d.Trigger2DType.OnTriggerStay2D:
				_proxy.RemoveOnTriggerStay2dDelegate(this.DoTriggerStay2D);
				break;
			case PlayMakerUnity2d.Trigger2DType.OnTriggerExit2D:
				_proxy.RemoveOnTriggerExit2dDelegate(this.DoTriggerExit2D);
				break;
			}
		}
		
		void StoreCollisionInfo(Collider2D collisionInfo)
		{
			storeCollider.Value = collisionInfo.gameObject;
		}
		
		public void DoTriggerEnter2D(Collider2D collisionInfo)
		{
			if (trigger == PlayMakerUnity2d.Trigger2DType.OnTriggerEnter2D)
			{
				if (collisionInfo.gameObject.tag == collideTag.Value || collideTag.IsNone || string.IsNullOrEmpty(collideTag.Value) )
				{
					StoreCollisionInfo(collisionInfo);
					Fsm.Event(sendEvent);
				}
			}
		}
		
		public void DoTriggerStay2D(Collider2D collisionInfo)
		{
			if (trigger == PlayMakerUnity2d.Trigger2DType.OnTriggerStay2D)
			{
				if (collisionInfo.gameObject.tag == collideTag.Value || collideTag.IsNone || string.IsNullOrEmpty(collideTag.Value) )
				{
					StoreCollisionInfo(collisionInfo);
					Fsm.Event(sendEvent);
				}
			}
		}
		
		public void DoTriggerExit2D(Collider2D collisionInfo)
		{
			if (trigger == PlayMakerUnity2d.Trigger2DType.OnTriggerExit2D)
			{
				if (collisionInfo.gameObject.tag == collideTag.Value || collideTag.IsNone || string.IsNullOrEmpty(collideTag.Value))
				{
					StoreCollisionInfo(collisionInfo);
					Fsm.Event(sendEvent);
				}
			}
		}
		
		public override string ErrorCheck()
		{
			string text = string.Empty;
			if (Owner != null && Owner.GetComponent<Collider2D>() == null && Owner.GetComponent<Rigidbody2D>() == null)
			{
				text += "Owner requires a RigidBody2D or Collider2D!\n";
			}
			return text;
		}
	}
}

 检测发送事件限制CheckSendEventLimit:

using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class CheckSendEventLimit : FsmStateAction
{
    public FsmGameObject gameObject;
    public FsmEventTarget target;
    public FsmEvent trueEvent;
    public FsmEvent falseEvent;

    public override void Reset()
    {
	gameObject = null;
	target = null;
	trueEvent = null;
	falseEvent = null;
    }

    public override void OnEnter()
    {
	if (gameObject.Value)
	{
	    LimitSendEvents component = Owner.gameObject.GetComponent<LimitSendEvents>(); 
	    if(component && !component.Add(gameObject.Value))
	    {
		Fsm.Event(target, falseEvent);
	    }
	    else
	    {
		Fsm.Event(target, trueEvent);
	    }
	}
	base.Finish();
    }

}

我们为每一种Slash再添加一个新的脚本叫LimitSendEvents.cs:

 这个就是限制发送事件的,

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LimitSendEvents : MonoBehaviour
{
    public Collider2D monitorCollider;
    private List<GameObject> sentList = new List<GameObject>();
    private bool? previousColliderState;

    private void OnEnable()
    {
	sentList.Clear();
    }

    private void Update()
    {
	if (monitorCollider)
	{
	    bool enabled = monitorCollider.enabled;
	    bool? flag = previousColliderState;
	    if(enabled == flag.GetValueOrDefault() && flag != null)
	    {
		return;
	    }
	    previousColliderState = new bool?(monitorCollider.enabled);
	}
	if(sentList.Count > 0)
	{
	    sentList.Clear();
	}
    }

    public bool Add(GameObject obj)
    {
	if (!sentList.Contains(obj))
	{
	    sentList.Add(obj);
	    return true;
	}
	return false;
    }

}

回到编辑器中,把每一种Slash的PolygonCollider2D拖进来

最后一个自定义行为Action脚本叫TakeDamage:

using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class TakeDamage : FsmStateAction
{
    public FsmGameObject Target; //FSM的目标,一般是自己
    public FsmInt AttackType; //攻击类型
    public FsmBool CircleDirection; //是否是圆形方向
    public FsmInt DamageDealt; //伤害值
    public FsmFloat Direction; //受伤方向
    public FsmBool IgnoreInvulnerable; //是否忽略无敌
    public FsmFloat MagnitudeMultiplier;//伤害倍值
    public FsmFloat MoveAngle; //移动的角度
    public FsmBool MoveDirection; //移动的方向
    public FsmFloat Multiplier;//伤害倍值
    public FsmInt SpecialType; //特殊类型

    public override void Reset()
    {
	base.Reset();
	Target = new FsmGameObject
	{
	    UseVariable = true
	};
	AttackType = new FsmInt
	{
	    UseVariable = true
	};
	CircleDirection = new FsmBool
	{
	    UseVariable = true
	};
	DamageDealt = new FsmInt
	{
	    UseVariable = true
	};
	Direction = new FsmFloat
	{
	    UseVariable = true
	};
	IgnoreInvulnerable = new FsmBool
	{
	    UseVariable = true
	};
	MagnitudeMultiplier = new FsmFloat
	{
	    UseVariable = true
	};
	MoveAngle = new FsmFloat
	{
	    UseVariable = true
	};
	MoveDirection = new FsmBool
	{
	    UseVariable = true
	};
	Multiplier = new FsmFloat
	{
	    UseVariable = true
	};
	SpecialType = new FsmInt
	{
	    UseVariable = true
	};
    }

    public override void OnEnter()
    {
	base.OnEnter();
	HitTaker.Hit(Target.Value, new HitInstance
	{
	    Source = Owner,
	    AttackType = (AttackTypes)AttackType.Value,
	    CircleDirection = CircleDirection.Value,
	    DamageDealt = DamageDealt.Value,
	    IgnoreInvulnerable = IgnoreInvulnerable.Value,
	    MagnitudeMultiplier = MagnitudeMultiplier.Value,
	    MoveAngle = MoveAngle .Value,
	    MoveDirection = MoveDirection.Value,
	    Multiplier = Multiplier.IsNone ? 1f:Multiplier.Value,
	    SpecialType = (SpecialTypes)SpecialType.Value,
	    IsExtraDamage = false
	},3);
	base.Finish();
    }

}

 创建一个新的静态类HitTaker,正如注释描述的,这个类是为了检测targetGameObject自己this,父对象parent,爷对象grandparent有咩有IHitResponder,有的话执行Hit()函数,

using System;
using UnityEngine;

public static class HitTaker
{
    private const int DefaultRecursionDepth = 3;
    public static void Hit(GameObject targetGameObject,HitInstance damageInstance,int recursionDepth = DefaultRecursionDepth)
    {
	if (targetGameObject != null)
	{
	    Transform transform = targetGameObject.transform;
	    //说白了就是检测targetGameObject自己this,父对象parent,爷对象grandparent有咩有IHitResponder,有的话执行Hit
	    for (int i = 0; i < recursionDepth; i++) 
	    {
		IHitResponder component = transform.GetComponent<IHitResponder>();
		if(component != null)
		{
		    component.Hit(damageInstance);
		}
		transform = transform.parent;
		if(transform == null)
		{
		    break;
		}
	    }
	}
    }
}

 其实这个IHitResponder 只是一个接口,只有一个要实现的方法就是Hit();下一章我们将创建类来继承这个接口

using System;

public interface IHitResponder 
{
    void Hit(HitInstance damageInstance);
}

还是就是受击实例化结构体类型HitTaker:我们还在这个下面添加了AttackType攻击类型和Special Type特殊类型两个数组。

using System;
using UnityEngine;

[Serializable]
public struct HitInstance
{
    public GameObject Source;
    public AttackTypes AttackType;
    public bool CircleDirection;
    public int DamageDealt;
    public float Direction;
    public bool IgnoreInvulnerable;
    public float MagnitudeMultiplier;
    public float MoveAngle;
    public bool MoveDirection;
    public float Multiplier;
    public SpecialTypes SpecialType;
    public bool IsExtraDamage;
    
    public float GetActualDirection(Transform target)
    {
	if(Source != null && target != null && CircleDirection)
	{
	    Vector2 vector = target.position - Source.transform.position;
	    return Mathf.Atan2(vector.y, vector.x) * 57.29578f;
	}
	return Direction;
    }

}

public enum AttackTypes
{
    Nail,
    Generic
}

public enum SpecialTypes
{
    None,
    Acid
}

然后我们就可以创建State了:

 如果层级是9和20就执行CANCEL事件回到Idle状态,如果骨钉伤害=0也执行CANCEL事件,如果CheckSentEventLimit为false就执行FALSE事件

下两个状态就是Parent和GrandParent,我们只需要把上一个状态填Collider 的改成Parent和GrandParent游戏对象,但别忘了改第一个行为GetParent!

 

至此我们完成了基本的攻击行为Attack!播放一下发现没有问题!

二、为敌人制作生命系统

1.使用代码制作生命系统

创建新的脚本HealthManager.cs添加到每一个敌人的游戏对象上。

还记得我上面说的接口IHitResponder吗?我们要继承这个接口并实现里面的方法:

using System;
using System.Collections;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;

public class HealthManager : MonoBehaviour, IHitResponder
{
    public void Hit(HitInstance hitInstance)
    {
	
    }
}

除了基本的组件 

    private BoxCollider2D boxCollider;
    private tk2dSpriteAnimator animator;
    private tk2dSprite sprite;

以外我们想想生命系统需要什么?首先要有hp, enemyType; //敌人类型,Vector3 effectOrigin; //生效偏移量,isDead死了没有?还要一开始判断它死了没有,有了这些想法后我们做个简易版本的生命系统:

using System;
using System.Collections;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;

public class HealthManager : MonoBehaviour, IHitResponder
{
    private BoxCollider2D boxCollider;

    private tk2dSpriteAnimator animator;
    private tk2dSprite sprite;

    [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>();

	    animator = GetComponent<tk2dSpriteAnimator>();
	    sprite = GetComponent<tk2dSprite>();
    }

    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(hitInstance.DamageDealt < 0f)
	    {
	        return;
	    }
	    FSMUtility.SendEventToGameObject(hitInstance.Source, "DEALT DAMAGE", false);
	    int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	    TakeDamage(hitInstance);
    }


    private void TakeDamage(HitInstance hitInstance)
    {
	

    }



    public void SendDeathEvent()
    {
	    if (OnDeath != null)
	    {
	        OnDeath();
	    }
    }


    protected IEnumerator CheckPersistence()
    {
	    yield return null;
	    if (isDead)
	    {
	        gameObject.SetActive(false);
	    }
	    yield break;
    }

}

 三、为敌人制作受伤系统 

1.使用代码制作受伤系统

        其实说白了就是完善这个HealthManager.cs脚本,在此之前我们现在静态类Extensions.cs添加一个新的静态方法:

    public static float GetPositionY(this Transform t)
    {
            return t.position.y;
    }

        然后回到HealthManager.cs,我们添加几个新的方法,无敌时间evasionByHitRemaining,是否在某个方向上阻挡所有攻击的IsBlockingByDirection(int cardinalDirection,AttackTypes attackType),无敌Invincible(HitInstance hitInstance),受到伤害TakeDamage(HitInstance hitInstance),以及完善Hit()函数:

using System;
using System.Collections;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;

public class HealthManager : MonoBehaviour, IHitResponder
{
    private BoxCollider2D boxCollider;

    private tk2dSpriteAnimator animator;
    private tk2dSprite sprite;

    [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>();

	    animator = GetComponent<tk2dSpriteAnimator>();
	    sprite = GetComponent<tk2dSprite>();
    }

    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)
		{

		}
		else if(cardinalDirection == 2)
		{

		}
	    }

	    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;
    }

    private 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);
	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;
	}
	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;
	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;
    }

}

核心代码一句话hp = Mathf.Max(hp - num,-50) 

还有一个就是空内容的类,这个暂时先不管它:

using UnityEngine;

public class DontClinkGates : MonoBehaviour
{

}

回到编辑器中,我们设置好HealthManager.cs的内容:

2.制作受伤特效

现在我们还差受伤特效没有实现,我们想想受伤特效也是有很多共性的,而且还有一个接收受伤特效的方法要实现,于是再创建一个接口:

using System;
using UnityEngine;

public interface IHitEffectReciever
{
    void ReceiverHitEffect(float attackDirection);
}

为每一个敌人创建一个新的脚本EnemyHitEffectsBlackKnight.cs ,使其继承这个接口并实现里面的方法:

using System;
using UnityEngine;

public class EnemyHitEffectsBlackKnight : MonoBehaviour,IHitEffectReciever
{
    public Vector3 effectOrigin;
    [Space]
    public AudioSource audioPlayerPrefab;
    public AudioEvent enemyDamage;
    [Space]
    public GameObject hitFlashOrange;
    public GameObject hitPuffLarge;
    private SpriteFlash spriteFlash;

    private bool didFireThisFrame;

    private void Awake()
    {
	spriteFlash = GetComponent<SpriteFlash>();
    }

    protected void Update()
    {
	didFireThisFrame = false;
    }

    public void ReceiverHitEffect(float attackDirection)
    {
	if (didFireThisFrame)
	    return;
	FSMUtility.SendEventToGameObject(this.gameObject, "DAMAGE FLASH", false);
	enemyDamage.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
	if (spriteFlash)
	{
	    spriteFlash.flashInfected();
	}
	GameObject gameObject = Instantiate(hitFlashOrange, transform.position + effectOrigin, Quaternion.identity);
	switch (DirectionUtils.GetCardinalDirection(attackDirection))
	{
	    case 0:
		gameObject.transform.eulerAngles = new Vector3(0f, 90f, 270f);
		break;
	    case 1:
		gameObject.transform.eulerAngles = new Vector3(270f, 90f, 270f);
		break;
	    case 2:
		gameObject.transform.eulerAngles = new Vector3(180f, 90f, 270f);
		break;
	    case 3:
		gameObject.transform.eulerAngles = new Vector3(-72.5f, -180f, -180f);
		break;
	}
	didFireThisFrame = true;
    }

   
}

 再为每一个敌人创建一个新的脚本SpriteFlash.cs ,里面是负责实现Sprite精灵图闪烁效果的,通过控制material中的"_FlashAmount"(线性插值的方法),更改flashColour等等

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpriteFlash : MonoBehaviour
{
    private Renderer rend;
    private Color flashColour;

    private float amount;
    private float amountCurrent;

    private float timeUp;
    private float stayTime;
    private float timeDown;

    private int flashingState;
    private float flashTimer;
    private float t;

    private bool repeatFlash;
    private bool cancelFlash;

    private MaterialPropertyBlock block;
    private bool sendToChildren = true;

    private void Start()
    {
	if(rend == null)
	{
	    rend = GetComponent<Renderer>();
	}
	if (block == null)
	{
	    block = new MaterialPropertyBlock();
	}
    }

    private void OnDisable()
    {
	if (rend == null)
	{
	    rend = GetComponent<Renderer>();
	}
	if (block == null)
	{
	    block = new MaterialPropertyBlock();
	}
	block.SetFloat("_FlashAmount", 0f);
	rend.SetPropertyBlock(block);
	flashTimer = 0f;
	flashingState = 0;
	repeatFlash = false;
	cancelFlash = false;

    }

    private void Update()
    {
	if (cancelFlash)
	{
	    block.SetFloat("_FlashAmount", 0f);
	    rend.SetPropertyBlock(block);
	    flashingState = 0;
	    cancelFlash = false;
	}
	if(flashingState == 1)
	{
	    if (flashTimer < timeUp)
	    {
		flashTimer += Time.deltaTime;
		t = flashTimer / timeUp;
		amountCurrent = Mathf.Lerp(0f, amount, t);
		block.SetFloat("_FlashAmount", amountCurrent);
		rend.SetPropertyBlock(block);
	    }
	    else
	    {
		block.SetFloat("_FlashAmount", amount);
		rend.SetPropertyBlock(block);
		flashTimer = 0f;
		flashingState = 2;
	    }
	}
	if(flashingState == 2)
	{
	    if(flashTimer < stayTime)
	    {
		flashTimer += Time.deltaTime;
	    }
	    else
	    {
		flashTimer = 0f;
		flashingState = 3;
	    }
	}
	if(flashingState == 3)
	{
	    if (flashTimer < timeDown)
	    {
		flashTimer += Time.deltaTime;
		t = flashTimer / timeDown;
		amountCurrent = Mathf.Lerp(amount, 0f, t);
		block.SetFloat("_FlashAmount", amountCurrent);
		rend.SetPropertyBlock(block);
	    }
	    else
	    {
		block.SetFloat("_FlashAmount", 0f);
		rend.SetPropertyBlock(block);
		flashTimer = 0f;
		if (repeatFlash)
		{
		    flashingState = 1;
		}
		else
		{
		    flashingState = 0;
		}
	    }
	}
    }

    public void flashInfected()
    {
	if (block == null)
	{
	    block = new MaterialPropertyBlock();
	}
	flashColour = new Color(1f, 0.31f, 0f);
	amount = 0.9f;
	timeUp = 0.01f;
	timeDown = 0.25f;
	block.Clear();
	block.SetColor("_FlashColor", flashColour);
	flashingState = 1;
	flashTimer = 0f;
	repeatFlash = false;
	SendToChildren(new Action(flashInfected));
    }

    private void SendToChildren(Action function)
    {
	if (!sendToChildren)
	    return;
	foreach (SpriteFlash spriteFlash  in GetComponentsInChildren<SpriteFlash>())
	{
	    if(!(spriteFlash == null))
	    {
		spriteFlash.sendToChildren = false;
		spriteFlash.GetType().GetMethod(function.Method.Name).Invoke(spriteFlash, null);
	    }
	}
    }
}

再创建一个结构体AudioEvent: 

using System;
using UnityEngine;

[Serializable]
public struct AudioEvent
{
    public AudioClip Clip;
    public float PitchMin;
    public float PitchMax;
    public float Volume;

    public void Reset()
    {
	PitchMin = 0.75f;
	PitchMax = 1.25f;
	Volume = 1f;
    }

    public float SelectPitch()
    {
	if (Mathf.Approximately(PitchMin, PitchMax))
	{
	    return PitchMax;
	}
	return UnityEngine.Random.Range(PitchMin, PitchMax);
    }

    public void SpawnAndPlayOneShot(AudioSource prefab, Vector3 position)
    {
	if (Clip == null)
	    return;
	if(Volume < Mathf.Epsilon)
	    return;
	if (prefab == null)
	    return;
	AudioSource audioSource = GameObject.Instantiate(prefab, position,Quaternion.identity);
	audioSource.volume = Volume;
	audioSource.pitch = SelectPitch();
	audioSource.PlayOneShot(Clip);
    }

}

回到HealthManager.cs当中,添加    private IHitEffectReciever hitEffectReceiver;并且

hitEffectReceiver = GetComponent<IHitEffectReciever>();

 现阶段完整的HealthManager.cs如下所示:

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 tk2dSpriteAnimator animator;
    private tk2dSprite sprite;

    [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>();
	animator = GetComponent<tk2dSpriteAnimator>();
	sprite = GetComponent<tk2dSprite>();
    }

    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)
		{

		}
		else if(cardinalDirection == 2)
		{

		}
	    }

	    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;
    }

    private 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);
	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;
	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;
    }

}

 回到Unity编辑器中,我是这样设置的:

两个自己创建的预制体:

 

这里我为它们创建了一个临时的脚本就是一段时间回收这个物体,但我还没有做对象池所以只能粗暴的Destory(gameObject)了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AutoRecycle : MonoBehaviour
{
    //这个脚本是暂时性的,用在暂时处理自动回收一次性生成的物体和粒子系统,等后续开发就可以删除掉了
    public float recycleTimer = 1f;

    private void Update()
    {
	recycleTimer -= Time.deltaTime;
	if(recycleTimer <= 0f)
	{
	    Destroy(gameObject); // TODO:
	}
    }
}


总结

最后看看我填的参数,需要注意到是你的ATTACK_DURATION和你的攻击动画播放时间Clip Time要保持一致

 

下一期我们来做后坐力系统Recoil和玩家的生命系统和受伤系统。本文一共七万多字,看完记得做下眼保健操

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

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

相关文章

移动端列表筛选封装

适合场景&#xff1a;Vue2vant 移动端项目&#xff0c;数据填充添加全部选项及相关逻辑处理&#xff0c;支持多选、单选以及筛选状态返回 效果图 选中交互 使用说明 <filter-box ref"filterBox" :isMultiple"true" //是否多选:params"waitData&q…

ant design vue实现表格序号递增展示~

1、代码实例 //current当前页数 //pageSize每页记录数 const columns [{title: 序号,width: 100,customRender: ({ index }) > ${index (current.value - 1) * pageSize.value 1},align: center,fixed: left,} ] 2、效果图

虚拟机:4、配置12.5的cuda和gromacs

前言&#xff1a;本机环境是win11&#xff0c;通过wsl2安装了ubuntu实例并已实现gpu直通&#xff0c;现在需要下载12.5的cuda 一、查看是否有gpu和合适的cuda版本 在ubuntu实例中输入 nvidia-smi输出如下&#xff1a; 说明该实例上存在gpu驱动&#xff0c;且适合的CUDA版本…

解决银河麒麟操作系统在单用户模式下根分区只读的问题

解决银河麒麟操作系统在单用户模式下根分区只读的问题 1、问题描述2、问题解决方法 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在使用银河麒麟操作系统时&#xff0c;有时我们可能需要进入单用户模式来进行系统维护或修复。然而&#x…

软考高级:中台相关知识 AI 解读

中台&#xff08;Middle Platform&#xff09;是近年来在软件开发和企业架构中兴起的一种理念和架构模式&#xff0c;尤其在中国的互联网企业中得到了广泛应用。中台的核心思想是通过构建一个共享的服务和能力平台&#xff0c;支持前端业务的快速迭代和创新&#xff0c;从而提升…

企业职工薪资查询系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;员工管理&#xff0c;部门管理&#xff0c;工资信息管理&#xff0c;工资安排管理&#xff0c;考勤信息管理&#xff0c;交流论坛&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#…

2024年最新 信息安全 标准 汇总

背景 信息安全标准是安全专家智慧的结晶&#xff0c;是安全最佳实践的概括总结&#xff0c;是非常好的入门/参考手册&#xff0c;是信息安全建设的理论基础和行动指南。 本页对TC260发布的所有信息安全标准&#xff0c;进行了分类汇总&#xff0c;并提供在线预览和批量下载&am…

【深度学习】03-神经网络01-4 神经网络的pytorch搭建和参数计算

# 计算模型参数,查看模型结构,我们要查看有多少参数&#xff0c;需要先安装包 pip install torchsummary import torch import torch.nn as nn from torchsummary import summary # 导入 summary 函数&#xff0c;用于计算模型参数和查看模型结构# 创建神经网络模型类 class Mo…

【ComfyUI】控制光照节点——ComfyUI-IC-Light-Native

原始代码&#xff08;非comfyui&#xff09;&#xff1a;https://github.com/lllyasviel/IC-Light comfyui实现1&#xff08;600星&#xff09;&#xff1a;https://github.com/kijai/ComfyUI-IC-Light comfyui实现2&#xff08;500星&#xff09;&#xff1a;https://github.c…

【QT】QSS基础

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;QT 目录 &#x1f449;&#x1f3fb;基本语法&#x1f449;&#x1f3fb;从⽂件加载样式表&#x1f449;&#x1f3fb;选择器伪类选择器 &#x1f449;&…

动手学深度学习9.1. 门控循环单元(GRU)-笔记练习(PyTorch)

本节课程地址&#xff1a;门控循环单元&#xff08;GRU&#xff09;_哔哩哔哩_bilibili 本节教材地址&#xff1a;9.1. 门控循环单元&#xff08;GRU&#xff09; — 动手学深度学习 2.0.0 documentation (d2l.ai) 本节开源代码&#xff1a;...>d2l-zh>pytorch>chap…

K8S服务发布

一 、服务发布方式对比 二者主要区别在于&#xff1a; 1. 部署复杂性&#xff1a;传统的服务发布方式通常涉及手动配置 和管理服务器、网络设置、负载均衡等&#xff0c;过程相对复 杂且容易出错。相比之下&#xff0c;Kubernetes服务发布方式 通过使用容器编排和自动化部署工…

高灵敏度电容式触摸IC在弹簧触控按键中的应用

电容式触摸IC-弹簧触控按键-是通过检测人体与传感器之间的电容变化来实现触摸控制。这种技术具有高灵敏度、稳定性好、防水性强等优点&#xff0c;广泛应用于家用电器、消费电子、工业控制等领域。 弹簧触控按键的特点&#xff1a; 1. 高灵敏度&#xff1a;即使隔着绝缘材料&a…

Java语言的Springboot框架+云快充协议1.5+充电桩系统+新能源汽车充电桩系统源码

介绍 云快充协议云快充1.5协议云快充协议开源代码云快充底层协议云快充桩直连桩直连协议充电桩系统桩直连协议 有需者咨询&#xff0c;非诚勿扰&#xff1b; 软件架构 1、提供云快充底层桩直连协议&#xff0c;版本为云快充1.5&#xff0c;对于没有对接过充电桩系统的开发者…

[vulnhub] Jarbas-Jenkins

靶机链接 https://www.vulnhub.com/entry/jarbas-1,232/ 主机发现端口扫描 扫描网段存活主机&#xff0c;因为主机是我最后添加的&#xff0c;所以靶机地址是135的 nmap -sP 192.168.75.0/24 // Starting Nmap 7.93 ( https://nmap.org ) at 2024-09-21 14:03 CST Nmap scan…

求职Leetcode题目(11)

1.最长连续序列 解题思路: 方法一&#xff1a; • 首先对数组进行排序&#xff0c;这样我们可以直接比较相邻的元素是否连续。• 使用一个变量 cur_cnt 来记录当前的连续序列长度。• 遍历排序后的数组&#xff1a; 如果当前元素与前一个元素相等&#xff0c;则跳过&#xf…

文档矫正算法:DocTr++

文档弯曲矫正&#xff08;Document Image Rectification&#xff09;的主要作用是在图像处理领域中&#xff0c;对由于拍摄、扫描或打印过程中产生的弯曲、扭曲文档进行校正&#xff0c;使其恢复为平整、易读的形态。 一. 论文和代码 论文地址&#xff1a;https://arxiv.org/…

AI辅助编码工具如何影响着程序员开发群体

AI辅助编码工具的出现对程序员开发群体产生了深远的影响&#xff0c;有一些初步基础的程序员&#xff0c;可以借助AI工具的加持&#xff0c;生产效率大大提升&#xff0c;达到中高级程序员的水平。 这些影响可以从多个角度来分析&#xff1a; 提高开发效率&#xff1a; AI工具…

跳蚤市场小程序|基于微信小程序的跳蚤市场(源码+数据库+文档)

跳蚤市场小程序目录 基于微信小程序的饮品点单系统的设计与实现 一、前言 二、系统功能设计 三、系统实现 管理员功能实现 商品信息管理 商品订单管理 论坛管理 用户管理 5.1.5 新闻信息管理 用户功能实现 四、数据库设计 1、实体ER图 2、具体的表设计如下所示&a…

毕业设计选题:基于ssm+vue+uniapp的英语学习激励系统小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…