[Unity Demo]从零开始制作空洞骑士Hollow Knight第十一集:制作法术系统的回血机制和火球机制

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

文章目录


前言

Hello大家好久不见,隔了一天没发文章是因为这期的工程也挺复杂的,警告:此篇文章难度偏高,非常不适合刚刚入门的或者没看过我前几期的读者,之所以做了这么久是因为一堆奇奇怪怪的bug真的差点给我整崩溃了然后有些素材我又找不到只能对着Texture2D去PS一张张扣下来都给我整笑了,请读者如果在阅读后感到身体不适请立刻退出这篇文章。

这期我们的主题是:制作法术系统的回血机制和法球机制,友情提示我盲猜会有一堆图片,所以用流量的请检查自己流量是否够用。


一、制作法术系统的回血机制

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

还是老规矩,我们来为小骑士添加几个新的tk2dSprite和tk2dSpriteAnimation:

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

然后就是创建动画了:

然后还有特效效果SpellEffect的tk2dSprite和tk2dSpriteAnimation:这些都是我在PS

为我们的小骑士生成一个新的子对象叫Focus Effect,为里面添加几个新的子对象:

Lines Anim:

Dust R:

Dust L只需要改变Transform的值即可:

其中的PlayerMaker就是上一期我们讲的延迟游戏对象SetActive = false

2.使用代码和PlayMakerFSM制作法术系统

接下来就是用代码来制作法术系统了,首先到PlayerData.cs中我们来添加几个新的变量都是和法术有关的,以及一些方法主要是获得和设置Int和Bool类型的变量:

cs 复制代码
using System;
using System.Collections.Generic;
using System.Reflection;
using GlobalEnums;
using UnityEngine;

[Serializable]
public class PlayerData
{
    private static PlayerData _instance;
    public static PlayerData instance
    {
	get
	{
	    if(_instance == null)
	    {
		_instance = new PlayerData();
	    }
	    return _instance;
	}
	set
	{
	    _instance = value;
	}
    }

    public bool disablePause;

    public int health;
    public int maxHealth;
    public int maxHealthBase;
    public int prevHealth;

    public int nailDamage;

    public int maxMP; //最大MP容量
    public int MPCharge; //当前MP存储量
    public int MPReverse; //花费的MP
    public int MPReverseMax; //花费的最大MP
    public bool soulLimited; //是否灵魂容量被限制了
    public int focusMP_amount; //聚集法术所需要的MP

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

    public bool hasSpell; //是否有法术
    public int fireballLevel;//火球法术等级,0表示没有
    public int quakeLevel;//地震法术等级,0表示没有
    public int screamLevel;//狂啸法术等级,0表示没有

    public bool overcharmed;

    public bool gotCharm_7; //快速聚集
    public bool equippedCharm_7;

    public bool gotCharm_10; //英勇者勋章
    public bool equippedCharm_10;

    public bool gotCharm_11; //吸虫之巢
    public bool equippedCharm_11;

    public bool gotCharm_17; 
    public bool equippedCharm_17;

    public bool gotCharm_19; //萨满之石
    public bool equippedCharm_19;

    public bool gotCharm_28; //乌恩符咒
    public bool equippedCharm_28;

    public bool gotCharm_31; //冲刺大师
    public bool equippedCharm_31;

    public bool gotCharm_34; //深度聚集
    public bool equippedCharm_34;

    public int CurrentMaxHealth
    {
	get
	{
	    return maxHealth;
	}
    }


    protected PlayerData()
    {
	SetupNewPlayerData();
    }

    public void Reset()
    {
	SetupNewPlayerData();
    }

    private void SetupNewPlayerData()
    {
	disablePause = false;

	health = 5;
	maxHealth = 5;
	maxHealthBase = 5;
	prevHealth = health;
	nailDamage = 5;

	maxMP = 99;
	MPCharge = 0;
	MPReverse = 0;
	MPReverseMax = 0;
	soulLimited = false;
	focusMP_amount = 33;

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

	hasSpell = false;
	fireballLevel = 0;
	quakeLevel = 0;
	screamLevel = 0;

	overcharmed = false;
	gotCharm_7 = false;
	equippedCharm_7 = false;
	gotCharm_10 = false;
	equippedCharm_10 = false;
	gotCharm_11 = false;
	equippedCharm_11 = false;
	gotCharm_17 = false;
	equippedCharm_17 = false;
	gotCharm_19 = false;
	equippedCharm_19 = false;
	gotCharm_28 = false;
	equippedCharm_28 = false;
	gotCharm_31 = true;
	equippedCharm_31 = true;
	gotCharm_34 = false;
	equippedCharm_34 = false;
    }

    public void AddHealth(int amount)
    {
	if (health + amount >= maxHealth)
	{
	    health = maxHealth;
	}
	else
	{
	    health += amount;
	}
	if (health >= CurrentMaxHealth)
	{
	    health = maxHealth;
	}
    }

    public void TakeHealth(int amount)
    {
	if(amount > 0 && health == maxHealth && health != CurrentMaxHealth)
	{
	    health = CurrentMaxHealth;
	}
	if(health - amount < 0)
	{
	    health = 0;
	    return;
	}
	health -= amount;
    }

    public void MaxHealth()
    {
	health = CurrentMaxHealth;
    }

    public bool AddMPCharge(int amount)
    {
	bool result = false;
	if(soulLimited && maxMP != 66)
	{
	    maxMP = 66;
	}
	if (!soulLimited && maxMP != 99)
	{
	    maxMP = 99;
	}
	if(MPCharge + amount > maxMP)
	{
	    if(MPReverse < MPReverseMax)
	    {
		MPReverse += amount - (maxMP - MPCharge);
		result = true;
		if(MPReverse > MPReverseMax)
		{
		    MPReverse = MPReverseMax;
		}
	    }
	    MPCharge = maxMP;
	}
	else
	{
	    MPCharge += amount;
	    result = true;
	}
	return result;
    }

    public void TakeMP(int amount)
    {
	if(amount < MPCharge)
	{
	    MPCharge -= amount;
	    if(MPCharge < 0)
	    {
		MPCharge = 0;
		return;
	    }
	}
	else
	{
	    MPCharge = 0;
	}
    }

    public int GetInt(string intName)
    {
	if (string.IsNullOrEmpty(intName))
	{
	    Debug.LogError("PlayerData: Int with an EMPTY name requested.");
	    return -9999;
	}
	FieldInfo fieldInfo = GetType().GetField(intName);
	if(fieldInfo != null)
	{
	    return (int)fieldInfo.GetValue(instance);
	}
	Debug.LogError("PlayerData: Could not find int named " + intName + " in PlayerData");
	return -9999;
    }

    public bool GetBool(string boolName)
    {
	if (string.IsNullOrEmpty(boolName))
	{
	    return false;
	}
	FieldInfo field = GetType().GetField(boolName);
	if (field != null)
	{
	    return (bool)field.GetValue(instance);
	}
	Debug.Log("PlayerData: Could not find bool named " + boolName + " in PlayerData");
	return false;
    }

}

回到我们的HeroActions.cs中,我们来给法术系统添加几个新行为,一个是回血机制行为的focus,一个是快速法术的quickcast:

cs 复制代码
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 PlayerAction cast;
    public PlayerAction focus;
    public PlayerAction quickCast;


    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");
	cast = CreatePlayerAction("Cast");
	focus = CreatePlayerAction("Focus");
	quickCast = CreatePlayerAction("QuickCast");
    }
}

来到InputHandler.cs中把刚刚创建的变量添加到方法中:

cs 复制代码
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");
	AddKeyBinding(inputActions.cast, "F");
	AddKeyBinding(inputActions.quickCast, "Q");
    }

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

}

接下来就是到HeroAnimationController.cs我们添加几个新的方法,因为在空洞骑士这个游戏中,回血的时候不能移动和播放其它动画,所以我们得创建一个属性来记录是否可以播放动画还有一个ActorStates记录失去控制之前的动画。

public ActorStates stateBeforeControl { get; private set; }

public bool controlEnabled { get; private set; }

public void StartControl()

{

actorStates = heroCtrl.hero_state;

controlEnabled = true;

PlayIdle();

}

public void StopControl()

{

if(controlEnabled)

{

controlEnabled = false;

stateBeforeControl = actorStates;

}

}

cs 复制代码
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; }
    public ActorStates stateBeforeControl { get; private set; }
    public bool controlEnabled { 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 (!controlEnabled)
	{
	    animator.Stop();
	}
	if(heroCtrl.hero_state == ActorStates.airborne)
	{
	    animator.PlayFromFrame("Airborne", 7);
	    return;
	}
	PlayIdle();
    }

    private void Update()
    {
	if(controlEnabled)
	{
	    UpdateAnimation();
	}
	else if (cState.facingRight)
	{
	    wasFacingRight = true;
	}
	else
	{
	    wasFacingRight = false;
	}
    }

    private void UpdateAnimation()
    {
	changedClipFromLastFrame = false;
	if (playLanding)
	{
	    Play("Land");
	    animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
	    playLanding = false;
	}
	if (playRunToIdle)
	{
	    Play("Run To Idle");
	    animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
	    playRunToIdle = false;
	}
	if (playBackDashToIdleEnd)
	{
	    Play("Backdash Land 2");
	    //处理animation播放完成后的事件(其实并不会播放)
	    animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
	    playDashToIdle = false;
	}
	if (playDashToIdle)
	{
	    Play("Dash To Idle");
	    //处理animation播放完成后的事件
	    animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
	    playDashToIdle = false;
	}
	if (actorStates == ActorStates.no_input)
	{
	    //TODO:
	    if (cState.recoilFrozen)
	    {
		Play("Stun");
	    }
	    else if (cState.recoiling)
	    {
		Play("Recoil");
	    }
	}
	else if (cState.dashing)
	{
	    if (heroCtrl.dashingDown)
	    {
		Play("Dash Down");
	    }
	    else
	    {
		Play("Dash"); //通过cState.dashing判断是否播放Dash动画片段
	    }
	}
	else if (cState.backDashing)
	{
	    Play("Back Dash");
	}
	else if(cState.attacking)
	{
	    if (cState.upAttacking)
	    {
		Play("UpSlash");
	    }
	    else if (cState.downAttacking)
	    {
		Play("DownSlash");
	    }
	    else if (!cState.altAttack)
	    {
		Play("Slash");
	    }
	    else
	    {
		Play("SlashAlt");
	    }
	}
	else if (actorStates == ActorStates.idle)
	{
	    //TODO:
	    if (CanPlayIdle())
	    {
		PlayIdle();
	    }
	}
	else if (actorStates == ActorStates.running)
	{
	    if (!animator.IsPlaying("Turn"))
	    {
		if (cState.inWalkZone)
		{
		    if (!animator.IsPlaying("Walk"))
		    {
			Play("Walk");
		    }
		}
		else
		{
		    PlayRun();
		}
	    }
	}
	else if (actorStates == ActorStates.airborne)
	{
	    if (cState.jumping)
	    {
		if (!animator.IsPlaying("Airborne"))
		{
		    animator.PlayFromFrame("Airborne", 0);
		}
	    }
	    else if (cState.falling)
	    {
		if (!animator.IsPlaying("Airborne"))
		{
		    animator.PlayFromFrame("Airborne", 7);
		}
	    }
	    else if (!animator.IsPlaying("Airborne"))
	    {
		animator.PlayFromFrame("Airborne", 3);
	    }
	}
	//(其实并不会播放)
	else if (actorStates == ActorStates.dash_landing)
	{
	    animator.Play("Dash Down Land");
	}
	else if(actorStates == ActorStates.hard_landing)
	{
	    animator.Play("HardLand");
	}
	if (cState.facingRight)
	{
	    if(!wasFacingRight && cState.onGround && CanPlayTurn())
	    {
		Play("Turn");
	    }
	    wasFacingRight = true;
	}
	else
	{
	    if (wasFacingRight && cState.onGround && CanPlayTurn())
	    {
		Play("Turn");
	    }
	    wasFacingRight = false;
	}
	ResetPlays();
    }

    private void AnimationCompleteDelegate(tk2dSpriteAnimator anim, tk2dSpriteAnimationClip clip)
    {
	if(clip.name == "Land")
	{
	    PlayIdle();
	}
	if(clip.name == "Run To Idle")
	{
	    PlayIdle();
	}
	if(clip.name == "Backdash To Idle")//(其实并不会播放)
	{
	    PlayIdle();
	}
	if(clip.name == "Dash To Idle")
	{
	    PlayIdle();
	}
    }

    private void Play(string clipName)
    {
	if(clipName != animator.CurrentClip.name)
	{
	    changedClipFromLastFrame = true;
	}
	animator.Play(clipName);
    }

    private void PlayRun()
    {
	animator.Play("Run");
    }

    public void PlayIdle()
    {
	animator.Play("Idle");
    }

    public void StopAttack()
    {
	if(animator.IsPlaying("UpSlash") || animator.IsPlaying("DownSlash"))
	{
	    animator.Stop();
	}
    }

    public void FinishedDash()
    {
	playDashToIdle = true;
    }

    private void ResetAll()
    {
	playLanding = false;
	playRunToIdle = false;
	playDashToIdle = false;
	wasFacingRight = false;
	controlEnabled = true;
    }

    private void ResetPlays()
    {
	playLanding = false;
	playRunToIdle = false;
	playDashToIdle = false;
    }

    public void UpdateState(ActorStates newState)
    {
	if(controlEnabled &&  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;
	}
    }

    public void PlayClip(string clipName)
    {
	if (controlEnabled)
	{
	    Play(clipName);
	}
    }

    public void StartControl()
    {
	actorStates = heroCtrl.hero_state;
	controlEnabled = true;
	PlayIdle();
    }

    public void StopControl()
    {
	if(controlEnabled)
	{
	    controlEnabled = false;
	    stateBeforeControl = actorStates;
	}
    }
    public void StartControlWithoutSettingState()
    {
	controlEnabled = true;
	if (stateBeforeControl == ActorStates.running && actorStates == ActorStates.running)
	{
	    actorStates = ActorStates.idle;
	}
    }

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

}

来到HeroControllerStates.cs,除了我们要创建几个和法术有关的变量外,还需要获得某个状态的方法和设置某个状态的方法:

cs 复制代码
[Serializable]
public class HeroControllerStates
{
    public bool facingRight;
    public bool onGround;
    public bool wasOnGround;
    public bool attacking;
    public bool altAttack;
    public bool upAttacking;
    public bool downAttacking;
    public bool inWalkZone;
    public bool jumping;
    public bool falling;
    public bool dashing;
    public bool backDashing;
    public bool touchingWall;
    public bool wallSliding;
    public bool willHardLand;
    public bool recoilFrozen;
    public bool recoiling;
    public bool recoilingLeft;
    public bool recoilingRight;
    public bool freezeCharge;
    public bool focusing;
    public bool dead;
    public bool hazardDeath;
    public bool invulnerable;
    public bool preventDash;
    public bool preventBackDash;
    public bool dashCooldown;
    public bool backDashCooldown;
    public bool isPaused;

    public HeroControllerStates()
    {
        facingRight = false;
        onGround = false;
        wasOnGround = false;
        attacking = false;
        altAttack = false;
        upAttacking = false;
        downAttacking = false;
        inWalkZone = false;
        jumping = false;
        falling = false;
        dashing = false;
        backDashing = false;
        touchingWall = false;
        wallSliding = false;
        willHardLand = false;
        recoilFrozen = false;
        recoiling = false;
        recoilingLeft = false;
        recoilingRight = false;
        freezeCharge = false;
        focusing = false;
        dead = false;
        hazardDeath = false;
        invulnerable = false;
        preventDash = false;
        preventBackDash = false;
	dashCooldown = false;
        backDashCooldown = false;
	isPaused = false;
    }

    /// <summary>
    /// 设置一个新的状态(通常用在playmakerFSM上)
    /// </summary>
    /// <param name="stateName"></param>
    /// <param name="value"></param>
    public void SetState(string stateName, bool value)
    {
        FieldInfo field = GetType().GetField(stateName);
        if (field != null)
        {
            try
            {
                field.SetValue(HeroController.instance.cState, value);
                return;
            }
            catch (Exception ex)
            {
                string str = "Failed to set cState: ";
                Exception ex2 = ex;
                Debug.LogError(str + ((ex2 != null) ? ex2.ToString() : null));
                return;
            }
        }
        Debug.LogError("HeroControllerStates: Could not find bool named" + stateName + "in cState");
    }

    /// <summary>
    /// 获取一个新的状态(通常用在playmakerFSM上)
    /// </summary>
    /// <param name="stateName"></param>
    /// <returns></returns>
    public bool GetState(string stateName)
    {
        FieldInfo field = GetType().GetField(stateName);
        if (field != null)
        {
            return (bool)field.GetValue(HeroController.instance.cState);
        }
        Debug.LogError("HeroControllerStates: Could not find bool named" + stateName + "in cState");
        return false;
    }
}

终于来到HeroController.cs中,我们来设置回血机制所需要的变量:

private bool drainMP; //是否正在流走MP

private float drainMP_timer; //流走MP的计时器

private float drainMP_time; //流走MP花费的时间

private float MP_drained; //已经流走的MP数量

private float focusMP_amount; //使用focus回血所需要的MP数量

private float preventCastByDialogueEndTimer;

public PlayMakerFSM spellControl;

在Update函数中:

if (drainMP)

{

drainMP_timer += Time.deltaTime;

while (drainMP_timer >= drainMP_time) //drainMP_time在无护符下等于0.027

{

MP_drained += 1f;

drainMP_timer -= drainMP_time;

TakeMp(1);

if(MP_drained == focusMP_amount)

{

MP_drained -= drainMP_time;

proxyFSM.SendEvent("HeroCtrl-FocusCompleted");

}

}

}

preventCastByDialogueEndTimer -= Time.deltaTime;

设置获取MP和消耗MP的方法,增加血量和消耗血量的方法,是否能聚集,是否能释放法术,能否输入和忽略输入,失去控制和重新获得控制:

cs 复制代码
    public void AddMPCharge(int amount)
    {
        int mpreverse = playerData.MPReverse;
        playerData.AddMPCharge(amount);

        if(playerData.MPReverse != mpreverse && gm)
	    {

	    }
    }

    public void SetMPCharge(int amount)
    {
        playerData.MPCharge = amount;
        //TODO:
    }


    public void SoulGain()
    {
        int num;
        if(playerData.MPCharge < playerData.maxMP)
	    {
            num = 11;
	    }
	    else
	    {
            num = 6;
	    }
        int mpreverse = playerData.MPReverse;
        playerData.AddMPCharge(num);
        if(playerData.MPReverse != mpreverse)
	    {

	    }
    }

    public void TakeMp(int amount)
    {
        if(playerData.MPCharge > 0)
	    {
            playerData.TakeMP(amount);
            if(amount > 1)
	        {

	        }
	    }
    }

    public void AddHealth(int amount)
    {
        playerData.AddHealth(amount);
        proxyFSM.SendEvent("HeroCtrl-Healed");
    }

    public void TakeHealth(int amount)
    {
        playerData.TakeHealth(amount);
        proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
    }

    public void MaxHealth()
    {
        proxyFSM.SendEvent("HeroCtrl-MaxHealth");
	    playerData.MaxHealth();
    }


    public void StartMPDrain(float time)
    {
        Debug.LogFormat("Start MP Drain");
        drainMP = true;
        drainMP_timer = 0f;
        MP_drained = 0f;
        drainMP_time = time; //
        focusMP_amount = (float)playerData.GetInt("focusMP_amount");
    }

    public void StopMPDrain()
    {
        drainMP = false;
    }
    
    public bool CanFocus()
    {
        return !gm.isPaused && hero_state != ActorStates.no_input && !cState.dashing && !cState.backDashing && (!cState.attacking || attack_time > ATTACK_RECOVERY_TIME) && !cState.recoiling && cState.onGround && !cState.recoilFrozen && !cState.hazardDeath && CanInput();
    }

    public bool CanCast()
    {
        return !gm.isPaused && !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.recoiling && !cState.recoilFrozen && CanInput() && preventCastByDialogueEndTimer <= 0f;
    }

    public bool CanInput()
    {
        return acceptingInput;
    }

    public void IgnoreInput()
    {
	    if (acceptingInput)
	    {
            acceptingInput = false;
            ResetInput();
	    }
    }

    public void AcceptInput()
    {
        acceptingInput = true;
    }

    /// <summary>
    /// 放弃控制
    /// </summary>
    public void RelinquishControl()
    {
        if(!controlReqlinquished && !cState.dead)
	    {    
            ResetInput();
            ResetMotion();
            IgnoreInput();
            controlReqlinquished = true;

            ResetAttacks();
            touchingWallL = false;
            touchingWallR = false;
	    }
    }

    /// <summary>
    /// 重新获得控制
    /// </summary>
    public void RegainControl()
    {
        AcceptInput();
        hero_state = ActorStates.idle;
        if(controlReqlinquished && !cState.dead)
	    {
            AffectedByGravity(true);
            SetStartingMotionState();
            controlReqlinquished = false;
	        if (startWithWallslide)
	        {

                cState.willHardLand = false;
                cState.touchingWall = true;

                if(transform.localScale.x< 0f)
		        {
                    touchingWallR = true;
                    return;
		        }
                touchingWallL = true;
	        }
	        else
	        {
		    if (startWithJump)
		    {
                    HeroJumpNoEffect();

                    startWithJump = false;
                    return;
		    }
                if (startWithFullJump)
                {
                    HeroJump();

                    startWithFullJump = false;
                    return;
                }
                if (startWithDash)
                {
                    HeroDash();

                    startWithDash = false;
                    return;
                }
                if (startWithAttack)
                {
                    DoAttack();

                    startWithAttack = false;
                    return;
                }
                cState.touchingWall = false;
                touchingWallL = false;
                touchingWallR = false;
            }
	    }
    }

这个SoulGain()我们要在HealthManager.cs中来调用:

cs 复制代码
public void TakeDamage(HitInstance hitInstance)
    {
	Debug.LogFormat("Enemy Take Damage");
	int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	directionOfLastAttack = cardinalDirection;
	FSMUtility.SendEventToGameObject(gameObject, "HIT", false);
	FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
	FSMUtility.SendEventToGameObject(gameObject, "TOOK DAMAGE", false);
	if(recoil != null)
	{
	    recoil.RecoilByDirection(cardinalDirection,hitInstance.MagnitudeMultiplier);
	}
	switch (hitInstance.AttackType)
	{
	    case AttackTypes.Nail:
		if(hitInstance.AttackType == AttackTypes.Nail && enemyType !=3 && enemyType != 6)
		{
		    HeroController.instance.SoulGain();
		}
		Vector3 position = (hitInstance.Source.transform.position + transform.position) * 0.5f + effectOrigin;
		break;
	    case AttackTypes.Generic:
		break;
	    case AttackTypes.Spell:
		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)
	{
	    NonFatalHit(hitInstance.IgnoreInvulnerable);
	}
	else
	{
	    Die(new float?(hitInstance.GetActualDirection(transform)), hitInstance.AttackType, hitInstance.IgnoreInvulnerable);
	}
    }

这里我们添加了一个新的AttackType类型就叫Spell:

cs 复制代码
public enum AttackTypes
{
    Nail,
    Generic,
    Spell
}

完整的HeroController.cs如下所示:

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using GlobalEnums;
using UnityEngine;
using System.Reflection;

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

    public bool acceptingInput = true;
    public bool controlReqlinquished; //控制是否被放弃

    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;//跳跃的食欲

    private NailSlash slashComponent; //决定使用哪种攻击的NailSlash
    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 bool drainMP; //是否正在流走MP
    private float drainMP_timer; //流走MP的计时器
    private float drainMP_time; //流走MP花费的时间
    private float MP_drained; //已经流走的MP数量
    private float focusMP_amount; //使用focus回血所需要的MP数量
    private float preventCastByDialogueEndTimer;
    public PlayMakerFSM spellControl;


    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 delegate void TakeDamageEvent();
    public event TakeDamageEvent OnTakenDamage;
    public delegate void OnDeathEvent();
    public event OnDeathEvent OnDeath;

    public bool takeNoDamage; //不受到伤害
    public PlayMakerFSM damageEffectFSM; //负责的受伤效果playmakerFSM
    public DamageMode damageMode; //受伤类型
    private Coroutine takeDamageCoroutine; //受伤协程
    private float parryInvulnTimer;  //无敌时间
    public float INVUL_TIME;//无敌时间

    public float DAMAGE_FREEZE_DOWN;  //受伤冻结的上半程时间
    public float DAMAGE_FREEZE_WAIT; //受伤冻结切换的时间
    public float DAMAGE_FREEZE_UP;//受伤冻结的下半程时间

    private int recoilSteps; 
    private float recoilTimer; //后坐力计时器
    private bool recoilLarge; //是否是更大的后坐力
    private Vector2 recoilVector; //后坐力二维上的速度

    public float RECOIL_HOR_VELOCITY; //后坐力X轴上的速度
    public float RECOIL_HOR_VELOCITY_LONG; //后坐力X轴上更大的速度
    public float RECOIL_DOWN_VELOCITY; //后坐力Y轴上的速度
    public float RECOIL_HOR_STEPS; //后坐力X轴的步
    public float RECOIL_DURATION; //后坐力持续时间
    public float RECOIL_VELOCITY; //后坐力时的速度(是两个轴上都适用的)


    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; //是否开启掉落时相机抖动

    private float floatingBufferTimer;
    private float FLOATING_CHECK_TIME = 0.18f;

    private bool startWithWallslide;
    private bool startWithJump;
    private bool startWithFullJump;
    private bool startWithDash;
    private bool startWithAttack;

    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 new MeshRenderer renderer;
    private InvulnerablePulse invPulse;
    private SpriteFlash spriteFlash;
    public PlayMakerFSM proxyFSM { get; private set; }

    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;
        FLOATING_CHECK_TIME = 0.18f;
    }

    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>();
	renderer = GetComponent<MeshRenderer>();
        invPulse = GetComponent<InvulnerablePulse>();
        spriteFlash = GetComponent<SpriteFlash>();
        proxyFSM = FSMUtility.LocateFSM(gameObject, "ProxyFSM");
    }

    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);
	}
        if (spellControl == null)
        {
            Debug.Log("SpellControl came up null, locating manually");
	    spellControl = FSMUtility.LocateFSM(gameObject, "Spell Control");
        }
    }

    private void Update()
    {
        current_velocity = rb2d.velocity;
        FallCheck();
        FailSafeCheck();
        if (hero_state == ActorStates.running && !cState.dashing && !cState.backDashing && !controlReqlinquished)
        {
            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)
        {
            if (cState.recoiling)
            {
                if (recoilTimer < RECOIL_DURATION)
                {
                    recoilTimer += Time.deltaTime;
                }
                else
                {
                    CancelDamageRecoil();
                    if ((prev_hero_state == ActorStates.idle || prev_hero_state == ActorStates.running) && !CheckTouchingGround())
                    {
                        cState.onGround = false;
                        SetState(ActorStates.airborne);
                    }
                    else
                    {
                        SetState(ActorStates.previous);
                    }

                }
            }
        }
        else if (hero_state != ActorStates.no_input)
        {
            LookForInput();
            if (cState.recoiling)
            {
                cState.recoiling = false;
                AffectedByGravity(true);
            }
            if (cState.attacking && !cState.dashing)
            {
                attack_time += Time.deltaTime;
                if (attack_time >= attackDuration)
                {
                    ResetAttacks();
                    animCtrl.StopAttack();
                }
            }

            if(hero_state == ActorStates.idle)
	    {
                if(!controlReqlinquished && !gm.isPaused)
		{
                    //TODO:
		}
                
	    }
        }
        LookForQueueInput();
        if (drainMP)
	{
            drainMP_timer += Time.deltaTime;
            while (drainMP_timer >= drainMP_time) //drainMP_time在无护符下等于0.027
            {
                MP_drained += 1f;
                drainMP_timer -= drainMP_time;
                TakeMp(1);

                if(MP_drained == focusMP_amount)
		{
                    MP_drained -= drainMP_time;
                    proxyFSM.SendEvent("HeroCtrl-FocusCompleted");

		}
	    }
	}
	if (attack_cooldown > 0f)
	{
            attack_cooldown -= Time.deltaTime;
	}
        if (dashCooldownTimer > 0f) //计时器在Update中-= Time.deltaTime
        {
            dashCooldownTimer -= Time.deltaTime;
	}

	preventCastByDialogueEndTimer -= Time.deltaTime;

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

    private void FixedUpdate()
    {
        if(cState.recoilingLeft || cState.recoilingRight)
	{
            if(recoilSteps <= RECOIL_HOR_STEPS)
	    {
                recoilSteps++;
	    }
	    else
	    {
                CancelRecoilHorizonal();
	    }
	}
        if(hero_state == ActorStates.hard_landing || hero_state == ActorStates.dash_landing)
	{
            ResetMotion();
	}
        else if(hero_state == ActorStates.no_input)
	{
	    if (cState.recoiling)
	    {
                AffectedByGravity(false);
                rb2d.velocity = recoilVector;
	    }
	}
        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();
                        CancelAttack();
                    }
                    else if (move_input < 0f && cState.facingRight)
                    {
                        FlipSprite();
                        CancelAttack();
                    }
                }
                if(cState.recoilingLeft)
		{
                    float num;
                    if (recoilLarge)
                    {
                        num = RECOIL_HOR_VELOCITY_LONG;
                    }
                    else
                    {
                        num = RECOIL_HOR_VELOCITY;
                    }
                    if(rb2d.velocity.x > -num)
		    {
                        rb2d.velocity = new Vector2(-num, rb2d.velocity.y);
		    }
		    else
		    {
                        rb2d.velocity = new Vector2(rb2d.velocity.x - num, rb2d.velocity.y);
		    }
		}
                if (cState.recoilingRight)
                {
                    float num2;
                    if(recoilLarge)
		    {
                        num2 = RECOIL_HOR_VELOCITY_LONG;
		    }
                    else
		    {
                        num2 = RECOIL_HOR_VELOCITY;
                    }
                    if (rb2d.velocity.x < num2)
                    {
                        rb2d.velocity = new Vector2(num2, rb2d.velocity.y);
                    }
                    else
                    {
                        rb2d.velocity = new Vector2(rb2d.velocity.x + num2, rb2d.velocity.y);
                    }
                }
            }
	}

	if (cState.jumping) //如果cState.jumping就Jump
        {
            Jump();
	}
	if (cState.dashing)//如果cState.dashing就Dash
        {
            Dash();
	}
        //限制速度
        if(rb2d.velocity.y < -MAX_FALL_VELOCITY && !controlReqlinquished )
	{
            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()
    {

        cState.recoiling = false;
        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 && !cState.dead && !cState.hazardDeath && !controlReqlinquished;
    }

    private void CancelAttack()
    {
	if (cState.attacking)
	{
            slashComponent.CancelAttack();
            ResetAttacks();
	}
    }

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

    public void AddMPCharge(int amount)
    {
        int mpreverse = playerData.MPReverse;
        playerData.AddMPCharge(amount);

        if(playerData.MPReverse != mpreverse && gm)
	{

	}
    }

    public void SetMPCharge(int amount)
    {
        playerData.MPCharge = amount;
        //TODO:
    }


    public void SoulGain()
    {
        int num;
        if(playerData.MPCharge < playerData.maxMP)
	{
            num = 11;
	}
	else
	{
            num = 6;
	}
        int mpreverse = playerData.MPReverse;
        playerData.AddMPCharge(num);
        if(playerData.MPReverse != mpreverse)
	{

	}
    }

    public void TakeMp(int amount)
    {
        if(playerData.MPCharge > 0)
	{
            playerData.TakeMP(amount);
            if(amount > 1)
	    {

	    }
	}
    }

    public void AddHealth(int amount)
    {
        playerData.AddHealth(amount);
        proxyFSM.SendEvent("HeroCtrl-Healed");
    }

    public void TakeHealth(int amount)
    {
        playerData.TakeHealth(amount);
        proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
    }

    public void MaxHealth()
    {
        proxyFSM.SendEvent("HeroCtrl-MaxHealth");
	playerData.MaxHealth();
    }


    public void StartMPDrain(float time)
    {
        Debug.LogFormat("Start MP Drain");
        drainMP = true;
        drainMP_timer = 0f;
        MP_drained = 0f;
        drainMP_time = time; //
        focusMP_amount = (float)playerData.GetInt("focusMP_amount");
    }

    public void StopMPDrain()
    {
        drainMP = false;
    }
    
    public bool CanFocus()
    {
        return !gm.isPaused && hero_state != ActorStates.no_input && !cState.dashing && !cState.backDashing && (!cState.attacking || attack_time > ATTACK_RECOVERY_TIME) && !cState.recoiling && cState.onGround && !cState.recoilFrozen && !cState.hazardDeath && CanInput();
    }

    public bool CanCast()
    {
        return !gm.isPaused && !cState.dashing && hero_state != ActorStates.no_input && !cState.backDashing && (!cState.attacking || attack_time >= ATTACK_RECOVERY_TIME) && !cState.recoiling && !cState.recoilFrozen && CanInput() && preventCastByDialogueEndTimer <= 0f;
    }

    public bool CanInput()
    {
        return acceptingInput;
    }

    public void IgnoreInput()
    {
	if (acceptingInput)
	{
            acceptingInput = false;
            ResetInput();
	}
    }

    public void AcceptInput()
    {
        acceptingInput = true;
    }

    /// <summary>
    /// 放弃控制
    /// </summary>
    public void RelinquishControl()
    {
        if(!controlReqlinquished && !cState.dead)
	{
            ResetInput();
            ResetMotion();
            IgnoreInput();
            controlReqlinquished = true;

            ResetAttacks();
            touchingWallL = false;
            touchingWallR = false;
	}
    }

    /// <summary>
    /// 重新获得控制
    /// </summary>
    public void RegainControl()
    {
        AcceptInput();
        hero_state = ActorStates.idle;
        if(controlReqlinquished && !cState.dead)
	{
            AffectedByGravity(true);
            SetStartingMotionState();
            controlReqlinquished = false;
	    if (startWithWallslide)
	    {

                cState.willHardLand = false;
                cState.touchingWall = true;

                if(transform.localScale.x< 0f)
		{
                    touchingWallR = true;
                    return;
		}
                touchingWallL = true;
	    }
	    else
	    {
		if (startWithJump)
		{
                    HeroJumpNoEffect();

                    startWithJump = false;
                    return;
		}
                if (startWithFullJump)
                {
                    HeroJump();

                    startWithFullJump = false;
                    return;
                }
                if (startWithDash)
                {
                    HeroDash();

                    startWithDash = false;
                    return;
                }
                if (startWithAttack)
                {
                    DoAttack();

                    startWithAttack = false;
                    return;
                }
                cState.touchingWall = false;
                touchingWallL = false;
                touchingWallR = false;
            }
	}
    }

    private void SetStartingMotionState()
    {
        SetStartingMotionState(false);
    }

    private void SetStartingMotionState(bool preventRunDip)
    {
        move_input = (acceptingInput || preventRunDip) ? inputHandler.inputActions.moveVector.X : 0f;
        cState.touchingWall = false;
	if (CheckTouchingGround())
	{
            cState.onGround = true;
            SetState(ActorStates.grounded);
            
	}
	else
	{
            cState.onGround = false;
            SetState(ActorStates.airborne);
	}
        animCtrl.UpdateState(hero_state);
    }


    /// <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);

        cState.recoiling = false;

	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.attacking || attack_time >= ATTACK_RECOVERY_TIME)  &&!cState.preventBackDash && !cState.backDashCooldown && !controlReqlinquished && !cState.recoilFrozen && !cState.recoiling && 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;
	}

        if(rb2d.velocity.y == 0f && !cState.onGround && !cState.falling && !cState.jumping && !cState.dashing && hero_state != ActorStates.hard_landing && hero_state != ActorStates.no_input)
	{
	    if (CheckTouchingGround())
	    {
                floatingBufferTimer += Time.deltaTime;
                if(floatingBufferTimer > FLOATING_CHECK_TIME)
		{
		    if (cState.recoiling)
		    {
                        CancelDamageRecoil();
		    }
                    BackOnGround();
                    floatingBufferTimer = 0f;
                    return;
		}
	    }
	    else
	    {
                floatingBufferTimer = 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();
        CancelRecoilHorizonal();
        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;
    }

    public void RecoilLeft()
    {
        if(!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
	{
            CancelDash();
            recoilSteps = 0;
            cState.recoilingLeft = true;
            cState.recoilingRight = false;
            recoilLarge = false;
            rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY, rb2d.velocity.y);
	}
    }

    public void RecoilRight()
    {
        if (!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
        {
            CancelDash();
            recoilSteps = 0;
            cState.recoilingLeft = false;
            cState.recoilingRight = true;
            recoilLarge = false;
            rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY, rb2d.velocity.y);
        }
    }

    public void RecoilLeftLong()
    {
        if (!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
        {
            CancelDash();
            ResetAttacks();
            recoilSteps = 0;
            cState.recoilingLeft = true;
            cState.recoilingRight = false;
            recoilLarge = true;
            rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);
        }
    }

    public void RecoilRightLong()
    {
        if (!cState.recoilingLeft && !cState.recoilingRight && !controlReqlinquished)
        {
            CancelDash();
            ResetAttacks();
            recoilSteps = 0;
            cState.recoilingLeft = false;
            cState.recoilingRight = true;
            recoilLarge = true;
            rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);
        }
    }

    public void RecoilDown()
    {
        CancelJump();
        if(rb2d.velocity.y > RECOIL_DOWN_VELOCITY && !controlReqlinquished)
	{
            rb2d.velocity = new Vector2(rb2d.velocity.x, RECOIL_DOWN_VELOCITY);
	}
    }

    public void CancelRecoilHorizonal()
    {
        cState.recoilingLeft = false;
        cState.recoilingRight = false;
        recoilSteps = 0;
    }

    private bool CanTakeDamage()
    {
        return damageMode != DamageMode.NO_DAMAGE && !cState.invulnerable && !cState.recoiling && !cState.dead;
    }

    public void TakeDamage(GameObject go,CollisionSide damageSide,int damageAmount,int hazardType)
    {
        bool spawnDamageEffect = true;
        if (damageAmount > 0)
        {
            if (CanTakeDamage())
            {
                if (damageMode == DamageMode.HAZARD_ONLY && hazardType == 1)
                {
                    return;
                }
                if (parryInvulnTimer > 0f && hazardType == 1)
                {
                    return;
                }

                proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
                CancelAttack();

                if (cState.touchingWall)
                {
                    cState.touchingWall = false;
                }
                if (cState.recoilingLeft || cState.recoilingRight)
                {
                    CancelRecoilHorizonal();
                }
                audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
                if (!takeNoDamage)
                {
                    playerData.TakeHealth(damageAmount);
                }

                if (damageAmount > 0 && OnTakenDamage != null)
                {
                    OnTakenDamage();
                }
                if (playerData.health == 0)
                {
                    StartCoroutine(Die());
                    return;
                }
                if (hazardType == 2)
                {
                    Debug.LogFormat("Die From Spikes");
                    return;
                }
                if (hazardType == 3)
                {
                    Debug.LogFormat("Die From Acid");
                    return;
                }
                if (hazardType == 4)
                {
                    Debug.LogFormat("Die From Lava");
                    return;
                }
                if (hazardType == 5)
                {
                    Debug.LogFormat("Die From Pit");
                    return;
                }
                StartCoroutine(StartRecoil(damageSide, spawnDamageEffect, damageAmount));
                return;
            }
            else if (cState.invulnerable && !cState.hazardDeath)
	    {
                if(hazardType == 2)
		{
		    if (!takeNoDamage)
		    {
                        playerData.TakeHealth(damageAmount);
		    }
                    proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
                    if(playerData.health == 0)
		    {
                        StartCoroutine(Die());
                        return;
		    }
                    audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
                    StartCoroutine(DieFromHazard(HazardTypes.SPIKES, (go != null) ? go.transform.rotation.z : 0f));
                    return;
		}
                else if (hazardType == 3)
                {             
                    playerData.TakeHealth(damageAmount);
                    proxyFSM.SendEvent("HeroCtrl-HeroDamaged");
                    if (playerData.health == 0)
                    {
                        StartCoroutine(Die());
                        return;
                    }
                    audioCtrl.PlaySound(HeroSounds.TAKE_HIT);
                    StartCoroutine(DieFromHazard(HazardTypes.ACID, 0f));
                    return;
                }
                else if(hazardType == 4)
		{
                    Debug.LogFormat("Die From Lava");
                }
            }
	}
    }

    private IEnumerator Die()
    {
	if (OnDeath != null)
	{
            OnDeath();
	}
	if (!cState.dead)
	{
            playerData.disablePause = true;

            rb2d.velocity = Vector2.zero;
            CancelRecoilHorizonal();

            AffectedByGravity(false);
            HeroBox.inactive = true;
            rb2d.isKinematic = true;
            SetState(ActorStates.no_input);
            cState.dead = true;
            ResetMotion();
            ResetHardLandingTimer();
            renderer.enabled = false;
            gameObject.layer = 2;

            yield return null;

	}
    }

    private IEnumerator DieFromHazard(HazardTypes hazardType,float angle)
    {
	if (!cState.hazardDeath)
	{
            playerData.disablePause = true;

            SetHeroParent(null);
            SetState(ActorStates.no_input);
            cState.hazardDeath = true;
            ResetMotion();
            ResetHardLandingTimer();
            AffectedByGravity(false);
            renderer.enabled = false;
            gameObject.layer = 2;
            if(hazardType == HazardTypes.SPIKES)
	    {

	    }
            else if(hazardType == HazardTypes.ACID)
	    {

	    }
            yield return null;

	}
    }

    private IEnumerator StartRecoil(CollisionSide impactSide, bool spawnDamageEffect, int damageAmount)
    {
	if (!cState.recoiling)
	{
            playerData.disablePause = true;
            ResetMotion();
            AffectedByGravity(false);
            if(impactSide == CollisionSide.left)
	    {
		recoilVector = new Vector2(RECOIL_VELOCITY, RECOIL_VELOCITY * 0.5f);
		if (cState.facingRight)
		{
                    FlipSprite();
		}
	    }
            else if (impactSide == CollisionSide.right)
            {
                recoilVector = new Vector2(-RECOIL_VELOCITY, RECOIL_VELOCITY * 0.5f);
                if (!cState.facingRight)
                {
                    FlipSprite();
                }
            }
	    else
	    {
                recoilVector = Vector2.zero;
	    }
            SetState(ActorStates.no_input);
            cState.recoilFrozen = true;
	    if (spawnDamageEffect)
	    {
                damageEffectFSM.SendEvent("DAMAGE");
                if(damageAmount > 1)
		{

		}
	    }
            StartCoroutine(Invulnerable(INVUL_TIME));
            yield return takeDamageCoroutine = StartCoroutine(gm.FreezeMoment(DAMAGE_FREEZE_DOWN, DAMAGE_FREEZE_WAIT, DAMAGE_FREEZE_UP, 0.0001f));
            cState.recoilFrozen = false;
            cState.recoiling = true;
            playerData.disablePause = false;
        }
    }

    private IEnumerator Invulnerable(float duration)
    {
        cState.invulnerable = true;
        yield return new WaitForSeconds(DAMAGE_FREEZE_DOWN);
        invPulse.StartInvulnerablePulse();
        yield return new WaitForSeconds(duration);
        invPulse.StopInvulnerablePulse();
	cState.invulnerable = false;
        cState.recoiling = false;
    }

    public void CancelDamageRecoil()
    {
        cState.recoiling = false;
        recoilTimer = 0f;
        ResetMotion();
        AffectedByGravity(true);

    }

    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.recoiling = false;
        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)
			    {
                                RecoilLeft();
			    }
			    else
			    {
                                RecoilRight();
			    }
			}
			else if (attackDir == AttackDirection.upward)
			{
                            RecoilDown();
			}
		    }
		}
		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)
		{
                    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;
    }

    public void SetHeroParent(Transform newParent)
    {
        transform.parent = newParent;
        if (newParent == null)
        {
	    DontDestroyOnLoad(gameObject);
        }
    }

    /// <summary>
    /// 设置一个新的HeroControllerStates状态
    /// </summary>
    /// <param name="stateName"></param>
    /// <param name="value"></param>
    public void SetCState(string stateName, bool value)
    {
        cState.SetState(stateName, value);
    }

    /// <summary>
    /// 获取当前HeroControllerStates状态
    /// </summary>
    /// <param name="stateName"></param>
    /// <returns></returns>
    public bool GetCState(string stateName)
    {
        return cState.GetState(stateName);
    }

    public void StartAnimationControl()
    {
        animCtrl.StartControl();
    }

    public void StopAnimationControl()
    {
        animCtrl.StopControl();
    }


}

[Serializable]
public class HeroControllerStates
{
    public bool facingRight;
    public bool onGround;
    public bool wasOnGround;
    public bool attacking;
    public bool altAttack;
    public bool upAttacking;
    public bool downAttacking;
    public bool inWalkZone;
    public bool jumping;
    public bool falling;
    public bool dashing;
    public bool backDashing;
    public bool touchingWall;
    public bool wallSliding;
    public bool willHardLand;
    public bool recoilFrozen;
    public bool recoiling;
    public bool recoilingLeft;
    public bool recoilingRight;
    public bool freezeCharge;
    public bool focusing;
    public bool dead;
    public bool hazardDeath;
    public bool invulnerable;
    public bool preventDash;
    public bool preventBackDash;
    public bool dashCooldown;
    public bool backDashCooldown;
    public bool isPaused;

    public HeroControllerStates()
    {
        facingRight = false;
        onGround = false;
        wasOnGround = false;
        attacking = false;
        altAttack = false;
        upAttacking = false;
        downAttacking = false;
        inWalkZone = false;
        jumping = false;
        falling = false;
        dashing = false;
        backDashing = false;
        touchingWall = false;
        wallSliding = false;
        willHardLand = false;
        recoilFrozen = false;
        recoiling = false;
        recoilingLeft = false;
        recoilingRight = false;
        freezeCharge = false;
        focusing = false;
        dead = false;
        hazardDeath = false;
        invulnerable = false;
        preventDash = false;
        preventBackDash = false;
	dashCooldown = false;
        backDashCooldown = false;
	isPaused = false;
    }

    /// <summary>
    /// 设置一个新的状态(通常用在playmakerFSM上)
    /// </summary>
    /// <param name="stateName"></param>
    /// <param name="value"></param>
    public void SetState(string stateName, bool value)
    {
        FieldInfo field = GetType().GetField(stateName);
        if (field != null)
        {
            try
            {
                field.SetValue(HeroController.instance.cState, value);
                return;
            }
            catch (Exception ex)
            {
                string str = "Failed to set cState: ";
                Exception ex2 = ex;
                Debug.LogError(str + ((ex2 != null) ? ex2.ToString() : null));
                return;
            }
        }
        Debug.LogError("HeroControllerStates: Could not find bool named" + stateName + "in cState");
    }

    /// <summary>
    /// 获取一个新的状态(通常用在playmakerFSM上)
    /// </summary>
    /// <param name="stateName"></param>
    /// <returns></returns>
    public bool GetState(string stateName)
    {
        FieldInfo field = GetType().GetField(stateName);
        if (field != null)
        {
            return (bool)field.GetValue(HeroController.instance.cState);
        }
        Debug.LogError("HeroControllerStates: Could not find bool named" + stateName + "in cState");
        return false;
    }

}

你可能注意到代码中关于法术有关的代码少之又少,这是因为我打算用playmakerFSM制作整个法术系统,给小骑士创建一个新的playMakerFSM就叫Spell Control:

底下是一个基本的法术系统playmakerFSM,所以这就是我说的图片很多小心流量

我们先来把变量和事件都添加上去:

需要特别设置的变量如下所示:

然后我将逐个介绍每种状态和每种状态要用某些行为的含义:

首先一开始先顿个一帧切换到FINISHED

初始化Init,主要是获取需要的子对象:

初始化结束后进入不活跃Inactive状态:

这两个是我新建的自定义脚本,用于监听Cast和QuickCast的按键输入:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("Controls")]
    [Tooltip("Listens for an action button press (using HeroActions InControl mappings).")]
    public class ListenForCast : FsmStateAction
    {
	[Tooltip("Where to send the event.")]
	public FsmEventTarget eventTarget;
	public FsmEvent wasPressed;
	public FsmEvent wasReleased;
	public FsmEvent isPressed;
	public FsmEvent isNotPressed;
	public FsmBool activeBool;
	public bool stateEntryOnly;

	private GameManager gm;
	private InputHandler inputHandler;

	public override void Reset()
	{
	    eventTarget = null;
	    activeBool = new FsmBool
	    {
		UseVariable = true
	    };
	}

	public override void OnEnter()
	{
	    gm = GameManager.instance;
	    inputHandler = gm.GetComponent<InputHandler>();
	    CheckForInput();
	    if (stateEntryOnly)
	    {
		Finish();
	    }
	}

	
	public override void OnUpdate()
	{
	    CheckForInput();
	}

	private void CheckForInput()
	{
	    if (!gm.isPaused && (activeBool.IsNone || activeBool.Value))
	    {
		if (inputHandler.inputActions.cast.WasPressed)
		{
		    Fsm.Event(wasPressed);
		}
		if (inputHandler.inputActions.cast.WasReleased)
		{
		    Fsm.Event(wasReleased);
		}
		if (inputHandler.inputActions.cast.IsPressed)
		{
		    Fsm.Event(isPressed);
		}
		if (!inputHandler.inputActions.cast.IsPressed)
		{
		    Fsm.Event(isNotPressed);
		}
	    }
	}
    }

}
cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("Controls")]
    [Tooltip("Listens for an action button press (using HeroActions InControl mappings).")]
    public class ListenForQuickCast : FsmStateAction
    {
	[Tooltip("Where to send the event.")]
	public FsmEventTarget eventTarget;
	public FsmEvent wasPressed;
	public FsmEvent wasReleased;
	public FsmEvent isPressed;
	public FsmEvent isNotPressed;

	private GameManager gm;
	private InputHandler inputHandler;

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

	public override void OnEnter()
	{
	    gm = GameManager.instance;
	    inputHandler = gm.GetComponent<InputHandler>();
	}

	public override void OnUpdate()
	{
	    if (!gm.isPaused)
	    {
		if (inputHandler.inputActions.quickCast.WasPressed)
		{
		    Fsm.Event(wasPressed);
		}
		if (inputHandler.inputActions.quickCast.WasReleased)
		{
		    Fsm.Event(wasReleased);
		}
		if (inputHandler.inputActions.quickCast.IsPressed)
		{
		    Fsm.Event(isPressed);
		}
		if (!inputHandler.inputActions.quickCast.IsPressed)
		{
		    Fsm.Event(isNotPressed);
		}
	    }
	}
    }
}

如果按下按钮后就判断是否有上下的按键输入,有的话就进入判断地震和狂啸状态,没有的话就等时间到后进入回血状态:

这两个是我新建的自定义脚本,用于监听上Up和下Down的按键输入:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory("Controls")]
    [Tooltip("Listens for an action button press (using HeroActions InControl mappings).")]
    public class ListenForDown : FsmStateAction
    {

	[Tooltip("Where to send the event.")]
	public FsmEventTarget eventTarget;
	public FsmEvent wasPressed;
	public FsmEvent wasReleased;
	public FsmEvent isPressed;
	public FsmEvent isNotPressed;

	[UIHint(UIHint.Variable)]
	public FsmBool isPressedBool;

	public bool stateEntryOnly;

	private GameManager gm;
	private InputHandler inputHandler;

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

	public override void OnEnter()
	{
	    gm = GameManager.instance;
	    inputHandler = gm.GetComponent<InputHandler>();
	    CheckForInput();
	    if (stateEntryOnly)
	    {
		Finish();
	    }
	}

	public override void OnUpdate()
	{
	    CheckForInput();
	}

	private void CheckForInput()
	{
	    if (!gm.isPaused)
	    {
		if (inputHandler.inputActions.down.WasPressed)
		{
		    Fsm.Event(wasPressed);
		}
		if (inputHandler.inputActions.down.WasReleased)
		{
		    Fsm.Event(wasReleased);
		}
		if (inputHandler.inputActions.down.IsPressed)
		{
		    Fsm.Event(isPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = true;
		    }
		}
		if (!inputHandler.inputActions.down.IsPressed)
		{
		    Fsm.Event(isNotPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = false;
		    }
		}
	    }
	}
    }
}
cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory("Controls")]
    [Tooltip("Listens for an action button press (using HeroActions InControl mappings).")]
    public class ListenForUp : FsmStateAction
    {
	[Tooltip("Where to send the event.")]
	public FsmEventTarget eventTarget;
	public FsmEvent wasPressed;
	public FsmEvent wasReleased;
	public FsmEvent isPressed;
	public FsmEvent isNotPressed;

	[UIHint(UIHint.Variable)]
	public FsmBool isPressedBool;

	public bool stateEntryOnly;

	private GameManager gm;
	private InputHandler inputHandler;

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

	public override void OnEnter()
	{
	    gm = GameManager.instance;
	    inputHandler = gm.GetComponent<InputHandler>();
	    CheckForInput();
	    if (stateEntryOnly)
	    {
		Finish();
	    }
	}


	public override void OnUpdate()
	{
	    CheckForInput();
	}

	private void CheckForInput()
	{
	    if (!gm.isPaused)
	    {
		if (inputHandler.inputActions.up.WasPressed)
		{
		    Fsm.Event(wasPressed);
		}
		if (inputHandler.inputActions.up.WasReleased)
		{
		    Fsm.Event(wasReleased);
		}
		if (inputHandler.inputActions.up.IsPressed)
		{
		    Fsm.Event(isPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = true;
		    }
		}
		if (!inputHandler.inputActions.up.IsPressed)
		{
		    Fsm.Event(isNotPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = false;
		    }
		}
	    }
	}

    }
}

如果进入Can Focus?状态通过HeroController.cs的CanFocus()方法和GameManager里面的PlayerData的Int值类型够不够来判断:

这里有一个自定义行为脚本CallMethodProper .cs:

cs 复制代码
using System;
using System.Reflection;
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory(ActionCategory.ScriptControl)]
    public class CallMethodProper : FsmStateAction
    {
	[RequiredField]
	[Tooltip("The game object that owns the Behaviour.")]
	public FsmOwnerDefault gameObject;

	[RequiredField]
	[UIHint(UIHint.Behaviour)]
	[Tooltip("The Behaviour that contains the method to start as a coroutine.")]
	public FsmString behaviour;

	[UIHint(UIHint.Method)]
	[Tooltip("Name of the method to call on the component")]
	public FsmString methodName;

	[Tooltip("Method paramters. NOTE: these must match the method's signature!")]
	public FsmVar[] parameters;

	[ActionSection("Store Result")]
	[UIHint(UIHint.Variable)]
	[Tooltip("Store the result of the method call.")]
	public FsmVar storeResult;

	private UnityEngine.Object cachedBehaviour;
	private Type cachedType;
	private MethodInfo cachedMethodInfo;
	private ParameterInfo[] cachedParameterInfo;
	private object[] parametersArray;
	private string errorString;

	private MonoBehaviour component;

	public override void OnEnter()
	{
	    parametersArray = new object[parameters.Length];
	    DoMethodCall();
	    Finish();
	}

	private void DoMethodCall()
	{
	    if (behaviour.Value == null)
	    {
		Finish();
		return;
	    }
	    GameObject ownerDefaultTarget = base.Fsm.GetOwnerDefaultTarget(gameObject);
	    if (ownerDefaultTarget == null)
	    {
		return;
	    }
	    component = (ownerDefaultTarget.GetComponent(behaviour.Value) as MonoBehaviour);
	    if (component == null)
	    {
		LogWarning("CallMethodProper: " + ownerDefaultTarget.name + " missing behaviour: " + behaviour.Value);
		return;
	    }
	    if (cachedMethodInfo == null)
	    {
		errorString = string.Empty;
		if (!DoCache())
		{
		    Debug.LogError(errorString);
		    Finish();
		    return;
		}
	    }
	    object value = null;
	    if (cachedParameterInfo.Length == 0)
	    {
		value = cachedMethodInfo.Invoke(cachedBehaviour, null);
	    }
	    else
	    {
		for (int i = 0; i < parameters.Length; i++)
		{
		    FsmVar fsmVar = parameters[i];
		    fsmVar.UpdateValue();
		    parametersArray[i] = fsmVar.GetValue();
		}
		try
		{
		    value = cachedMethodInfo.Invoke(cachedBehaviour, parametersArray);
		}
		catch (Exception ex)
		{
		    string str = "CallMethodProper error on ";
		    string ownerName = Fsm.OwnerName;
		    string str2 = " -> ";
		    Exception ex2 = ex;
		    Debug.LogError(str + ownerName + str2 + ((ex2 != null) ? ex2.ToString() : null));
		}
	    }
	    if (storeResult.Type != VariableType.Unknown)
	    {
		storeResult.SetValue(value);
	    }
	}

	private bool DoCache()
	{
	    cachedBehaviour = component;
	    cachedType = component.GetType();
	    cachedMethodInfo = cachedType.GetMethod(methodName.Value);
	    if (cachedMethodInfo == null)
	    {
		errorString = errorString + "Method Name is invalid: " + methodName.Value + "\n";
		Finish();
		return false;
	    }
	    cachedParameterInfo = cachedMethodInfo.GetParameters();
	    return true;
	}

    }

}

然后就到了设置动画播放Set Slug Anim的时候了,这里我们通过有无护符乌恩之形(equippedCharm_28)来判断播放哪种回血动画:

然后就到了开始回血的Focus Start状态了:设置HeroControllerState.freezeCharge状态为true,HeroControllerState.focusing为true,并开始播放音效了,玩家失去控制并停止动画控制的方法,播放新的动画等待FocusStartTimer结束后进入下一个状态:

到了设置回血速度Set Focus Speed的状态:通过判断是否装备快速聚集(equippedCharm_7)护符 来设置不同的回血速度:

到了设置深度回血速度Deep Focus Speed的状态同样也是根据护符来判断的:

进入判断是否是Slug?状态:我们还没做到这个部分所以就不管了,让它进slug speed状态(不过肯定不会进的我们不会触发SLUG事件)

然后进入Focus状态:设置子对象Line Anim的MeshRenderere为true,然后播放里面的动画,并设置Dust L和Dust R的emission为true,执行函数StartMPDrain(TimePerMPDrain),

自定义脚本SetParticleEmissionRate.cs:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("Particle System")]
    [Tooltip("Set particle emission on or off on an object with a particle emitter")]
    public class SetParticleEmissionRate : FsmStateAction
    {
	[RequiredField]
	[Tooltip("The particle emitting GameObject")]
	public FsmOwnerDefault gameObject;

	public FsmFloat emissionRate;
	public bool everyFrame;
	private ParticleSystem emitter;

	public override void Reset()
	{
	    gameObject = null;
	    emissionRate = null;
	    everyFrame = false;
	}

	public override void OnEnter()
	{
	    if(gameObject != null)
	    {
		GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
		if (ownerDefaultTarget)
		{
		    emitter = ownerDefaultTarget.GetComponent<ParticleSystem>();
		}
		DoSetEmitRate();
		if (!everyFrame)
		{
		    Finish();
		}
	    }
	}

	public override void OnUpdate()
	{
	    DoSetEmitRate();
	}

	private void DoSetEmitRate()
	{
	    if (emitter)
	    {
		emitter.emissionRate = emissionRate.Value;
	    }
	}
    }

}

如果Focus完成后,我们的HeroController.cs就会向小骑士的另一个playmaker叫proxyFSM发送事件:HeroCtrl-FocusCompleted,然后proxyFSM就会向自身所有的playmaker状态机(包括我们上面创建的spell control)发送FOCUS COMPLETED事件,然后就可以进入Spore Cloud状态了:

cs 复制代码
if (drainMP)
	{
            drainMP_timer += Time.deltaTime;
            while (drainMP_timer >= drainMP_time) //drainMP_time在无护符下等于0.027
            {
                MP_drained += 1f;
                drainMP_timer -= drainMP_time;
                TakeMp(1);

                if(MP_drained == focusMP_amount)
		{
                    MP_drained -= drainMP_time;
                    proxyFSM.SendEvent("HeroCtrl-FocusCompleted");

		}
	    }
	}

我们来到proxyFSM创建新的事件:

Sproe状态中判断冷却时间以及是否有两个护符,如果有equippedCharm_10英勇者勋章就执行DUNG事件进入Dung Cloud状态:

进入Dung Cloud状态:其实我还没做,所以就不用介绍了,进入FINISHED进入Set HP Amount状态

状态Set HP Amount:通过护符判断该回多少滴血:

来到focusHealth状态:首先执行**SpriteFlash.cs脚本中的flashFocusHeal(这个后面会讲,要留意)方法,**播放一次性的AudioSource,执行HeroController.cs中的StopMPDrain()方法停止MP流失,小骑士播放动画Focus Get,执行HeroController.cs中的AddHealth()方法增加一滴血,然后记录满血的值maxHealth,等0.2s进入下一个状态。

这里有几个自定义的行为脚本我先贴出来吧:

cs 复制代码
using System;
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory(ActionCategory.Audio)]
    [Tooltip("Instantiate an Audio Player object and play a oneshot sound via its Audio Source.")]
    public class AudioPlayerOneShotSingle : FsmStateAction
    {
	[RequiredField]
	[Tooltip("The object to spawn. Select Audio Player prefab.")]
	public FsmGameObject audioPlayer;

	[RequiredField]
	[Tooltip("Object to use as the spawn point of Audio Player")]
	public FsmGameObject spawnPoint;

	[ObjectType(typeof(AudioClip))]
	public FsmObject audioClip;

	public FsmFloat pitchMin;
	public FsmFloat pitchMax;
	public FsmFloat volume = 1f;
	public FsmFloat delay;
	public FsmGameObject storePlayer;

	private AudioSource audio;
	private float timer;

	public override void Reset()
	{
	    spawnPoint = null;
	    pitchMin = 1f;
	    pitchMax = 1f;
	    volume = 1f;
	}

	public override void OnEnter()
	{
	    timer = 0f;
	    if(delay.Value == 0f)
	    {
		DoPlayRandomClip();
		Finish();
	    }
	}
	
	public override void OnUpdate()
	{
	    if (delay.Value > 0f)
	    {
		timer += Time.deltaTime;
		return;
	    }
	    DoPlayRandomClip();
	    Finish();
	}

	private void DoPlayRandomClip()
	{
	    if(!audioPlayer.IsNone && !spawnPoint.IsNone && spawnPoint.Value != null)
	    {

		GameObject value = audioPlayer.Value;
		Vector3 position = spawnPoint.Value.transform.position;
		Vector3 up = Vector3.up;
		if (audioPlayer.Value != null)
		{
		    GameObject gameObject = GameObject.Instantiate(audioPlayer.Value, position, Quaternion.Euler(up));
		    audio = gameObject.GetComponent<AudioSource>();
		    storePlayer.Value = gameObject;
		    AudioClip audioClip = this.audioClip.Value as AudioClip;
		    float pitch = UnityEngine.Random.Range(pitchMin.Value, pitchMax.Value);
		    audio.pitch = pitch;
		    audio.volume = volume.Value;
		    if (audioClip != null)
		    {
			audio.PlayOneShot(audioClip);
			return;
		    }
		}
		else
		{
		    Debug.LogError("AudioPlayer object not set!");
		}
	    }
	}
    }

}

判断是否满血Full HP?的状态:如果MP小于回血所需要的MP就执行CANCEL事件回到Focus状态,FINISHED事件进入Set Full状态:

Set Full状态:

Focus Get Finish状态:完成回血后的状态,淡出音量,HeroControllerState.focusing等于false,Lines Anim播放Focus Effect End动画,小骑士自身播放Focus Get Once动画,停止Dust L,Dust R的粒子系统的Emission为0,执行HeroController的StopMPDrain,设置指为0.02,等0.3秒后进入下一个状态:

这里有一个自定义行为脚本FadeAudio.cs:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory(ActionCategory.Audio)]
    [Tooltip("Sets the Volume of the Audio Clip played by the AudioSource component on a Game Object.")]
    public class FadeAudio : ComponentAction<AudioSource>
    {
	[RequiredField]
	[CheckForComponent(typeof(AudioSource))]
	public FsmOwnerDefault gameObject;

	public FsmFloat startVolume;
	public FsmFloat endVolume;
	public FsmFloat time;

	private float timeElapsed;
	private float timePercentage;
	private bool fadingDown;

	public override void Reset()
	{
	    gameObject = null;
	    startVolume = 1f;
	    endVolume = 0f;
	    time = 1f;
	}

	public override void OnEnter()
	{
	    if(startVolume.Value > endVolume.Value)
	    {
		fadingDown = true;
	    }
	    else
	    {
		fadingDown = false;
	    }
	    GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
	    if (UpdateCache(ownerDefaultTarget))
	    {
		audio.volume = startVolume.Value;
	    }
	}

	public override void OnUpdate()
	{
	    DoSetAudioVolume();
	}

	public override void OnExit()
	{
	    GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
	    if (UpdateCache(ownerDefaultTarget))
	    {
		audio.volume = endVolume.Value;
	    }
	}

	private void DoSetAudioVolume()
	{
	    GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
	    if (UpdateCache(ownerDefaultTarget))
	    {
		timeElapsed += Time.deltaTime;
		timePercentage = timeElapsed / time.Value * 100f;
		float num = (endVolume.Value - startVolume.Value) * (timePercentage / 100f);
		audio.volume = audio.volume + num;
		if (fadingDown && audio.volume <= endVolume.Value)
		{
		    audio.volume = endVolume.Value;
		    Finish();
		}
		else if (!fadingDown && audio.volume >= endVolume.Value)
		{
		    audio.volume = endVolume.Value;
		    Finish();
		}
		timeElapsed = 0f;
	    }
	}


    }

}

到了Regain Control重新获得控制的状态:HeroControllerState里面的状态freezeCharge设置为false停止播放Audio,将Lines Anim的meshrenderer设置为false,执行HeroController的RegainControl()和StartAnimationControl()两个方法

检测是否要回到不活跃状态的Back in?:如果是就回,不是的话进入Can Focus状态:

Grace Check:宽限期检测,如果是第一次则进入First Grace Check状态,如果不是就进入Focus Cancel状态:

First Grace Check状态:执行SetMPCharge()函数直接设置成初始MP值即可:

至此我们完成了完整的法术系统的回血机制,这些大多都是通过playmakerFSM状态机来实现的。

二、制作法术系统的法球机制

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

首先先把图片和动画导进来吧:

OK接下来我们就来制作法球了,继续到Spell Control的playmakerFSM中,当BUTTON DOWN选择BUTTON UP事件后,先判断Can Cast?:

选择法术类型Spell Chocie:

由于我只做了法球,所以法球和地震的状态我们就直接CANCEL到Has Fireball?状态即可:

直接进入Cast状态的Quick Cast,我们还是一样先判断能否Cast:

同样也是选择:

有火球法术吗?Has Fireball?:获取playerdata的fireballLevel。

是否是在墙边:如果是的话就执行FlipSprite()翻转人物:

准备执行fireball阶段:小骑士播放动画Fireball Antic,执行函数RelinquishControl()和StopAnimationControl(),AffectedByGravity()设置为false,小骑士速度设置为0,监听Cast的输入,我们可以在InputManager中设置好。

检测火球等级的Level Check:

火球为1级的Fireball 1:

播放动画Fireball1 Cast,执行函数TakeMp()消耗MP,生成一个预制体,这个是法球生成位置的预制体,等会再说,发送事件FINISHED到 Fireball Recoil

发送火球的后坐力Fireball Recoil状态:

Spell End状态:播放结束后执行函数RegainControl()和StartAnimationControl()回到inactive状态

2.制作法术系统的法球机制

接下来就是要制作预制体Fireball Top

这个particles我找不到素材,你们就自由发挥吧:

这个Fireball Blast的playmakerFSM内容如下:就是等动画播放完成后回收它:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("Object Pool")]
    [Tooltip("Recycles the Owner of the Fsm. Useful for Object Pool spawned Prefabs that need to kill themselves, e.g., a projectile that explodes on impact.")]
    public class RecycleSelf : FsmStateAction
    {
	// TODO:后续有了ObjectPool的时候就替换掉
	public override void OnEnter()
	{
	    if (Owner != null)
		GameObject.Destroy(Owner);
	    Finish();
	}
    }
}

再给Fireball Top一个新的FSM:Fireball Cast

每一个状态如下,我感觉不用我介绍了,就直接贴图自己看看吧:

这里的自定义脚本SpawnObjectFromGlobalPool.cs如下:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory(ActionCategory.GameObject)]
    [Tooltip("Spawns a prefab Game Object from the Global Object Pool on the Game Manager.")]
    public class SpawnObjectFromGlobalPool : FsmStateAction
    {

	[RequiredField]
	[Tooltip("GameObject to create. Usually a Prefab.")]
	public FsmGameObject gameObject;

	[Tooltip("Optional Spawn Point.")]
	public FsmGameObject spawnPoint;

	[Tooltip("Position. If a Spawn Point is defined, this is used as a local offset from the Spawn Point position.")]
	public FsmVector3 position;

	[Tooltip("Rotation. NOTE: Overrides the rotation of the Spawn Point.")]
	public FsmVector3 rotation;

	[UIHint(UIHint.Variable)]
	[Tooltip("Optionally store the created object.")]
	public FsmGameObject storeObject;

	public override void Reset()
	{
	    gameObject = null;
	    spawnPoint = null;
	    position = new FsmVector3
	    {
		UseVariable = true
	    };
	    rotation = new FsmVector3
	    {
		UseVariable = true
	    };
	    storeObject = null;
	}

	public override void OnEnter()
	{
	    if (gameObject.Value != null)
	    {
		Vector3 a = Vector3.zero;
		Vector3 euler = Vector3.up;
		if (spawnPoint.Value != null)
		{
		    a = spawnPoint.Value.transform.position;
		    if (!position.IsNone)
		    {
			a += position.Value;
		    }
		    euler = ((!rotation.IsNone) ? rotation.Value : spawnPoint.Value.transform.eulerAngles);
		}
		else
		{
		    if (!position.IsNone)
		    {
			a = position.Value;
		    }
		    if (!rotation.IsNone)
		    {
			euler = rotation.Value;
		    }
		}
		if (gameObject != null)
		{
		    //TODO:以后创造完对象池后记得替换掉
		    GameObject value = GameObject.Instantiate(gameObject.Value, a, Quaternion.Euler(euler));
		    storeObject.Value = value;
		}
	    }
	    Finish();
	}


    }

}

自定义行为的脚本如下:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory(ActionCategory.GameObject)]
    [Tooltip("Spawns a random amount of chosen GameObject from global pool and fires them off in random directions.")]
    public class FlingObjectsFromGlobalPool : RigidBody2dActionBase
    {
	[RequiredField]
	[Tooltip("GameObject to spawn.")]
	public FsmGameObject gameObject;
	[Tooltip("GameObject to spawn at (optional).")]
	public FsmGameObject spawnPoint;
	[Tooltip("Position. If a Spawn Point is defined, this is used as a local offset from the Spawn Point position.")]
	public FsmVector3 position;
	[Tooltip("Minimum amount of objects to be spawned.")]
	public FsmInt spawnMin;
	[Tooltip("Maximum amount of objects to be spawned.")]
	public FsmInt spawnMax;
	[Tooltip("Minimum speed objects are fired at.")]
	public FsmFloat speedMin;
	[Tooltip("Maximum speed objects are fired at.")]
	public FsmFloat speedMax;
	[Tooltip("Minimum angle objects are fired at.")]
	public FsmFloat angleMin;
	[Tooltip("Maximum angle objects are fired at.")]
	public FsmFloat angleMax;
	[Tooltip("Randomises spawn points of objects within this range. Leave as 0 and all objects will spawn at same point.")]
	public FsmFloat originVariationX;
	public FsmFloat originVariationY;
	[Tooltip("Optional: Name of FSM on object you want to send an event to after spawn")]
	public FsmString FSM;
	[Tooltip("Optional: Event you want to send to object after spawn")]
	public FsmString FSMEvent;
	private float vectorX;
	private float vectorY;
	private bool originAdjusted;

	public override void Reset()
	{
	    gameObject = null;
	    spawnPoint = null;
	    position = new FsmVector3
	    {
		UseVariable = true
	    };
	    spawnMin = null;
	    spawnMax = null;
	    speedMin = null;
	    speedMax = null;
	    angleMin = null;
	    angleMax = null;
	    originVariationX = null;
	    originVariationY = null;
	    FSM = new FsmString
	    {
		UseVariable = true
	    };
	    FSMEvent = new FsmString
	    {
		UseVariable = true
	    };
	}

	public override void OnEnter()
	{
	    if (gameObject.Value != null)
	    {
		Vector3 a = Vector3.zero;
		Vector3 zero = Vector3.zero;
		if (spawnPoint.Value != null)
		{
		    a = spawnPoint.Value.transform.position;
		    if (!position.IsNone)
		    {
			a += position.Value;
		    }
		}
		else if (!position.IsNone)
		{
		    a = position.Value;
		}
		int num = Random.Range(spawnMin.Value, spawnMax.Value + 1);
		for (int i = 1; i <= num; i++)
		{
		    //TODO:以后创造完对象池后记得替换掉
		    GameObject gameObject = GameObject.Instantiate(this.gameObject.Value, a, Quaternion.Euler(zero));
		    float x = gameObject.transform.position.x;
		    float y = gameObject.transform.position.y;
		    float z = gameObject.transform.position.z;
		    if (originVariationX != null)
		    {
			x = gameObject.transform.position.x + Random.Range(-originVariationX.Value, originVariationX.Value);
			originAdjusted = true;
		    }
		    if (originVariationY != null)
		    {
			y = gameObject.transform.position.y + Random.Range(-originVariationY.Value, originVariationY.Value);
			originAdjusted = true;
		    }
		    if (originAdjusted)
		    {
			gameObject.transform.position = new Vector3(x, y, z);
		    }
		    base.CacheRigidBody2d(gameObject);
		    float num2 = Random.Range(speedMin.Value, speedMax.Value);
		    float num3 = Random.Range(angleMin.Value, angleMax.Value);
		    vectorX = num2 * Mathf.Cos(num3 * 0.017453292f);
		    vectorY = num2 * Mathf.Sin(num3 * 0.017453292f);
		    Vector2 velocity;
		    velocity.x = vectorX;
		    velocity.y = vectorY;
		    rb2d.velocity = velocity;
		    if (!FSM.IsNone)
		    {
			FSMUtility.LocateFSM(gameObject, FSM.Value).SendEvent(FSMEvent.Value);
		    }
		}
	    }
	    Finish();
	}


    }

}
cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory(ActionCategory.GameObject)]
    [Tooltip("Fling")]
    public class FlingObject : RigidBody2dActionBase
    {
	[RequiredField]
	public FsmOwnerDefault flungObject;
	public FsmFloat speedMin;
	public FsmFloat speedMax;
	public FsmFloat AngleMin;
	public FsmFloat AngleMax;
	private float vectorX;
	private float vectorY;
	private bool originAdjusted;

	public override void Reset()
	{
	    flungObject = null;
	    speedMin = null;
	    speedMax = null;
	    AngleMin = null;
	    AngleMax = null;
	}
	public override void OnEnter()
	{
	    if(flungObject != null)
	    {
		GameObject owenrDefaultTarget = Fsm.GetOwnerDefaultTarget(flungObject);
		if(owenrDefaultTarget != null)
		{
		    float num = Random.Range(speedMin.Value, speedMax.Value);
		    float num2 = Random.Range(AngleMin.Value, AngleMax.Value);
		    vectorX = num * Mathf.Cos(num2 * 0.017453292f);
		    vectorY = num * Mathf.Sin(num2 * 0.017453292f);
		    Vector2 velocity;
		    velocity.x = vectorX;
		    velocity.y = vectorY;
		    CacheRigidBody2d(owenrDefaultTarget);
		    rb2d.velocity = velocity;
		}
	    }
	    Finish();
	}


    }

}
cs 复制代码
using System;
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory(ActionCategory.Physics2D)]
    [Tooltip("Sets the Angular Velocity of a Game Object. NOTE: Game object must have a rigidbody 2D.")]
    public class SetAngularVelocity2d : RigidBody2dActionBase
    {
	[RequiredField]
	[CheckForComponent(typeof(Rigidbody2D))]
	public FsmOwnerDefault gameObject;

	public FsmFloat angularVelocity;

	public override void Reset()
	{
	    angularVelocity = null;
	    everyFrame = false;
	}

	public bool everyFrame;
	public override void OnEnter()
	{
	    CacheRigidBody2d(Fsm.GetOwnerDefaultTarget(gameObject));
	    DoSetVelocity();
	    if (!everyFrame)
	    {
		Finish();
	    }
	}

	public override void Awake()
	{
	    Fsm.HandleFixedUpdate = true;
	}

	public override void OnPreprocess()
	{
	    Fsm.HandleFixedUpdate = true;
	}

	public override void OnFixedUpdate()
	{
	    DoSetVelocity();
	}

	private void DoSetVelocity()
	{
	    if (rb2d == null)
	    {
		return;
	    }
	    if (!angularVelocity.IsNone)
	    {
		rb2d.angularVelocity = angularVelocity.Value;
	    }
	}
    }

}

这个Fireball就是生成的有实际伤害和碰撞体积的火球,我们一会来创建

这个Spell Fluke就是装备吸虫之巢护符后释放法术放出来的虫子,我们先给它一个空的游戏对象日后再来做。

最后我们来创建火球预制体:Fireball它有Rigibody2d,collider2d,tk2dsprite和animation,以及两个playmakerFSM。

它拥有一个检测地形的子对象:

Dribble R和Dribble L都是粒子系统,我也找不到素材所以你们自由发挥吧:

particle system同理也自由发挥吧:

先从我们熟悉的playmakerFSMdamages_enemy开始,这一个我们在制作玩家攻击系统就介绍过了,其实都差不多只需要改改伤害和后坐力倍数即可:

剩下两个状态的行为就是把collider改成parent和gparent即可:

然后创建一个新的playmakerFSM叫Fireball Control:

第一个状态是设置造成的伤害:

Pause状态:开启meshrenderer和设置collider激活状态

自定义行为脚本如下:

cs 复制代码
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory(ActionCategory.Physics2D)]
    [Tooltip("Set BoxCollider2D to active or inactive. Can only be one collider on object.")]
    public class SetCollider : FsmStateAction
    {
	[RequiredField]
	[Tooltip("The particle emitting GameObject")]
	public FsmOwnerDefault gameObject;

	public FsmBool active;

	public override void Reset()
	{
	    gameObject = null;
	    active = false;
	}
	public override void OnEnter()
	{
	    if (gameObject != null)
	    {
		BoxCollider2D component = Fsm.GetOwnerDefaultTarget(gameObject).GetComponent<BoxCollider2D>();
		if (component != null)
		{
		    component.enabled = active.Value;
		}
	    }
	    Finish();
	}
    }

}

初始化判断左和右:

碰撞检测Idle状态:

自定义行为脚本:

cs 复制代码
using System;
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory(ActionCategory.Physics2D)]
    [Tooltip("Detect 2D collisions between the Owner of this FSM and other Game Objects that have RigidBody2D components.\nNOTE: The system events, COLLISION ENTER 2D, COLLISION STAY 2D, and COLLISION EXIT 2D are sent automatically on collisions with any object. Use this action to filter collisions by Tag.")]
    public class Collision2dEventLayer : FsmStateAction
    {
	[Tooltip("The type of collision to detect.")]
	public PlayMakerUnity2d.Collision2DType collision;
	[UIHint(UIHint.Tag)]
	[Tooltip("Filter by Tag.")]
	public FsmString collideTag;
	[UIHint(UIHint.Layer)]
	[Tooltip("Filter by Layer.")]
	public FsmInt 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;
	[UIHint(UIHint.Variable)]
	[Tooltip("Store the force of the collision. NOTE: Use Get Collision Info to get more info about the collision.")]
	public FsmFloat storeForce;

	private PlayMakerUnity2DProxy _proxy;

	public override void Reset()
	{
	    collision = PlayMakerUnity2d.Collision2DType.OnCollisionEnter2D;
	    collideTag = new FsmString
	    {
		UseVariable = true
	    };
	    sendEvent = null;
	    storeCollider = null;
	    storeForce = null;
	}

	public override void OnEnter()
	{
	    _proxy = Owner.GetComponent<PlayMakerUnity2DProxy>();
	    if(_proxy == null)
	    {
		_proxy = Owner.AddComponent<PlayMakerUnity2DProxy>();
	    }
	    switch (collision)
	    {
		case PlayMakerUnity2d.Collision2DType.OnCollisionEnter2D:
		    _proxy.AddOnCollisionEnter2dDelegate(new PlayMakerUnity2DProxy.OnCollisionEnter2dDelegate(DoCollisionEnter2D));
		    break;
		case PlayMakerUnity2d.Collision2DType.OnCollisionStay2D:
		    _proxy.AddOnCollisionStay2dDelegate(new PlayMakerUnity2DProxy.OnCollisionStay2dDelegate(DoCollisionStay2D));
		    break;
		case PlayMakerUnity2d.Collision2DType.OnCollisionExit2D:
		    _proxy.AddOnCollisionExit2dDelegate(new PlayMakerUnity2DProxy.OnCollisionExit2dDelegate(DoCollisionExit2D));
		    break;
		default:
		    break;
	    }
	}

	
	public override void OnExit()
	{
	    if(_proxy == null)
	    {
		return;
	    }
	    switch (collision)
	    {
		case PlayMakerUnity2d.Collision2DType.OnCollisionEnter2D:
		    _proxy.RemoveOnCollisionEnter2dDelegate(new PlayMakerUnity2DProxy.OnCollisionEnter2dDelegate(DoCollisionEnter2D));
		    break;
		case PlayMakerUnity2d.Collision2DType.OnCollisionStay2D:
		    _proxy.RemoveOnCollisionStay2dDelegate(new PlayMakerUnity2DProxy.OnCollisionStay2dDelegate(DoCollisionStay2D));
		    break;
		case PlayMakerUnity2d.Collision2DType.OnCollisionExit2D:
		    _proxy.RemoveOnCollisionExit2dDelegate(new PlayMakerUnity2DProxy.OnCollisionExit2dDelegate(DoCollisionExit2D));
		    break;
		default:
		    break;
	    }
	}

	public new void DoCollisionEnter2D(Collision2D collisionInfo)
	{
	    if (collision == PlayMakerUnity2d.Collision2DType.OnCollisionEnter2D && (collisionInfo.collider.gameObject.tag == collideTag.Value || collideTag.IsNone || string.IsNullOrEmpty(collideTag.Value)) && (collisionInfo.gameObject.layer == collideLayer.Value || collideLayer.IsNone))
	    {
		StoreCollisionInfo(collisionInfo);
		Fsm.Event(sendEvent);
	    }
	}

	public new void DoCollisionStay2D(Collision2D collisionInfo)
	{
	    if (collision == PlayMakerUnity2d.Collision2DType.OnCollisionStay2D && (collisionInfo.collider.gameObject.tag == collideTag.Value || collideTag.IsNone || string.IsNullOrEmpty(collideTag.Value)) && (collisionInfo.gameObject.layer == collideLayer.Value || collideLayer.IsNone))
	    {
		StoreCollisionInfo(collisionInfo);
		Fsm.Event(sendEvent);
	    }
	}

	public new void DoCollisionExit2D(Collision2D collisionInfo)
	{
	    if (collision == PlayMakerUnity2d.Collision2DType.OnCollisionExit2D && (collisionInfo.collider.gameObject.tag == collideTag.Value || collideTag.IsNone || string.IsNullOrEmpty(collideTag.Value)) && (collisionInfo.gameObject.layer == collideLayer.Value || collideLayer.IsNone))
	    {
		StoreCollisionInfo(collisionInfo);
		Fsm.Event(sendEvent);
	    }
	}

	private void StoreCollisionInfo(Collision2D collisionInfo)
	{
	    storeCollider.Value = collisionInfo.gameObject;
	    storeForce.Value = collisionInfo.relativeVelocity.magnitude;
	}

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

    }

}

完成后效果开始淡去:

如果是碰到墙壁后的效果:

最后我们来讲讲spriteFlash,首先添加好方法flashFocusHeal():

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    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>();
	    prevColor = rend.material.color;
	}
	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);
		block.SetColor("_FlashColor", prevColor);
		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 flashFocusHeal()
    {
	Start();
	flashColour = new Color(1f, 1f, 1f);
	amount = 0.85f;
	timeUp = 0.1f;
	stayTime = 0.01f;
	timeDown = 0.35f;
	block.Clear();
	block.SetColor("_FlashColor", flashColour);
	flashingState = 1;
	flashTimer = 0f;
	repeatFlash = false;
	//SendToChildren(new Action(flashFocusHeal));
    }

    private void flashFocusGet()
    {
	Start();
	flashColour = new Color(1f, 1f, 1f);
	amount = 0.5f;
	timeUp = 0.1f;
	stayTime = 0.01f;
	timeDown = 0.35f;
	block.Clear();
	block.SetColor("_FlashColor", flashColour);
	flashingState = 1;
	flashTimer = 0f;
	repeatFlash = false;
	SendToChildren(new Action(flashFocusGet));
    }

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

然后我们要新建shader目的是由发光效果和颜色变化的效果:

cs 复制代码
Shader "Sprites/Sprites_Default-ColorFlash"
{
    Properties
    {
        _MainTex ("Sprite Texture", 2D) = "white" {}
        _FlashColor("Flash Color",Color) = (1,1,1,1)
        _FlashAmount("Flash Amount",Range(0.0,1.0)) = 0.0
        _Color("Color", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap("Pixel snap",Float) = 0.0
    }
    SubShader
    {
        Tags { "CanUseSpriteAtlas" = "true" "IGNOREPROJECTOR" = "true" "PreviewType" = "Plane" "QUEUE" = "Transparent" "RenderType" = "Transparent"}

        Cull Off
        Lighting Off
        ZWrite Off
        Fog{ Mode Off }
        Blend SrcAlpha OneMinusSrcAlpha

        CGPROGRAM
         #pragma surface surf Lambert alpha vertex:vert
         #pragma multi_compile DUMMY PIXELSNAP_ON

         sampler2D _MainTex;
         fixed4 _Color;
         fixed4 _FlashColor;
         float _FlashAmount,_SelfIllum;

         struct Input
         {
             float2 uv_MainTex;
             fixed4 color;
         };

         void vert(inout appdata_full v, out Input o)
         {
             #if defined(PIXELSNAP_ON) && !defined(SHADER_API_FLASH)
             v.vertex = UnityPixelSnap(v.vertex);
             #endif
             v.normal = float3(0,0,-1);

             UNITY_INITIALIZE_OUTPUT(Input, o);
             o.color = _FlashColor;
         }

         void surf(Input IN, inout SurfaceOutput o)
         {
             fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color;
             o.Albedo = lerp(c.rgb,float3(1.0,1.0,1.0),_FlashAmount);
             o.Emission = lerp(c.rgb,float3(1.0,1.0,1.0),_FlashAmount) * _SelfIllum;
             o.Alpha = c.a;
         }
         ENDCG
    }
    Fallback "Transparent/VertexLit"
}
cs 复制代码
Shader "Sprites/Sprites_Cherry-Default"
{
    Properties
    {
        _MainTex("Sprite Texture", 2D) = "white" {}
        _FlashColor("Flash Color",Color) = (1,1,1,1)
        _FlashAmount("Flash Amount",Range(0.0,1.0)) = 0.0
        _Color("Color", Color) = (1,1,1,1)
        [MaterialToggle] PixelSnap("Pixel snap",Float) = 0.0
    }
    SubShader
    {
            Tags { "CanUseSpriteAtlas" = "true" "IGNOREPROJECTOR" = "true" "PreviewType" = "Plane" "QUEUE" = "Transparent" "RenderType" = "Transparent"}

            Cull Off
            Lighting Off
            ZWrite Off
            Fog{ Mode Off }
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
             #pragma surface surf Lambert alpha vertex:vert
             #pragma multi_compile DUMMY PIXELSNAP_ON

             sampler2D _MainTex;
             fixed4 _Color;
             float _FlashAmount,_SelfIllum;

             struct Input
             {
                 float2 uv_MainTex;
                 fixed4 color;
             };

             void vert(inout appdata_full v, out Input o)
             {
                 #if defined(PIXELSNAP_ON) && !defined(SHADER_API_FLASH)
                 v.vertex = UnityPixelSnap(v.vertex);
                 #endif
                 v.normal = float3(0,0,-1);

                 UNITY_INITIALIZE_OUTPUT(Input, o);
                 o.color = _Color;
             }

             void surf(Input IN, inout SurfaceOutput o)
             {
                 fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color;
                 o.Albedo = lerp(c.rgb,float3(1.0,1.0,1.0),_FlashAmount);
                 o.Emission = lerp(c.rgb,float3(1.0,1.0,1.0),_FlashAmount) * _SelfIllum;
                 o.Alpha = c.a;
             }
             ENDCG
    }
Fallback "Transparent/VertexLit"
}

然后更改小骑士和敌人的Material 的shader为Sprites_Default-ColorFlash

他们就拥有了闪光的效果了

还有就是上一期我忘记给敌人的生命系统添加受击后的无敌时间了,这里我们加上去:

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 Recoil recoil;
    private tk2dSpriteAnimator animator;
    private tk2dSprite sprite;
    private DamageHero damageHero;

    [Header("Asset")]
    [SerializeField] private AudioSource audioPlayerPrefab; //声音播放器预制体

    [Header("Body")]
    [SerializeField] public int hp; //血量
    [SerializeField] public int enemyType; //敌人类型
    [SerializeField] private Vector3 effectOrigin; //生效偏移量

    public bool isDead;

    private int directionOfLastAttack; //最后一次受到攻击的方向
    private float evasionByHitRemaining; //被攻击后的剩余无敌时间
    private const string CheckPersistenceKey = "CheckPersistence";

    public delegate void DeathEvent();
    public event DeathEvent OnDeath;

    protected void Awake()
    {
	boxCollider = GetComponent<BoxCollider2D>();
	hitEffectReceiver = GetComponent<IHitEffectReciever>();
	recoil = GetComponent<Recoil>();
	animator = GetComponent<tk2dSpriteAnimator>();
	sprite = GetComponent<tk2dSprite>();
	damageHero = GetComponent<DamageHero>();
    }

    protected void OnEnable()
    {
	StartCoroutine(CheckPersistenceKey);
    }

    protected void Start()
    {
	evasionByHitRemaining = -1f;
    }

    protected void Update()
    {
	evasionByHitRemaining -= Time.deltaTime;
    }

    public void Hit(HitInstance hitInstance)
    {
	if (isDead)
	{
	    return;
	}
	if(evasionByHitRemaining > 0f) 
	{ 
	    return;
	}
	if(hitInstance.DamageDealt < 0f)
	{
	    return;
	}
	FSMUtility.SendEventToGameObject(hitInstance.Source, "DEALT DAMAGE", false);
	int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	if (IsBlockingByDirection(cardinalDirection, hitInstance.AttackType))
	{
	    Invincible(hitInstance);
	    return;
	}
	TakeDamage(hitInstance);
    }

    public void Invincible(HitInstance hitInstance)
    {
	int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	directionOfLastAttack = cardinalDirection;
	FSMUtility.SendEventToGameObject(gameObject, "BLOCKED HIT", false);
	FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
	if (!(GetComponent<DontClinkGates>() != null))
	{
	    FSMUtility.SendEventToGameObject(gameObject, "HIT", false);

	    if(hitInstance.AttackType == AttackTypes.Nail)
	    {
		if(cardinalDirection == 0)
		{
		    HeroController.instance.RecoilLeft();
		}
		else if(cardinalDirection == 2)
		{
		    HeroController.instance.RecoilRight();
		}
	    }

	    Vector2 v;
	    Vector3 eulerAngles;
	    if (boxCollider != null)
	    {
		switch (cardinalDirection)
		{
		    case 0:
			v = new Vector2(transform.GetPositionX() + boxCollider.offset.x - boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
			eulerAngles = new Vector3(0f, 0f, 0f);
			break;
		    case 1:
			v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Max(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y - boxCollider.size.y * 0.5f));
			eulerAngles = new Vector3(0f, 0f, 90f);
			break;
		    case 2:
			v = new Vector2(transform.GetPositionX() + boxCollider.offset.x + boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
			eulerAngles = new Vector3(0f, 0f, 180f);
			break;
		    case 3:
			v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Min(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y + boxCollider.size.y * 0.5f));
			eulerAngles = new Vector3(0f, 0f, 270f);
			break;
		    default:
			break;
		}
	    }
	    else
	    {
		v = transform.position;
		eulerAngles = new Vector3(0f, 0f, 0f);
	    }
	}
	evasionByHitRemaining = 0.15f;
    }

    public void TakeDamage(HitInstance hitInstance)
    {
	Debug.LogFormat("Enemy Take Damage");
	int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	directionOfLastAttack = cardinalDirection;
	FSMUtility.SendEventToGameObject(gameObject, "HIT", false);
	FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
	FSMUtility.SendEventToGameObject(gameObject, "TOOK DAMAGE", false);
	if(recoil != null)
	{
	    recoil.RecoilByDirection(cardinalDirection,hitInstance.MagnitudeMultiplier);
	}
	switch (hitInstance.AttackType)
	{
	    case AttackTypes.Nail:
		if(hitInstance.AttackType == AttackTypes.Nail && enemyType !=3 && enemyType != 6)
		{
		    HeroController.instance.SoulGain();
		}
		Vector3 position = (hitInstance.Source.transform.position + transform.position) * 0.5f + effectOrigin;
		break;
	    case AttackTypes.Generic:
		break;
	    case AttackTypes.Spell:
		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)
	{
	    NonFatalHit(hitInstance.IgnoreInvulnerable);
	}
	else
	{
	    Die(new float?(hitInstance.GetActualDirection(transform)), hitInstance.AttackType, hitInstance.IgnoreInvulnerable);
	}
    }

    private void NonFatalHit(bool ignoreEvasion)
    {
	if (!ignoreEvasion)
	{
	    evasionByHitRemaining = 0.2f;
	}
    }

    public void Die(float? v, AttackTypes attackType, bool ignoreInvulnerable)
    {
	if (isDead)
	{
	    return;
	}
	if (sprite)
	{
	    sprite.color = Color.white;
	}
	FSMUtility.SendEventToGameObject(gameObject, "ZERO HP", false);
	isDead = true;
	if(damageHero != null)
	{
	    damageHero.damageDealt = 0;
	}
	SendDeathEvent();
	Destroy(gameObject); //TODO:
    }

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

    public bool IsBlockingByDirection(int cardinalDirection,AttackTypes attackType)
    {

	switch (cardinalDirection)
	{

	    default:
		return false;
	}

    }

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

}

总结

为了方便测试,我们来给GameManager游戏对象添加一个新的脚本就叫CheatManager.cs方便我们测试用:

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CheatManager : MonoBehaviour
{
    private GameManager gm;
    [SerializeField]public PlayerData playerData;
    private void Awake()
    {
	gm = GameManager.instance;
	playerData = gm.playerData;
    }

    private void Update()
    {
	if (Input.GetKeyDown(KeyCode.P))
	{
	    playerData.hasDash = true;
	    playerData.MPCharge = playerData.maxMP;
	    playerData.fireballLevel = 1;
	}
    }

}

进入游戏,我们按P键获得满蓝状态和拥有火球术的状态:

按下Q键和F键试一下效果:

掉了15滴血,完美

下一期我们来设置一下相机还有布置一下场景吧。

相关推荐
魔士于安2 小时前
Unity太空战舰完整工程,包含战损,实时战损
游戏·unity·游戏引擎·贴图·模型
Nuopiane3 小时前
MyPal3(10)视锥体剔除
unity
爱搞虚幻的阿恺5 小时前
RPG游戏开发【加餐】实现游戏小地图的简单方法
游戏·ue5·游戏引擎·虚幻
海海不瞌睡(捏捏王子)5 小时前
Unity知识点概要
unity·1024程序员节
学不完的5 小时前
Zrlog面试问答及问题解决方案
linux·运维·nginx·unity·游戏引擎
小清兔6 小时前
unity游戏制作中问题汇总(持续更新)
游戏·unity·游戏引擎
WiChP8 小时前
【V0.1B4】从零开始的2D游戏引擎开发之路
前端·javascript·游戏引擎
mxwin20 小时前
Unity Shader SRP深入理解内置渲染管线与 URP/HDRP 的底层架构差异
unity·游戏引擎·单一职责原则
mxwin1 天前
Unity Shader 渲染管线深度解析 — Shader 三阶段
unity·游戏引擎·shader·uv
mxwin1 天前
Unity Shader 数学与几何变换 深入理解渲染管线中的坐标系转换:从模型空间到屏幕空间的完整变换链
unity·游戏引擎·shader