Unity复刻胡闹厨房复盘 模块一 新输入系统订阅链与重绑定
本文仅作学习交流,不做任何商业用途
郑重感谢siki老师的汉化教程与代码猴的免费教程以及搬运烤肉的小伙伴
版本:Unity6
模板:3D 核心
渲染管线:URP
------------------------------------------------------------------------------------------------------------------------------
在此之前你应该对新输入系统有一定的了解,关于新输入系统的用法与解释:Unity新输入系统 之 InputAction(输入配置文件最基本的单位)_unity inputaction-CSDN博客
关于新输入系统的简单实战:Unity 新输入系统实战 控制2D角色移动动画(俯视)-CSDN博客
目录
实现功能与逻辑拆解
1.获取WASD的基础输入编辑
2.自定义操作按键
3.按键重绑
4 .重绑定后的保存与读取
全局概览
我将输入管理类命名为GameInput 其创建时候就写为了单例模式,因为但凡是拥有全局唯一实例的类 或者 是该类的生命周期占据了整个场景 就应该写为单例模式
实现功能与逻辑拆解
1.获取WASD的基础输入
对于新输入系统的创建与生成C#文件我便不再赘述,Unity6自带
你可以在InputAction看到其已经定义好了很多内容
首先声明输入系统的C#类 然后开启
action = new InputSystem_Actions();
action.Enable();
直接读取输入值 可以看到Move这一Action是Value的动作类型以及Vector2的控制类型
因此代码就可以直接这么写:
public Vector3 GetInputKeyboard() {
Vector2 direcation = action.Player.Move.ReadValue<Vector2>();
//float x = Input.GetAxisRaw("Horizontal");
//float z = Input.GetAxisRaw("Vertical");
Vector3 move = new Vector3(direcation.x, 0, direcation.y);
move = move.normalized;
return move;
}
至于为什么返回单位化后的向量可以看这一篇,其并不是本文的重点 :Unity中的数学应用 之 角色移动中单位化向量的妙用 (小学难度)-CSDN博客
2.自定义操作按键
这里有个前置知识点:发布者-订阅模式的特点就是发布者发布事件与调用 ,订阅者只需要订阅上就完事了
对于此部分可以自定义Action为Button类型
对于具体按键可以勾选分类:
代码是这么写的:
private static GameInput instance;
public static GameInput Instance => instance;
private InputSystem_Actions action;
public EventHandler interact;
public EventHandler operater;
public EventHandler pause;
private void Awake() {
if (instance == null) {
instance = this;
}
else {
Destroy(instance);
}
action = new InputSystem_Actions();
if (PlayerPrefs.HasKey(PLAYERBDINGINFO))
action.LoadBindingOverridesFromJson(PlayerPrefs.GetString(PLAYERBDINGINFO));
action.Enable();
//触发订阅—E
action.Player.Interact.started += Interact_started;
//触发订阅-F
action.Player.Operater.started += Operater_started;
//触发订阅-ESC
action.Player.Pause.started += Pause_started;
}
private void OnDestroy() {
action.Player.Interact.started -= Interact_started;
action.Player.Operater.started -= Operater_started;
action.Player.Pause.started -= Pause_started;
action.Dispose();
}
private void Pause_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
pause?.Invoke(this, EventArgs.Empty);
}
private void Operater_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
operater?.Invoke(this, EventArgs.Empty);
}
private void Interact_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
interact?.Invoke(this, EventArgs.Empty);
}
解释:下面部分是对按键手势(Action)的订阅
action.Enable();
//触发订阅—E
action.Player.Interact.started += Interact_started;
//触发订阅-F
action.Player.Operater.started += Operater_started;
//触发订阅-ESC
action.Player.Pause.started += Pause_started;
订阅的谁呢?是如下三个函数 注意其参数都是自动生成的
private void Pause_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
pause?.Invoke(this, EventArgs.Empty);
}
private void Operater_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
operater?.Invoke(this, EventArgs.Empty);
}
private void Interact_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
interact?.Invoke(this, EventArgs.Empty);
}
而三个函数内又包裹了一层C#提供可传参委托
其参数sender代表事件的发布者 也就是GameIput类本身
e是EventArgs
类型或者派生自EventArgs
的类型,通常用于传递和事件相关的信息,这里传值为EventArgs.Empty 也就是空
也就是他们三个
public EventHandler interact;
public EventHandler operater;
public EventHandler pause;
所以对于他们三个的调用也就嵌在了对新输入系统手势的订阅上
那么谁去订阅呢?Player 你会发现Player这里又是一层封装?但不是委托与事件
BaseCounter 是柜台基类 我们以后会讲,curCounter用于存储当前玩家获得的柜台
private BaseCounter curCounter;
void Start() {
//Application.targetFrameRate = 60;
this.HoldPoints = transform.Find("PlayerHoldPoint");
GameInput.Instance.interact += OnInterAction;
GameInput.Instance.operater += OnOperaterAction;
}
private void OnInterAction(object sender, EventArgs s) {
curCounter?.Interact(this);
}
private void OnOperaterAction(object sender, EventArgs e) {
curCounter?.InteractOperater(this);
}
Interact与InteractOperater的定义在柜台类基类之中是两个虚方法,由子类去实现
using UnityEngine;
public class BaseCounter : FoodObjcetHolder {
[SerializeField] protected Transform SelectPrefab;
public void SelectPrefabSecureAssign(string name) {
if (SelectPrefab == null) {
SelectPrefab = transform.Find(name);
}
}
public virtual void Interact(Player player) {
Debug.Log("未对父类进行重写");
}
public virtual void InteractOperater(Player player) {
}
public void CounterSelect() {
SelectPrefab.gameObject?.SetActive(true);
}
public void CounterUnSelect() {
SelectPrefab.gameObject?.SetActive(false);
}
}
子类实现我们不去考虑,目前对于一个按键的订阅链我们已经整理完了
由下图所示
你要说这不是脱了裤子放p吗?其实不然 其道理在于
如果只是GameInput类自我消化 只需要第一条黑线 但是GameInput类要与Player类进行交互 而事件的发布订阅解决了这个问题,所以有了第二条黑线
Player也不负责执行柜台的逻辑 所以就需要第三条黑线
3.按键重绑
这个的原理如下
1.获取对应的Action下的按键
action.Player.Move;
action.Player.Interact;
action.Player.Operater;
action.Player.Pause;
2.通过对应的索引去得到对应值键 也就是下图
在回调函数中执行其他办法,注意最后那个Start()方法一定要开启不然没有任何反应
actionKey.PerformInteractiveRebinding(index).OnComplete((callback) => {
Debug.Log("重新绑定完成");
action.Player.Enable();
SettingUI.Instance.UpdateUI();
SettingUI.Instance.HideBindingInfo();
PlayerPrefs.SetString(PLAYERBDINGINFO,action.SaveBindingOverridesAsJson());
}).Start();
先别管actionKey,也别管函数块内部做了什么,重点下面这个API是重绑定的关键:
PerformInteractiveRebinding(index).OnComplete
做了一个枚举去得到所有不同的输入
public enum E_BindingKey{
w,
a,
s,
d,
e,
f,
esc
}
重新绑定只需要传入一个枚举值,就可以
public void ReBinding(E_BindingKey e_BindingKey){
Debug.Log("进入重新绑定");
action.Player.Disable();
InputAction actionKey = null;
int index = -1;
switch (e_BindingKey) {
case E_BindingKey.w:
index = 2;
actionKey = action.Player.Move;
break;
case E_BindingKey.a:
index = 6;
actionKey = action.Player.Move;
break;
case E_BindingKey.s:
index = 4;
actionKey = action.Player.Move;
break;
case E_BindingKey.d:
index = 8;
actionKey = action.Player.Move;
break;
case E_BindingKey.e:
index = 0;
actionKey = action.Player.Interact;
break;
case E_BindingKey.f:
index = 0;
actionKey = action.Player.Operater;
break;
case E_BindingKey.esc:
index = 0;
actionKey = action.Player.Pause;
break;
default:
break;
}
if(actionKey != null) {
actionKey.PerformInteractiveRebinding(index).OnComplete((callback) => {
Debug.Log("重新绑定完成");
action.Player.Enable();
SettingUI.Instance.UpdateUI();
SettingUI.Instance.HideBindingInfo();
PlayerPrefs.SetString(PLAYERBDINGINFO,action.SaveBindingOverridesAsJson());
}).Start();
}else{
Debug.Log("actionKey为空");
}
//actionKey.Dispose();
}
4 .重绑定后的保存与读取
重新加载会将场景所有类的内存回收,但是存在本地的可以持久化,因此无论是何种持久化方式都可以通过如下两个API去存取成json字符串
这里用PlayerPrefs演示
写入:
PlayerPrefs.SetString(PLAYERBDINGINFO,action.SaveBindingOverridesAsJson());
读取:
action.LoadBindingOverridesFromJson(PlayerPrefs.GetString(PLAYERBDINGINFO));
至于放在哪里 不用我多说了相信你对unity的生命周期并不陌生
全局概览
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public enum E_BindingKey{
w,
a,
s,
d,
e,
f,
esc
}
public class GameInput : MonoBehaviour {
private const string PLAYERBDINGINFO = "PLAYERBDINGINFO";
private static GameInput instance;
public static GameInput Instance => instance;
private InputSystem_Actions action;
public EventHandler interact;
public EventHandler operater;
public EventHandler pause;
private void Awake() {
if (instance == null) {
instance = this;
}
else {
Destroy(instance);
}
action = new InputSystem_Actions();
if (PlayerPrefs.HasKey(PLAYERBDINGINFO))
action.LoadBindingOverridesFromJson(PlayerPrefs.GetString(PLAYERBDINGINFO));
action.Enable();
//触发订阅—E
action.Player.Interact.started += Interact_started;
//触发订阅-F
action.Player.Operater.started += Operater_started;
//触发订阅-ESC
action.Player.Pause.started += Pause_started;
}
private void OnDestroy() {
action.Player.Interact.started -= Interact_started;
action.Player.Operater.started -= Operater_started;
action.Player.Pause.started -= Pause_started;
action.Dispose();
}
private void Pause_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
pause?.Invoke(this, EventArgs.Empty);
}
private void Operater_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
operater?.Invoke(this, EventArgs.Empty);
}
private void Interact_started(UnityEngine.InputSystem.InputAction.CallbackContext obj) {
interact?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// 读取输入
/// </summary>
/// <returns>移动朝向</returns>
public Vector3 GetInputKeyboard() {
Vector2 direcation = action.Player.Move.ReadValue<Vector2>();
//float x = Input.GetAxisRaw("Horizontal");
//float z = Input.GetAxisRaw("Vertical");
Vector3 move = new Vector3(direcation.x, 0, direcation.y);
move = move.normalized;
return move;
}
public void ReBinding(E_BindingKey e_BindingKey){
Debug.Log("进入重新绑定");
action.Player.Disable();
InputAction actionKey = null;
int index = -1;
switch (e_BindingKey) {
case E_BindingKey.w:
index = 2;
actionKey = action.Player.Move;
break;
case E_BindingKey.a:
index = 6;
actionKey = action.Player.Move;
break;
case E_BindingKey.s:
index = 4;
actionKey = action.Player.Move;
break;
case E_BindingKey.d:
index = 8;
actionKey = action.Player.Move;
break;
case E_BindingKey.e:
index = 0;
actionKey = action.Player.Interact;
break;
case E_BindingKey.f:
index = 0;
actionKey = action.Player.Operater;
break;
case E_BindingKey.esc:
index = 0;
actionKey = action.Player.Pause;
break;
default:
break;
}
if(actionKey != null) {
actionKey.PerformInteractiveRebinding(index).OnComplete((callback) => {
Debug.Log("重新绑定完成");
action.Player.Enable();
SettingUI.Instance.UpdateUI();
SettingUI.Instance.HideBindingInfo();
PlayerPrefs.SetString(PLAYERBDINGINFO,action.SaveBindingOverridesAsJson());
}).Start();
}else{
Debug.Log("actionKey为空");
}
//actionKey.Dispose();
}
public string GetBindingKey(E_BindingKey e_BindingKey){
switch (e_BindingKey) {
case E_BindingKey.w:
return action.Player.Move.bindings[2].ToDisplayString();
case E_BindingKey.a:
return action.Player.Move.bindings[6].ToDisplayString();
case E_BindingKey.s:
return action.Player.Move.bindings[4].ToDisplayString();
case E_BindingKey.d:
return action.Player.Move.bindings[8].ToDisplayString();
case E_BindingKey.e:
return action.Player.Interact.bindings[0].ToDisplayString();
case E_BindingKey.f:
return action.Player.Operater.bindings[0].ToDisplayString();
case E_BindingKey.esc:
return action.Player.Pause.bindings[0].ToDisplayString();
default:
return "";
}
}
}