角色移动、技能快捷键、编辑器操作和界面关闭都需要读取输入。如果业务代码到处直接调用 Input.GetKeyDown(),按键规则、输入焦点和改键功能就会分散在不同系统中。
MyFramework 使用 InputSystem 统一收集键盘、鼠标和触摸状态,再通过 KeyMappingSystem 将具体按键映射为游戏行为。
项目地址:
需要注意,这里的 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 编辑器操作和界面输入可以使用同一套输入状态,同时保持游戏行为与具体按键之间的独立。