自学内容网 自学内容网

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

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

文章目录


前言

        警告:此篇文章难度较高而且复杂繁重,非常不适合刚刚入门的或者没看过我前几期的读者,我做了一整天才把明显的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和玩家的生命系统和受伤系统。本文一共七万多字,看完记得做下眼保健操


原文地址:https://blog.csdn.net/dangoxiba/article/details/142428788

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!