Unity MyFramework 用法说明(二十七):使用 InputSystem 与 KeyMappingSystem 统一管理输入和快捷键

角色移动、技能快捷键、编辑器操作和界面关闭都需要读取输入。如果业务代码到处直接调用 Input.GetKeyDown(),按键规则、输入焦点和改键功能就会分散在不同系统中。

MyFramework 使用 InputSystem 统一收集键盘、鼠标和触摸状态,再通过 KeyMappingSystem 将具体按键映射为游戏行为。

项目地址:

github.com/ZHOURUIH/My...

需要注意,这里的 InputSystem 是 MyFramework 对 UnityEngine.Input 的封装,不是 Unity 官方的 Input System Package。

一、两个系统分别负责什么

两个系统解决的问题不同:

bash 复制代码
InputSystem
├── 读取键盘状态
├── 统一鼠标与触摸输入
├── 识别单击和双击
├── 监听固定快捷键
├── 处理组合键
└── 区分场景输入与输入框输入

KeyMappingSystem
├── 将游戏行为映射到 KeyCode
├── 保存默认按键
├── 修改当前按键
├── 检测按键冲突
└── 提供用于界面显示的按键名称

可以简单理解为:

bash 复制代码
InputSystem 负责"用户按了什么"

KeyMappingSystem 负责"这个按键代表什么行为"

二、为什么需要按键映射

直接在移动逻辑中判断:

bash 复制代码
if (Input.GetKeyDown(KeyCode.W))
{
	moveUp();
}

意味着"向上移动"这个行为和 W 键永久绑定。

玩家改成方向键后,需要修改所有使用 W 的代码。更合理的方式是先定义行为 ID:

bash 复制代码
public static class KEY_MAPPING
{
	public const int MOVE_UP = 1;
	public const int MOVE_DOWN = 2;
	public const int ATTACK = 3;
	public const int OPEN_PACK = 4;
}

业务系统只判断行为:

bash 复制代码
if (mKeyMappingSystem.isMappingKeyDown(
	KEY_MAPPING.MOVE_UP))
{
	moveUp();
}

至于"向上移动"当前对应 W 还是方向键,由 KeyMappingSystem 决定。

三、注册默认快捷键

游戏初始化时注册行为和默认按键:

bash 复制代码
using UnityEngine;
using static FrameBaseHotFix;

public static class GameKeyMapping
{
	public static void init()
	{
		registe(
			KEY_MAPPING.MOVE_UP,
			KeyCode.W,
			"向上移动");

		registe(
			KEY_MAPPING.MOVE_DOWN,
			KeyCode.S,
			"向下移动");

		registe(
			KEY_MAPPING.ATTACK,
			KeyCode.Space,
			"普通攻击");

		registe(
			KEY_MAPPING.OPEN_PACK,
			KeyCode.B,
			"打开背包");
	}

	private static void registe(
		int mapping,
		KeyCode key,
		string actionName)
	{
		if (!mKeyMappingSystem.setKeyMapping(
			mapping,
			key,
			actionName))
		{
			return;
		}

		mKeyMappingSystem.setDefaultKeyMapping(
			mapping,
			key);
	}
}

其中保存了三类信息:

bash 复制代码
Mapping ID
表示游戏行为

当前按键
表示玩家目前使用的按键

默认按键
用于恢复默认设置

actionName 主要用于快捷键设置界面,例如显示:

bash 复制代码
向上移动    W
普通攻击    Space
打开背包    B

四、区分按下瞬间和持续按住

只在按下的这一帧触发:

bash 复制代码
if (mKeyMappingSystem.isMappingKeyCurrentDown(
	KEY_MAPPING.OPEN_PACK))
{
	LT.LOAD<UIPack>();
}

适合:

bash 复制代码
打开界面
释放一次技能
确认操作
拾取物品

持续按住期间一直返回 true

bash 复制代码
if (mKeyMappingSystem.isMappingKeyDown(
	KEY_MAPPING.MOVE_UP))
{
	moveUp();
}

适合:

bash 复制代码
角色移动
摄像机移动
持续蓄力
连续操作

两者最终会调用 InputSystem

bash 复制代码
public bool isMappingKeyCurrentDown(int mapping)
{
	return mInputSystem.isKeyCurrentDown(
		getKeyMapping(mapping),
		FOCUS_MASK.SCENE);
}

public bool isMappingKeyDown(int mapping)
{
	return mInputSystem.isKeyDown(
		getKeyMapping(mapping),
		FOCUS_MASK.SCENE);
}

所以映射按键默认只处理场景输入,不会在输入框中输入文字时误触发游戏快捷键。

五、运行时修改按键

玩家在设置界面中按下一个新键后,可以重新设置映射:

bash 复制代码
protected bool changeKeyMapping(
	int mapping,
	KeyCode newKey)
{
	if (!mKeyMappingSystem.setKeyMapping(
		mapping,
		newKey))
	{
		return false;
	}

	// 在这里由应用层保存玩家的快捷键配置
	return true;
}

setKeyMapping() 会检查新按键是否已经被其他行为占用:

bash 复制代码
if (key != KeyCode.None &&
	mKeyMapping.containsValue(
		item =>
			item.mKey == key &&
			item.mMappingID != mappingID))
{
	logError("有冲突的按键:" + key);
	return false;
}

例如 B 已经用于打开背包,再把它设置为普通攻击时,接口会返回 false

框架负责映射和冲突检测,玩家按键配置的持久化则由应用层处理。

六、显示当前按键名称

获取某个行为当前绑定的按键:

bash 复制代码
KeyCode key =
	mKeyMappingSystem.getKeyMapping(
		KEY_MAPPING.OPEN_PACK);

获取用于界面显示的名称:

bash 复制代码
string keyName =
	mKeyMappingSystem.getKeyMappingName(
		KEY_MAPPING.OPEN_PACK);

获取行为名称:

bash 复制代码
string actionName =
	mKeyMappingSystem.getKeyMappingActionName(
		KEY_MAPPING.OPEN_PACK);

快捷键设置界面可以直接显示:

bash 复制代码
mActionText.setText(actionName);
mKeyText.setText(keyName);

框架已经为常见按键准备了显示名称,例如:

bash 复制代码
KeyCode.Space       → Space
KeyCode.Delete      → Del
KeyCode.PageUp      → PgUp
KeyCode.UpArrow     → ↑
KeyCode.Keypad1     → Num1

恢复默认设置时,可以读取:

bash 复制代码
KeyCode defaultKey =
	mKeyMappingSystem.getDefaultMappingKey(mapping);

再重新调用 setKeyMapping()

七、固定快捷键不需要建立映射

并不是所有按键都允许玩家修改。

例如:

bash 复制代码
Esc 关闭界面
Ctrl + S 保存UGC作品
F1 切换调试信息

这类固定或临时快捷键,可以直接通过 InputSystem 注册:

bash 复制代码
public class UGCShortcutSystem : FrameSystem
{
	public override void init()
	{
		base.init();

		mInputSystem.listenKeyCurrentDown(
			KeyCode.S,
			onSave,
			this,
			COMBINATION_KEY.CTRL);

		mInputSystem.listenKeyCurrentDown(
			KeyCode.Escape,
			onEscape,
			this);
	}

	public override void destroy()
	{
		mInputSystem?.unlistenKey(this);

		base.destroy();
	}

	protected void onSave()
	{
		saveUGCContent();
	}

	protected void onEscape()
	{
		closeCurrentPanel();
	}

	protected void saveUGCContent()
	{
	}

	protected void closeCurrentPanel()
	{
	}
}

监听时需要传入监听者:

bash 复制代码
this

销毁时可以一次性移除该监听者注册的所有快捷键:

bash 复制代码
mInputSystem.unlistenKey(this);

避免对象销毁后旧回调继续执行。

八、组合键如何处理

框架支持:

bash 复制代码
Ctrl
Shift
Alt

例如监听 Ctrl + Shift + S

bash 复制代码
mInputSystem.listenKeyCurrentDown(
	KeyCode.S,
	onSaveAs,
	this,
	COMBINATION_KEY.CTRL |
	COMBINATION_KEY.SHIFT);

InputSystem 每帧统计当前组合键状态:

bash 复制代码
COMBINATION_KEY combination =
	COMBINATION_KEY.NONE;

combination |=
	isAnyKeyDown(
		KeyCode.LeftControl,
		KeyCode.RightControl)
	? COMBINATION_KEY.CTRL
	: COMBINATION_KEY.NONE;

然后与监听信息进行比较:

bash 复制代码
if (info.mCombinationKey == combination)
{
	info.mCallback?.Invoke();
}

这里使用的是完全匹配。

注册 Ctrl + S 后,如果当前实际按下的是 Ctrl + Shift + S,不会触发这条监听。

九、输入框为什么不会触发场景快捷键

InputSystem 保存了当前输入焦点:

bash 复制代码
protected int mFocusMask;

每帧检查已经注册的输入框:

bash 复制代码
bool inputting = false;

foreach (IInputField item in mInputFieldList)
{
	if (item.isVisible() &&
		item.isFocused())
	{
		inputting = true;
		break;
	}
}

setMask(
	inputting
		? FOCUS_MASK.UI
		: FOCUS_MASK.SCENE);

查询按键时会检查掩码:

bash 复制代码
public bool isKeyCurrentDown(
	KeyCode key,
	FOCUS_MASK mask = FOCUS_MASK.SCENE)
{
	return mEnableKey &&
		Input.GetKeyDown(key) &&
		hasMask(mask);
}

因此,当玩家正在聊天输入框中输入 B 时,不会同时打开背包。

这比每个业务系统分别判断"当前是否正在输入"更加可靠。

十、统一鼠标和触摸输入

InputSystem 将鼠标按键和移动端触点统一保存为:

bash 复制代码
SafeDictionary<int, TouchPoint>

获取本帧完成的单击:

bash 复制代码
if (mInputSystem.getTouchClick(
	out TouchPoint touch))
{
	Vector3 position =
		touch.getCurPosition();
}

获取双击:

bash 复制代码
if (mInputSystem.getTouchDoubleClick(
	out TouchPoint touch))
{
	Vector3 position =
		touch.getCurPosition();
}

获取触点移动量:

bash 复制代码
TouchPoint point =
	mInputSystem.getTouchPoint(pointerID);

Vector3 moveDelta =
	point?.getMoveDelta() ??
	Vector3.zero;

TouchPoint 会记录:

bash 复制代码
按下位置
当前位置
上一帧位置
本帧移动量
按下与抬起时间
是否单击
是否双击
是否持续按下

桌面端鼠标左、中、右键也会被当作固定触点,因此部分点击逻辑不需要分别编写鼠标版和触屏版。

十一、InputSystem 的每帧流程

InputSystem.update() 的主要流程是:

bash 复制代码
读取鼠标或触屏按下、抬起
        ↓
更新所有 TouchPoint 的位置
        ↓
根据时间和距离识别单击、双击
        ↓
检查当前是否有输入框获得焦点
        ↓
设置 UI 或 SCENE 输入掩码
        ↓
收集本帧按下和抬起的键
        ↓
计算 Ctrl、Shift、Alt 状态
        ↓
分发固定快捷键监听

lateUpdate() 再清除当前帧状态:

bash 复制代码
CurrentDown
CurrentUp
Click
DoubleClick

所以这类状态只在当前帧有效,业务系统应当在正常更新阶段读取。

十二、总结

MyFramework 将输入分为两个层次:

bash 复制代码
InputSystem
统一采集键盘、鼠标、触摸和输入焦点

KeyMappingSystem
把具体按键转换为可配置的游戏行为

使用原则可以概括为:

bash 复制代码
玩家可以修改的游戏操作
使用 KeyMappingSystem

固定的系统或调试快捷键
使用 listenKeyCurrentDown

持续操作
使用 isMappingKeyDown

单次操作
使用 isMappingKeyCurrentDown

对象销毁
调用 unlistenKey

输入框正在输入
由 FOCUS_MASK 自动屏蔽场景快捷键

这样角色移动、技能快捷键、UGC 编辑器操作和界面输入可以使用同一套输入状态,同时保持游戏行为与具体按键之间的独立。

相关推荐
_ZHOURUI_H_3 小时前
Unity MyFramework 用法说明(二十五):使用 AtlasManager 统一管理图集与 Sprite 引用
ui·unity·游戏引擎·unity3d·游戏开发
_zhourui_h_5 小时前
Unity MyFramework 用法说明(二十六):使用 RedPointSystem 构建树形红点逻辑
unity3d
SmalBox7 小时前
【节点】[Not节点]原理解析与实际应用
unity3d·游戏开发·图形学
万亿少女的梦1681 天前
基于Unity3D的双状态场景解密游戏设计与实现
unity3d·游戏开发·场景渲染·交互系统·叙事设计
SmalBox1 天前
【节点】[Nand节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[IsNaN节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[IsInfinite节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_4 天前
Unity MyFramework 用法说明(二十二):使用 AudioManager 统一管理背景音乐与 2D/3D 音效
unity3d
SmalBox4 天前
【节点】[IsFrontFace节点]原理解析与实际应用
unity3d·游戏开发·图形学