C++控制台吃豆子游戏实现解析
本文将深入解析如何利用C++语言和Windows控制台API开发一款经典的“吃豆子”(Pac-Man风格)小游戏。该程序无需依赖大型图形库,仅通过字符输出和控制台光标定位即可实现流畅的游戏体验。内容涵盖核心数据结构设计、随机地图生成、碰撞检测算法以及键盘事件处理机制,适合希望了解底层游戏循环实现的开发者参考。
项目运行效果与环境
游戏在标准Windows命令行窗口中运行,采用字符矩阵表示地图元素:黑色方块(■)代表不可逾越的墙壁,数字或特定符号代表可食用的豆子,而角色(豆人)则通过动态清除与重绘实现移动。按下 ESC 键可随时退出游戏。
程序通过 mode con cols=50 lines=21 命令调整控制台大小以适配游戏区域,确保显示比例协调。
核心模块与数据结构设计
整个游戏由主逻辑入口 main.cpp 和核心功能库 lib.h/cpp 组成。为了实现简洁性,代码使用了结构体来封装关键实体:
- POS (坐标): 记录元素的 X/Y 整数坐标。
- BODY (角色): 包含当前坐标
pos和运动方向Direction。 - WALL (墙体): 存储所有墙壁的坐标数组及总长度。
- FOOD (食物): 存储当前屏幕上剩余豆子的位置、数量及隐藏状态。
- HALL (可行通道): 预处理好的非墙壁区域坐标列表,用于限制豆子随机生成的范围。
第1步:编译与初始化环境
在开始编码前,请确保你的 C++ 编译器(如 MSVC 或 MinGW)支持 Windows API。以下头文件是必要的:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
初始化阶段 (Init 函数) 负责获取控制台句柄、隐藏默认光标,并根据预定义的二维数组 short wall[17][24] 构建关卡。其中 0 表示墙壁,-1 表示通道。
第2步:绘制地图与生成食物
游戏启动时,MakeWall 函数遍历墙数组,将对应坐标的字符替换为 “■”。同时,系统会在走廊数组中随机选取 7 个有效位置生成豆子,并通过 Print 函数将其渲染到控制台。
第3步:响应按键与控制移动
这是游戏循环的核心。Move 函数每 200 毫秒执行一次,通过 GetKey() 调用 Windows API GetAsyncKeyState 监听上下左右键及 ESC 键。
- 方向改变: 若检测到按键,更新角色的
Direction属性。 - 移动判定: 根据当前方向计算新坐标。
- 碰撞检测:
IsKnock函数检查新坐标是否与墙壁重合。若相撞,保持原位置不变;若与食物重合,则扣除一个豆子并增加分数。 - 画面刷新: 先在上一个位置打印空格
" "进行擦除,再在新位置打印角色符号。
完整源代码实现
以下是可直接编译运行的核心代码逻辑。主循环不断调用移动和渲染函数,直到玩家退出或完成当前关卡。
1. 主程序 (main.cpp)
#include "lib.h"
#pragma once
extern int level;
int main()
{
FOOD food;
WALL wall;
BODY CurPos;
HALL hall;
int iExit = 0;
while(1)
{
if(iExit)
break;
Init(&food,&wall,&CurPos,&hall);
MakeWall(&wall);
while(1)
{
Sleep(200);
int res = Move(&food,&wall,&CurPos);
if(1 == res) //重新开局
break;
else if(-1 == res) //用户退出
{
iExit = 1;
break;
}
ShowScore(&CurPos,&food);
}
}
return 0;
}
2. 头文件定义 (lib.h)
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
//标识用户按下的方向键
#define KEY_UP WM_USER + 1
#define KEY_DOWN WM_USER + 2
#define KEY_LEFT WM_USER + 3
#define KEY_RIGHT WM_USER + 4
//标识用户当前运动方向
#define DIRECTION_UP WM_USER + 5
#define DIRECTION_DOWN WM_USER + 6
#define DIRECTION_LEFT WM_USER + 7
#define DIRECTION_RIGHT WM_USER + 8
//标识要打印的元素类型
#define PRINT_WALL WM_USER + 9
#define PRINT_FOOD WM_USER + 10
#define PRINT_BODY WM_USER + 11
#define PRINT_CLEAN WM_USER + 12
#define KNOCK_WALL WM_USER + 13
#define KNOCK_FOOD WM_USER + 14
struct POS
{
int x;
int y;
};
struct WALL
{
POS pos[9999];
int len;
};
struct FOOD
{
POS pos[8];
int len;
int IsHidden;
};
struct BODY
{
POS pos;
int Direction;
};
struct HALL{
POS pos[200];
int len;
};
void Init(FOOD *pFood,WALL *pWall,BODY *pBody,HALL *pHall);
//进行一些初始化操作
void Print(POS *pos,int TYPE,int ix = 0);
//完成打印功能
void MakeWall(WALL *pWall);
//生成墙
void MakeFood(HALL *pHall,FOOD *pFood);
//生成食物
int Move(FOOD *pFood,WALL *pWall,BODY *pBody);
//用户移动
void ShowScore(BODY *pBody,FOOD *pFood);
//显示等级 和剩余豆子数
int IsOver(POS pos1,POS pos2,int TYPE);
//判断2点是否重合
int IsKnock(WALL *pWall,BODY *pBody,FOOD *pFood,int TYPE);
//判断是否撞墙、吃到豆子
int GetKey();
//得到用户按键
3. 逻辑实现 (lib.cpp)
#include "lib.h"
#pragma once
HANDLE hMain_Out;
HANDLE hMain_In;
CONSOLE_CURSOR_INFO info;
int iBeans = 0;
int level = 1;
//地图定义 0为墙 -1为通道
short wall[17][24] = { ... }; //此处省略具体地图数据,详见原文lib.cpp部分
//判断是否碰撞
int IsKnock(WALL *pWall,BODY *pBody,FOOD *pFood,int TYPE)
{
if(KNOCK_WALL == TYPE)
{
int i;
for(i = 0 ; i < pWall->len ; ++i)
{
if(IsOver(pBody->pos,pWall->pos[i],KNOCK_WALL))
return 1;
}
}
else if(KNOCK_FOOD == TYPE)
{
int i;
for(i = 1 ; i <= pFood->len ; ++i)
{
if(IsOver(pFood->pos[i],pBody->pos,KNOCK_WALL))
return i;
}
}
return 0;
}
//用户移动逻辑
int Move(FOOD *pFood,WALL *pWall,BODY *pBody)
{
BODY prePos = *pBody;
int iKey = GetKey();
if(-1 == iKey) //用户退出
return -1;
if(iKey)
{
pBody->Direction = iKey + 4;
iKey = 0;
}
if(0 == iKey)
{
if(DIRECTION_UP == pBody->Direction)
--pBody->pos.y;
else if(DIRECTION_DOWN == pBody->Direction)
++pBody->pos.y;
else if(DIRECTION_LEFT == pBody->Direction)
--pBody->pos.x;
else if(DIRECTION_RIGHT == pBody->Direction)
++pBody->pos.x;
}
if(IsKnock(pWall,pBody,pFood,KNOCK_WALL))
{
*pBody = prePos;
return 0;
}
int ix = IsKnock(pWall,pBody,pFood,KNOCK_FOOD);
if(ix)
{
++iBeans;
int i;
for(i = ix ; i <= (pFood->len - 1) ; ++i)
pFood->pos[i] = pFood->pos[i + 1];
--(pFood->len);
if(0 == pFood->len)
{
++level;
return 1;
}
}
Print(&prePos.pos,PRINT_CLEAN);
Print(&pBody->pos,PRINT_BODY);
return 0;
}
//坐标重合判断
int IsOver(POS pos1,POS pos2,int TYPE)
{
if(KNOCK_WALL == TYPE)
if((pos1.x == pos2.x && pos1.y == pos2.y) || (pos2.x + 1 == pos1.x && pos2.y == pos1.y))
return 1;
return 0;
}
//生成墙体
void MakeWall(WALL *pWall)
{
int x,y;
int ix = 0;
for(x = 0 ; x <= 16 ; ++x)
{
for(y = 0 ; y <= 23 ; ++y)
{
if(0 == wall[x][y])
{
pWall->pos[ix].x = 2 * y;
pWall->pos[ix].y = x;
Print(&pWall->pos[ix++],PRINT_WALL);
}
}
}
pWall->len = ix;
}
//初始化函数
void Init(FOOD *pFood,WALL *pWall,BODY *pBody,HALL *pHall)
{
hMain_Out = GetStdHandle(STD_OUTPUT_HANDLE);
hMain_In = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleCursorInfo(hMain_Out,&info);
info.bVisible = FALSE;
SetConsoleCursorInfo(hMain_Out,&info);
pFood->len = 0;
pWall->len = 0;
pHall->len = 0;
system("mode con cols=50 lines=21");
int x,y;
int ix = 0;
for(x = 0 ; x < 17 ; ++x)
{
for(y = 0 ; y < 24 ; ++y)
{
if(wall[x][y])
{
pHall->pos[ix].x = 2 * y;
pHall->pos[ix++].y = x;
}
}
}
pHall->len = ix;
pBody->pos.x = 2;
pBody->pos.y = 1;
pBody->Direction = DIRECTION_DOWN;
printf("%d %d\n",pHall->pos[0].x,pHall->pos[0].y);
int i;
MakeFood(pHall,pFood);
for(i = 1 ; i <= 7 ; ++i)
{
Print(&pFood->pos[i],PRINT_FOOD,i);
}
}
//获取按键
int GetKey()
{
if(GetAsyncKeyState(VK_UP))
return KEY_UP;
if(GetAsyncKeyState(VK_DOWN))
return KEY_DOWN;
if(GetAsyncKeyState(VK_LEFT))
return KEY_LEFT;
if(GetAsyncKeyState(VK_RIGHT))
return KEY_RIGHT;
if(GetAsyncKeyState(VK_ESCAPE))
return -1;
return 0;
}
//渲染功能
void Print(POS *p,int TYPE,int ix)
{
COORD pos;
pos.X = p->x;
pos.Y = p->y;
SetConsoleCursorPosition(hMain_Out,pos);
if(PRINT_WALL == TYPE)
printf("■");
else if(PRINT_FOOD == TYPE)
printf("%d",ix);
else if(PRINT_BODY == TYPE)
printf("\1");
else if(PRINT_CLEAN == TYPE)
printf(" ");
}
//显示分数
void ShowScore(BODY *pBody,FOOD *pFood)
{
COORD pos;
pos.X = 0;
pos.Y = 18;
SetConsoleCursorPosition(hMain_Out,pos);
printf("\tBeans left : %d | pos : x=%d,y=%d\n",pFood->len,pBody->pos.x,pBody->pos.y);
printf("\t\tLevel : %d",level);
}
//生成食物
void MakeFood(HALL *pHall,FOOD *pFood)
{
srand((unsigned)time(NULL));
int tot;
for(tot = 1 ; tot <= 7 ; ++tot)
{
int ixFood = rand() * pHall->len / RAND_MAX;
pFood->pos[tot].x = pHall->pos[ixFood].x;
pFood->pos[tot].y = pHall->pos[ixFood].y;
}
pFood->len = 7;
}
常见问题与调试建议
1. 字符重叠导致画面闪烁:
上述代码通过 Sleep(200) 控制帧率,若觉得卡顿可适当调低。但需注意频繁调用 printf 可能导致性能瓶颈,优化方案可使用 WriteConsoleOutput 批量写入缓冲区。
2. 光标未隐藏:
在 Windows 环境下,务必在初始化时设置 info.bVisible = FALSE,否则移动时的光标跳变会严重干扰视觉体验。
3. 跨平台兼容性:
本实现强依赖于 Windows.h 和特定的控制台命令。若需移植至 Linux/macOS,需改用 curses 库或 ANSI 转义序列来处理光标移动和键盘输入。
以上就是基于C++实现控制台界面吃豆子游戏的详细内容,更多关于C++控制台游戏开发的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













