9、C++项目实战:贪吃蛇游戏(控制台版)
贪吃蛇,这玩意儿几乎是每个程序员的入门必修课。我当年在学校机房,用Turbo C敲出第一条能动的蛇时,那种兴奋感至今还记得。说白了,这个项目虽然小,但五脏俱全——你得处理输入、管理状态、控制帧率,还得防着自己把自己绕死。
今天咱们就用C++,在控制台里把这个经典游戏复现一遍。我会把踩过的坑、用过的技巧都抖出来。
核心设计思路
先别急着写代码。你想想看,贪吃蛇最核心的是什么?
- 蛇身:一串坐标点的集合,会动会变长
- 食物:随机出现,吃了就长
- 地图:边界撞上就死
- 方向控制:WASD或者方向键
- 游戏循环:不断刷新、检测、渲染
我个人习惯把数据结构和逻辑分开。蛇身用双端队列(deque)最合适——头插尾删,完美模拟移动。
关键点:蛇的移动不是每个格子都动,而是“头前进一格,尾删除一格”。吃到食物时,尾不删,长度就+1了。
数据结构设计
先定义几个基础类型:
struct Point {
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
enum class Direction { UP, DOWN, LEFT, RIGHT };
enum class GameState { RUNNING, PAUSED, GAME_OVER };
蛇身我用deque:
std::deque<Point> snake;
Point food;
Direction dir;
GameState state;
int score;
嗯,这里要注意:deque支持头尾快速插入删除,正好符合蛇的移动逻辑。我早期用vector写过,每次移动都要搬移数据,效率差不说,代码还丑。
游戏循环——心脏
控制台游戏的核心就是一个死循环,不断做三件事:
- 处理输入:检测键盘,改变方向
- 更新逻辑:移动蛇、检测碰撞、吃食物
- 渲染画面:清屏、画地图、画蛇、画食物
while (state == GameState::RUNNING) {
handleInput();
update();
render();
Sleep(100); // 控制速度
}
我曾经犯过一个低级错误——把Sleep放在handleInput前面,结果方向响应延迟严重,蛇都撞墙了键盘还没反应。后来改成先处理输入再更新,手感立马好了。
移动逻辑——别把自己绕死
蛇的移动分三步:
void update() {
Point head = snake.front();
Point next = head;
switch (dir) {
case Direction::UP: next.y--; break;
case Direction::DOWN: next.y++; break;
case Direction::LEFT: next.x--; break;
case Direction::RIGHT: next.x++; break;
}
// 检测是否吃到食物
if (next == food) {
snake.push_front(next); // 头前进,尾不动
score += 10;
spawnFood();
} else {
snake.push_front(next); // 头前进
snake.pop_back(); // 尾删除
}
// 碰撞检测
checkCollision();
}
这里有个坑:你不能让蛇直接掉头。比如当前方向是UP,你按DOWN,蛇头会直接撞到自己身体。我一般加个方向锁:
void handleInput() {
if (_kbhit()) {
char ch = _getch();
Direction newDir = dir;
switch (ch) {
case 'w': case 'W': newDir = Direction::UP; break;
case 's': case 'S': newDir = Direction::DOWN; break;
case 'a': case 'A': newDir = Direction::LEFT; break;
case 'd': case 'D': newDir = Direction::RIGHT; break;
}
// 禁止反向
if ((dir == Direction::UP && newDir != Direction::DOWN) ||
(dir == Direction::DOWN && newDir != Direction::UP) ||
(dir == Direction::LEFT && newDir != Direction::RIGHT) ||
(dir == Direction::RIGHT && newDir != Direction::LEFT)) {
dir = newDir;
}
}
}
注意:_kbhit()和_getch()是Windows下的控制台函数。如果你在Linux/macOS上编译,需要用termios库替代。跨平台的话,建议封装一层输入接口。
碰撞检测——生死一线
碰撞分两种:撞墙和撞自己。
void checkCollision() {
Point head = snake.front();
// 撞墙
if (head.x < 0 || head.x >= WIDTH ||
head.y < 0 || head.y >= HEIGHT) {
state = GameState::GAME_OVER;
return;
}
// 撞自己(从第2个节点开始检查)
for (size_t i = 1; i < snake.size(); ++i) {
if (head == snake[i]) {
state = GameState::GAME_OVER;
return;
}
}
}
为什么从第2个节点开始?因为蛇头就是第一个节点,自己跟自己比肯定相等。我刚开始写的时候忘了这个,蛇一出生就game over,调试了半天才发现。
渲染——控制台也能好看
控制台渲染说白了就是打印字符。我用二维数组表示地图:
void render() {
system("cls"); // 清屏
// 初始化地图
char map[HEIGHT][WIDTH];
memset(map, ' ', sizeof(map));
// 画边界
for (int i = 0; i < WIDTH; ++i) {
map[0][i] = '#';
map[HEIGHT-1][i] = '#';
}
for (int i = 0; i < HEIGHT; ++i) {
map[i][0] = '#';
map[i][WIDTH-1] = '#';
}
// 画蛇
for (auto& p : snake) {
map[p.y][p.x] = 'O';
}
// 蛇头特殊标记
map[snake.front().y][snake.front().x] = '@';
// 画食物
map[food.y][food.x] = '$';
// 打印
for (int y = 0; y < HEIGHT; ++y) {
for (int x = 0; x < WIDTH; ++x) {
std::cout << map[y][x];
}
std::cout << '\n';
}
std::cout << "Score: " << score << '\n';
}
小技巧:用system("cls")清屏虽然方便,但会闪烁。如果你追求流畅,可以用SetConsoleCursorPosition直接定位光标重绘,不过代码量会大一些。我一般教学用cls,够用了。
食物生成——别生到蛇身上
食物不能出现在蛇身上,这个逻辑要写清楚:
void spawnFood() {
do {
food.x = rand() % (WIDTH - 2) + 1;
food.y = rand() % (HEIGHT - 2) + 1;
} while (std::find(snake.begin(), snake.end(), food) != snake.end());
}
随机数记得先srand(time(NULL))初始化。我见过有人忘了这步,结果每次启动游戏食物都在同一个位置,那还玩个啥。
完整项目结构
我把代码组织成三个文件,这样清晰:
- SnakeGame.h:声明类、枚举、常量
- SnakeGame.cpp:实现所有逻辑
- main.cpp:入口,创建游戏对象并启动
类的大概样子:
class SnakeGame {
public:
SnakeGame(int width, int height);
void run();
private:
void handleInput();
void update();
void render();
void checkCollision();
void spawnFood();
std::deque<Point> snake;
Point food;
Direction dir;
GameState state;
int score;
int width, height;
};
SVG流程图:贪吃蛇核心逻辑
避坑指南
我总结几个新手容易踩的坑:
- 方向反转:上面说过了,加个方向锁就行
- 蛇身重叠:移动时先加头再删尾,顺序别反。反了的话,蛇头会吃到刚删掉的尾
- 食物刷在蛇身上:用do-while循环保证位置合法
- 控制台闪烁:如果介意,用双缓冲或者直接操作控制台缓冲区
- 速度控制:Sleep的时间不要固定,可以随着分数增加逐渐减少,游戏会越来越刺激
扩展思路:做完基础版后,你可以加些花样——穿墙模式、加速道具、障碍物、排行榜。我有个学生加了“毒食物”,吃了会缩短蛇身,结果成了全班最受欢迎的游戏。
好了,贪吃蛇的核心就这些。代码量不大,但每个环节都考验你对C++基础的理解。动手写一遍,比看十遍都管用。
公众号:蓝海资料掘金营,微信deep3321