提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、通过InControl插件实现绑定玩家输入
- 二、制作小骑士移动和空闲动画
- 1.制作动画
- 2.玩家移动和翻转图像
- 3.状态机思想实现动画切换
- 总结
前言
好久没来CSDN看看,突然看到前两年自己写的文章从零开始制作空洞骑士只做了一篇就突然烂尾了,刚好最近开始学习做Unity,我决定重启这个项目,从零开始制作空洞骑士!第一集我们导入了素材和远程git管理项目,OK这期我们就从通过InControl插件实现绑定玩家输入以及制作小骑士移动和空闲动画。
一、通过InControl插件实现绑定玩家输入
其实这一部挺难的,因为InControl插件你在网上绝对不超过五个视频资料,但没办法空洞骑士就是用这个来控制键盘控制器输入的,为了原汁原味就只能翻一下InControl提供的Examples示例里学习。
学习完成后直接来看我写的InputHandler.cs和GameManager.cs
在GameManager.cs中我们暂且先只用实现一个单例模式:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
public static GameManager instance
{
get
{
if(_instance == null)
{
_instance = FindObjectOfType<GameManager>();
}
if (_instance == null)
{
Debug.LogError("Couldn't find a Game Manager, make sure one exists in the scene.");
}
else if (Application.isPlaying)
{
DontDestroyOnLoad(_instance.gameObject);
}
return _instance;
}
}
private void Awake()
{
if(_instance != this)
{
_instance = this;
DontDestroyOnLoad(this);
return;
}
if(this != _instance)
{
Destroy(gameObject);
return;
}
}
}
在来到InputHandler.cs之前,我们还需要创建映射表HeroActions:
using System;
using InControl;
public class HeroActions : PlayerActionSet
{
public PlayerAction left;
public PlayerAction right;
public PlayerAction up;
public PlayerAction down;
public PlayerTwoAxisAction moveVector;
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;
}
}
OK到了最关键的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
});
}
//暂时没有GameSettings后续会创建的,这里是指将键盘按键绑定到HeroActions 中
private void MapKeyboardLayoutFromGameSettings()
{
AddKeyBinding(inputActions.up, "W");
AddKeyBinding(inputActions.down, "S");
AddKeyBinding(inputActions.left, "A");
AddKeyBinding(inputActions.right, "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
}));
}
}
给我们的小骑士创建一个HeroController.cs,检测输入最关键的是一行代码:
move_input = inputHandler.inputActions.moveVector.Vector.x;
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 RUN_SPEED = 5f;
private Rigidbody2D rb2d;
private BoxCollider2D col2d;
private GameManager gm;
private InputHandler inputHandler;
private void Awake()
{
SetupGameRefs();
}
private void SetupGameRefs()
{
rb2d = GetComponent<Rigidbody2D>();
col2d = GetComponent<BoxCollider2D>();
gm = GameManager.instance;
inputHandler = gm.GetComponent<InputHandler>();
}
void Start()
{
}
void Update()
{
orig_Update();
}
private void orig_Update()
{
if (hero_state == ActorStates.no_input)
{
}
else if(hero_state != ActorStates.no_input)
{
LookForInput();
}
}
private void FixedUpdate()
{
if (hero_state != ActorStates.no_input)
{
Move(move_input);
}
}
private void Move(float move_direction)
{
if(acceptingInput)
{
rb2d.velocity = new Vector2(move_direction * RUN_SPEED, rb2d.velocity.y);
}
}
private void LookForInput()
{
if (acceptingInput)
{
move_input = inputHandler.inputActions.moveVector.Vector.x;
}
}
}
[Serializable]
public class HeroControllerStates
{
public bool facingRight;
public bool onGround;
public HeroControllerStates()
{
facingRight = true;
onGround = false;
}
}
记得一句话:FixedUpdate()处理物理移动,Update()处理逻辑
二、制作小骑士移动和空闲动画
1.制作动画
素材找到Idle和Walk文件夹,创建两个同名animation,sprite往上面一放自己就做好了。
可能你注意到我没有给这两个动画连线,其实是我想做个动画状态机,通过核心代码
animator.Play()来管理动画的切换。
2.玩家移动和翻转图像
我们还需要给小骑士添加更多的功能比如翻转图像:
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 RUN_SPEED = 5f;
private Rigidbody2D rb2d;
private BoxCollider2D col2d;
private GameManager gm;
private InputHandler inputHandler;
public HeroControllerStates cState;
private HeroAnimatorController animCtrl;
private void Awake()
{
SetupGameRefs();
}
private void SetupGameRefs()
{
if (cState == null)
cState = new HeroControllerStates();
rb2d = GetComponent<Rigidbody2D>();
col2d = GetComponent<BoxCollider2D>();
animCtrl = GetComponent<HeroAnimatorController>();
gm = GameManager.instance;
inputHandler = gm.GetComponent<InputHandler>();
}
void Start()
{
}
void Update()
{
orig_Update();
}
private void orig_Update()
{
if (hero_state == ActorStates.no_input)
{
}
else if(hero_state != ActorStates.no_input)
{
LookForInput();
}
}
private void FixedUpdate()
{
if (hero_state != ActorStates.no_input)
{
Move(move_input);
if(move_input > 0f && !cState.facingRight )
{
FlipSprite();
}
else if(move_input < 0f && cState.facingRight)
{
FlipSprite();
}
}
}
private void Move(float move_direction)
{
if (cState.onGround)
{
SetState(ActorStates.grounded);
}
if(acceptingInput)
{
rb2d.velocity = new Vector2(move_direction * RUN_SPEED, rb2d.velocity.y);
}
}
public void FlipSprite()
{
cState.facingRight = !cState.facingRight;
Vector3 localScale = transform.localScale;
localScale.x *= -1f;
transform.localScale = localScale;
}
private void LookForInput()
{
if (acceptingInput)
{
move_input = inputHandler.inputActions.moveVector.Vector.x;
}
}
/// <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);
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.layer == LayerMask.NameToLayer("Wall"))
{
cState.onGround = true;
}
}
private void OnCollisionStay2D(Collision2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Wall"))
{
cState.onGround = true;
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Wall"))
{
cState.onGround = false;
}
}
}
[Serializable]
public class HeroControllerStates
{
public bool facingRight;
public bool onGround;
public HeroControllerStates()
{
facingRight = true;
onGround = false;
}
}
创建命名空间GlobalEnums,创建一个新的枚举类型:
using System;
namespace GlobalEnums
{
public enum ActorStates
{
grounded,
idle,
running,
airborne,
wall_sliding,
hard_landing,
dash_landing,
no_input,
previous
}
}
3.状态机思想实现动画切换
有了这些我们就可以有效控制动画切换,创建一个新的脚本给Player:
可以看到我们创建了两个属性记录当前的actorState和上一个actorState来实现动画切换(后面会用到的)
using System;
using GlobalEnums;
using UnityEngine;
public class HeroAnimatorController : MonoBehaviour
{
private Animator animator;
private AnimatorClipInfo[] info;
private HeroController heroCtrl;
private HeroControllerStates cState;
private string clipName;
private float currentClipLength;
public ActorStates actorStates { get; private set; }
public ActorStates prevActorState { get; private set; }
private void Start()
{
animator = GetComponent<Animator>();
heroCtrl = GetComponent<HeroController>();
actorStates = heroCtrl.hero_state;
PlayIdle();
}
private void Update()
{
UpdateAnimation();
}
private void UpdateAnimation()
{
//info = animator.GetCurrentAnimatorClipInfo(0);
//currentClipLength = info[0].clip.length;
//clipName = info[0].clip.name;
if(actorStates == ActorStates.no_input)
{
//TODO:
}
else if(actorStates == ActorStates.idle)
{
//TODO:
PlayIdle();
}
else if(actorStates == ActorStates.running)
{
PlayRun();
}
}
private void PlayRun()
{
animator.Play("Run");
}
public void PlayIdle()
{
animator.Play("Idle");
}
public void UpdateState(ActorStates newState)
{
if(newState != actorStates)
{
prevActorState = actorStates;
actorStates = newState;
}
}
}
总结
最后给大伙看看效果怎么样,可以看到运行游戏后cState,hero_state和prev_hero_state都没有问题,动画也正常播放:
累死我了我去睡个觉顺便上传到github,OK大伙晚安醒来接着更新。