[Unity Demo]从零开始制作空洞骑士Hollow Knight第七集:制作小骑士完整的冲刺Dash行为
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作一个完整的小骑士冲刺Dash行为
- 1.制作动画以及使用UNITY编辑器编辑
- 2.使用代码实现完整的冲刺行为控制
- 总结
前言
大家又好久不见(虽然就过了半天)这期我决定继续完善小骑士的行为,上一期我们实现了小骑士完整的跳跃降落行为,这一期我们就来做小骑士的冲刺行为,这其中涉及到素材的导入,创建tk2dSprite和tk2dSpriteAnimation,以及代码控制行为等等,难度适中,希望你能够耐心阅读。
一、制作一个完整的小骑士冲刺Dash行为
1.制作动画以及使用UNITY编辑器编辑
我们先把素材导入后,开始回到tk2dspriteEditor中,由于我们第二期就已经制作了小骑士的spritecollection和spriteanimation,所以我们直接把图片拖进去即可。
创建两个动画Dash和Dash To Idle:
2.使用代码实现完整的冲刺行为控制
如果你玩过空洞骑士一段时间,你可能会知道玩家的冲刺时要到苍绿之境中击败大黄蜂才能获得的,所以我们需要一个脚本来记录玩家数据,脚本包含玩家是否拥有某些能力,是否能使用这个能力,就比如这个dash,因此我们要创建PlayerData.cs:
using System;
using System.Collections.Generic;
using System.Reflection;
using GlobalEnums;
using UnityEngine;
[Serializable]
public class PlayerData
{
private static PlayerData _instance;
public static PlayerData instance
{
get
{
if(_instance == null)
{
_instance = new PlayerData();
}
return _instance;
}
set
{
_instance = value;
}
}
public bool hasDash;
public bool canDash;
protected PlayerData()
{
SetupNewPlayerData();
}
public void Reset()
{
SetupNewPlayerData();
}
private void SetupNewPlayerData()
{
hasDash = true; //测试阶段先设置为true方便测试
canDash = true;
}
}
还是先从最简单的音频管理开始吧,只需要创建一个AudioSource并把dash的audioclip拖上去即可
using System.Collections;
using System.Collections.Generic;
using GlobalEnums;
using UnityEngine;
public class HeroAudioController : MonoBehaviour
{
private HeroController heroCtrl;
private void Awake()
{
heroCtrl = GetComponent<HeroController>();
}
[Header("Sound Effects")]
public AudioSource softLanding;
public AudioSource jump;
public AudioSource footStepsRun;
public AudioSource footStepsWalk;
public AudioSource falling;
public AudioSource dash;
private Coroutine fallingCo;
public void PlaySound(HeroSounds soundEffect)
{
if(!heroCtrl.cState.isPaused)
{
switch (soundEffect)
{
case HeroSounds.FOOTSETP_RUN:
if(!footStepsRun.isPlaying && !softLanding.isPlaying)
{
footStepsRun.Play();
return;
}
break;
case HeroSounds.FOOTSTEP_WALK:
if (!footStepsWalk.isPlaying && !softLanding.isPlaying)
{
footStepsWalk.Play();
return;
}
break;
case HeroSounds.SOFT_LANDING:
RandomizePitch(softLanding, 0.9f, 1.1f);
softLanding.Play();
break;
case HeroSounds.JUMP:
RandomizePitch(jump, 0.9f, 1.1f);
jump.Play();
break;
case HeroSounds.DASH:
dash.Play();
break;
case HeroSounds.FALLING:
fallingCo = StartCoroutine(FadeInVolume(falling, 0.7f));
falling.Play();
break;
default:
break;
}
}
}
public void StopSound(HeroSounds soundEffect)
{
if(soundEffect == HeroSounds.FOOTSETP_RUN)
{
footStepsRun.Stop();
return;
}
if (soundEffect == HeroSounds.FOOTSTEP_WALK)
{
footStepsWalk.Stop();
return;
}
switch (soundEffect)
{
case HeroSounds.FALLING:
falling.Stop();
if(fallingCo != null)
{
StopCoroutine(fallingCo);
}
return;
default:
return;
}
}
public void StopAllSounds()
{
softLanding.Stop();
jump.Stop();
falling.Stop();
dash.Stop();
footStepsRun.Stop();
footStepsWalk.Stop();
}
public void PauseAllSounds()
{
softLanding.Pause();
jump.Pause();
falling.Pause();
dash.Pause();
footStepsRun.Pause();
footStepsWalk.Pause();
}
public void UnPauseAllSounds()
{
softLanding.UnPause();
jump.UnPause();
falling.UnPause();
dash.UnPause();
footStepsRun.UnPause();
footStepsWalk.UnPause();
}
/// <summary>
/// 音量淡入线性插值的从0到1
/// </summary>
/// <param name="src"></param>
/// <param name="duration"></param>
/// <returns></returns>
private IEnumerator FadeInVolume(AudioSource src, float duration)
{
float elapsedTime = 0f;
src.volume = 0f;
while (elapsedTime < duration)
{
elapsedTime += Time.deltaTime;
float t = elapsedTime / duration;
src.volume = Mathf.Lerp(0f, 1f, t);
yield return null;
}
}
/// <summary>
/// 随机旋转一个在和之间的pitch的值返回给audiosource
/// </summary>
/// <param name="src"></param>
/// <param name="minPitch"></param>
/// <param name="maxPitch"></param>
private void RandomizePitch(AudioSource src, float minPitch, float maxPitch)
{
float pitch = Random.Range(minPitch, maxPitch);
src.pitch = pitch;
}
/// <summary>
/// 重置audiosource的pitch
/// </summary>
/// <param name="src"></param>
private void ResetPitch(AudioSource src)
{
src.pitch = 1f;
}
}
回到脚本HeroAnimationController.cs中,我们要创建Dash相关动画的代码 管理Dash的动画行为:
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 playRunToIdle;//播放"Run To Idle"动画片段
private bool playDashToIdle; //播放"Dash To Idle"动画片段
private bool changedClipFromLastFrame;
public ActorStates actorStates { get; private set; }
public ActorStates prevActorStates { get; private set; }
private void Awake()
{
heroCtrl = HeroController.instance;
cState = heroCtrl.cState;
animator = GetComponent<tk2dSpriteAnimator>();
}
private void Start()
{
pd = PlayerData.instance;
ResetAll();
actorStates = heroCtrl.hero_state;
if(heroCtrl.hero_state == ActorStates.airborne)
{
animator.PlayFromFrame("Airborne", 7);
return;
}
PlayIdle();
}
private void Update()
{
UpdateAnimation();
if (cState.facingRight)
{
wasFacingRight = true;
return;
}
wasFacingRight = false;
}
private void UpdateAnimation()
{
changedClipFromLastFrame = false;
if (playRunToIdle)
{
Play("Run To Idle");
animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);
playRunToIdle = 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.dashing)
{
}
}
else if (cState.dashing)
{
Play("Dash"); //通过cState.dashing判断是否播放Dash动画片段
}
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);
}
}
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 == "Run 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 FinishedDash()
{
playDashToIdle = true;
}
private void ResetAll()
{
playRunToIdle = false;
playDashToIdle = false;
}
private void ResetPlays()
{
playRunToIdle = false;
playDashToIdle = false;
}
public void UpdateState(ActorStates newState)
{
if(newState != actorStates)
{
if(actorStates == ActorStates.running && newState == ActorStates.idle && !playRunToIdle && !cState.inWalkZone)
{
playRunToIdle = true;
}
prevActorStates = actorStates;
actorStates = newState;
}
}
private bool CanPlayIdle()
{
return !animator.IsPlaying("Land") && !animator.IsPlaying("Run To Idle") && !animator.IsPlaying("Dash To Idle") && !animator.IsPlaying("Backdash Land") && !animator.IsPlaying("Backdash Land 2") && !animator.IsPlaying("LookUpEnd") && !animator.IsPlaying("LookDownEnd") && !animator.IsPlaying("Exit Door To Idle") && !animator.IsPlaying("Wake Up Ground") && !animator.IsPlaying("Hazard Respawn");
}
private bool CanPlayTurn()
{
return !animator.IsPlaying("Wake Up Ground") && !animator.IsPlaying("Hazard Respawn"); ;
}
}
既然涉及到角色新的动作,我们就要新注册一个Dash的Action:
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 jump;
public PlayerAction dash;
public HeroActions()
{
left = CreatePlayerAction("Left");
left.StateThreshold = 0.3f;
right = CreatePlayerAction("Right");
right.StateThreshold = 0.3f;
up = CreatePlayerAction("Up");
up.StateThreshold = 0.3f;
down = CreatePlayerAction("Down");
down.StateThreshold = 0.3f;
moveVector = CreateTwoAxisPlayerAction(left, right, up, down);
moveVector.LowerDeadZone = 0.15f;
moveVector.UpperDeadZone = 0.95f;
jump = CreatePlayerAction("Jump");
dash = CreatePlayerAction("Dash");
}
}
这里我们绑定键盘的D键作为冲刺的设备输入:
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.jump, "X");
AddKeyBinding(inputActions.dash, "D");
}
private static void AddKeyBinding(PlayerAction action, string savedBinding)
{
Mouse mouse = Mouse.None;
Key key;
if (!Enum.TryParse(savedBinding, out key) && !Enum.TryParse(savedBinding, out mouse))
{
return;
}
if (mouse != Mouse.None)
{
action.AddBinding(new MouseBindingSource(mouse));
return;
}
action.AddBinding(new KeyBindingSource(new Key[]
{
key
}));
}
}
还需要扩展FSMUtility脚本的方法:
using System;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;
public static class FSMUtility
{
private static List<List<PlayMakerFSM>> fsmListPool;
private const int FsmListPoolSizeMax = 20;
static FSMUtility()
{
fsmListPool = new List<List<PlayMakerFSM>>();
}
private static List<PlayMakerFSM> ObtainFsmList()
{
if (fsmListPool.Count > 0)
{
List<PlayMakerFSM> result = fsmListPool[fsmListPool.Count - 1];
fsmListPool.RemoveAt(fsmListPool.Count - 1);
return result;
}
return new List<PlayMakerFSM>();
}
private static void ReleaseFsmList(List<PlayMakerFSM> fsmList)
{
fsmList.Clear();
if (fsmListPool.Count < FsmListPoolSizeMax)
{
fsmListPool.Add(fsmList);
}
}
public static PlayMakerFSM GetFSM(GameObject go)
{
return go.GetComponent<PlayMakerFSM>();
}
public static GameObject GetSafe(this FsmOwnerDefault ownerDefault, FsmStateAction stateAction)
{
if (ownerDefault.OwnerOption == OwnerDefaultOption.UseOwner)
{
return stateAction.Owner;
}
return ownerDefault.GameObject.Value;
}
public static void SendEventToGameObject(GameObject go, string eventName, bool isRecursive = false)
{
if (go != null)
{
SendEventToGameObject(go, FsmEvent.FindEvent(eventName), isRecursive);
}
}
public static void SendEventToGameObject(GameObject go, FsmEvent ev, bool isRecursive = false)
{
if (go != null)
{
List<PlayMakerFSM> list = ObtainFsmList();
go.GetComponents<PlayMakerFSM>(list);
for (int i = 0; i < list.Count; i++)
{
list[i].Fsm.Event(ev);
}
ReleaseFsmList(list);
if (isRecursive)
{
Transform transform = go.transform;
for (int j = 0; j < transform.childCount; j++)
{
SendEventToGameObject(transform.GetChild(j).gameObject, ev, isRecursive);
}
}
}
}
}
接下来到HeroController.cs中,我们仍然需要像jump行为一样为它创建steps和QueueSteps, 进入Dash()后执行的物理事件,HeroDash()处理声音逻辑以及生成Effect效果物体吗,CancelDash()取消冲刺,FinishedDash()完成冲刺,CanDash()能否冲刺,以及inputHandler.inputActions.dash.IsPressed作为输入检测是否按下按键。
using System;
using System.Collections;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using GlobalEnums;
using UnityEngine;
public class HeroController : MonoBehaviour
{
public ActorStates hero_state;
public ActorStates prev_hero_state;
public bool acceptingInput = true;
public float move_input;
public float vertical_input;
private Vector2 current_velocity;
public float WALK_SPEED = 3.1f;//走路速度
public float RUN_SPEED = 5f;//跑步速度
public float JUMP_SPEED = 5f;//跳跃的食欲
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 bool airDashed;//是否是在空中冲刺
public PlayMakerFSM dashBurst;
public GameObject dashParticlesPrefab;
public float DASH_SPEED; //冲刺时的速度
public float DASH_TIME; //冲刺时间
public float DASH_COOLDOWN; //冲刺冷却时间
public int DASH_QUEUE_STEPS; //最大冲刺队列的步
private float prevGravityScale;
public bool touchingWall; //是否接触到墙
public bool touchingWallL; //是否接触到的墙左边
public bool touchingWallR; //是否接触到的墙右边
private Rigidbody2D rb2d;
private BoxCollider2D col2d;
private GameManager gm;
public PlayerData playerData;
private InputHandler inputHandler;
public HeroControllerStates cState;
private HeroAnimationController animCtrl;
private HeroAudioController audioCtrl;
private static HeroController _instance;
public static HeroController instance
{
get
{
if (_instance == null)
_instance = FindObjectOfType<HeroController>();
if(_instance && Application.isPlaying)
{
DontDestroyOnLoad(_instance.gameObject);
}
return _instance;
}
}
public HeroController()
{
JUMP_QUEUE_STEPS = 2;
JUMP_RELEASE_QUEUE_STEPS = 2;
}
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>();
}
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);
}
}
void Update()
{
orig_Update();
}
private void orig_Update()
{
current_velocity = rb2d.velocity;
FallCheck();
if(hero_state == ActorStates.running)
{
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.no_input)
{
}
else if(hero_state != ActorStates.no_input)
{
LookForInput();
}
LookForQueueInput();
if (dashCooldownTimer > 0f) //计时器在Update中-= Time.deltaTime
{
dashCooldownTimer -= Time.deltaTime;
}
}
private void FixedUpdate()
{
if (hero_state != ActorStates.no_input && !cState.dashing)
{
Move(move_input);
if(move_input > 0f && !cState.facingRight )
{
FlipSprite();
}
else if(move_input < 0f && cState.facingRight)
{
FlipSprite();
}
}
if (cState.jumping) //如果cState.jumping就Jump
{
Jump();
}
if (cState.dashing)//如果cState.dashing就Dash
{
Dash();
}
//限制速度
if(rb2d.velocity.y < -MAX_FALL_VELOCITY)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, -MAX_FALL_VELOCITY);
}
if (jumpQueuing)
{
jumpQueueSteps++;
}
if (dashQueuing) //跳跃队列开始
{
dashQueueSteps++;
}
if(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);
}
}
/// <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 Dash()
{
AffectedByGravity(false); //不受到重力影响
if(dash_timer > DASH_TIME)
{
FinishedDashing();//大于则结束冲刺
return;
}
float num;
num = DASH_SPEED;
if (cState.facingRight)
{
if (CheckForBump(CollisionSide.right))
{
}
else
{
rb2d.velocity = new Vector2(num, 0f); //为人物的velocity赋值DASH_SPEED
}
}
else if (CheckForBump(CollisionSide.left))
{
}
else
{
rb2d.velocity = new Vector2(-num, 0f);
}
dash_timer += Time.deltaTime;
}
private void HeroDash()
{
if (!cState.onGround)
{
airDashed = true;
}
audioCtrl.StopSound(HeroSounds.FOOTSETP_RUN);
audioCtrl.StopSound(HeroSounds.FOOTSTEP_WALK);
audioCtrl.PlaySound(HeroSounds.DASH);
if (inputHandler.inputActions.right.IsPressed)
{
FaceRight();
}
else if (inputHandler.inputActions.left.IsPressed)
{
FaceLeft();
}
cState.dashing = true;
dashQueueSteps = 0;
HeroActions heroActions = inputHandler.inputActions;
dashBurst.transform.localPosition = new Vector3(4.11f, -0.55f, 0.001f); //生成dashBurst后设置位置和旋转角
dashBurst.transform.localEulerAngles = new Vector3(0f, 0f, 0f);
dashCooldownTimer = DASH_COOLDOWN;
dashBurst.SendEvent("PLAY"); //发送dashBurst的FSM的事件PLAY
dashParticlesPrefab.GetComponent<ParticleSystem>().enableEmission = true;
if (cState.onGround)
{
}
}
/// <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.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;
}
}
/// <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;
}
}
/// <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);
}
}
}
else
{
cState.falling = false;
}
}
/// <summary>
/// 翻转小骑士的localScale.x
/// </summary>
public void FlipSprite()
{
cState.facingRight = !cState.facingRight;
Vector3 localScale = transform.localScale;
localScale.x *= -1f;
transform.localScale = localScale;
}
public void FaceRight()
{
cState.facingRight = true;
Vector3 localScale = transform.localScale;
localScale.x = -1f;
transform.localScale = localScale;
}
public void FaceLeft()
{
cState.facingRight = false;
Vector3 localScale = transform.localScale;
localScale.x = 1f;
transform.localScale = localScale;
}
private void LookForInput()
{
if (acceptingInput)
{
move_input = inputHandler.inputActions.moveVector.Vector.x; //获取X方向的键盘输入
vertical_input = inputHandler.inputActions.moveVector.Vector.y;//获取Y方向的键盘输入
FilterInput();//规整化
if (inputHandler.inputActions.jump.WasReleased && jumpReleaseQueueingEnabled)
{
jumpReleaseQueueSteps = JUMP_RELEASE_QUEUE_STEPS;
jumpReleaseQueuing = true;
}
if (!inputHandler.inputActions.jump.IsPressed)
{
JumpReleased();
}
if (!inputHandler.inputActions.dash.IsPressed)
{
if(cState.preventDash && !cState.dashCooldown)
{
cState.preventDash = false;
}
dashQueuing = false;
}
}
}
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.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();
}
}
}
/// <summary>
/// 可以跳跃吗
/// </summary>
/// <returns></returns>
private bool CanJump()
{
if(hero_state == ActorStates.no_input || hero_state == ActorStates.hard_landing || hero_state == ActorStates.dash_landing || cState.jumping)
{
return false;
}
if (cState.onGround)
{
return true; //如果在地面上就return true
}
return false;
}
/// <summary>
/// 小骑士跳跃行为播放声音以及设置cstate.jumping
/// </summary>
private void HeroJump()
{
audioCtrl.PlaySound(HeroSounds.JUMP);
cState.jumping = true;
jumpQueueSteps = 0;
jumped_steps = 0;
}
private void HeroJumpNoEffect()
{
audioCtrl.PlaySound(HeroSounds.JUMP);
cState.jumping = true;
jumpQueueSteps = 0;
jumped_steps = 0;
}
/// <summary>
/// 取消跳跃
/// </summary>
public void CancelHeroJump()
{
if (cState.jumping)
{
CancelJump();
if(rb2d.velocity.y > 0f)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, 0f);
}
}
}
private void JumpReleased()
{
if(rb2d.velocity.y > 0f &&jumped_steps >= JUMP_STEPS_MIN)
{
if (jumpReleaseQueueingEnabled)
{
if(jumpReleaseQueuing && jumpReleaseQueueSteps <= 0)
{
rb2d.velocity = new Vector2(rb2d.velocity.x, 0f); //取消跳跃并且设置y轴速度为0
CancelJump();
}
}
else
{
rb2d.velocity = new Vector2(rb2d.velocity.x, 0f);
CancelJump();
}
}
jumpQueuing = false;
}
/// <summary>
/// 设置玩家的ActorState的新类型
/// </summary>
/// <param name="newState"></param>
private void SetState(ActorStates newState)
{
if(newState == ActorStates.grounded)
{
if(Mathf.Abs(move_input) > Mathf.Epsilon)
{
newState = ActorStates.running;
}
else
{
newState = ActorStates.idle;
}
}
else if(newState == ActorStates.previous)
{
newState = prev_hero_state;
}
if(newState != hero_state)
{
prev_hero_state = hero_state;
hero_state = newState;
animCtrl.UpdateState(newState);
}
}
/// <summary>
/// 回到地面上时执行的函数
/// </summary>
public void BackOnGround()
{
cState.falling = false;
jump_steps = 0;
SetState(ActorStates.grounded);
cState.onGround = true;
airDashed = false;
}
/// <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(collision.gameObject.GetComponent<SteepSlope>() == null && hero_state != ActorStates.hard_landing)
{
BackOnGround();
}
}
}
}
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(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 bool CheckForBump(CollisionSide side)
{
float num = 0.025f;
float num2 = 0.2f;
Vector2 vector = new Vector2(col2d.bounds.min.x + num2, col2d.bounds.min.y + 0.2f);
Vector2 vector2 = new Vector2(col2d.bounds.min.x + num2, col2d.bounds.min.y - num);
Vector2 vector3 = new Vector2(col2d.bounds.max.x - num2, col2d.bounds.min.y + 0.2f);
Vector2 vector4 = new Vector2(col2d.bounds.max.x - num2, col2d.bounds.min.y - num);
float num3 = 0.32f + num2;
RaycastHit2D raycastHit2D = default(RaycastHit2D);
RaycastHit2D raycastHit2D2 = default(RaycastHit2D);
if(side == CollisionSide.left)
{
Debug.DrawLine(vector2, vector2 + Vector2.left * num3, Color.cyan, 0.15f);
Debug.DrawLine(vector, vector + Vector2.left * num3, Color.cyan, 0.15f);
raycastHit2D = Physics2D.Raycast(vector2, Vector2.left, num3, LayerMask.GetMask("Terrain"));
raycastHit2D2 = Physics2D.Raycast(vector, Vector2.left, num3, LayerMask.GetMask("Terrain"));
}
else if (side == CollisionSide.right)
{
Debug.DrawLine(vector4, vector4 + Vector2.right * num3, Color.cyan, 0.15f);
Debug.DrawLine(vector3, vector3 + Vector2.right * num3, Color.cyan, 0.15f);
raycastHit2D = Physics2D.Raycast(vector4, Vector2.right, num3, LayerMask.GetMask("Terrain"));
raycastHit2D2 = Physics2D.Raycast(vector3, Vector2.right, num3, LayerMask.GetMask("Terrain"));
}
else
{
Debug.LogError("Invalid CollisionSide specified.");
}
if(raycastHit2D2.collider != null && raycastHit2D.collider == null)
{
Vector2 vector5 = raycastHit2D2.point + new Vector2((side == CollisionSide.right) ? 0.1f : -0.1f, 1f);
RaycastHit2D raycastHit2D3 = Physics2D.Raycast(vector5, Vector2.down, 1.5f, LayerMask.GetMask("Terrain"));
Vector2 vector6 = raycastHit2D2.point + new Vector2((side == CollisionSide.right) ? -0.1f : 0.1f, 1f);
RaycastHit2D raycastHit2D4 = Physics2D.Raycast(vector6, Vector2.down, 1.5f, LayerMask.GetMask("Terrain"));
if(raycastHit2D3.collider != null)
{
Debug.DrawLine(vector5, raycastHit2D3.point, Color.cyan, 0.15f);
if (!(raycastHit2D4.collider != null))
{
return true;
}
Debug.DrawLine(vector6, raycastHit2D4.point, Color.cyan, 0.15f);
float num4 = raycastHit2D3.point.y - raycastHit2D4.point.y;
if(num4 > 0f)
{
Debug.Log("Bump Height: " + num4.ToString());
return true;
}
}
}
return false;
}
/// <summary>
/// 找到碰撞点的方向也就是上下左右
/// </summary>
/// <param name="collision"></param>
/// <returns></returns>
private CollisionSide FindCollisionSide(Collision2D collision)
{
Vector2 normal = collision.GetSafeContact().Normal ;
float x = normal.x;
float y = normal.y;
if(y >= 0.5f)
{
return CollisionSide.bottom;
}
if (y <= -0.5f)
{
return CollisionSide.top;
}
if (x < 0)
{
return CollisionSide.right;
}
if (x > 0)
{
return CollisionSide.left;
}
Debug.LogError(string.Concat(new string[]
{
"ERROR: unable to determine direction of collision - contact points at (",
normal.x.ToString(),
",",
normal.y.ToString(),
")"
}));
return CollisionSide.bottom;
}
}
[Serializable]
public class HeroControllerStates
{
public bool facingRight;
public bool onGround;
public bool wasOnGround;
public bool inWalkZone;
public bool jumping;
public bool falling;
public bool dashing;
public bool touchingWall;
public bool preventDash;
public bool dashCooldown;
public bool isPaused;
public HeroControllerStates()
{
facingRight = false;
onGround = false;
wasOnGround = false;
inWalkZone = false;
jumping = false;
falling = false;
dashing = false;
touchingWall = false;
preventDash = false;
dashCooldown = false;
isPaused = false;
}
}
回到Unity编辑器中,我们要给HeroController.cs上的序列化参数赋值了:
需要注意的是,你的DASH_TIME最好和tk2dSpriteAnimation的Dash动画片段的Clip Time的数值相同,不然可能会显得画面不同步,像我就直接两个都设置成0.5
小骑士游戏对象下新开一个叫Effect的子对象,里面存储效果的游戏对象:
首先是第一个Dash Ash:
然后是第二个效果Dash Burst
可以看到我们要单独为它创造一个tk2dSpriteCollection和tk2dSpriteAnimation:
然后它还需要一个PlayMaker FSM:
我们通过开关它上面的Mesh Renderer来控制显示,首先要自定义一个playmaker action就叫SetMeshRenderer:
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
[ActionCategory(ActionCategory.GameObject)]
[Tooltip("Set Mesh Renderer to active or inactive. Can only be one Mesh Renderer on object. ")]
public class SetMeshRenderer : FsmStateAction
{
[RequiredField] public FsmOwnerDefault gameObject;
public FsmBool active;
public override void Reset()
{
gameObject = null;
active = false;
}
public override void OnEnter()
{
if (gameObject != null)
{
GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);
if(ownerDefaultTarget != null)
{
MeshRenderer component = ownerDefaultTarget.GetComponent<MeshRenderer>();
if(component != null)
{
component.enabled = active.Value;
}
}
}
Finish();
}
}
}
状态机的事件如下:它并不需要变量
而且只有两种状态,我们在代码中通过SendEvent发送事件来控制它们切换:
总结
至此,我们制作了一个完整的小骑士冲刺Dash,我们赶紧运行游戏看看效果如何吧:
平地上的Dash:
空中的Dash:
这些都是有冷却时间的,不能一直按一直冲。
还有就是离开地面后小骑士只能再按一下Dash,此后cState.Dashing就一直是false,直到下一次碰到地面cState.Dashing为true后才能按下一次Dash: