# TetrisGame **Repository Path**: lsqyling/TetrisGame ## Basic Information - **Project Name**: TetrisGame - **Description**: Qt 6.8 / QML + C++20 经典俄罗斯方块,无边框窗口、程序化芯片音效、Korobeiniki 背景音乐。 - **Primary Language**: C++ - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-06-28 - **Last Updated**: 2026-08-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 俄罗斯方块 — Classic Tetris Qt 6.8 / QML + C++20 经典俄罗斯方块,无边框窗口、程序化芯片音效、Korobeiniki 背景音乐。

Qt 6.8 C++20 CMake 3.25+

--- ## 目录 - [快速开始](#快速开始) - [项目架构](#项目架构) - [核心设计](#核心设计) - [C++ ↔ QML 桥接](#c--qml-桥接) - [游戏引擎](#游戏引擎-gameengine) - [棋盘渲染](#棋盘渲染-gameboardqml) - [音频引擎](#音频引擎-audioengine) - [窗口 UI](#窗口-ui-mainqml) - [游戏控制](#游戏控制) - [游戏机制](#游戏机制) - [双实现模式](#双实现模式) - [关键设计决策](#关键设计决策) --- ## 快速开始 ```powershell # 配置(指向你的 Qt 6.8 安装路径) cmake -B build -DCMAKE_PREFIX_PATH=D:/dev/tools/Qt/6.8.3/msvc2022_64 # 编译 cmake --build build --config Release # 运行 ./build/Release/TetrisGame.exe ``` **依赖:** Qt 6.8+ (Quick + Multimedia)、CMake 3.25+、MSVC 2022 (C++20) --- ## 项目架构 ``` TetrisGame/ ├── CMakeLists.txt # 构建配置 (qt_add_qml_module × 2) ├── main.cpp # 入口:注册 C++ 单例、加载 QML 模块 ├── cmake/ # CMake 辅助 (编译选项、windeployqt) ├── src/ │ ├── GameEngine.h / .cpp # C++ 游戏引擎 │ └── AudioEngine.h / .cpp # C++ 芯片音效合成 ├── qml/ │ ├── Main.qml # 根窗口 (无边框、浮动按钮、拖拽) │ ├── qmldir # QML 模块清单 │ ├── basic/ │ │ └── TetrisConfig.qml # 单例配置 (颜色、布局常量) │ ├── components/ │ │ ├── GameBoard.qml # 棋盘 (原生 Rectangle + Repeater) │ │ ├── NextPiecePreview.qml # 下一方块预览 (Canvas) │ │ ├── ScorePanel.qml # 分数/等级/消行 + 开始/暂停按钮 │ │ ├── GameButton.qml # 主题按钮 (primary/secondary) │ │ ├── KeyLabel.qml # 键盘快捷键徽章 │ │ └── BackgroundEffect.qml # 粒子动画背景 (Canvas) │ └── pages/ │ └── GamePage.qml # 三栏主布局 (左/中/右) └── index.html # 浏览器参考实现 (无编译,逻辑相同) ``` ### 分层职责 | 层 | 技术 | 职责 | |----|------|------| | **游戏逻辑** | C++ `GameEngine` | 棋盘状态、碰撞检测、消行计分、7-bag 随机、墙踢 | | **音频** | C++ `AudioEngine` | PCM 波形合成、BGM 循环、SFX 播放 | | **渲染** | QML `Rectangle` + `Repeater` | 棋盘 400 格原生渲染、方块颜色绑定 | | **UI** | QML Window | 无边框窗口、浮动按钮、全窗口拖拽、键盘处理 | --- ## 核心设计 ### C++ ↔ QML 桥接 C++ 对象通过 `qmlRegisterSingletonInstance` 注册为 QML 单例——**不使用 `QQmlContext::setContextProperty()`**(后者在 Qt 6 中不会传递到 QML 模块)。 ```cpp // main.cpp GameEngine gameEngine; AudioEngine audioEngine; qmlRegisterSingletonInstance("GameBackend", 1, 0, "GameEngine", &gameEngine); qmlRegisterSingletonInstance("GameBackend", 1, 0, "AudioEngine", &audioEngine); ``` ```qml // QML 中直接使用 import GameBackend 1.0 Text { text: GameEngine.score.toLocaleString() } GameButton { onClicked: GameEngine.startGame() } ``` ### QML 模块系统 项目包含两个 QML 模块: | 模块 | URI | 类型 | 说明 | |------|-----|------|------| | TetrisGame | `TetrisGame` | `qt_add_qml_module` | 主模块,包含所有页面和组件 | | TetrisConfig | `TetrisConfig` | 静态库 + `QT_QML_SINGLETON_TYPE` | 全局配置单例(颜色、布局) | `qmldir` 显式列出所有组件名称和路径,确保 qmlcachegen 正确解析模块内的类型引用。 ### 游戏引擎 (`GameEngine`) **状态模型:** ``` m_board: QList> → 20 行 × 10 列 0 = 空 1…7 = 方块类型 + 1(1-based 存储) m_currentPiece: Piece → { type, shape, x, y } 当前下落中的方块 m_bag: QList → 7-bag 随机队列 ``` **核心算法:** | 算法 | 细节 | |------|------| | 随机 | 7-bag Fisher-Yates 洗牌,袋空自动填充 | | 碰撞 | 遍历 shape 矩阵 + 边界检查 (bx<0、bx>=COLS、by>=ROWS) | | 旋转 | 顺时针矩阵转置 + 8 位墙踢偏移 `{(0,0),(-1,0),(1,0),(0,-1),(-1,-1),(1,-1),(-2,0),(2,0)}` | | 幽灵 | `computeGhostY()` 逐格下落至碰撞,渲染时 20% 透明度 | | 重力 | `QTimer` 单次触发模式,15 级速度 (800ms→40ms) | | 计分 | `{0,100,300,500,800}[lines] × level`,软降 +1/格,硬降 +2/格 | | 消行 | 从底部扫描→移除满行→顶部补空行→emit `linesCleared(count)` | **单格访问器(关键设计决策):** Qt 6.8 qmlcachegen 无法在 QML JavaScript 中可靠访问嵌套 `QVariantList`(`board[r][c]`、`shape[r][c]` 静默失败)。解决方案是提供返回单个 `int` 的 `Q_INVOKABLE` 方法: ```cpp Q_INVOKABLE int boardCell(int row, int col) const; // 锁定方块 (0=空, 1-7) Q_INVOKABLE int currentPieceCell(int row, int col) const; // 激活方块 (0/1) Q_INVOKABLE int ghostCell(int row, int col) const; // 幽灵方块 (0/1) ``` **状态修订计数器:** QML 绑定系统只能追踪 `Q_PROPERTY` 依赖,无法追踪 `Q_INVOKABLE` 方法的内部依赖。因此在 `GameEngine` 中增加了一个计数器属性: ```cpp Q_PROPERTY(int stateRevision READ stateRevision NOTIFY stateChanged) // notifyAll() 中:m_stateRevision++; emit stateChanged(); ``` 在 QML 中,每个绑定第一行读取 `GameEngine.stateRevision`,确保 `stateChanged` 时强制执行重算。 ### 棋盘渲染 (`GameBoard.qml`) **核心决策:零 Canvas,纯原生 QML `Rectangle`。** Qt 6.8 的 Canvas `onPaint` 在此项目配置下完全无法使用——内联定义、独立组件文件、`requestPaint()` 持续循环、Timer 触发、信号连接……全部失败。这是 qmlcachegen 编译器层面的问题。 **最终方案:400 个 `Rectangle`,两层 `Repeater`。** | 层 | Repeater | z | 内容 | |----|----------|---|------| | 主体 | 200 项 (20×10) | 0 | 锁定方块 + 激活方块(合并绑定) | | 幽灵 | 200 项 (20×10) | 1 | 幽灵方块 (opacity: 0.2) | ```qml // 每格仅调用一个 C++ 高层方法——零 QML 条件逻辑 // Each cell calls ONE high-level C++ method — zero QML conditionals color: { GameEngine.stateRevision; return GameEngine.cellColor(row, col); } ``` 所有游戏逻辑(当前方块优先级、游戏结束检查、锁定方块颜色映射)封装在 `GameEngine::cellColor()` 中。幽灵层同理使用 `ghostVisible()` + `ghostColor()`。每个方块带有 4 个子 `Rectangle`(高光 + 阴影),实现 3D 立体效果。 ### 音频引擎 (`AudioEngine`) **程序化 PCM 合成——无外部音频文件。** | 组件 | 技术 | |------|------| | 格式 | 44.1kHz, 16-bit, 单声道 | | 音效波形 | 方波 + 正弦混合,快速包络衰减 | | BGM 波形 | 旋律:方波;低音:三角波 | | 播放 | `QAudioSink` + `QBuffer` | | 连接管理 | `Qt::SingleShotConnection` 防止累积 | **BGM 生命周期管理:** ``` startMusic() → m_musicPlaying=true, startBGMPlayback() pauseMusic() → m_audioSink->suspend() (保留播放位置) resumeMusic() → m_audioSink->resume() (断点续播) playBuffer(sfx) → m_audioSink->stop() (打断 BGM) → 播放 SFX → QTimer::singleShot(50ms) (SFX 结束后延迟恢复 BGM) stopMusic() → m_musicPlaying=false, m_audioSink->stop() ``` 暂停/继续使用 `suspend()`/`resume()` 而非 `stop()`/`start()`,BGM 从断点续播不间断。SFX 中断后通过 50ms 延迟定时器恢复——快速连续音效时只执行最后一次恢复。 ### 窗口 UI (`Main.qml`) **无边框窗口 + 浮动按钮 + 全窗口拖拽。** ``` z 层级 (顶 → 底): z:10 → 关闭/最小化浮动按钮 (hover 150ms 淡入) z:1 → GamePage (开始/暂停按钮、棋盘) z:0 → 全窗口拖拽 MouseArea (空白区域 startSystemMove) z:0 → BackgroundEffect (粒子动画背景) ``` | 特性 | 实现 | |------|------| | 无边框 | `flags: Qt.FramelessWindowHint \| Qt.Window` | | 关闭按钮 | 右上角,橘红 (`#e81123`),hover 淡入,leave 淡出 | | 最小化按钮 | 关闭按钮左侧,半透明,hover 淡入,leave 淡出 | | 窗口拖拽 | 全窗口 MouseArea (z:0),空白区域 `startSystemMove()` | | 键盘焦点 | `Component.onCompleted: contentItem.forceActiveFocus()` | --- ## 游戏控制 | 按键 | 操作 | |------|------| | `↓` / `J` | 下移 (软降 +1 分/行) | | `←` / `H` | 左移 | | `→` / `L` | 右移 | | `Space` | 顺时针旋转 (+ 8 位墙踢) | | `Enter` | 硬降 (+2 分/行) / 开始游戏 | | `P` | 暂停 / 继续 | | `M` | 切换背景音乐 | --- ## 游戏机制 ### 方块类型 | 索引 | 类型 | 尺寸 | 颜色 | |------|------|------|------| | 0 | I | 4×4 | `#00f0f0` 青色 | | 1 | O | 2×2 | `#f0f000` 黄色 | | 2 | T | 3×3 | `#a000f0` 紫色 | | 3 | S | 3×3 | `#00f000` 绿色 | | 4 | Z | 3×3 | `#f00000` 红色 | | 5 | J | 3×3 | `#0000f0` 蓝色 | | 6 | L | 3×3 | `#f0a000` 橙色 | I 方块从 y=-1 生成(第 0 行以上)。 ### 速度曲线 | 等级 | 间隔 | 等级 | 间隔 | |------|------|------|------| | 1 | 800ms | 6 | 380ms | | 2 | 720ms | 7 | 290ms | | 3 | 630ms | 8 | 200ms | | 4 | 550ms | 9 | 140ms | | 5 | 470ms | 10-15 | 100→40ms | 每消除 10 行等级 +1,最高 15 级。 ### 计分 | 消行数 | 基础分 | |--------|--------| | 1 | 100 | | 2 | 300 | | 3 | 500 | | 4 (Tetris) | 800 | 最终得分 = 基础分 × 当前等级。软降每格 +1,硬降每格 +2。 --- ## 双实现模式 项目包含两个功能相同的实现: | 实现 | 入口 | 渲染 | 音频 | |------|------|------|------| | Qt / C++ | `main.cpp` → QML | Rectangle + Repeater | C++ `QAudioSink` | | 浏览器 | `index.html` | HTML Canvas 2D | Web Audio API | 两者共享**完全相同的游戏逻辑**——方块形状、墙踢、计分公式、7-bag 随机算法、速度等级、幽灵方块计算。HTML 版本无需编译,可直接打开运行,适合逻辑原型设计。 --- ## 关键设计决策 ### Qt 6.8 qmlcachegen 限制及应对 | 问题 | 影响范围 | 解决方案 | |------|----------|----------| | Canvas `onPaint` 不触发 | 游戏棋盘不可见 | 原生 `Rectangle` + `Repeater` 替代 Canvas | | 嵌套 `QVariantList` 访问失败 | `board[r][c]` 返回错误值 | 逐格 `Q_INVOKABLE int` 访问器 | | QML 绑定不追踪 `Q_INVOKABLE` 依赖 | 状态变化不重绘 | `stateRevision` Q_PROPERTY 强制重算 | | `;` 作为属性分隔符 | 编译错误 | 所有属性独立成行 (QTP0004 已禁用) | ### 性能考量 - **400 个 Rectangle** (200 主层 + 200 幽灵层):每个 `stateChanged` 周期内 400 次绑定重算 + ~600 次 C++ 方法调用。在 30fps 下约每秒 12,000 次调用——桌面平台完全可忽略不计。 - 原生 QML Rectangle 的 GPU 合成比 Canvas 2D 软件光栅化性能更优。 - 单格访问器(`boardCell` 等)只进行基本边界检查 + 数组索引,开销极小。 --- ## 重构历程 / Refactoring Journey ### 第一阶段:Canvas → Rectangle 原始设计使用 QML `Canvas` Context2D 渲染棋盘。Qt 6.8 qmlcachegen 存在编译器级缺陷:`onPaint` 完全不触发(无论 `requestPaint()` 自循环、Timer 还是信号连接均失败)。 **方案:** 用 400 个原生 QML `Rectangle`(`Repeater` 两层:主层 200 + 幽灵层 200)替代 Canvas。QML 绑定系统自动响应 C++ `stateChanged` 信号,无需手动触发重绘。 ### 第二阶段:QVariantList → 逐格 int 访问器 Canvas 方案失败后,QML 通过 `QVariantList` 属性(`boardData`、`currentPieceShape`)读取游戏状态。qmlcachegen 编译的 QML JavaScript 无法可靠访问嵌套 `QVariantList`(`board[r][c]`、`shape[r][c]` 静默返回错误值)。 **方案:** 新增 `Q_INVOKABLE int boardCell(row, col)` 等逐格方法,返回单个 `int`。新增 `stateRevision` 计数器 `Q_PROPERTY`——所有绑定首行读取它以强制重算(因 QML 绑定系统无法追踪 `Q_INVOKABLE` 方法的内部依赖)。 ### 第三阶段:QML JS 现代化 将全部 QML JavaScript 代码中的 `var` 替换为 `const`/`let`: | 文件 | 变更 | 说明 | |------|------|------| | `NextPiecePreview.qml` | 12 处 | `onPaint` 循环 + `drawBlock` 函数 | | `BackgroundEffect.qml` | 20+ 处 | 粒子初始化 + `onPaint` 双循环渲染 | | `GameBoard.qml` | 1 处 | `boardCell()` 返回值 | ### 第四阶段:逻辑下沉 C++ QML 绑定中包含游戏条件判断(当前方块优先级、游戏结束状态、幽灵位置计算)和重复 C++ 调用(`pieceColor()` 每帧被调用 ~400 次)。 **方案:** 新增三个 C++ 高层渲染方法,封装全部游戏逻辑: ```cpp // 一个调用返回最终颜色(处理当前方块 > 锁定方块优先级) Q_INVOKABLE QString cellColor(int row, int col) const; // 一个调用返回幽灵可见性(处理幽灵位置 + 游戏结束状态) Q_INVOKABLE bool ghostVisible(int row, int col) const; // 一个调用返回幽灵颜色(缓存,全层复用) Q_INVOKABLE QString ghostColor() const; ``` **Before → After(GameBoard.qml 主层):** ``` // 重构前:6 行 QML JS,3 种 C++ 方法,双层条件判断 color: { GameEngine.stateRevision; if (GameEngine.currentPieceCell(row, col) !== 0 && !GameEngine.gameOver) return GameEngine.pieceColor(GameEngine.currentPieceType); const bv = GameEngine.boardCell(row, col); if (bv !== 0) return GameEngine.pieceColor(bv - 1); return "transparent"; } // 重构后:1 个 C++ 调用,零 QML 条件逻辑 color: { GameEngine.stateRevision; return GameEngine.cellColor(row, col); } ``` ### 最终架构原则 ``` ┌─────────────────────────────────────────┐ │ C++ GameEngine │ │ · 全部游戏状态和逻辑 │ │ · 高层渲染方法 (cellColor 等) │ │ · stateRevision 变更通知 │ └──────────┬──────────────────────────────┘ │ Q_PROPERTY / Q_INVOKABLE ▼ ┌─────────────────────────────────────────┐ │ QML GameBoard │ │ · 400 个 Rectangle (Repeater) │ │ · 每个绑定 = 1 行 C++ 调用 + stateRevision│ │ · 零游戏逻辑、零条件判断 │ └─────────────────────────────────────────┘ ``` --- ## 许可 MIT License — Copyright (c) 2026