游戏输入
通常而言游戏的输入或者操作指的是玩家通过特定硬件设备输入到游戏系统的信号,正常的游戏应该对这些输入信号有所响应,或者是移动角色或者是切换画面等等。
玩家使用的输入设备可能有许多,但相对最常见的输入设备有以下几种:
- 键盘和鼠标
- 手柄或是摇杆
- 手机的触摸屏
Unity引擎本身是对玩家输入进行了统一的抽象化和集中管理的,Project Settings里的Input Manager就是服务与此。
但这个引擎自带的输入管理模块在实际使用中并非适用于任意开发场景,尤其是考虑到玩家会有多种不同的输入设备需求的情况下。
因此有必要考察更多一些输入管理的方案。
自定义管理器
原生的输入管理器使用起来很直观和方便,只要在设置面板里提前定义好虚拟键位和轴的映射,随后通过静态方法获取输入即可。
// 最常见的一些输入获取方法 Input.GetAxis("Horizontal"); Input.GetButtonDown("Fire"); Input.GetKeyDown(KeyCode.Space);
在制作Demo或者项目本身的输入需求并不太多的情况下,直接使用这些方法可以让开发过程加速不少,但这种用法的缺点同样明显,那就是难以拓展。
举一个例子如下,如果游戏只在某些时候响应输入指令,直接使用Input方法的情况会导致大量的if语句被使用,而且一旦分散到不同的脚本中会迅速增加调试难度。
解决方案是多种多样的,其中相对直接和简单的方法是对Input做一层封装,将输入指令的获取和使用分离开来。
比如定义一套抽象化的输入接口,并且使用一个InputManager来统一管理输入。
// 抽象化的输入接口,可以按需拓展 public interface IPlayerInput { Vector2 AxisInput(); Vector2 RawAxisInput(); bool AttackDown(); bool AttackPress(); bool AttackUp(); bool JumpDown(); bool JumpPress(); bool JumpUp(); bool Confirm(); bool Exit(); } // 输入管理器 public class InputManager : MonoBehaviour { public static InputManager Instance { get; private set; } // 单例引用 private IPlayerInput playerInput; // 输入接口引用 public PlayerInputData Data { get; private set; } // 所有输入数据 private bool allowPlayerInput = true; // 当前是否允许接收输入 private void Awake() { DontDestroyOnLoad(gameObject); // 场景变换时不销毁 Instance = this; } public void SetupPlayerInput(IPlayerInput input) { playerInput = input; } public void SwitchPlayerInput(bool isOn) { allowPlayerInput = isOn; } private void Update() { if (playerInput != null) { if (allowPlayerInput) { Data = new PlayerInputData() { axis = playerInput.AxisInput(), rawAxis = playerInput.RawAxisInput(), attackDown = playerInput.LeftAttackDown(), attackPress = playerInput.LeftAttackPress(), attackUp = playerInput.LeftAttackUp(), jumpDown = playerInput.RightAttackDown(), jumpPress = playerInput.RightAttackPress(), jumpUp = playerInput.RightAttackUp(), confirm = playerInput.Confirm(), exit = playerInput.Exit() }; } else { // 不允许接收输入时全部重置为默认值 Data = new PlayerInputData() { axis = Vector2.zero, rawAxis = Vector2.zero, attackDown = false, attackPress = false, attackUp = false, jumpDown = false, jumpPress = false, jumpUp = false, confirm = false, exit = false }; } } } public struct PlayerInputData { public Vector2 axis; public Vector2 rawAxis; public bool attackDown; public bool attackPress; public bool attackUp; public bool jumpDown; public bool jumpPress; public bool jumpUp; public bool confirm; public bool exit; } }
编写这样的代码之后,只需要在场景中创建一个空对象并挂载InputManager脚本,随后使用自定义的IPlayerInput接口实现类来初始化管理器即可。
输入指令的获取很简单,在任何需要的地方使用
InputManager.Instance.Data.axis;
这样访问即可。
对Unity自带的输入管理器二次封装的方案还有很多,这里只是一种相对简单和直接的方案。
SimpleInput插件
Unity的资源商店里有一款免费插件,名字就是SimpleInput,它是模仿Unity本来的Input类编写的并拓展了一些很有用的功能,使用上也相对简单,基本和Input类相同。
通常情况下,使用SimpleInput插件的方法简单到只需要将Input替换为SimpleInput,它们的方法调用以及返回值都是一致的。
示例如下
SimpleInput.GetAxis("Horizontal"); SimpleInput.GetButtonDown("Fire"); SimpleInput.GetKeyDown(KeyCode.Space);
而SimpleInput插件针对输入所做的拓展功能中有一个是比较有用的,那便是自定义输入追踪。
Unity默认的输入管理模块是将具体的物理按键或者轴绑定到一个名称上,这样便可以使用预先定义好的名称去获取输入。
但这种方式会留下一个问题,如果玩家的输入并非来自物理按键或者轴呢,比如说玩家通过屏幕上的两个滑块来控制两个不同的输入轴,默认情况下要实现这个功能可能需要另外实现一套输入方案。
SimpleInput提供了自定义输入追踪的功能来帮助开发者以同样的方案适配各种不同的输入模式,需要用到的便是SimpleInput类中的BaseInput基类以及它的三个实现类。
在插件自带的示例场景中有自定义输入的相关代码,其使用方式如下
// 创建需要监听的轴,按钮或者键位 private SimpleInput.AxisInput horizontalAxis = new SimpleInput.AxisInput("Horizontal"); private SimpleInput.AxisInput verticalAxis = new SimpleInput.AxisInput("Vertical"); private SimpleInput.ButtonInput attackButton = new SimpleInput.ButtonInput("Attack"); private SimpleInput.ButtonInput jumpButton = new SimpleInput.ButtonInput("Jump"); private SimpleInput.KeyInput confirmKey = new SimpleInput.KeyInput(KeyCode.F); private SimpleInput.KeyInput exitKey = new SimpleInput.KeyInput(KeyCode.Escape); // 初始化并启用追踪 horizontalAxis.StartTracking(); verticalAxis.StartTracking(); attackButton.StartTracking(); jumpButton.StartTracking(); confirmKey.StartTracking(); exitKey.StartTracking(); // 配置一个Update方法用于更新输入的值 private void UpdateInput() { horizontalAxis.value = ... verticalAxis.value = ... attackButton.value = ... jumpButton.value = ... confirmKey.value = ... exitKey.value = ... } // 然后将其注册到SimpleInput中 SimpleInput.OnUpdate += UpdateInput;
如此一来,SimpleInput便会将自定义的这些输入名称添加到监听列表中,在其它地方调用方法即可获取到这些自定义的输入值。
以这个功能为基础可以制作各种各样的输入界面。
手柄输入插件
手柄是一类相当重要的输入设备,对于主机开发者而言,对应的平台会提供相应的开发工具套件因而不必担心手柄的输入问题,但PC平台的游戏开发稍微有些不同。
Unity本身是支持获取手柄输入的,在Unity编辑器的输入管理中可以将手柄的按键和摇杆绑定上某个名字,此后用Input类提供的方法即可获取。
然而这套开发流程会显得有些复杂,超过十个不同的手柄按键以及最大28个轴的支持的确让人迷惑,再加上不同类型的手柄需要映射的轴与按键有所区别,因此使用原生功能直接进行开发可能并非最佳选择。
因此需要借助第三方插件来减少工作,下面用到的XInputInterface插件能较好地帮助开发者适配手柄的输入功能。该插件是开源的,在Github上能找到它的代码,单纯使用的话直接下载Release版本即可。
插件包含两个DLL文件,将它们放入Assets内的Plugins文件夹下,等到Unity完成插件的引用刷新之后便可以在代码中使用它们。
// XInputInterface插件在使用时要引入其命名空间 using XInputDotNetPure;
该插件的核心数据结构是GamePadState,实际操作中只要每帧获取到最新的State即可得到当前手柄上每个按键的状态,因而有些时候需要保存前一次更新的State来实现一些特别的输入需求。
// 先定义出玩家索引和手柄状态的数据结构 PlayerIndex playerIndex; GamePadState state; GamePadState prevState; bool isConnected = false; public void ConnectGamePad() { if(!isConnected) { for (int i = 0; i < 4; ++i) { PlayerIndex testPlayerIndex = (PlayerIndex)i; state = GamePad.GetState(testPlayerIndex); if (state.IsConnected) { playerIndex = testPlayerIndex; prevState = state; isConnected = true; break; } } } } // 轮询到可用的手柄之后便可以通过state来获取输入了 state.IsConnected // 用于检查当前手柄的连接状态 state.Buttons.A // 手柄上的A键(XBOX手柄) state.Buttons.B // 手柄上的B键(XBOX手柄) state.Buttons.X // 手柄上的X键(XBOX手柄) state.Buttons.Y // 手柄上的Y键(XBOX手柄) state.Buttons.Start // 手柄上的开始键(XBOX手柄) state.Buttons.Back // 手柄上的回退键(XBOX手柄) state.Buttons.Guide // 手柄上的导航键(XBOX手柄) state.Buttons.LeftShoulder // 手柄左肩按钮 state.Triggers.Left // 手柄左扳机 state.Buttons.RightShoulder // 手柄右肩按钮 state.Triggers.Right // 手柄右扳机 state.ThumbSticks.Left.X // 手柄左摇杆横轴 state.ThumbSticks.Left.Y // 手柄左摇杆纵轴 state.Buttons.LeftStick // 手柄左摇杆按下 state.ThumbSticks.Right.X // 手柄右摇杆横轴 state.ThumbSticks.Right.Y // 手柄右摇杆纵轴 state.Buttons.RightStick // 手柄右摇杆按下 state.DPad.Up // 手柄十字按钮上键 state.DPad.Right // 手柄十字按钮右键 state.DPad.Down // 手柄十字按钮下键 state.DPad.Left // 手柄十字按钮左键 // 如果希望实现类似Input.GetButtonDown这种方法,那么就需要添加一个每帧执行的方法 public void UpdateGamePad() { if(isConnected) { prevState = state; state = GamePad.GetState(playerIndex); } } // 此后每次获取输入时便可以通过对比前后两个状态的区别来实现GetButtonDown的功能
虚拟摇杆和控制器
这个算是一点点拓展思考,虚拟摇杆和控制器大部分时候用于缺乏物理输入设备的场景,比如手机或者平板之类的触屏设备,为了能较好地平衡用户体验和开发工作,虚拟摇杆是比较常见的一种解决方案。
实现虚拟摇杆的第三方插件很多,比如大名鼎鼎的EasyTouch就有针对虚拟摇杆的一系列工具,因此如果是在进行实际项目的开发工作,能选用成熟的第三方插件自然是最好的。
在这里就稍微探究一下虚拟摇杆的实现思路和方法,以及如何利用SimpleInput插件让它变得好用起来。
虚拟摇杆的外观和运作方式一般就是一个大圆表示摇杆活动范围,一个小圆表示摇杆的柄帽,对于这种简单结构的虚拟摇杆可以用UGUI很轻松地构造出来。
一种典型的构造方法如下:
- Joystick(根元素,包含Image组件,表示大圆)
- Pad(子元素,包含Image组件,表示小圆)
注意根元素的Image组件上Raycast Target要勾选上
实现原理很简单,通过捕捉Joystick范围内的触摸动作来更新Pad对象的位置即可,具体的方案至少有两种,下面选择了基于ScrollRect的实现方案。
public class JoystickControl : ScrollRect { private float moveRadius; // 摇杆的最大移动范围半径 public Vector2 stickAxis { get; private set; } // 摇杆当前的相对位置坐标 public event Action<Vector2> joystickMoveCallback; // 摇杆被推动时的回调,每帧调用 public event Action joystickMoveEndCallback; // 松开摇杆时的回调 protected override void Awake() { stickAxis = Vector2.zero; } protected override void Start() { moveRadius = GetComponent<RectTransform>().sizeDelta.x * 0.5f; content.gameObject.SetActive(false); } public override void OnDrag(PointerEventData eventData) { base.OnDrag(eventData); content.gameObject.SetActive(true); Vector2 position = content.anchoredPosition; if (position.magnitude > moveRadius) { position = position.normalized * moveRadius; SetContentAnchoredPosition(position); } } public override void OnEndDrag(PointerEventData eventData) { base.OnEndDrag(eventData); content.gameObject.SetActive(false); stickAxis = Vector2.zero; joystickMoveEndCallback?.Invoke(); } private void Update() { if (content.gameObject.activeInHierarchy) { // 摇杆被激活时才进行回调 stickAxis = content.anchoredPosition / moveRadius; joystickMoveCallback?.Invoke(stickAxis); } } }
将JoystickControl脚本挂载到Joystick对象上即可使用,需要额外注意的是,Joystick对象的锚点需要保持为(0.5,0.5),否则的话要在代码中加上关于锚点的偏移量。
基于ScrollRect实现的虚拟摇杆优点在于无需自己编写摇杆推动的阻尼效果和回弹动画,ScrollRect已经实现好了这两样东西;而缺点也很明显,继承了Unity自带的类后无法在编辑器面板上随意暴露所需的字段,要么通过代码设置,要么就编写辅助脚本。
有了可以运作的虚拟摇杆之后,现在可以考虑一下易用性,无论是虚拟按钮还是摇杆,其实时输入的数据都保存在自身内部,简单粗暴地将其暴露出来或者使用回调方法让外部可以访问自然是可行的方案之一,但如果利用好SimpleInput的自定义输入追踪功能,虚拟摇杆也可以和按钮以及轴的名称绑定在一起,使用起来方便很多。
实现方法也不复杂,将当前所有的虚拟输入组件全部集中到一个脚本中进行管理,并且在其中注册好SimpleInput的追踪方法即可。
public class VirtualInputPanel : MonoBehaviour { public JoystickControl joystick; private SimpleInput.AxisInput horizontalAxis; private SimpleInput.AxisInput verticalAxis; private void Awake() { horizontalAxis = new SimpleInput.AxisInput("Horizontal"); // 新建横轴类 verticalAxis = new SimpleInput.AxisInput("Vertical"); // 新建纵轴类 horizontalAxis.StartTracking(); // 开始追踪横轴 verticalAxis.StartTracking(); // 开始追踪纵轴 SimpleInput.OnUpdate += Update; // 让SimpleInput代理每帧的刷新 } private void Update() { if(joystick != null) { // 存在虚拟摇杆时每帧更新轴的值 horizontalAxis.value = joystick.stickAxis.x; verticalAxis.value = joystick.stickAxis.y; } } private void OnDestroy() { // 销毁对象时停止追踪和代理 horizontalAxis.StopTracking(); verticalAxis.StopTracking(); SimpleInput.OnUpdate -= Update; } }
挂载了VirtualInputPanel之后,SimpleInput便会自动开始追踪虚拟摇杆的输入,开发者此时可以在任何地方使用SimpleInput.GetAxis("Horizontal")以及SimpleInput.GetAxis("Vertical")来获取摇杆输入了。
如果想要高效并且节省时间的话,Unity 商城的 Rewired 是一个近乎完美的解决方案,手柄随时动态插拔,支持全平台,移植的时候几乎都不用过多考虑。
@Steamer:在正式的项目中使用成熟的第三方解决方案肯定是相对更好的选择,不说手柄,移动端也经常能见到EasyTouch一站式解决方案的用法 O(∩_∩)O