按键码子系统(CategoryKeycode)
该模块定义了用于标识键盘按键和修饰键的核心常量,是 SDL 键盘输入处理的基础数据类型定义模块。
函数
- (无)
数据类型
- SDL_Keycode:按键码类型(用于标识键盘上的「符号键」,如 SDLK_a 代表字母 A、SDLK_ESCAPE 代表 ESC 键,与键盘布局相关)
- SDL_Keymod:修饰键状态类型(用于标识 Shift/Ctrl/Alt/GUI 等修饰键的按下状态,可通过位运算组合多个修饰键)
结构体
- (无)
枚举
- (无)
宏
- (无)
FreeBASIC 示例代码
' 引入 SDL 相关声明(需确保 FreeBASIC 已链接 SDL3 库)
#Include "SDL.bi"
' 辅助函数:解析修饰键状态并返回描述字符串
Function GetModStateString(ByVal modState As SDL_Keymod) As String
Dim As String modStr = ""
' 位运算判断修饰键状态(支持多键组合)
If (modState And SDL_MOD_SHIFT) Then modStr &= "Shift "
If (modState And SDL_MOD_CTRL) Then modStr &= "Ctrl "
If (modState And SDL_MOD_ALT) Then modStr &= "Alt "
If (modState And SDL_MOD_GUI) Then modStr &= "GUI " ' Windows/Command 键
If (modState And SDL_MOD_CAPS) Then modStr &= "CapsLock "
If (modState And SDL_MOD_NUM) Then modStr &= "NumLock "
If (modStr = "") Then modStr = "无修饰键"
Return modStr
End Function
' 辅助函数:打印按键码相关信息
Sub PrintKeycodeInfo(ByVal keycode As SDL_Keycode)
Dim As ZString Ptr keyName = SDL_GetKeyName(keycode)
Dim As SDL_Scancode scancode = SDL_GetScancodeFromKey(keycode)
Dim As ZString Ptr scName = SDL_GetScancodeName(scancode)
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"按键码:0x%X | 按键名称:%s | 对应扫描码:0x%X | 扫描码名称:%s", _
keycode, keyName, scancode, scName)
SDL_free(keyName)
SDL_free(scName)
End Sub
' 主程序
Dim As SDL_Window Ptr window = NULL
Dim As SDL_Event evt
Dim As Boolean quit = False
' 1. 初始化 SDL
If (SDL_Init(SDL_INIT_VIDEO) < 0) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL 初始化失败:%s", SDL_GetError())
End 1
End If
' 2. 创建窗口
window = SDL_CreateWindow("SDL Keycode 示例", _
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, _
600, 400, SDL_WINDOW_SHOWN)
If (window = NULL) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "创建窗口失败:%s", SDL_GetError())
SDL_Quit()
End 1
End If
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "=== SDL_Keycode / SDL_Keymod 示例 ===")
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "按任意键查看按键码信息 | 按 F1 查看当前修饰键状态 | 按 ESC 退出")
' 3. 主事件循环
While (Not quit)
While (SDL_PollEvent(@evt))
Select Case evt.type
' 窗口关闭事件
Case SDL_QUITEVENT
quit = True
' 键盘按键事件
Case SDL_EVENT_KEY_DOWN
Select Case evt.key.key
Case SDLK_ESCAPE
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "按下 ESC 键,退出程序")
quit = True
Case SDLK_F1
' 获取并打印当前修饰键状态
Dim As SDL_Keymod modState = SDL_GetModState()
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "当前修饰键状态:%s", GetModStateString(modState))
Case Else
' 打印按键码详细信息
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "==== 按键信息 ====")
PrintKeycodeInfo(evt.key.key)
' 打印该按键的修饰键状态
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "按下时的修饰键:%s", GetModStateString(evt.key.mod))
End Select
' 手动设置修饰键示例(按 F2 模拟按下 Shift+Ctrl)
Case SDL_EVENT_KEY_UP
If (evt.key.key = SDLK_F2) Then
Dim As SDL_Keymod newModState = SDL_MOD_SHIFT Or SDL_MOD_CTRL
SDL_SetModState(newModState)
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "手动设置修饰键状态为:%s", GetModStateString(newModState))
End If
End Select
Wend
SDL_Delay(10)
Wend
' 4. 清理资源
SDL_DestroyWindow(window)
SDL_Quit()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "程序正常退出")
End 0
核心常量补充说明
在实际使用中,SDL 预定义了大量常用的 SDL_Keycode 常量,以下是最常用的分类:
| 类别 | 示例常量 | 说明 |
|---|---|---|
| 字母键 | SDLK_a, SDLK_b, SDLK_Z | 大小写统一用大写字母标识 |
| 数字键 | SDLK_0, SDLK_1, SDLK_9 | 主键盘数字键 |
| 功能键 | SDLK_F1, SDLK_F2, SDLK_F12 | 功能键 F1-F12 |
| 控制键 | SDLK_ESCAPE, SDLK_RETURN, SDLK_TAB | 常用控制键 |
| 方向键 | SDLK_UP, SDLK_DOWN, SDLK_LEFT, SDLK_RIGHT | 方向键 |
| 修饰键 | 无(修饰键用 SDL_Keymod 表示) | Shift/Ctrl/Alt 等 |
SDL_Keymod 常用常量:
SDL_MOD_SHIFT:Shift 键(左/右 Shift 均适用)SDL_MOD_CTRL:Ctrl 键(左/右 Ctrl 均适用)SDL_MOD_ALT:Alt 键(左/右 Alt 均适用)SDL_MOD_GUI:GUI 键(Windows 键/苹果 Command 键)SDL_MOD_CAPS:CapsLock 锁定状态SDL_MOD_NUM:NumLock 锁定状态
总结
- SDL_Keycode 是标识「按键符号」的核心类型,与键盘布局绑定(如 SDLK_a 始终代表字母 A,无论键盘布局如何);
- SDL_Keymod 是标识修饰键状态的类型,支持位运算组合(如
SDL_MOD_SHIFT Or SDL_MOD_CTRL表示同时按下 Shift+Ctrl); - 该模块无独立函数,仅定义基础数据类型和常量,需配合键盘子系统(CategoryKeyboard)的函数使用(如
SDL_GetKeyName、SDL_GetModState)。
SDL3 键盘使用最佳实践
核心前提
键盘输入看似简单,但一旦超出你常用的语言/地区范围(甚至在同一地区),就会变得异常复杂。
几乎所有游戏,无论采用何种输入方案,都应该提供按键绑定配置功能:
- 这不仅能解决不同键盘布局的适配问题;
- 还能让用户将按键调整到最舒适的位置(尤其对行动不便的用户)。
即使不做可视化配置界面,至少也要提供配置文件——这会让很多用户感激不尽!
四种核心使用场景
SDL 处理键盘输入主要有四种常见方式,应用可能在不同场景下混用不同方式。
场景1:「101键游戏手柄」模式
很多游戏把键盘当作「超多按键的手柄」,而非文本输入工具。典型例子是 FPS 游戏的「WASD」移动:
- 你关心的是物理按键位置,而非按键上印的符号;
- 比如法语键盘对应「ZQSD」、日语平假名键盘对应「てちとし」,但都是键盘上相同的物理位置。
这种场景下应使用 SDL_Scancode(扫描码):
- 扫描码绑定物理按键位置,与按键上的符号无关;
- 底层默认参考美式 QWERTY 键盘布局,这恰好符合「只关心位置」的需求。
事件(Events) vs 状态(States)
处理键盘/鼠标/手柄按键时,开发者常先想到事件,但两者的选择会直接影响体验:
概念区别
- 事件(Events):系统/SDL 通知你「发生了某件事」(如按键按下/释放),不一定需要立即响应;
- 适合「单次动作」(如跳跃、换弹);
- 事件不会每帧触发,即使有按键重复,也会有延迟。
- 状态(States):代表「事件处理完成后按键的最新状态」;
- 适合「持续动作」(如移动);
- 可在每一帧查询,避免事件重复的延迟问题。
按键重复(Key Repeat)说明
- 按键事件不会每帧发送,SDL 也无法感知你的帧循环时机;
- 系统会触发「按键重复事件」:首次重复延迟约1秒,之后每200ms一次(具体值由系统/用户设置决定);
- 用事件处理移动会导致「卡顿」(类似文本编辑器按住按键的字符输入间隔)。
实战示例
状态查询(适合持续动作)
通过 SDL_GetKeyboardState 获取按键状态数组(索引为扫描码),SDL 管理数组生命周期,建议缓存以减少调用次数。
' 功能:返回玩家移动方向(1=前进,-1=后退,0=不动)
Function GetMoveDirection() As Integer
Dim As UByte Ptr keyStates = SDL_GetKeyboardState(NULL)
Dim As Integer direction = 0
' 同时按下前进/后退键时,方向抵消(符合直觉)
If (keyStates[SDL_SCANCODE_W] = 1) Then
direction += 1 ' 美式键盘 W 键位置:前进
End If
If (keyStates[SDL_SCANCODE_S] = 1) Then
direction -= 1 ' 美式键盘 S 键位置:后退
End If
Return direction
End Function
事件处理(适合单次动作)
从 SDL_EVENT_KEY_DOWN/UP 事件中提取 scancode 字段,触发单次动作。
Enum Action
ACTION_NONE
ACTION_RELOAD
ACTION_JUMP
End Enum
' 功能:根据按键事件返回玩家要执行的动作
Function GetPlayerAction(ByRef evt As SDL_Event) As Action
If (evt.type <> SDL_EVENT_KEY_DOWN) Then
Return ACTION_NONE
End If
Select Case evt.key.scancode
Case SDL_SCANCODE_R
Return ACTION_RELOAD ' R 键位置:换弹
Case SDL_SCANCODE_SPACE
Return ACTION_JUMP ' 空格键位置:跳跃
Case Else
Return ACTION_NONE
End Select
End Function
场景2:「特定按键」模式
部分游戏需要检测「按键上的符号」,而非物理位置:
- 比如「按 I 打开背包」—— 不管 I 键在键盘的哪个位置;
- 也包括 ESC 取消、Enter 确认等通用按键。
(但仍建议提供按键绑定!有些键盘可能没有 I 键。)
这种场景下应使用 SDL_Keycode(按键码):
- 按键码绑定「符号」,与物理位置无关;
- 同样从
SDL_EVENT_KEY_DOWN/UP事件中获取。
' 功能:循环等待用户按下 ESC 键退出
Dim As Boolean quitApp = False
Dim As SDL_Event evt
While (Not quitApp)
While (SDL_PollEvent(@evt))
If (evt.type = SDL_EVENT_KEY_DOWN) Then
If (evt.key.key = SDLK_ESCAPE) Then
quitApp = True
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "按下 ESC,退出程序")
End If
End If
Wend
SDL_Delay(10)
Wend
场景3:「聊天框/文本输入」模式
Unicode 处理极其复杂!如果需要输入文本(如角色命名、聊天框),绝对不要用按键事件——这无法适配全球不同的键盘和语言。
正确做法:
- 调用
SDL_StartTextInput启用文本输入; - 监听
SDL_EVENT_TEXT_INPUT事件(系统会返回 UTF-8 编码的 Unicode 字符串); - 输入完成后调用
SDL_StopTextInput。
这种方式的优势:
- 适配系统原生输入方式(如移动平台弹出虚拟键盘、多语言输入法候选词);
- 你无法自己实现覆盖全球所有语言的输入方案,不要尝试基于按键事件造轮子。
缺点:虚拟键盘可能遮挡游戏界面,需要提前设计布局。
' 功能:实现简单的文本输入框
Dim As Boolean inputComplete = False
Dim As String inputText = ""
Dim As SDL_Rect inputArea = Type<SDL_Rect>(100, 100, 400, 50) ' 输入区域
Dim As Integer cursorPos = 0
Dim As SDL_Event evt
Dim As SDL_Window Ptr window = SDL_CreateWindow("文本输入示例", _
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, _
600, 400, SDL_WINDOW_SHOWN)
' 设置文本输入区域并启用输入
SDL_SetTextInputArea(window, @inputArea, cursorPos)
SDL_StartTextInput(window)
While (Not inputComplete)
While (SDL_PollEvent(@evt))
Select Case evt.type
Case SDL_EVENT_KEY_DOWN
' ESC 取消 / Enter 确认
If (evt.key.key = SDLK_ESCAPE Or evt.key.key = SDLK_RETURN) Then
SDL_StopTextInput(window)
inputComplete = True
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "输入完成:%s", inputText)
End If
Case SDL_EVENT_TEXT_INPUT
' 追加输入的文本(UTF-8 编码)
inputText &= *evt.text.text
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "当前输入:%s", inputText)
End Select
Wend
' 更新输入区域(适配输入法候选词位置)
SDL_SetTextInputArea(window, @inputArea, cursorPos)
SDL_Delay(10)
Wend
SDL_DestroyWindow(window)
全屏游戏如需自定义输入法UI,可设置 SDL_HINT_IME_IMPLEMENTED_UI 提示,并处理 SDL_EVENT_TEXT_EDITING 和 SDL_EVENT_TEXT_EDITING_CANDIDATES 事件。参考示例:testime.c
场景4:「文本编辑器」模式
这是特殊场景,非必要不要使用——多数时候你只是想「错误地从零实现聊天框」。
如果要做 SDL 版 Vim 这类工具,需要处理:
- 修饰键组合(如 Shift+Z 两次);
- 小键盘方向键(NumLock 切换数字/方向);
- 按键事件与文本输入事件的关联。
此时需要同时处理按键事件和输入事件,并通过 SDL_GetKeyFromScancode 获取带修饰键的按键码:
' 功能:检测按下的 $ 键(美式键盘 Shift+4 / 法语键盘直接按 $)
Dim As Boolean quitApp = False
Dim As SDL_Event evt
While (Not quitApp)
While (SDL_PollEvent(@evt))
If (evt.type = SDL_EVENT_KEY_DOWN) Then
' 获取带修饰键的按键码
Dim As SDL_Keycode keycode = SDL_GetKeyFromScancode(evt.key.scancode, evt.key.mod, False)
If (keycode = Asc("$")) Then
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "按下了 $ 键!")
End If
' ESC 退出
If (evt.key.key = SDLK_ESCAPE) Then
quitApp = True
End If
End If
Wend
SDL_Delay(10)
Wend
注意:很多按键不生成字符,部分字符需要多键组合/输入法生成,这是小众需求,不适合通用输入场景。
向用户展示按键名称
实现自定义按键绑定时,需要在UI中显示按键名称(如「按 [空格] 跳跃」),应使用 SDL_GetKeyName 配合按键码:
' 功能:打印用户按下的按键名称
Dim As Boolean quitApp = False
Dim As SDL_Event evt
While (Not quitApp)
While (SDL_PollEvent(@evt))
If (evt.type = SDL_EVENT_KEY_DOWN) Then
Dim As ZString Ptr keyName = SDL_GetKeyName(evt.key.key)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "你按下了 %s 键", keyName)
SDL_free(keyName) ' 释放字符串
' ESC 退出
If (evt.key.key = SDLK_ESCAPE) Then
quitApp = True
End If
End If
Wend
SDL_Delay(10)
Wend
注意:
SDL_GetKeyName仅返回大写字符,这适合向用户展示「按这个符号的按键」。
完整示例(整合所有场景)
#Include "SDL.bi"
Enum PlayerAction
ACTION_NONE
ACTION_JUMP
ACTION_RELOAD
End Enum
' 获取移动方向(状态查询)
Function GetMoveDirection() As Integer
Dim As UByte Ptr keyStates = SDL_GetKeyboardState(NULL)
Dim As Integer dir = 0
If (keyStates[SDL_SCANCODE_W]) Then dir += 1
If (keyStates[SDL_SCANCODE_S]) Then dir -= 1
Return dir
End Function
' 获取玩家动作(事件处理)
Function GetPlayerAction(ByRef evt As SDL_Event) As PlayerAction
If (evt.type <> SDL_EVENT_KEY_DOWN) Then Return ACTION_NONE
Select Case evt.key.scancode
Case SDL_SCANCODE_SPACE: Return ACTION_JUMP
Case SDL_SCANCODE_R: Return ACTION_RELOAD
Case Else: Return ACTION_NONE
End Select
End Function
' 主程序
Dim As SDL_Window Ptr window = NULL
Dim As SDL_Event evt
Dim As Boolean quit = False, textInputActive = False
Dim As String chatText = ""
' 初始化 SDL
If (SDL_Init(SDL_INIT_VIDEO) < 0) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL 初始化失败:%s", SDL_GetError())
End 1
End If
' 创建窗口
window = SDL_CreateWindow("SDL 键盘最佳实践示例", _
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, _
800, 600, SDL_WINDOW_SHOWN)
If (window = NULL) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "创建窗口失败:%s", SDL_GetError())
SDL_Quit()
End 1
End If
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "=== 操作说明 ===")
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "W/S:移动(状态查询)| 空格:跳跃/R:换弹(事件)| T:打开聊天框 | ESC:退出")
' 主循环
While (Not quit)
' 处理移动(每帧查询状态)
Dim As Integer moveDir = GetMoveDirection()
If (moveDir <> 0) Then
Static As Integer lastDir = 0
If (moveDir <> lastDir) Then
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "移动方向:%s", IIf(moveDir=1, "前进", "后退"))
lastDir = moveDir
End If
End If
' 处理事件
While (SDL_PollEvent(@evt))
Select Case evt.type
Case SDL_QUITEVENT
quit = True
Case SDL_EVENT_KEY_DOWN
' ESC 退出
If (evt.key.key = SDLK_ESCAPE) Then
quit = True
End If
' T 键打开聊天框
If (evt.key.key = SDLK_t And Not textInputActive) Then
textInputActive = True
chatText = ""
SDL_StartTextInput(window)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "聊天框已打开,输入文本后按 Enter 发送")
End If
' 处理玩家动作
Dim As PlayerAction action = GetPlayerAction(evt)
Select Case action
Case ACTION_JUMP: SDL_LogInfo(SDL_LOG_CATEGORY_GAME, "玩家跳跃")
Case ACTION_RELOAD: SDL_LogInfo(SDL_LOG_CATEGORY_GAME, "玩家换弹")
End Select
Case SDL_EVENT_TEXT_INPUT
If (textInputActive) Then
chatText &= *evt.text.text
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "聊天输入:%s", chatText)
End If
Case SDL_EVENT_KEY_UP
' Enter 发送聊天
If (evt.key.key = SDLK_RETURN And textInputActive) Then
SDL_StopTextInput(window)
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "发送聊天:%s", chatText)
textInputActive = False
End If
End Select
Wend
SDL_Delay(10)
Wend
' 清理资源
SDL_DestroyWindow(window)
SDL_Quit()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "程序正常退出")
End 0
总结
- 核心原则:始终提供按键绑定配置,适配不同键盘和用户需求;
- 场景选择:
- 物理位置(移动)→ 扫描码 + 状态查询;
- 按键符号(ESC/Enter)→ 按键码 + 事件;
- 文本输入(聊天/命名)→
SDL_StartTextInput+ 文本输入事件; - 复杂编辑器 → 混合按键/输入事件 + 修饰键处理;
- 关键避坑:不要基于按键事件实现多语言文本输入,依赖系统原生输入法接口。
评论一下?