提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作一个完整的小骑士冲刺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的事件PLAYdashParticlesPrefab.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轴速度为0CancelJump();}}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: