[Unity Demo]从零开始制作空洞骑士Hollow Knight第十一集:制作法术系统的回血机制和火球机制
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作法术系统的回血机制
- 1.制作动画以及使用UNITY编辑器编辑
- 2.使用代码和PlaymakerFSM制作回血机制
- 二、制作法术系统的火球机制
- 1.制作动画以及使用UNITY编辑器编辑
- 2.使用代码和PlaymakerFSM制作火球机制
- 总结
前言
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类型的变量:
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:
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中把刚刚创建的变量添加到方法中:
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;
}
}
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,除了我们要创建几个和法术有关的变量外,还需要获得某个状态的方法和设置某个状态的方法:
[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的方法,增加血量和消耗血量的方法,是否能聚集,是否能释放法术,能否输入和忽略输入,失去控制和重新获得控制:
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中来调用:
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:
public enum AttackTypes
{
Nail,
Generic,
Spell
}
完整的HeroController.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的按键输入:
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);
}
}
}
}
}
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的按键输入:
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;
}
}
}
}
}
}
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:
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:
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状态了:
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进入下一个状态。
这里有几个自定义的行为脚本我先贴出来吧:
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:
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内容如下:就是等动画播放完成后回收它:
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如下:
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();
}
}
}
自定义行为的脚本如下:
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();
}
}
}
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();
}
}
}
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激活状态
自定义行为脚本如下:
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状态:
自定义行为脚本:
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():
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目的是由发光效果和颜色变化的效果:
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"
}
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
、
他们就拥有了闪光的效果了
还有就是上一期我忘记给敌人的生命系统添加受击后的无敌时间了,这里我们加上去:
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方便我们测试用:
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滴血,完美
下一期我们来设置一下相机还有布置一下场景吧。