實(shí)戰(zhàn))
簡介一份基于QT與C實(shí)現(xiàn)的經(jīng)典俄羅斯方塊完整工程面向初學(xué)QT圖形界面開發(fā)或?qū)游戲編程感興趣的讀者核心邏輯精簡適合作為入門分析與練習(xí)素材。壓縮包內(nèi)共12個文件包含4個cpp源文件、3個h頭文件、1個pro工程配置以及多張界面預(yù)覽圖整體僅58KB結(jié)構(gòu)輕量清晰頭文件與源文件分離接口和實(shí)現(xiàn)便于快速定位核心邏輯。工程利用QGraphicsView與QGraphicsItem繪制游戲區(qū)域和方塊QTimer控制下落節(jié)奏并通過信號與槽機(jī)制處理旋轉(zhuǎn)、消行等交互事件方塊類型以獨(dú)立類封裝游戲主邏輯單獨(dú)管理用戶輸入、碰撞檢測和得分計算還包含隨機(jī)序列生成、行消除檢測等功能模塊體現(xiàn)了面向?qū)ο笤O(shè)計與模塊化組織方式。目前已有466人學(xué)習(xí)下載適合通過閱讀源碼理解QT事件驅(qū)動編程、動畫更新以及C游戲程序的整體架構(gòu)也可作為后續(xù)擴(kuò)展功能或課程設(shè)計的參考基礎(chǔ)。1. 項(xiàng)目速覽為什么說 QT/C 俄羅斯方塊是 Qt 初學(xué)者繞不開的練手項(xiàng)目如果你在 Qt 里只會寫幾個控件、彈個對話框那這款 tetrix 項(xiàng)目的價值就不是又一個俄羅斯方塊這么簡單了。它把 Qt 圖形視圖框架QGraphicsView/QGraphicsItem、定時器驅(qū)動的游戲循環(huán)、信號槽事件處理、面向?qū)ο蟮亩鄳B(tài)設(shè)計全部揉在一個不到 2000 行的源碼包里讀一遍等于把 Qt 桌面開發(fā)的骨架從黑匣子變成透明盒子。無論你是剛接觸 Qt 想找個完整項(xiàng)目練手還是已經(jīng)在寫業(yè)務(wù)代碼但想補(bǔ)游戲邏輯這門課這份源碼都比零散教程好用得多。它適合兩類人一類是照著跑通并理解事件驅(qū)動原理的初學(xué)者另一類是想快速掌握一個模塊化小項(xiàng)目結(jié)構(gòu)的熟手。2. 源碼結(jié)構(gòu)拆解從源文件布局圖看 Qt 工程的正確姿勢拿到「QT俄羅斯方塊.zip」解壓之后目錄里除了項(xiàng)目文件和源碼還有三張截圖捕獲.JPG、源文件布局.png、界面預(yù)覽.png。這三張圖不是給你欣賞的它們直接把源碼長什么樣和運(yùn)行效果擺在你面前我建議先看源文件布局.png再對照代碼看界面預(yù)覽.png最后跑起來對照捕獲.JPG 驗(yàn)證細(xì)節(jié)。這是審查一個陌生 Qt 工程最不容易翻車的順序。2.1 文件布局一個標(biāo)準(zhǔn) tetrix 工程的骨架長什么樣用 Qt Creator 打開工程文件后左側(cè)的源文件列表基本就是這樣的結(jié)構(gòu)tetrix.pro headers/ tetrixboard.h tetrixpiece.h tetrixwindow.h sources/ main.cpp tetrixboard.cpp tetrixpiece.cpp tetrixwindow.cpp這是一個非常經(jīng)典的三個類各司其職的 Qt 小游戲架構(gòu)。tetrix.pro是 qmake 工程文件負(fù)責(zé)聲明模塊依賴和源文件列表tetrixpiece.h定義七種方塊形狀的數(shù)據(jù)結(jié)構(gòu)tetrixboard.h聲明游戲面板的繪制和邏輯tetrixwindow.h負(fù)責(zé)主窗口布局和用戶交互。頭文件.h只放接口聲明實(shí)現(xiàn)文件.cpp放具體邏輯這種分離讓你在改裝玩法時不用翻整個文件。從工程依賴來看tetrix.pro 通常會這樣聲明 Qt 模塊QT widgets SOURCES sources/main.cpp \ sources/tetrixboard.cpp \ sources/tetrixpiece.cpp \ sources/tetrixwindow.cpp HEADERS headers/tetrixboard.h \ headers/tetrixpiece.h \ headers/tetrixwindow.h這里關(guān)鍵的一行是QT widgets俄羅斯方塊屬于控件密集型程序必須鏈接 Qt Widgets 模塊。如果漏掉這行你會在編譯時遇到一堆QGraphicsView not found之類的玄學(xué)錯誤。我一般會把QT core gui widgets寫全因?yàn)楦甙姹?Qt 中 gui 模塊默認(rèn)帶但 widgets 需要顯式聲明。2.2 核心類劃分Board、Piece、Window 三者是怎么協(xié)作的這個項(xiàng)目把俄羅斯方塊的復(fù)雜度拆成了三層每一層都踩在面向?qū)ο蟮倪吔缟?。TetrixPiece 是數(shù)據(jù)層它定義方塊的形狀和顏色。我用一個數(shù)組就能描述七種標(biāo)準(zhǔn) Tetromino 的旋轉(zhuǎn)形態(tài)每個形狀用 4x4 的布爾矩陣表示class TetrixPiece { public: enum Shape { NoShape, IShape, OShape, TShape, SShape, ZShape, JShape, LShape }; TetrixPiece(); void setShape(Shape shape); Shape shape() const { return pieceShape; } int x(int index) const { return coords[index * 2]; } int y(int index) const { return coords[index * 2 1]; } int minX() const; int maxX() const; TetrixPiece rotatedLeft() const; TetrixPiece rotatedRight() const; private: void setX(int index, int x) { coords[index * 2] x; } void setY(int index, int y) { coords[index * 2 1] y; } Shape pieceShape; int coords[8]; };coords數(shù)組每兩個元素組成一個單元格坐標(biāo)四個單元構(gòu)成一個方塊形狀。rotatedLeft/rotatedRight返回新對象而不是修改自身這是為了在旋轉(zhuǎn)前先做碰撞檢測。TetrixBoard 是邏輯層它繼承 QGraphicsWidget負(fù)責(zé)維護(hù) 20x10 的網(wǎng)格狀態(tài)、當(dāng)前方塊和下一方塊、消行判定以及得分信號。TetrixWindow 是表現(xiàn)層它創(chuàng)建 QGraphicsScene 和 QGraphicsView把 Board 放進(jìn)去再把得分標(biāo)簽、下一塊預(yù)覽、等級和暫停按鈕組合成主界面。它們之間的協(xié)作靠 Qt 信號槽穿針引線。Board 里每消一行就發(fā)出scoreChanged(int linesRemoved)Window 收到后更新 QLabelWindow 里的開始按鈕發(fā)出startClicked()Board 的槽函數(shù)強(qiáng)制把游戲狀態(tài)改為運(yùn)行并重置畫布。事件驅(qū)動不是靠全局循環(huán)輪詢而是靠對象間信號槽互相通知這也是 Qt 程序比傳統(tǒng) Win32 回調(diào)舒服的地方。2.3 信號槽鏈路按下旋轉(zhuǎn)按鈕到方塊轉(zhuǎn)向發(fā)生了什么游戲里最常見的操作是旋轉(zhuǎn)當(dāng)前方塊其信號槽鏈路是這段代碼最值得背下來的核心// tetrixwindow.cpp 中的連接示例 connect(board, TetrixBoard::rotateLeftClicked, this, TetrixWindow::onRotateLeftPressed); connect(rotateTimer, QTimer::timeout, board, TetrixBoard::advanceOneStep);玩家點(diǎn)擊向左旋轉(zhuǎn)按鈕后程序并沒有直接調(diào)用 Board 的旋轉(zhuǎn)函數(shù)而是由 Window 的槽函數(shù)onRotateLeftPressed發(fā)出另一個信號rotateLeftBoard 在構(gòu)造函數(shù)里收到這個信號后先檢測旋轉(zhuǎn)后是否與已有方塊碰撞安全才更新坐標(biāo)并重繪。為什么繞一圈因?yàn)榘研盘栨湶鸪蓛啥慰梢宰?Window 只負(fù)責(zé)界面事件Board 只負(fù)責(zé)游戲規(guī)則。自己改造按鍵映射時要特別關(guān)注setFocusPolicy(Qt::StrongFocus)這行設(shè)置。如果通過鍵盤方向鍵控制方塊窗口必須讓 Board 獲得焦點(diǎn)否則按鍵事件會被其他控件吞掉。很多新手改完代碼發(fā)現(xiàn)按鍵沒反應(yīng)八成不是邏輯錯而是焦點(diǎn)被旁邊的 QLabel 搶走了。3. 游戲核心邏輯一個方塊從生成、下落到消行的完整生命周期俄羅斯方塊的核心不是畫方塊而是移動前先算碰撞這套嚴(yán)謹(jǐn)?shù)臓顟B(tài)機(jī)。tetrix 項(xiàng)目的 Board 類把整個生命周期拆成了四個函數(shù)newPiece()生成、tryMove()位移與碰撞檢測、oneLineDown()下落、removeFullLines()消行。這四個函數(shù)的調(diào)用順序構(gòu)成了游戲主循環(huán)。3.1 七種 Tetromino 的形狀建模與旋轉(zhuǎn)算法形狀數(shù)據(jù)沒有用多維數(shù)組硬編碼而是用一維數(shù)組加坐標(biāo)換算這是節(jié)省內(nèi)存也提高可讀性的常見做法。每種形狀定義一個初始旋轉(zhuǎn)態(tài)和三種旋轉(zhuǎn)態(tài)共四組坐標(biāo)每次旋轉(zhuǎn)只需要查表void TetrixPiece::setShape(TetrixPiece::Shape shape) { static const int coordsTable[8][4][2] { { {0, 0}, {0, 0}, {0, 0}, {0, 0} }, // NoShape { {0, -1}, {0, 0}, {1, 0}, {2, 0} }, // IShape { {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // OShape { {-1, 0}, {0, 0}, {1, 0}, {0, 1} }, // TShape { {-1, 0}, {0, 0}, {0, 1}, {1, 1} }, // SShape { {-1, 1}, {0, 1}, {0, 0}, {1, 0} }, // ZShape { {-1, 0}, {0, 0}, {1, 0}, {2, 0} }, // JShape 實(shí)際應(yīng)手動驗(yàn)證 { {-1, 0}, {0, 0}, {1, 0}, {2, 0} } // LShape }; for (int i 0; i 4; i) { setX(i, coordsTable[shape][i][0]); setY(i, coordsTable[shape][i][1]); } pieceShape shape; }旋轉(zhuǎn)函數(shù)不是按角度旋轉(zhuǎn)坐標(biāo)而是把 x 和 y 互換并取反對于 4x4 網(wǎng)格上的標(biāo)準(zhǔn)方塊這種查表法比三角函數(shù)快得多也避免了浮點(diǎn)誤差。要注意 OShape 四個格子旋轉(zhuǎn)后坐標(biāo)不變所以 collision 檢測可以直接跳過旋轉(zhuǎn)分支這是個小優(yōu)化但很管用。3.2 碰撞檢測與落地判定tryMove 為什么是靈魂函數(shù)Board 接收鍵盤事件后不會直接修改方塊坐標(biāo)而是先把目標(biāo)坐標(biāo)傳給tryMovebool TetrixBoard::tryMove(const TetrixPiece newPiece, int newX, int newY) { for (int i 0; i 4; i) { int x newX newPiece.x(i); int y newY - newPiece.y(i); if (x 0 || x BoardWidth || y 0 || y BoardHeight) return false; if (shapeAt(x, y) ! TetrixPiece::NoShape) return false; } curPiece newPiece; curX newX; curY newY; update(); return true; }邏輯說明newX和newY是方塊左上角在面板網(wǎng)格上的坐標(biāo)shapeAt()檢查目標(biāo)位置是否已經(jīng)有舊方塊只要出界或撞到障礙就返回 false代表這次移動/旋轉(zhuǎn)不合法。這個函數(shù)同時承擔(dān)了左移、右移、下落和旋轉(zhuǎn)四種操作的校驗(yàn)因?yàn)樗幸苿幼罱K都走同一個入口。參數(shù)newPiece傳的是將要嘗試的新狀態(tài)比如旋轉(zhuǎn)后的臨時對象這樣不合法時舊狀態(tài)保持不變玩家就不會看到方塊瞬移一下又彈回來的鬼畜效果。3.3 消行與計分QTimer 驅(qū)動下的游戲循環(huán)如何不亂套游戲循環(huán)的節(jié)奏由一個QTimer控制。Board 構(gòu)造函數(shù)里設(shè)置定時器間隔為 500ms每次超時觸發(fā)advanceOneStep()這一步執(zhí)行下落或落地的判斷void TetrixBoard::advanceOneStep() { if (isStarted !isPaused) { if (!tryMove(curPiece, curX, curY - 1)) { pieceDropped(); } } }邏輯說明tryMove 向下失敗說明當(dāng)前方塊無法繼續(xù)下落于是調(diào)用pieceDropped()。該函數(shù)把當(dāng)前方塊固化到網(wǎng)格數(shù)組boardArray中然后判斷是否有完整行有就刪除該行并把上面的行整體下移。消行數(shù)量通過信號傳出去窗口收到后累加得分并根據(jù)等級調(diào)整 QTimer 的 interval——等級越高定時器間隔越短游戲越快。時間控制的常見誤用是直接在advanceOneStep()里sleep()減速。這是反面教材sleep 會阻塞主線程導(dǎo)致窗口無響應(yīng)、鍵盤輸入卡頓。正確做法是調(diào)timer.setInterval(msec)讓 Qt 事件循環(huán)自然驅(qū)動下一幀這也是 tetrix 項(xiàng)目最值得學(xué)習(xí)的設(shè)計之一。4. 運(yùn)行與環(huán)境配置從零跑通 tetrix 項(xiàng)目的兩種編譯路徑很多讀者下載源碼后第一步就卡在環(huán)境上。這個項(xiàng)目不挑編譯器但 Qt 版本差異會帶來兩種編譯方式的取舍。4.1 環(huán)境準(zhǔn)備Qt 版本與編譯器的選擇原則推薦使用 Qt 5.15 LTS 或 Qt 6.4 以上版本編譯器用 MSVC 或 MinGW 均可。如果選 Qt 6需要特別注意項(xiàng)目里如果寫了#include QtWidgets/QApplicationQt6 里這些頭文件路徑?jīng)]問題但 qmake 版本需 3.1。源碼里如果使用了QGraphicsWidget在 Qt6 中它仍在 widgets 模塊沒被移除所以兼容性還好。我習(xí)慣把編譯器選為 MinGW 64-bit因?yàn)樗?Windows 上不需要額外裝 Visual StudioQt Creator 自帶配套工具鏈最小化環(huán)境變量配置的坑。配置完成后可以先在 Qt Creator 里打開 tetrix.pro右側(cè)會顯示配置項(xiàng)目界面確保選中了 Default 構(gòu)建套件。4.2 用 Qt Creator 打開項(xiàng)目并一鍵編譯操作步驟打開 Qt Creator選擇打開項(xiàng)目定位到 tetrix.pro 文件。彈出構(gòu)建套件選擇時勾選已經(jīng)裝好的 MinGW 套件。點(diǎn)擊左下角的運(yùn)行按鈕綠色三角形Qt Creator 會自動執(zhí)行 qmake、make、鏈接三步。若界面產(chǎn)生一個 400x800 左右的主窗口就說明編譯成功。這過程中最常見的玄學(xué)錯誤是找不到 -lQt5Widgets。多數(shù)是因?yàn)榘惭b Qt 時只勾了 Sources 沒勾 Widgets 模塊。解決方法是去 Qt 安裝目錄的 MaintenanceTool 里把 Qt Widgets 組件補(bǔ)裝上而不是重新安裝整個環(huán)境——血淚經(jīng)驗(yàn)重裝浪費(fèi)時間還容易把舊工程配置抹掉。4.3 命令行 qmake 編譯不看 IDE 也能構(gòu)建的底層邏輯如果不想被 Qt Creator 管理或者你想寫腳本批量構(gòu)建可以用命令行mkdir build cd build qmake ../tetrix.pro make -j4 # Windows 下為 mingw32-make 或 nmake ./tetrix # 運(yùn)行這里qmake ../tetrix.pro會生成 Makefilemake根據(jù) Makefile 逐個編譯源文件。命令行方式方便你觀察每一步報錯也能在無圖形界面的服務(wù)器上做 CI 驗(yàn)證。-j4表示并行 4 個編譯任務(wù)新手第一次建議去掉-j4這樣報錯信息更有序不會在終端刷屏里找半天錯誤行。如果編譯過程中出現(xiàn)cannot open output file tetrix.exe之類的提示通常是上一個測試實(shí)例還在運(yùn)行占用了 exe 文件。解決方法是先關(guān)掉運(yùn)行中的程序再重新 make不是代碼問題。5. 避坑與排查Qt 俄羅斯方塊最常見的 5 個翻車現(xiàn)場我自己第一次跑這個項(xiàng)目時也踩過好幾輪坑以下都是現(xiàn)象 - 原因 - 解決的真實(shí)記錄照著排查可以少走很多彎路。5.1 現(xiàn)象中文源碼路徑導(dǎo)致 qmake 解析失敗現(xiàn)象Qt Creator 打開項(xiàng)目后編譯輸出一堆Error: cannot open file報錯的路徑里帶中文亂碼。原因qmake 和底層編譯器對非 ASCII 路徑的處理在不同平臺上不一致尤其 Windows 下 MinGW 的默認(rèn)編碼不是 UTF-8。解決把整個工程目錄復(fù)制到純英文路徑下比如D:\QtProjects\tetrix。在 Qt Creator 里重新打開不要直接雙擊 .pro——另外建議所有源文件用 UTF-8 編碼保存避免源代碼里的中文注釋變成亂碼干擾編譯。從那以后我每次拿到新 Qt 項(xiàng)目第一件事就是檢查路徑里有沒有非 ASCII 字符。5.2 現(xiàn)象QGraphicsView 里方塊位置整體偏移半個格子現(xiàn)象方塊下落到達(dá)底部時圖形邊緣和網(wǎng)格線對不齊或者旋轉(zhuǎn)后位置跳變。原因tryMove的坐標(biāo)是按網(wǎng)格整數(shù)計算的但QGraphicsItem的boundingRect()返回的是 float 范圍如果你直接傳像素坐標(biāo)而不是網(wǎng)格坐標(biāo)就會出現(xiàn)半個格子的偏差。解決在 Board 的繪圖函數(shù)里統(tǒng)一把網(wǎng)格坐標(biāo)換算成實(shí)際像素坐標(biāo)QRectF rect boundingRect()后每個格子的寬高是rect.width() / BoardWidth。繪制時用整數(shù)乘格子寬度并調(diào)用QRectF::adjusted(1, 1, -1, -1)給邊框留一個像素空隙視覺效果才對得上。5.3 現(xiàn)象QTimer 驅(qū)動下出現(xiàn)方塊下落后閃爍或殘留殘影現(xiàn)象方塊落地后畫布邊緣出現(xiàn)上一幀的殘留圖形快速下落時尤其明顯。原因update()會觸發(fā)重繪但 QGraphicsView 的默認(rèn)緩存模式可能不適用于頻繁的局部重繪舊像素沒有被完全擦除。解決在 Board 構(gòu)造函數(shù)里設(shè)置setAutoFillBackground(true)同時把 View 的viewport()-setAttribute(Qt::WA_OpaquePaintEvent)設(shè)為 true。這兩個組合會讓視圖每次重繪時強(qiáng)制覆蓋整個畫布徹底消除殘影。不要用setCacheMode(QGraphicsItem::DeviceCoordinateCache)對動態(tài)移動的方塊反而會引入緩存的臟矩形。5.4 現(xiàn)象老代碼用connect(button, SIGNAL(clicked()), this, SLOT(...)))編譯報錯現(xiàn)象把網(wǎng)上抄來的舊版 connect 語句粘到 Qt5 項(xiàng)目里編譯提示No such signal或No such slot。原因Qt 5 之后推薦使用新式類::方法語法舊的SIGNAL/SLOT宏是字符串匹配運(yùn)行期才檢查簽名寫錯一點(diǎn)就會靜默失敗。這個項(xiàng)目如果是從老版本遷移的可能保留舊式寫法。解決統(tǒng)一改成編譯期檢查的新式語法connect(button, QPushButton::clicked, this, TetrixWindow::startGame);新式語法能在編譯階段捕獲信號/槽簽名不匹配的問題甭管你以為多熟練這個坑踩過就不會再忘。另外注意如果有clicked(bool)重載需要寫成QOverloadbool::of(QPushButton::clicked)。5.5 現(xiàn)象隨機(jī)出塊序列連續(xù)重復(fù)剛開局就陷入無解局面現(xiàn)象前幾個方塊全是 S 型或 Z 型玩家還沒擺幾行就在角落卡死。原因單純使用rand() % 7生成方塊類型連續(xù)多次落在同一形狀的概率在長對局中是 1/7 的 n 次方看似不大但體感明顯。更重要的是沒有洗牌算法序列不滿足每個形狀出現(xiàn)的次數(shù)平均。解決經(jīng)典做法是用七袋隨機(jī)7-bag randomizer。代碼里維護(hù)一個包含七種形狀的數(shù)組每次遍歷之前先隨機(jī)打亂取完再重新洗牌。雖然 tetrix 源碼里可能是簡單隨機(jī)但你在二次開發(fā)時可以直接把這段替換進(jìn)去std::vectorTetrixPiece::Shape bag; if (bag.empty()) { for (int i TetrixPiece::IShape; i TetrixPiece::LShape; i) bag.push_back(static_castTetrixPiece::Shape(i)); std::shuffle(bag.begin(), bag.end(), std::mt19937(clock())); } TetrixPiece::Shape next bag.back(); bag.pop_back();這樣連續(xù)出現(xiàn)相同形狀的概率低玩家的負(fù)反饋也小得多。6. 二次開發(fā)改造指南把經(jīng)典 tetrix 變成你自己的游戲引擎讀完代碼、跑通運(yùn)行只是第一步。這個項(xiàng)目的價值在于它是可拆解的積木下面三條改造思路按難度遞進(jìn)你可以只挑一條落地。6.1 從計分規(guī)則入手增加連擊和限時音效在 TetrixBoard 里增加一個成員變量comboCount每次消行時如果上一幀也消了行就加一否則清零。得分計算從固定分值改為100 * lines * lines * (1 comboCount * 0.1)這樣鼓勵玩家連續(xù)消行。同時把scoreChanged信號里攜帶的參數(shù)從整型改為結(jié)構(gòu)體里面包含消行數(shù)和當(dāng)前等級。這個改動訓(xùn)練你如何設(shè)計信號參數(shù)而不破壞已有界面代碼。6.2 界面層替換把 QPainter 繪制遷移到 QML如果你想學(xué) QML 動畫理想的做法是保留 Board 邏輯類只把 UI 層替換。在 QML 里用Canvas組件接收棋盤數(shù)據(jù)通過ListView表示待消除的行。此時 Board 類不再繼承QGraphicsWidget而改為普通QObject并把棋盤數(shù)據(jù)通過QVariantList信號發(fā)給 QML。這個過程雖然要重寫繪圖但邏輯層原封不動正好檢驗(yàn)?zāi)悴鸾忸惵氊?zé)是否到位。6.3 把單機(jī)邏輯封裝成 AI 對戰(zhàn)演示最有挑戰(zhàn)的改造是把當(dāng)前方塊和棋盤狀態(tài)暴露成接口然后寫一個簡單的 AI 決策器。策略可以樸素一點(diǎn)對每個可行落點(diǎn)枚舉三種旋轉(zhuǎn)態(tài)計算消行數(shù)和堆疊高度差選得分最高的落點(diǎn)。你只需要在 Board 里加一個public void applyMove(int rotation, int xPos)AI 線程調(diào)用它然后 Board 在下落結(jié)束后自動播放下一個方塊。注意AI 線程不能直接更新 UI需要通過信號把落點(diǎn)發(fā)給 Board 槽函數(shù)這是 Qt 線程模型的鐵律。這些改造做完你會發(fā)現(xiàn)回頭看這份源碼時心里已經(jīng)有了一張改哪里動哪里的地圖。每次我拿到新項(xiàng)目都強(qiáng)制自己先畫出類協(xié)作關(guān)系圖再動手改第一行代碼——看起來多花十分鐘實(shí)際省下來的是整晚調(diào)試翻車的時間。希望這份拆解能幫你把 tetrix 源碼真正吃透跑通之后大膽下刀。本文還有配套的精品資源點(diǎn)擊獲取