先分步测试每一步先运行 最小可跑代码运行环境是 Linux 虚拟机。文章的最后面有最终优化版 代码。第 1 步键盘输入单独测试 按方向键 能不能读到正确的值。#include iostream #include termios.h #include unistd.h #include fcntl.h #include cstdio // 初始化设置非阻塞输入 void EnableRawMode() { tcgetattr(STDIN_FILENO, g_oldTermios); struct termios raw g_oldTermios; // 运行游戏前关掉行缓冲 关掉回显 → 按键立即被程序读到屏幕不显示。 raw.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, raw); g_oldFlags fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags | O_NONBLOCK); // 加上非阻塞标志 } // 游戏结束后恢复终端 ( 打开行缓冲 打开回显 屏幕显示按键输入) void DisableRawMode() { tcsetattr(STDIN_FILENO, TCSANOW, g_oldTermios); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags); } // 只检测按键状态,不读取字符 int kbhit() { int ch getchar(); // 从 stdin 读一个字符 if(ch ! EOF) { ungetc(ch,stdin); // 把读出来的字符塞回输入流 return 1; } return 0; } // 读取按键字符 int getch() { return getchar(); } int main() { EnableRawMode(); std::cout 键盘测试 std::endl; std::cout 按方向键试试按 q 退出 std::endl; std::cout 按 p 测试 kbhit非阻塞检测 std::endl; std::cout std::endl; while (true) { if (kbhit()) { int key getch(); // 方向键在终端里是三字节的转义序列 ESC [ 字母 if (key 27) { // ESC if (kbhit()) { getch(); // 吃掉 [ key getch(); switch (key) { case A: std::cout ↑ 上箭头 std::endl; break; case B: std::cout ↓ 下箭头 std::endl; break; case C: std::cout → 右箭头 std::endl; break; case D: std::cout ← 左箭头 std::endl; break; default: std::cout 未知方向键: key std::endl; } } else { std::cout ESC 键 std::endl; } } // 普通按键 else if (key q || key Q) { std::cout 退出 std::endl; break; } else { std::cout 按键: (char)key (ASCII: key ) std::endl; } } // 模拟游戏主循环在做别的事在命令行打点 std::cout . std::flush; usleep(100000); // 100ms } DisableRawMode(); return 0; }第 2 步模拟游戏循环一个while循环每 100ms 打印一次tick按q退出。不画蛇先跑通循环。#include iostream #include chrono #include thread #include termios.h #include unistd.h #include fcntl.h #include cstdio struct termios g_oldTermios; int g_oldFlags; // 初始化设置非阻塞输入 void EnableRawMode() { tcgetattr(STDIN_FILENO, g_oldTermios); struct termios raw g_oldTermios; // 关掉行缓冲 关掉回显 → 按键立即被程序读到屏幕不显示。 raw.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, raw); g_oldFlags fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags | O_NONBLOCK); // 加上非阻塞标志 } // 游戏结束后恢复终端 ( 打开行缓冲 打开回显 屏幕显示按键输入) void DisableRawMode() { tcsetattr(STDIN_FILENO, TCSANOW, g_oldTermios); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags); } // 只检测按键状态,不读取字符 int kbhit() { int ch getchar(); // 从 stdin 读一个字符 if(ch ! EOF) { ungetc(ch,stdin); // 把读出来的字符塞回输入流 return 1; } return 0; } // 读取按键字符 int getch() { return getchar(); } int main() { EnableRawMode(); int tick 0; bool running true; while(running) { if(kbhit()) { int key getch(); if(key q || key Q) { running false; } } tick ; std::cout \rtick: tick (按 q 退出) std::flush; // 帧率控制100ms 一帧 std::this_thread::sleep_for(std::chrono::milliseconds(100)); } DisableRawMode(); std::cout \n循环结束, 退出。 std::endl; return 0; }第 3 步画边框 静态蛇 食物在屏幕上画一条不动的蛇验证 Board 和渲染。#include iostream #include chrono #include thread #include vector #include termios.h #include unistd.h #include fcntl.h #include cstdio struct termios g_oldTermios; int g_oldFlags; // 初始化设置非阻塞输入 void EnableRawMode() { tcgetattr(STDIN_FILENO, g_oldTermios); struct termios raw g_oldTermios; // 关掉行缓冲 关掉回显 → 按键立即被程序读到屏幕不显示。 raw.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, raw); g_oldFlags fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags | O_NONBLOCK); // 加上非阻塞标志 } // 游戏结束后恢复终端 ( 打开行缓冲 打开回显 屏幕显示按键输入) void DisableRawMode() { tcsetattr(STDIN_FILENO, TCSANOW, g_oldTermios); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags); } // 只检测按键状态,不读取字符 int kbhit() { int ch getchar(); // 从 stdin 读一个字符 if(ch ! EOF) { ungetc(ch,stdin); // 把读出来的字符塞回输入流 return 1; } return 0; } // 读取按键字符 int getch() { return getchar(); } const int W 30; // 地图宽 const int H 15; // 地图高 void Draw(const std::vectorint snakeX, const std::vectorint snakeY, int foodX,int foodY) { // 清屏 std::cout \033[2AJ\033[H std::endl; // 上边界 for(int i 0; i W2 ; i) std::cout #; std::cout std::endl; // 中间 for(int y 0; y H; y) { std::cout #; for(int x 0; x W; x) { bool drawn false; // 画蛇 for(size_t i 0; i snakeX.size(); i) { if(snakeX[i] x snakeY[i] y) { std::cout (i 0 ? : o); drawn true; break; } } // 画食物 , 防止蛇和食物重叠时蛇显示不完整 if(!drawn x foodX y foodY) { std::cout *; drawn true; } if(!drawn) std::cout ; } std::cout # std::endl; } // 下边界 for(int i 0; i W2 ; i) std::cout #; std::cout std::endl; } int main() { EnableRawMode(); // 蛇头在 (15,7)两节身体 std::vectorint snakeX {15,14,13}; std::vectorint snakeY {7,7,7}; int foodX 20, foodY 7; bool running true; while(running) { if(kbhit()) { int key getch(); if(key q || key Q) { running false; } } Draw(snakeX,snakeY,foodX,foodY); // 帧率控制100ms 一帧 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout (按 q 退出) std::endl; } DisableRawMode(); return 0; }第 4 步蛇自动移动 方向控制在静态蛇基础上加一个定时器让蛇自己往前跑加上键盘改变方向。蛇每 100ms 往前移动一格。#include iostream #include chrono #include thread #include vector #include deque #include termios.h #include unistd.h #include fcntl.h #include cstdio struct termios g_oldTermios; int g_oldFlags; // 初始化设置非阻塞输入 void EnableRawMode() { tcgetattr(STDIN_FILENO, g_oldTermios); struct termios raw g_oldTermios; // 关掉行缓冲 关掉回显 → 按键立即被程序读到屏幕不显示。 raw.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, raw); g_oldFlags fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags | O_NONBLOCK); // 加上非阻塞标志 } // 游戏结束后恢复终端 ( 打开行缓冲 打开回显 屏幕显示按键输入) void DisableRawMode() { tcsetattr(STDIN_FILENO, TCSANOW, g_oldTermios); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags); } // 只检测按键状态,不读取字符 int kbhit() { int ch getchar(); // 从 stdin 读一个字符 if(ch ! EOF) { ungetc(ch,stdin); // 把读出来的字符塞回输入流 return 1; } return 0; } // 读取按键字符 int getch() { return getchar(); } const int W 30; // 地图宽 const int H 15; // 地图高 enum Direction { UP, DOWN, LEFT, RIGHT }; void Draw(const std::dequeint snakeX, const std::dequeint snakeY, int foodX,int foodY) { // 清屏 std::cout \033[2AJ\033[H std::endl; // 上边界 for(int i 0; i W2 ; i) std::cout #; std::cout std::endl; // 中间 for(int y 0; y H; y) { std::cout #; for(int x 0; x W; x) { bool drawn false; // 画蛇 for(size_t i 0; i snakeX.size(); i) { if(snakeX[i] x snakeY[i] y) { std::cout (i 0 ? : o); drawn true; break; } } // 画食物 , 防止蛇和食物重叠时蛇显示不完整 if(!drawn x foodX y foodY) { std::cout *; drawn true; } if(!drawn) std::cout ; } std::cout # std::endl; } // 下边界 for(int i 0; i W2 ; i) std::cout #; std::cout std::endl; } int main() { EnableRawMode(); // 蛇用 deque方便头插尾删 std::dequeint snakeX {15,14,13}; std::dequeint snakeY {7,7,7}; Direction dir RIGHT; // 默认移动方向 int foodX 20, foodY 7; bool running true; while(running) { if(kbhit()) { int key getch(); if (key 27) { if (kbhit()) { getch(); key getch(); switch (key) { case A: dir UP; break; case B: dir DOWN; break; case C: dir RIGHT; break; case D: dir LEFT; break; default: break; } } else { std::cout ESC 键 std::endl; } } else if(key q || key Q) { running false; } } // 更新蛇自动移动 int newX snakeX.front(); int newY snakeY.front(); // 移动方向 switch(dir) { case UP: newY--; break; case DOWN: newY; break; case LEFT: newX--; break; case RIGHT: newX; break; } // 头插新位置 snakeX.push_front(newX); snakeY.push_front(newY); // 删尾巴保持长度不变 snakeX.pop_back(); snakeY.pop_back(); // 渲染 蛇身 Draw(snakeX,snakeY,foodX,foodY); // 帧率控制100ms 一帧 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::cout (按 q 退出) std::endl; } DisableRawMode(); return 0; }第 5 步吃食物 蛇身增长加上食物碰撞检测和身体增长。加头文件 #include cstdlib修改 main()int main() { EnableRawMode(); srand(time(0)); // 初始化随机种子 // 蛇用 deque方便头插尾删 std::dequeint snakeX {15,14,13}; std::dequeint snakeY {7,7,7}; Direction dir RIGHT; // 默认移动方向 int foodX 20, foodY 7; bool running true; while(running) { if(kbhit()) { int key getch(); if (key 27) { if (kbhit()) { getch(); key getch(); switch (key) { case A: dir UP; break; case B: dir DOWN; break; case C: dir RIGHT; break; case D: dir LEFT; break; default: break; } } else { std::cout ESC 键 std::endl; } } else if(key q || key Q) { running false; } } // 更新蛇自动移动 int newX snakeX.front(); int newY snakeY.front(); switch(dir) { case UP: newY--; break; case DOWN: newY; break; case LEFT: newX--; break; case RIGHT: newX; break; } // 头插新位置 snakeX.push_front(newX); snakeY.push_front(newY); // 判断是否吃到食物 bool ate (newX foodX newY foodY); if(ate) { foodX rand() % W; foodY rand() % H; } else { // 没吃到删尾巴保持长度 snakeX.pop_back(); snakeY.pop_back(); } // 渲染 蛇身 Draw(snakeX,snakeY,foodX,foodY); // 帧率控制200ms 一帧 std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::cout (按 q 退出) std::endl; } DisableRawMode(); return 0; }第 6 步碰撞检测撞墙、撞自己 → 游戏结束。修改 main() 增加 撞墙检测 和 撞自己检测int main() { EnableRawMode(); srand(time(0)); // 初始化随机种子 // 蛇用 deque方便头插尾删 std::dequeint snakeX {15,14,13}; std::dequeint snakeY {7,7,7}; Direction dir RIGHT; // 默认移动方向 int foodX 20, foodY 7; bool running true; while(running) { if(kbhit()) { int key getch(); if (key 27) { if (kbhit()) { getch(); key getch(); switch (key) { // 加上禁止反向 case A: if (dir ! DOWN) dir UP; break; case B: if (dir ! UP) dir DOWN; break; case C: if (dir ! LEFT) dir RIGHT; break; case D: if (dir ! RIGHT) dir LEFT; break; default: break; } } else { std::cout ESC 键 std::endl; } } else if(key q || key Q) { running false; } } // 更新蛇自动移动 int newX snakeX.front(); int newY snakeY.front(); switch(dir) { case UP: newY--; break; case DOWN: newY; break; case LEFT: newX--; break; case RIGHT: newX; break; default: break; } // 头插新位置 snakeX.push_front(newX); snakeY.push_front(newY); // 撞墙检测 if(newX 0 || newX W || newY 0 || newY H) { running false; break; } // 撞自己检测 bool hitSelf false; for(size_t i 1; i snakeX.size() ; i) { if(snakeX[i] newX snakeY[i] newY) { hitSelf true; break; } } if(hitSelf) { running false; break; } // 判断是否吃到食物 bool ate (newX foodX newY foodY); if(ate) { foodX rand() % W; foodY rand() % H; } else { // 没吃到删尾巴保持长度 snakeX.pop_back(); snakeY.pop_back(); } // 渲染 蛇身 Draw(snakeX,snakeY,foodX,foodY); // 帧率控制200ms 一帧 std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::cout (按 q 退出) std::endl; } std::cout \033[2J\033[H std::flush; // 清屏 std::cout GAME OVER std::endl; std::cout 蛇长: snakeX.size() std::endl; DisableRawMode(); return 0; }----------------------------------------------------------------------------------------------------------------------------下面是 优化版#include iostream #include deque #include vector #include chrono #include thread #include cstdlib #include ctime #include termios.h #include unistd.h #include fcntl.h #include cstdio struct termios g_oldTermios; int g_oldFlags; void EnableRawMode() { tcgetattr(STDIN_FILENO, g_oldTermios); struct termios raw g_oldTermios; raw.c_lflag ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, raw); g_oldFlags fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, g_oldFlags | O_NONBLOCK); } void DisableRawMode() { tcsetattr(STDIN_FILENO, TCSANOW, g_oldTermios); fcntl(STDIN_FILENO, F_SETFL,g_oldFlags); } int kbhit() { int ch getchar(); if(ch ! EOF) { ungetc(ch,stdin); return 1; } return 0; } int getch() { return getchar(); } // 在老式的 C/C 代码中定义常量通常用宏定义 #define // 现代写法使用 constexpr 替代宏 #define 类型更安全 constexpr int Width 40; constexpr int Height 20; // 在老式的 C/C 代码中定义枚举用 enum // 现代写法强类型枚举, 使用 enum class 防止命名污染, 使用时需要加前缀Direction::UP enum class Direction { UP,DOWN,LEFT,RIGHT }; // 蛇 class Snake { public: std::dequeint snakeX,snakeY; Direction dir; Snake() : dir(Direction::RIGHT) { snakeX { Width/2, Width/2 -1, Width/2 - 2}; snakeY { Height/2, Height/2, Height/2 }; } int HeadX() const { return snakeX.front(); } int HeadY() const { return snakeY.front(); } // 获取下一步的坐标用于预判碰撞和吃食物 void GetNextPos(int NextX, int NextY) const { NextX snakeX.front(); NextY snakeY.front(); switch (dir) { case Direction::UP: NextY--; break; case Direction::DOWN: NextY; break; case Direction::LEFT: NextX--; break; case Direction::RIGHT: NextX; break; } } void Move(bool grow) { int NextX,NextY; GetNextPos(NextX,NextY); // 蛇头移动 snakeX.push_front(NextX); snakeY.push_front(NextY); // 没吃到食物删尾巴保持长度 if(!grow) { snakeX.pop_back(); snakeY.pop_back(); } } // 撞自己检测 bool CollidesSelf() const { for(size_t i 1; i snakeX.size(); i){ if(snakeX[i] snakeX[0] snakeY[i] snakeY[0]) return true; } return false; } // 撞墙检测 bool CollidesWall() const { return snakeX[0] 0 || snakeX[0] Width || snakeY[0] 0 || snakeY[0] Height; } // 检查某个坐标是否被蛇身占据 ( 食物是否出现在蛇身上 ) bool Occupies(int x, int y) const { for(size_t i 0; i snakeX.size(); i) { if(snakeX[i] x snakeY[i] y) return true; } return false; } }; // 食物 class Food { public: int x,y; void Respawn(const Snake snake) { do { x rand() % Width; y rand() % Height; } while (snake.Occupies(x, y)); } }; // 渲染器 class Renderer { public: void InitScreen() const { std::cout \033[?1049h std::flush; // 让终端切换到全屏模式 /* \033 是 ESC 字符的八进制表示ASCII 码 27告诉终端“接下来的字符不是文本是一条控制命令” [ 是大多数终端控制指令的开头标记 ?1049 是具体的功能代码, 1049 代表“启用替代屏幕缓冲区” h High启用/高电平是动作指令意思是 Set Mode设置模式 l Low复位/低电平代表 Reset Mode重置模式/复位模式 std::flush 把缓冲区里的内容立刻刷到屏幕上。 */ } void Cleanup() const { std::cout \033[?1049l std::flush; // 关闭全屏模式 } /* std::endl 全部改用 \n让 C 标准库自行决定刷新时机或最后一次性刷新) */ void Draw(const Snake snake,const Food food, int score, int level, bool paused) const { // 仅移动光标到左上角而不是清屏减少闪烁 std::cout \033[H; DrawBorder(Width); // 上边界 for (int y 0; y Height; y) { std::cout #; for(int x 0; x Width; x) { if (snake.Occupies(x,y)) { std::cout ((x snake.HeadX() y snake.HeadY()) ? :o); } else if (food.x x food.y y) std::cout *; else std::cout ; } std::cout #\n; } DrawBorder(Width); // 下边界 std::cout Score: score; std::cout level: level; if (paused) std::cout [PAUSED]; std::cout \n; // 这里的 flush 保证实时性但比每行 std::endl 快得多 std::cout.flush(); } private: void DrawBorder(int w) const { for (int i 0; i w 2; i) std::cout #; std::cout \n; } }; // 游戏 class Game { Snake m_Snake; Food m_Food; Renderer m_Renderer; int m_Score; int m_Level; int m_Speed; bool m_Running; bool m_Paused; public: Game() : m_Score(0),m_Level(1),m_Speed(200),m_Running(true),m_Paused(false) { srand(time(0)); // 初始化随机种子 m_Food.Respawn(m_Snake); // 食物 } void Run() { EnableRawMode(); m_Renderer.InitScreen(); // 全屏模式 while(m_Running) { Input(); if (!m_Paused) { // 游戏暂停按键 Update(); } m_Renderer.Draw(m_Snake,m_Food,m_Score,m_Level, m_Paused); std::this_thread::sleep_for(std::chrono::milliseconds(m_Speed));// ms } m_Renderer.Cleanup(); // 关闭全屏模式 DisableRawMode(); // 游戏结束提示 std::cout \033[2J\033[H std::flush; std::cout GAME OVER \n; std::cout Final Score: m_Score \n; std::cout Final Level: m_Level \n; } private: void Input() { if (!kbhit()) return; int key getch(); // 处理方向键 (ANSI 转义序列: 27 91 xx) if(key 27 kbhit()) { getch(); // 吃掉 [ key getch(); switch(key) { // 统一转 WASD方向键和键盘同一套 switch case 65: key w; break; // Up case 66: key s; break; // Down case 68: key a; break; // Left case 67: key d; break; // Right } } switch (key) { case w: case W: if (m_Snake.dir ! Direction::DOWN) { m_Snake.dir Direction::UP; } break; case s: case S: if (m_Snake.dir ! Direction::UP) { m_Snake.dir Direction::DOWN; } break; case a: case A: if (m_Snake.dir ! Direction::RIGHT) { m_Snake.dir Direction::LEFT; } break; case d: case D: if (m_Snake.dir ! Direction::LEFT) { m_Snake.dir Direction::RIGHT; } break; case p: case P: m_Paused !m_Paused; break; case q: case Q: m_Running false; break; // 退出游戏循环结束 } } void Update() { // 预判下一步位置 int NextX,NextY; m_Snake.GetNextPos(NextX,NextY); // 判断是否吃到食物 (预判机制) bool ate (NextX m_Food.x NextY m_Food.y); // 蛇吃到食物时 尾巴不删除 身体增长 没吃到食物时 尾巴删除 移动 m_Snake.Move(ate); // 碰撞墙壁检测碰撞自身检测 if (m_Snake.CollidesWall() || m_Snake.CollidesSelf()) { m_Running false; return; } // 如果吃到食物更新分数和食物位置 if(ate) { m_Score 10; if(m_Score 150) { m_Level 4; m_Speed 50; } else if(m_Score 100) { m_Level 3; m_Speed 100; } else if(m_Score 50) { m_Level 2; m_Speed 150; } m_Food.Respawn(m_Snake); } } }; int main() { bool playAgain true; while(playAgain) { Game game; game.Run(); // 游戏结束后 std::cout 按 R 重新开始, Q 退出...\n; EnableRawMode(); while(1) { if(kbhit()) { int key getch(); if(key r || key R) break; // 运行 game.Run() if(key q || key Q) { playAgain false; // 循环结束 break; } } std::this_thread::sleep_for(std::chrono::milliseconds(50)); /* 如果用户什么都不按kbhit() 返回假 程序就会像死循环一样疯狂空转导致 CPU 占用率飙升。 所以要把 sleep 放在 if 外面 这样无论有没有按键程序都会“喘口气” */ } DisableRawMode(); } std::cout Game End!\n; return 0; }