↓ 跳过正文
  1. 文章/

Seythm:用 Rust 写一个终端原生音游

Rust Rust 终端 游戏开发 Ratatui 音游 TUI,projects

目录
这是什么

一个跑在终端里的 6 键节奏游戏。Rust 写的,ratatui 渲染界面,cpal 处理音频,支持导入 osu!mania 谱面。在 16ms 帧率的终端里做音游,听起来离谱,但跑起来真的能玩。


为什么要在终端里做音游
#

音游通常是图形密集型应用——下落的音符、炫酷的特效、实时的反馈。终端环境看起来完全不适合。

但终端有自己的优势:零依赖、SSH 可玩、资源占用极低。如果能在终端里做出流畅的音游体验,说明对系统底层的控制已经到位了。

说实话,一开始只是想练 Rust。选音游是因为技术挑战足够多:实时音频、毫秒级输入判定、60fps 渲染、文件格式解析。每个模块都能深入纯好玩。


架构
#

项目分 8 个模块,各管各的:

src/
├── app/          主应用流程:启动闪屏、歌曲选择、游戏循环
├── audio/        cpal 音频播放 + 时钟同步
├── chart/        谱面解析、调度、投影
├── config/       配置加载、按键映射、品牌定制
├── content/      曲目目录(内置 + osu 导入)
├── gameplay/     判定逻辑、计分、状态机
├── osu/          osu!mania 谱面解析与导入
├── runtime/      游戏时钟、输入处理、回放
└── ui/           ratatui 组件 + kitty 图片协议

核心依赖:

ratatui = "0.29"      # TUI 框架
crossterm = "0.28"    # 终端控制
cpal = "0.15"         # 跨平台音频
symphonia = "0.5"     # 音频解码(MP3/OGG/WAV)
image = "0.25"        # 封面图处理
为什么不用 crossterm 的音频

crossterm 只管终端控制(光标、颜色、输入事件),不管音频。音频需要单独的库。cpal 是 Rust 生态里最成熟的跨平台音频库,symphonia 负责解码各种音频格式。


游戏循环:16ms 一帧
#

音游对帧率敏感。Seythm 用固定 16ms 帧时间(约 60fps)驱动游戏循环:

const FRAME_TIME: Duration = Duration::from_millis(16);

每帧做三件事:

  1. 轮询输入——crossterm 的非阻塞事件,不会卡渲染
  2. 推进状态——音符下落、判定触发、计分更新
  3. 渲染 UI——ratatui 画到终端
loop {
    let frame_start = Instant::now();
    
    // 1. 输入
    poll_input(&mut app);
    
    // 2. 状态
    app.advance(current_time);
    
    // 3. 渲染
    terminal.draw(|f| render_demo(f, &app))?;
    
    // 等到下一帧
    let elapsed = frame_start.elapsed();
    if elapsed < FRAME_TIME {
        std::thread::sleep(FRAME_TIME - elapsed);
    }
}
为什么不用异步

游戏循环不需要 async。每一帧都是同步的:读输入 → 更新状态 → 画界面。异步反而会增加复杂度,没有实际收益。


判定系统
#

音游的核心是判定——玩家按下的时间和音符应该到达的时间差多少。

Seythm 实现了标准四档判定:

判定 分值 含义
Perfect 1000 几乎完全对齐
Great 800 稍有偏差
Good 500 勉强算命中
Miss 0 没按到或差太远

判定逻辑很直接——算偏移量,看落在哪个窗口:

pub fn classify_offset(self, offset_ms: i64) -> Judgment {
    let distance = offset_ms.abs();
    if distance <= self.perfect_ms {
        Judgment::Perfect
    } else if distance <= self.great_ms {
        Judgment::Great
    } else if distance <= self.good_ms {
        Judgment::Good
    } else {
        Judgment::Miss
    }
}

这里有个坑

判定窗口不能太大也不能太小。太大会让游戏无聊(怎么按都 Perfect),太小会让人崩溃(明明按对了还是 Miss)。参考 osu!mania 的默认值,再根据终端输入的延迟微调。


音频时钟同步
#

音频播放和游戏时钟必须同步。如果音频有延迟但游戏时钟没跟着调,判定就会偏——你觉得按对了,但系统说你偏了。

Seythm 用 cpal 做底层音频输出,配合独立的游戏时钟模块(runtime/clock.rs)。音频回调和游戏逻辑共享同一个时间基准。

// 音频回调里更新时钟
fn audio_callback(data: &mut [f32], info: &OutputCallbackInfo) {
    // cpal 提供精确的音频时间戳
    let audio_time = info.timestamp();
    // 用音频时间校准游戏时钟
    clock.sync(audio_time);
}

osu!mania 谱面导入
#

Seythm 支持导入 osu!mania 的 beatmap 文件。这意味着有成千上万的现成谱面可以用。

.osu 文件是一个文本格式,包含元数据、timing point、hit object 等信息:

[Metadata]
Title:某首歌
Artist:某个人
[TimingPoints]
1000,300,4,1,0,100,1,0
[HitObjects]
64,192,1000,1,0,0:0:0:0:

解析器在 osu/parser.rs 里,把文本格式转换成内部的 Chart 结构。导入流程在 osu/import.rs——读文件、解析、验证、保存到曲目目录。

为什么只支持 osu!mania

osu! 有多个模式(standard、taiko、catch、mania)。Seythm 是 6 键音游,只有 mania 模式的 6K 谱面能直接用。其他模式暂时没做。


终端图片协议
#

Seythm 支持在终端里显示专辑封面。实现方式是 kitty 图片协议——一种终端内嵌图片的标准。

// 检测终端是否支持 kitty 图片协议
fn terminal_supports_graphics() -> bool {
    // 发送查询指令,看终端回复什么
    // kitty/ghostty 会回复支持信息
    // 其他终端会忽略
}

如果终端不支持(比如 GNOME Terminal),自动降级为纯文本模式。游戏本身不受影响,只是没有封面图。


交互设计
#

终端的 UI 空间有限。Seythm 用 ratatui 的 widget 系统把界面拆成独立组件:

  • startup — 启动闪屏 + ASCII logo
  • song_select — 歌曲浏览、搜索、排序
  • gameplay — 6 车道下落音符 + 判定反馈
  • results — 结算画面(分数、准确率、最大连击)
  • settings — 设置面板(音量、延迟校准、主题)
  • calibration — 延迟校准工具

品牌信息(名称、标语、logo)通过 assets/brand.toml 配置,不硬编码在代码里。这样换一套品牌就是另一个产品。


测试
#

项目有 13 个测试文件:

tests/
├── judgment_windows.rs    判定窗口边界
├── scoring.rs             计分正确性
├── chart_parser.rs        谱面解析
├── osu_import.rs          osu 导入
├── osu_parser.rs          .osu 文件解析
├── replay_flow.rs         回放系统
├── clock_sync.rs          时钟同步
├── config_loading.rs      配置加载
├── content_catalog.rs     曲目目录
├── chart_scheduler.rs     谱面调度
├── demo_state.rs          演示模式
├── imported_view.rs       导入视图
└── judgment_windows.rs    判定窗口

游戏逻辑和 UI 完全分离,核心算法全部可测。这也是为什么模块拆得这么细——gameplay 不依赖 ui,chart 不依赖 audio,每个模块都能独立测试。


状态
#

Alpha 阶段。核心循环可玩,但产品还在快速迭代。

  • Linux:主要开发平台,最稳定
  • macOS:理论上可行,需要真机测试
  • Windows:代码能编译,但终端图片预览和设备行为需要验证
怎么跑
# 构建并运行
cargo run

# 自动演示模式(看 AI 怎么打)
cargo run -- autoplay

# 导入 osu!mania 谱面
cargo run -- --import-osu /path/to/beatmap-folder

相关文章