news 2026/9/30 5:31:18

6个C++控制台小游戏代码:Dev-C++可运行可复制练手项目

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
6个C++控制台小游戏代码:Dev-C++可运行可复制练手项目

玩C++的人大概都有这么一段经历:语法书翻了一本又一本,指针、引用、虚函数背得滚瓜烂熟,可真让你写点东西,脑子里却一片空白。我当年也是这样,直到有人甩给我一句话——别啃书了,写几个小游戏,一个星期顶你三个月。这话听着夸张,但真上手之后我发现确实有道理:游戏这个东西天生自带“反馈回路”,你写的每一行逻辑都会立刻体现在屏幕上,对错一眼就能看出来,这种即时反馈对编程入门的人来说太重要了。这篇内容就是围绕“6个C++游戏代码,Dev-C++都可以运行,可复制”这个主题展开的,我会把每个游戏的完整代码贴出来,更重要的是把背后的思路、踩过的坑、以及为什么这么写讲透。如果你手上正好装了Dev-C++(或者小熊猫Dev-C++这类衍生版本),想找点能直接跑起来的练手项目,那接下来的内容应该对你有用。难度从初学到中等依次递进,零基础能看懂前几个,有点基础的可以直接挑战后面的。

1. 为什么选C++写小游戏来练手

1.1 C++做控制台小游戏的天然优势

很多人一提游戏开发就想到Unity、想到虚幻引擎,觉得C++写游戏是不是太重了。其实完全不是一回事。我们要写的是控制台小游戏,也就是黑框框里用字符拼出来的那种。这类项目不依赖任何图形库,不用配OpenGL,不用装SDL,一个编译器加一个标准库就能跑,这正是C++最舒服的使用场景。

C++本身就是从C语言发展来的,它保留了C语言直接操作内存、贴近硬件的特性,同时又多了类、模板、STL这些高级工具。写小游戏的时候,你会自然而然地用到数组、循环、条件判断、函数封装、随机数、字符串处理这些最核心的语法点。这些东西你在语法书里看一百遍都不如自己写一遍猜数字游戏记得牢。比如随机数种子这个概念,书上说“要用当前时间初始化种子,否则每次运行结果一样”,你可能看了没感觉。但当你写完猜数字游戏,发现每次启动答案都是42,你就永远记住srand((unsigned)time(0))这行了。

另外一点,C++的控制台输出特别直观。cout一打,字符就出现在屏幕上,你不用去管渲染管线、不用管资源加载、不用管坐标变换,注意力能100%放在逻辑设计上。这对初学者来说非常关键,因为刚入门时最容易被打消积极性的就是“环境配置”和“上手门槛”。

1.2 为什么推荐Dev-C++而不是VS或者VSCode

这个问题我被问过无数次。先说结论:入门阶段Dev-C++确实够用,而且省心。Visual Studio功能强大,但它动辄几个G的安装体积,新建项目时一堆选项,对新手来说容易劝退。VSCode本身只是个编辑器,要跑C++得自己配编译器、配c_cpp_properties.json、配tasks.json,光是让“Hello World”跑起来就能折腾一下午。很多人搜“vscode配置c/c++环境”搜到崩溃,最后放弃了。

Dev-C++本质上是一个打包好的集成环境,里面自带MinGW编译器,装完就能用,点击“编译运行”直接出结果。它的界面朴素,但该有的都有:语法高亮、编译错误提示、调试器(虽然不太好用)。对于写控制台小游戏这种纯逻辑项目,它完全胜任。当然,如果你后面要做大项目,还是得换VS或者CLion,但那是后话。

注意:Dev-C++有多个版本,老版Orwell Dev-C++ 5.11已经停止维护,编译时默认标准可能比较老。如果代码里用到了C++11的新特性报错,记得在“工具-编译器选项”里勾选“编译时加入以下命令”,填上-std=c++11。

1.3 这6个游戏的难度梯度设计

选游戏不是随便挑的,得按照语法点的覆盖面来排。我挑的这6个,覆盖了从最基础的输入输出到稍微复杂的数组操作和状态管理:

序号游戏名称核心知识点难度建议用时
1猜数字随机数、循环、条件判断入门20分钟
2井字棋二维数组、函数封装、胜负判定入门偏上40分钟
3打字练习字符串、计时、字符比较中等50分钟
421点扑克枚举、随机发牌、状态管理中等60分钟
5贪吃蛇简化版数组队列、方向控制、按键检测中等偏上90分钟
6俄罗斯方块简化版矩阵操作、碰撞检测、消行逻辑进阶120分钟

这个顺序的好处是知识点是叠加的。你写完猜数字,已经会随机数和循环了;写井字棋的时候,二维数组的遍历和胜负判定又能复用循环;到了后面的贪吃蛇和俄罗斯方块,前面练出来的手感直接派上用场。整个过程像爬楼梯,每一步都不算陡。

2. 上手前的环境准备与避坑要点

2.1 Dev-C++的下载安装与编译器检查

装Dev-C++这件事本身没什么技术含量,但有几个细节值得说。网上搜“dev-c++下载”会出来一堆站点,建议直接找官方发布页或者GitHub上的小熊猫Dev-C++(这是国内维护的分支版本,更新更勤快,对C++11/14/17支持更好)。下载下来的安装包一般几十兆,一路下一步就行。

装完之后先做一件事:新建一个源文件,写个Hello World,编译运行看看能不能出结果。这一步能跑通,说明编译器没问题。如果报错说找不到g++,那就是编译器路径没配好,去“工具-编译器选项-目录”里检查一下bin目录是不是指向了安装目录下的MinGW64\bin。

这里插一句,很多人装完Dev-C++后,写代码时发现“编译”按钮是灰的,或者点“编译运行”没反应。这种情况通常是没有保存文件。Dev-C++和很多IDE不一样,它要求你必须先把文件保存成.cpp后缀,才允许编译,不然它不知道该编译哪个文件。所以养成习惯:新建源文件后先Ctrl+S保存,起个有意义的名字,比如guess_number.cpp。

2.2 新建项目的正确姿势

Dev-C++里有两个入口容易搞混:一个是“文件-新建-源代码”,一个是“文件-新建-项目”。写单个小游戏的话,用“源代码”就够了。项目模式适合多文件工程,会在文件夹里生成.dev工程文件、.o中间文件,反而显得乱。用源代码模式,一个.cpp文件从头写到尾,清清爽爽,也方便你复制给别人。

还有个小细节:默认新建的源文件编码可能是GBK,而有些代码里如果带了中文注释加特殊符号,保存时可能乱码。稳妥起见,在“工具-编辑器选项-显示”里看一下字体设置,中文用等宽字体显示更整齐。不过说实话,就算注释乱码了,代码本身还是能跑的,因为编译器只关心代码部分,注释会被忽略。

2.3 新手最容易踩的三个坑

第一个坑是分号。C++里语句结束必须加分号,漏一个就会引发一连串莫名其妙的报错,因为编译器会把下一行也当成上一句的一部分。我看到过不少新手,一个分号找了半小时。

第二个坑是中文标点。写代码时输入法忘了切换,把;打成;,把()打成(),编译器报的错稀奇古怪。这个坑几乎人人踩过,解决办法很简单:写代码时锁定英文输入法。

第三个坑是忘记清屏。控制台游戏如果每轮都往下打印,屏幕很快就被刷满了。Windows下用system("cls")清屏,记得包含<cstdlib>头文件。不过system调用会稍微慢一点,对游戏帧率有影响的话,后面贪吃蛇那节我会给出替代思路。

3. 六个可直接运行的C++游戏代码实现

3.1 猜数字游戏——随机数与循环的入门课

这是最经典的练手项目,代码短,逻辑清晰,适合刚学完基本语法的人。

#include <iostream> #include <cstdlib> #include <ctime> using namespace std; int main() { srand((unsigned)time(0)); // 用当前时间做随机种子 int target = rand() % 100 + 1; // 生成1-100的随机数 int guess, count = 0; cout << "我已经想好了一个1到100之间的数字,来猜猜看:" << endl; while (true) { cin >> guess; count++; if (guess > target) { cout << "太大了,再试试" << endl; } else if (guess < target) { cout << "太小了,再试试" << endl; } else { cout << "恭喜答对!一共猜了" << count << "次" << endl; break; } } return 0; }

这段代码的核心就三个点:随机数种子、取模运算限定范围、while(true)死循环加break退出。rand() % 100得到的是0到99,加1变成1到100,这个+1的位置新手经常搞错,写成rand() % 100 + 1是对的,写成(rand() % 100) + 1也一样,但写成rand() % (100 + 1)就变成了0到100,多了一个0。

值得展开说的是为什么必须用时间做种子。计算机里的随机数是“伪随机”,它其实是根据一个种子值算出来的一串数。如果你不调用srand,默认种子是1,那么每次程序启动算出来的序列都一模一样。用了srand((unsigned)time(0)),种子就变成了当前时间戳,每秒都在变,所以每次运行结果都不同。

提示:如果不想用时间,也可以让用户自己输入一个种子值,做成“同样的数字生成同样的题目”的效果,这在做测试的时候挺方便。

这个游戏可以立刻加点花样:比如限制猜的次数,超过10次就失败;或者把范围改成1到1000,难度立马上去。改动很小,但能让你体会到“修参数就能改玩法”的乐趣。

3.2 井字棋——二维数组与胜负判定

井字棋是练二维数组的绝佳项目。棋盘是3乘3的,正好对应一个char board[3][3]。

#include <iostream> using namespace std; char board[3][3]; int currentPlayer = 1; void initBoard() { for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) board[i][j] = ' '; } void printBoard() { cout << " 1 2 3" << endl; for (int i = 0; i < 3; i++) { cout << i + 1 << " "; for (int j = 0; j < 3; j++) { cout << board[i][j]; if (j < 2) cout << " | "; } cout << endl; if (i < 2) cout << " ---------" << endl; } } char checkWin() { // 检查横竖斜八条线 for (int i = 0; i < 3; i++) { if (board[i][0] != ' ' && board[i][0] == board[i][1] && board[i][1] == board[i][2]) return board[i][0]; if (board[0][i] != ' ' && board[0][i] == board[1][i] && board[1][i] == board[2][i]) return board[0][i]; } if (board[0][0] != ' ' && board[0][0] == board[1][1] && board[1][1] == board[2][2]) return board[0][0]; if (board[0][2] != ' ' && board[0][2] == board[1][1] && board[1][1] == board[2][0]) return board[0][2]; return ' '; } int main() { initBoard(); int moves = 0; while (true) { printBoard(); cout << "玩家" << currentPlayer << "请落子(行 列):"; int r, c; cin >> r >> c; r--; c--; // 用户输入从1开始,转成0开始的下标 if (r < 0 || r > 2 || c < 0 || c > 2 || board[r][c] != ' ') { cout << "这个位置不能下,重来" << endl; continue; } board[r][c] = (currentPlayer == 1) ? 'X' : 'O'; moves++; char winner = checkWin(); if (winner != ' ') { printBoard(); cout << "玩家" << currentPlayer << "获胜!" << endl; break; } if (moves == 9) { printBoard(); cout << "平局!" << endl; break; } currentPlayer = 3 - currentPlayer; // 1和2互相切换 } return 0; }

这里有个小技巧值得单独说:currentPlayer = 3 - currentPlayer。1变2,2变1,一行搞定,比写if-else优雅。这种“用算术代替分支”的写法在游戏逻辑里很常见,比如后面贪吃蛇方向反转也能用到类似思路。

胜负判定是这段代码的核心。横竖斜一共8条线都要检查,很多人写的时候只检查了横竖,忘了对角线,结果游戏出现“斜着连成三个却判不出胜负”的bug。我建议把8条线的检查都写全,宁可啰嗦也别漏。

落子时做合法性检查也很重要:坐标必须在0到2之间,且格子必须是空的。如果用户输入了已经占用的位置,直接continue跳过本次循环,重新提示。这里用continue比用else嵌套清爽得多。

提示:想让它变成人机对战,可以在玩家落子后加一段电脑逻辑。最简单的做法是遍历所有空位,找出能立刻获胜的位置;没有就随便下一个。再进一步就是经典的“上下左右看”,拦截玩家的连子。这个改造我强烈推荐做一遍,写完之后你对游戏AI的基本思路就入门了。

3.3 打字练习游戏——字符串处理与计时

这个游戏的玩法是:屏幕上随机显示一个单词,你在规定时间内把它敲出来,敲对了加分,敲错了扣分。核心用到了字符串比较和计时函数。

#include <iostream> #include <string> #include <cstdlib> #include <ctime> #include <ctime> using namespace std; int main() { string words[] = {"apple", "banana", "cherry", "dragon", "engine", "forest", "garden", "hunter", "island", "jacket"}; int total = 10; srand((unsigned)time(0)); int score = 0; clock_t startAll = clock(); for (int i = 0; i < total; i++) { string target = words[rand() % 10]; cout << "第" << i + 1 << "个词:" << target << endl; string input; clock_t t1 = clock(); cin >> input; clock_t t2 = clock(); double used = double(t2 - t1) / CLOCKS_PER_SEC; if (input == target) { score += 10; cout << "正确!用时" << used << "秒" << endl; } else { score -= 5; cout << "错了,正确答案是" << target << endl; } cout << "当前得分:" << score << endl << endl; } double totalTime = double(clock() - startAll) / CLOCKS_PER_SEC; cout << "全部完成,总用时" << totalTime << "秒,最终得分" << score << endl; return 0; }

这里的重点是clock()函数。它返回的是程序运行以来的CPU时钟数,单位是CLOCKS_PER_SEC分之一秒,所以(t2 - t1) / CLOCKS_PER_SEC就是秒数。要注意它统计的是CPU时间而不是真实的墙上时间,如果程序在等待用户输入,这段时间不会被计入。对打字游戏来说这其实是好事——你思考的时间不算,真正敲键盘的时间才算。

字符串比较用==就行,C++的string类重载了运算符,直接比内容是否相同。如果用的是C风格的char[],就得用strcmp,麻烦得多。这也是我一直推荐大家用std::string而不是字符数组的原因之一。

提示:words[rand() % 10]这里数组有10个元素,取模10正好。如果你改成20个单词,记得把10改成20,否则永远取不到后面10个。这个bug很难发现,因为程序不报错,只是有些单词从来不出现。

想让这个游戏更有意思,可以加个倒计时限制,比如每个词只有5秒钟。实现方法是在读取输入前记录时间,读完后判断是否超时。不过cin是阻塞的,用户不输入程序就一直等,所以严格的超时得用多线程或者非阻塞输入,那就不适合在Dev-C++这种入门环境里折腾了。初学者先做简单的版本就好。

3.4 21点扑克——枚举与状态管理

21点是练习枚举类型和状态管理的好项目。规则简单:你和电脑各拿牌,谁的点数接近21又不爆谁赢。

#include <iostream> #include <cstdlib> #include <ctime> using namespace std; enum Suit { HEART, DIAMOND, CLUB, SPADE }; int drawCard() { int card = rand() % 13 + 1; // 1到13 if (card > 10) card = 10; // J Q K 都算10 return card; } int main() { srand((unsigned)time(0)); char again = 'y'; while (again == 'y') { int playerTotal = drawCard() + drawCard(); int dealerTotal = drawCard() + drawCard(); cout << "你的初始点数是:" << playerTotal << endl; cout << "电脑明牌点数:" << dealerTotal / 2 << endl; char choice; while (playerTotal < 21) { cout << "要牌(h) 停牌(s):"; cin >> choice; if (choice == 'h') { int c = drawCard(); playerTotal += c; cout << "你抽到" << c << ",当前点数" << playerTotal << endl; } else { break; } } if (playerTotal > 21) { cout << "爆牌!你输了" << endl; } else { while (dealerTotal < 17) { dealerTotal += drawCard(); } cout << "电脑最终点数:" << dealerTotal << endl; if (dealerTotal > 21 || playerTotal > dealerTotal) { cout << "你赢了!" << endl; } else if (playerTotal == dealerTotal) { cout << "平局" << endl; } else { cout << "你输了" << endl; } } cout << "再来一局?(y/n):"; cin >> again; } return 0; }

enum(枚举)在这里让代码可读性好很多。虽然这个简化版没真正用到花色(只用了点数),但保留Suit的定义可以让你扩展时有个起点。真正的21点里,A可以当1也可以当11,这里为了简化只当1处理,这是刻意做的取舍——初学者别一上来就追求完整规则,先把主流程跑通,细节慢慢补。

电脑的操作逻辑用了经典的“庄家17点停牌”规则:点数小于17就必须继续要牌。这个规则简单却合理,能保证电脑不会太蠢。你会发现程序里while (dealerTotal < 17)一行就把这个策略实现了,这就是逻辑清晰的威力。

提示:这个游戏有个隐藏bug,如果玩家一直要牌最后爆了,电脑其实不用再抽牌了。代码里是先判断玩家爆没爆,再决定要不要让电脑操作,这个顺序很重要。如果写反了,即使玩家爆牌,电脑还会“表演”一番抽牌,看起来很奇怪。

3.5 贪吃蛇简化版——数组队列与方向控制

到了这个阶段,可以挑战稍微复杂一点的了。控制台版贪吃蛇需要用到conio.h里的非阻塞键盘检测,这在Windows下的Dev-C++是可以直接用的。

#include <iostream> #include <conio.h> #include <windows.h> #include <cstdlib> #include <ctime> using namespace std; const int W = 20, H = 15; int snakeX[100], snakeY[100]; int len = 3; int dir = 0; // 0上 1右 2下 3左 int foodX, foodY; int score = 0; void gotoxy(int x, int y) { COORD pos = {(SHORT)x, (SHORT)y}; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos); } void placeFood() { foodX = rand() % W; foodY = rand() % H; } void init() { srand((unsigned)time(0)); for (int i = 0; i < len; i++) { snakeX[i] = W / 2 - i; snakeY[i] = H / 2; } placeFood(); } void draw() { gotoxy(0, 0); for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { bool isSnake = false; for (int i = 0; i < len; i++) if (snakeX[i] == x && snakeY[i] == y) isSnake = true; if (x == foodX && y == foodY) cout << "*"; else if (isSnake) cout << "o"; else cout << " "; } cout << endl; } cout << "得分:" << score << endl; } bool hitSelf(int nx, int ny) { for (int i = 0; i < len; i++) if (snakeX[i] == nx && snakeY[i] == ny) return true; return false; } int main() { init(); while (true) { if (_kbhit()) { int key = _getch(); if (key == 224) { key = _getch(); if (key == 72 && dir != 2) dir = 0; if (key == 77 && dir != 3) dir = 1; if (key == 80 && dir != 0) dir = 2; if (key == 75 && dir != 1) dir = 3; } } int nx = snakeX[0], ny = snakeY[0]; if (dir == 0) ny--; if (dir == 1) nx++; if (dir == 2) ny++; if (dir == 3) nx--; if (nx < 0 || nx >= W || ny < 0 || ny >= H || hitSelf(nx, ny)) { cout << "游戏结束,最终得分:" << score << endl; break; } if (nx == foodX && ny == foodY) { len++; score += 10; placeFood(); } else { // 身体后移 for (int i = len - 1; i > 0; i--) { snakeX[i] = snakeX[i - 1]; snakeY[i] = snakeY[i - 1]; } } snakeX[0] = nx; snakeY[0] = ny; draw(); Sleep(150); } return 0; }

这段代码里有几个关键点值得讲。蛇身的存储用的是两个并行数组snakeX[]和snakeY[],下标0是蛇头。每次移动时,如果没吃到食物,就把数组整体往后挪一位,然后更新蛇头;如果吃到了食物,长度加一,身体不用挪,相当于尾巴长了一节。这是最朴素的实现方式,好处是不用动态内存,缺点是移动一次就要循环搬一次数组,效率不算高,但对20乘15的棋盘完全够用。

方向控制这里有个经典陷阱:不能允许蛇反向掉头,否则下一秒就会咬到自己。代码里if (key == 72 && dir != 2) dir = 0;就是这个意思——当前方向是下(2)时,按上键无效。这个判断如果用反了,游戏立刻变成“自杀模拟器”。

**_kbhit和_getch**是Windows专属函数。_kbhit()检测有没有按键,有的话返回非零;_getch()读取一个字符且不显示。方向键比较特殊,它会发送两个字节,第一个是224,第二个才是真正的方向码:72上、77右、80下、75左。忘了处理224这个前缀的话,按方向键会没反应。

提示:Sleep(150)控制游戏速度,单位是毫秒。数字越小蛇跑得越快,调到50就比较刺激了。如果你发现蛇“瞬移”,检查一下Sleep是不是漏了。

gotoxy用的是SetConsoleCursorPosition,比system("cls")更高效,因为它只移动光标而不清屏,闪烁会少很多。这个技巧在做所有控制台动画时都用得上。

3.6 俄罗斯方块简化版——矩阵操作与碰撞检测

俄罗斯方块是这6个里最复杂的,但拆开看,核心无非是消行和碰撞两件事。这里给一个可运行的骨架。

#include <iostream> #include <conio.h> #include <windows.h> #include <cstdlib> #include <ctime> using namespace std; const int W = 10, H = 18; int board[H][W]; // 7种方块,用4x4矩阵表示 int shapes[7][4][4] = { {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, // I {{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}}, // O {{0,0,0,0},{0,1,0,0},{1,1,1,0},{0,0,0,0}}, // T {{0,0,0,0},{1,0,0,0},{1,1,1,0},{0,0,0,0}}, // L {{0,0,0,0},{0,0,1,0},{1,1,1,0},{0,0,0,0}}, // J {{0,0,0,0},{0,1,1,0},{1,1,0,0},{0,0,0,0}}, // S {{0,0,0,0},{1,1,0,0},{0,1,1,0},{0,0,0,0}} // Z }; int curShape, curX, curY; bool checkCollision(int shape[4][4], int px, int py) { for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) if (shape[i][j]) { int nx = px + j, ny = py + i; if (nx < 0 || nx >= W || ny >= H) return true; if (ny >= 0 && board[ny][nx]) return true; } return false; } void lockPiece() { for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) if (shapes[curShape][i][j] && curY + i >= 0) board[curY + i][curX + j] = 1; } void clearLines() { for (int i = H - 1; i >= 0; i--) { bool full = true; for (int j = 0; j < W; j++) if (!board[i][j]) { full = false; break; } if (full) { for (int k = i; k > 0; k--) for (int j = 0; j < W; j++) board[k][j] = board[k - 1][j]; for (int j = 0; j < W; j++) board[0][j] = 0; i++; // 重新检查这一行(现在已经是上一行内容) } } } void newPiece() { curShape = rand() % 7; curX = W / 2 - 2; curY = 0; } void gotoxy(int x, int y) { COORD pos = {(SHORT)x, (SHORT)y}; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos); } void draw() { gotoxy(0, 0); for (int i = 0; i < H; i++) { cout << "|"; for (int j = 0; j < W; j++) { bool filled = board[i][j]; for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) if (shapes[curShape][r][c] && curY + r == i && curX + c == j) filled = true; cout << (filled ? "#" : " "); } cout << "|" << endl; } } int main() { srand((unsigned)time(0)); for (int i = 0; i < H; i++) for (int j = 0; j < W; j++) board[i][j] = 0; newPiece(); int tick = 0; while (true) { if (_kbhit()) { int key = _getch(); if (key == 224) { key = _getch(); if (key == 75 && !checkCollision(shapes[curShape], curX - 1, curY)) curX--; if (key == 77 && !checkCollision(shapes[curShape], curX + 1, curY)) curX++; if (key == 80 && !checkCollision(shapes[curShape], curX, curY + 1)) curY++; } else if (key == ' ') { while (!checkCollision(shapes[curShape], curX, curY + 1)) curY++; } } if (tick % 5 == 0) { if (!checkCollision(shapes[curShape], curX, curY + 1)) { curY++; } else { lockPiece(); clearLines(); newPiece(); if (checkCollision(shapes[curShape], curX, curY)) { draw(); cout << "游戏结束!" << endl; break; } } } draw(); Sleep(60); tick++; } return 0; }

碰撞检测是整个游戏的心脏。checkCollision函数做两件事:检查方块是否出界,检查方块是否撞到已经固定的格子。任何一个成立就返回true,表示不能往那个方向移动。有了这个函数,左右移动、下移、快速落底的逻辑都能复用同一套判断。

消行的逻辑是:从下往上扫描,发现满行就把上面所有行整体下移一格,同时把最上面一行清零。这里的i++配合循环的i--,是为了让当前行重新检查一次,因为下移之后原来这一行的内容变了。这个小细节如果写漏,一次消两行的时候就会出bug。

旋转在这个版本里我省略了,因为矩阵旋转写起来会让代码变长不少。旋转的本质是把4x4矩阵顺时针转90度,思路是newMatrix[i][j] = oldMatrix[3 - j][i]。你可以自己试着补上,加一个上方向键触发旋转,配合碰撞检测判断能不能转。

注意:方块的初始位置curY = 0,如果方块第一行是空的(比如I型方块的前两行都是0),那么实际显示会在靠下的位置,这是正常的。但游戏结束时判断checkCollision(curShape, curX, curY)要准确,否则会误判。

4. 常见问题与排查技巧实录

4.1 编译报错速查表

新手遇到的报错大部分都能归到几类,我整理了一张表,看到报错先对号入座:

报错信息关键词常见原因解决办法
expected ';' before上一行漏了分号检查报错行的上一行
'xxx' was not declared变量没定义或拼错检查变量名,注意大小写
stray '\' in program用了中文标点切换到英文输入法
no matching function for call函数参数类型不对检查调用时传的参数类型
undefined reference to函数只声明没定义,或拼写不一致检查函数定义
cannot open output file程序还在运行,文件被占用关掉之前的运行窗口再编译

其中最容易让人抓狂的是第一个。因为编译器报错的行号往往在真正出错行的下一行,你要养成“往上找一行”的习惯。

4.2 运行时异常与逻辑bug

编译通过了不代表没问题,游戏跑起来的行为可能和你想的完全不同。常见的运行时问题有这几类:

数组越界是最危险的,因为它不一定立刻崩溃。比如贪吃蛇里snakeX[100],如果你的蛇超过了100节,就会写到数组外面去,破坏别的变量。表现可能是“游戏突然变得很奇怪”,但不会报错。解决办法是把数组开大一点,同时在长度增加时加个上限判断。

随机数不随机,前面说过,是因为忘了初始化种子。每次都出一样的结果,八成是rand()没配srand。

死循环,通常是while条件写错了,或者循环里忘了改变量。比如写猜数字时,如果cin失败(用户输入了字母),cin会进入错误状态,guess保持不变,循环就转个没完。健壮的写法是在cin后加if (cin.fail()) { cin.clear(); cin.ignore(10000, '\n'); },不过初学者可以先不管这个。

画面闪烁,控制台游戏用system("cls")清屏时特别明显。解决办法就是前面提的gotoxy配合局部刷新,或者用双缓冲——先在内存里画好一帧,再一次性输出。

4.3 代码可复制性相关的隐藏问题

标题里说“可复制”,这里面其实有个坑。从网页上复制代码,经常会带上全角空格或者特殊字符,粘到Dev-C++里编译就报错。我的建议是复制之后先全选,用“格式-去除行尾空格”之类的功能清理一下,或者干脆重新敲一遍关键部分。

另一个问题是换行符。Windows、Linux、Mac的换行符不一样,从某些地方复制来的代码可能带着不兼容的换行,Dev-C++显示可能正常但编译异常。这种情况用记事本打开再另存为一下就好。

还有代码里的中文注释乱码。如果源文件保存成了UTF-8带BOM,有些老版本Dev-C++显示会出问题。稳妥的做法是把源文件保存为ANSI(GBK)编码,中文注释就不会乱码了。

提示:如果你把代码发给别人,最保险的是直接发.cpp文件,而不是复制粘贴文本。这样编码、换行都是原样的。

5. 从抄代码到改代码的进阶路径

5.1 给游戏加功能的三个思路

抄代码只是第一步,真正长本事的是改代码。我给你三个方向:

第一是改参数。猜数字把范围从100改成1000,贪吃蛇把速度调快,俄罗斯方块的棋盘加宽。改参数最简单,但能让你迅速理解哪些数字控制着什么。

第二是加功能。猜数字加个“显示历史猜测”,井字棋加个人机对战,打字游戏加个排行榜(用数组存最高分)。加功能会逼着你去理解原有代码的结构,知道在哪里插新代码。

第三是换实现。比如把贪吃蛇的数组实现换成deque,把井字棋的胜负判定写成查表法。同一种功能用不同方式实现,是加深理解的最佳途径。

5.2 从控制台走向图形界面的时机

控制台玩溜了之后,很多人会想搞图形界面。这里给个建议:别急着上OpenGL或SDL。可以先用Windows自带的GDI,也就是<windows.h>里的绘图函数,画个窗口、画几个方块,难度适中。等你能用GDI画出会动的贪吃蛇,再去学SDL或者更高级的引擎,会顺畅很多。

需要提醒的是,图形库的配置本身就是个门槛。Dev-C++配SDL需要手动加链接库,容易出错。如果这一步卡住了,可以用Visual Studio来做图形项目,它的包管理器省事很多。工具是为目的服务的,没必要死磕某一个IDE。

我个人实际操作的体会是,把这6个游戏从头到尾写一遍、改一遍,比看十本教程都管用。你会在过程中反复遇到数组越界、死循环、方向判断错误这些典型问题,解决它们的过程就是成长。写完贪吃蛇之后再去碰指针和内存管理,你会发现那些抽象概念突然变得具体了——因为你知道蛇的每一节数据在内存里是怎么排布的。最后一个建议:把改好的代码上传到自己的代码仓库,哪怕只有几十行,坚持下去,一年后回头看,那个文件夹就是你最好的学习记录。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:31:06

深度学习优化器全解析:SGD到AdamW的原理与PyTorch实践

做训练跑实验的兄弟&#xff0c;应该都体会过这种场景&#xff1a;模型结构没变&#xff0c;数据没换&#xff0c;就换了个优化器&#xff0c;收敛速度差出两三倍&#xff0c;最终精度也差出半个点以上。甚至有时候在 A 任务上跑得很稳的 Adam&#xff0c;切到 B 任务上直接 lo…

作者头像 李华
网站建设 2026/9/30 5:30:56

Windows10下用VMware虚拟机安装Ubuntu超详细教程

前阵子好几个朋友问我&#xff0c;Windows10 的电脑上到底怎么才能跑一套完整的 Linux 环境&#xff0c;既要能用 Ubuntu 做开发、跑服务&#xff0c;又不能影响日常办公和游戏。我的回答始终很统一&#xff1a;装虚拟机&#xff0c;这可能是普通用户接触 Linux 最稳妥、最不折…

作者头像 李华
网站建设 2026/9/30 5:30:55

游戏角色模型替换全流程解析:以高斯杰斯提斯为例

这次我们来看一个游戏角色模型替换的需求&#xff0c;目标角色先拿“高斯杰斯提斯”来当例子。这里说的“模型替换”不是改游戏数值&#xff0c;而是把游戏内的角色 3D 模型和贴图资源换掉&#xff0c;实现改皮肤、改外形、做二次创作这类效果。这类工作在游戏 Mod 圈里很常见&…

作者头像 李华
网站建设 2026/9/30 5:30:55

Tomcat安装配置与项目部署全流程详解

1. 写在前面&#xff1a;为什么每个Java开发都得会装Tomcat如果你接触过Java Web开发&#xff0c;那Tomcat这个名字你八成绕不开。它是目前应用最广的Servlet容器&#xff0c;也是Java Web开发者在本地开发、测试、上线阶段都会频繁打交道的一个基础组件。简单说&#xff0c;你…

作者头像 李华
网站建设 2026/9/30 5:30:22

网络货运平台运营方案:发票合规、后市场与金融风控实战解析

简介&#xff1a;一份聚焦网络货运平台运营的PDF方案&#xff0c;面向物流企业管理者、平台运营人员及行业研究者&#xff0c;系统梳理平台在营改增政策下的核心优势&#xff1a;解决个体运输户开票难、为小微车队提供运单/司机/车辆/调度/结算等管理工具&#xff0c;并通过平台…

作者头像 李华
网站建设 2026/9/30 5:30:19

vSAN扩容实操手册:纵向/横向扩容、Resync控制与故障域避坑

简介&#xff1a;《VMware vSAN 扩容手册 v1.1》是一份面向 VMware 运维与虚拟化工程师的官方实践指南&#xff0c;针对业务增长带来的 vSAN 容量与性能瓶颈&#xff0c;系统讲解横向扩容&#xff08;增加节点&#xff09;、纵向扩容&#xff08;增加磁盘/磁盘组&#xff09;及…

作者头像 李华