You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Swift 5控制台应用实现非阻塞键盘输入?含C代码转Swift需求

实现Swift 5非阻塞键盘输入的控制台游戏

嘿,我完全懂你的痛点——想用Swift做个带emoji动画的控制台游戏,既要让动画流畅跑不卡,又能随时响应键盘输入,结果发现原来C里用的FD_ZERO、FD_SET这些宏在Swift的Darwin模块里找不到,直接搬C代码根本跑不通对吧?别慌,咱们可以直接操作底层内存模拟这些宏的功能,再结合POSIX的select函数来实现非阻塞IO。

下面是完整的等效Swift实现,我会一步步给你解释:

核心思路

默认终端是**行缓冲(canonical)模式,必须按回车才会把输入传给程序,所以首先得把终端改成字符缓冲(non-canonical)**模式,让输入的字符立即生效。然后用select函数监听标准输入的文件描述符,设置一个极短的超时时间——这样每次循环里,程序先检查有没有键盘输入:有就处理,没有就继续跑动画,完美实现“无输入时游戏持续运行,有输入则响应”的需求。

完整代码示例

import Darwin

// 保存原始终端属性,退出时恢复
var originalTermios = termios()
tcgetattr(STDIN_FILENO, &originalTermios)
defer {
    tcsetattr(STDIN_FILENO, TCSANOW, &originalTermios)
}

// 设置终端为非规范模式,关闭回显
var termios = originalTermios
termios.c_lflag &= ~UInt(ICANON)  // 关闭行缓冲
termios.c_lflag &= ~UInt(ECHO)    // 关闭输入回显
termios.c_cc[VMIN] = 0            // 最少读取0个字符(无输入也返回)
termios.c_cc[VTIME] = 1           // 超时时间1/10秒(100ms)
tcsetattr(STDIN_FILENO, TCSANOW, &termios)

// 游戏状态示例:emoji的位置
var emojiPosition = 0
let maxPosition = 20

print("欢迎来到Emoji跑酷游戏!按左右方向键移动,按Q退出\n")

while true {
    // 1. 准备fd_set:因为Swift没有FD_ZERO/FD_SET宏,手动操作内存
    var fdSet = fd_set()
    withUnsafeMutablePointer(to: &fdSet) { ptr in
        // FD_ZERO:把fd_set的所有bit置0
        ptr.withMemoryRebound(to: Int8.self, capacity: MemoryLayout<fd_set>.size) { bytePtr in
            memset(bytePtr, 0, MemoryLayout<fd_set>.size)
        }
        // FD_SET:把stdin的文件描述符(0)对应的bit置1
        let fd = STDIN_FILENO
        let byteIndex = fd / 8
        let bitIndex = fd % 8
        ptr.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout<fd_set>.size) { bytePtr in
            bytePtr[byteIndex] |= (1 << bitIndex)
        }
    }

    // 2. 设置超时时间:这里设为10ms,平衡动画流畅度和响应速度
    var timeout = timeval(tv_sec: 0, tv_usec: 10_000)

    // 3. 调用select检查是否有输入
    let result = select(STDIN_FILENO + 1, &fdSet, nil, nil, &timeout)

    if result > 0 {
        // 有输入:读取字符并处理
        var input = [UInt8](repeating: 0, count: 3)  // 方向键是转义序列,占3个字节
        let bytesRead = read(STDIN_FILENO, &input, input.count)
        
        if bytesRead > 0 {
            switch input {
            case [0x1B, 0x5B, 0x43]:  // 右方向键 →
                emojiPosition = min(emojiPosition + 1, maxPosition)
            case [0x1B, 0x5B, 0x44]:  // 左方向键 ←
                emojiPosition = max(emojiPosition - 1, 0)
            case [0x71, _, _], [0x51, _, _]:  // q/Q 退出
                print("\n游戏结束!")
                exit(0)
            default:
                break
            }
        }
    }

    // 4. 无输入时更新游戏动画
    let spaces = String(repeating: " ", count: emojiPosition)
    print("\r\(spaces)🏃‍♂️", terminator: "")
    fflush(stdout)  // 强制刷新输出缓冲区,确保动画实时显示
}

关键细节解释

  1. 终端模式修改:

    • 用tcgetattr保存原始终端设置,defer语句确保程序退出时恢复,不然终端会一直处于无回显、行缓冲关闭的状态,影响后续使用。
    • ICANON关闭行缓冲,让输入的字符立即被程序读取;ECHO关闭回显,避免输入的方向键序列显示在屏幕上干扰动画。
  2. 模拟FD_ZERO/FD_SET:

    • fd_set本质是一个字节数组,每个bit对应一个文件描述符。我们通过内存重绑定,用memset把所有字节置0(等效FD_ZERO),然后手动把stdin对应的bit设为1(等效FD_SET)。
  3. select函数的作用:

    • select会监听指定的文件描述符集合,超时时间到了或者有数据可读时返回。返回值>0说明有输入,我们就读取并处理;返回0说明超时,直接跑动画。
  4. 方向键处理:

    • 控制台的方向键是转义序列(比如右方向键是ESC[C,对应十六进制0x1B 0x5B 0x43),所以要读取3个字节来判断。

内容的提问来源于stack exchange,提问作者Bee Jay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:29:57