贪吃蛇游戏文档
游戏简介
贪吃蛇是一款经典的控制台游戏,玩家需要操控一条蛇去吃掉随机出现在屏幕上的食物来增长蛇的长度,并避免蛇头撞到墙壁或自己的身体。本游戏使用 C++ 编写,利用 Windows API 实现了控制台窗口内的动态游戏界面。
开发环境
- 操作系统:Windows
- 编程语言:C++
- 编译器:适用于 C++ 的任何兼容编译器(如 MSVC)
- 依赖库 :
<iostream>
、<list>
、<thread>
、<chrono>
、<windows.h>
、<conio.h>
游戏启动
- 运行环境设置:确保你的开发环境已正确配置好 Windows API 和 C++ 编译器。
- 编译游戏源码:使用支持 C++ 的编译器编译游戏源代码。
- 启动游戏:通过命令行运行编译后的可执行文件。
游戏界面
- 屏幕尺寸:游戏默认屏幕尺寸为 120x30,但会根据实际控制台窗口大小自动调整。
- 初始布局 :游戏开始时,屏幕上会显示一个由等号
=
构成的边界框,并在顶部中间位置显示游戏标题和分数。
cpp
void GetConsoleSize(int& width, int& height) {
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
}
int main() {
HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
SetConsoleActiveScreenBuffer(hConsole);
DWORD dwBytesWritten = 0;
while (true) {
GetConsoleSize(nScreenWidth, nScreenHeight);
// Create display screen
wchar_t* screen = new wchar_t[nScreenHeight * nScreenWidth];
for (int i = 0; i < nScreenHeight * nScreenWidth; i++) {
screen[i] = L' ';
}
游戏规则
- 移动控制 :
- 使用键盘上的左右箭头键来控制蛇的移动方向。
- 按下箭头键后,蛇头会在下一个时间间隔内按照指定方向移动一格。
- 每次只能改变到相邻的方向(即不能立即从向左变为向右)。
这段代码是用于处理贪吃蛇游戏中的帧定时以及用户输入的部分。以下是逐行解析:
cpp
// Frame Timing, compensate for aspect ratio of command line
auto t1 = chrono::system_clock::now();
- 这一行记录了当前的时间点
t1
,作为后续计算时间差的基础。这是为了实现帧定时,即控制游戏的更新频率。
cpp
while ((chrono::system_clock::now() - t1) < ((nSnakeDirection % 2 == 1) ? 120ms : 200ms))
{
- 这个
while
循环将持续执行,直到当前时间与记录的时间点t1
之间的差达到了一个特定的毫秒数。具体来说,如果nSnakeDirection
是奇数,等待时间为 120 毫秒;如果是偶数,等待时间为 200 毫秒。这样做是为了控制游戏的帧率,使游戏运行更加平滑,并且可以根据方向的不同调整速度。
cpp
// Get Input,
bKeyRight = (0x8000 & GetAsyncKeyState((unsigned char)('\x27'))) != 0;
bKeyLeft = (0x8000 & GetAsyncKeyState((unsigned char)('\x25'))) != 0;
- 这两行代码检测用户是否按下了右箭头键(
\x27
)或左箭头键(\x25
)。GetAsyncKeyState
函数用于获取按键的状态,返回一个 16 位的整数,其中最高位(第 15 位)表示键是否被按下。通过与运算(0x8000 & ...)
可以判断该位是否为 1,从而确定键是否被按下。
cpp
if (bKeyRight && !bKeyRightOld)
{
nSnakeDirection++;
if (nSnakeDirection == 4) nSnakeDirection = 0;
}
if (bKeyLeft && !bKeyLeftOld)
{
nSnakeDirection--;
if (nSnakeDirection == -1) nSnakeDirection = 3;
}
- 这两个
if
语句检查是否检测到了新的按键动作。如果右键被按下并且之前没有被按下(即bKeyRightOld
为false
),则增加nSnakeDirection
的值,并确保其不超过 3(即循环到 0)。同理,如果左键被按下并且之前没有被按下,则减少nSnakeDirection
的值,并确保其不低于 0(即循环到 3)。这里的nSnakeDirection
值通常表示蛇的四个可能的方向:0(向上)、1(向右)、2(向下)、3(向左)。
cpp
bKeyRightOld = bKeyRight;
bKeyLeftOld = bKeyLeft;
}
- 最后这两行更新旧按键状态变量
bKeyRightOld
和bKeyLeftOld
,以便下次循环时可以比较新的按键状态。
总结:这段代码的主要作用是在一个固定的时间间隔内处理用户的输入,并根据输入更新蛇的移动方向。同时,通过控制时间间隔,实现了游戏帧率的控制,确保游戏运行流畅且响应及时。
cpp
在这里插入代码片
cpp
auto t1 = chrono::system_clock::now();
while ((chrono::system_clock::now() - t1) < ((nSnakeDirection % 2 == 1) ? 120ms : 200ms)) {
bKeyRight = (0x8000 & GetAsyncKeyState((unsigned char)('\x27'))) != 0;
bKeyLeft = (0x8000 & GetAsyncKeyState((unsigned char)('\x25'))) != 0;
if (bKeyRight && !bKeyRightOld) {
nSnakeDirection++;
if (nSnakeDirection == 4) nSnakeDirection = 0;
}
if (bKeyLeft && !bKeyLeftOld) {
nSnakeDirection--;
if (nSnakeDirection == -1) nSnakeDirection = 3;
}
bKeyRightOld = bKeyRight;
bKeyLeftOld = bKeyLeft;
}
switch (nSnakeDirection) {
case 0: snake1.push_front({ snake1.front().x, snake1.front().y - 1 }); break;
case 1: snake1.push_front({ snake1.front().x + 1, snake1.front().y }); break;
case 2: snake1.push_front({ snake1.front().x, snake1.front().y + 1 }); break;
case 3: snake1.push_front({ snake1.front().x - 1, snake1.front().y }); break;
}
- 食物生成 :
- 食物随机生成在屏幕上的任意一个空闲位置。
- 食物用
%
符号表示。
cpp
do {
nFoodX = rand() % nScreenWidth;
nFoodY = (rand() % (nScreenHeight - 3)) + 3;
} while (screen[nFoodY * nScreenWidth + nFoodX] != L' ');
- 得分机制 :
- 当蛇头与食物重合时,蛇的长度会增加,同时得分增加。
- 每吃掉一个食物,蛇会增长5个单位。
cpp
// Check collision with food
if (snake1.front().x == nFoodX && snake1.front().y == nFoodY) {
nScore++;
do {
nFoodX = rand() % nScreenWidth;
nFoodY = (rand() % (nScreenHeight - 3)) + 3;
} while (screen[nFoodY * nScreenWidth + nFoodX] != L' ');
for (int i = 0; i < 5; i++)
snake1.push_back({ snake1.back().x, snake1.back().y });
}
- 碰撞检测 :
- 若蛇头撞到墙壁或蛇身,则游戏结束。
- 游戏结束时,屏幕上会出现提示信息,告知玩家按空格键重新开始游戏。
- 检测与墙壁的碰撞 检测与食物的碰撞 检测与自身的碰撞
cpp
// Check collision with food
if (snake1.front().x == nFoodX && snake1.front().y == nFoodY) {
nScore++;
do {
nFoodX = rand() % nScreenWidth;
nFoodY = (rand() % (nScreenHeight - 3)) + 3;
} while (screen[nFoodY * nScreenWidth + nFoodX] != L' ');
for (int i = 0; i < 5; i++)
snake1.push_back({ snake1.back().x, snake1.back().y });
}
// Check collision with walls
if (snake1.front().x < 0 || snake1.front().x >= nScreenWidth ||
snake1.front().y < 3 || snake1.front().y >= nScreenHeight) {
bDead = true;
}
// Check collision with self
for (list<snake>::iterator i = next(snake1.begin()); i != snake1.end(); ++i)
if (i->x == snake1.front().x && i->y == snake1.front().y)
bDead = true;
- 游戏结束与重新开始 :
- 玩家可以通过按空格键来重新开始游戏。
cpp
// Wait for space to restart
while ((0x8000 & GetAsyncKeyState((unsigned char)('\x20'))) == 0);
游戏流程
-
初始化阶段:
- 游戏开始时,初始化屏幕大小、蛇的位置以及食物的位置。
- 显示初始的游戏界面,包括边界、初始蛇形、食物位置以及得分。
-
主游戏循环:
- 在主循环中,不断检查键盘输入,更新蛇的移动方向。
- 每过一定的时间间隔(根据方向不同,时间间隔也不同),蛇向前移动一格。
- 检查蛇头是否吃到食物,如果是,则增加蛇的长度并更新得分。
- 检查是否有碰撞发生,如果发生碰撞,则标记游戏结束。
- 清除屏幕,重新绘制所有元素,包括蛇、食物和边界。
- 如果游戏结束,显示游戏结束的信息。
清除屏幕并重新绘制所有游戏元素这一过程对于保持游戏的流畅性和视觉效果至关重要,它实际上就是维持游戏帧率的一部分工作。这个过程可以理解为游戏开发中的"帧循环",每次循环都会执行以下步骤:
- 检测输入:检查用户的键盘或其他输入设备,以确定蛇的移动方向。
- 更新状态:根据输入更新游戏的状态,例如蛇的位置、食物的位置等。
- 碰撞检测:检查蛇是否撞到了自己或者边界,以及是否吃到了食物。
- 清理画布:清除上一帧的画面,为新的画面做准备。这是为了避免旧的图形残留,造成视觉混乱。
- 重新绘制:在屏幕上重新绘制所有的游戏元素,包括蛇的身体、食物以及游戏边界等。
- 检查游戏结束条件:如果游戏结束(如蛇撞到自身或墙壁),则显示游戏结束的消息。
这个循环通常会以固定的频率运行,即帧率(FPS,Frames Per
Second)。帧率越高,游戏看起来就越流畅。在贪吃蛇游戏中,这个频率不需要非常高,因为游戏本身并不需要极其复杂的动画效果。但是,保持一定的帧率对于用户体验来说仍然是重要的,因为它决定了游戏反应的速度和流畅度。
通过不断地重复这个过程,游戏能够呈现出动态的效果,让玩家感觉游戏是在连续地进行而不是断断续续的。因此,每次循环中清除屏幕然后重新绘制所有元素,是为了确保每一帧都是最新的状态,从而达到刷新帧率的效果。
- 游戏结束处理 :
- 当检测到游戏结束条件满足时,停止蛇的移动。
- 在屏幕上显示游戏结束的信息,并等待玩家按键。
- 如果玩家按下了空格键,则重新开始游戏。
技术细节
- 控制台操作 :使用
GetConsoleScreenBufferInfo
和SetConsoleScreenBufferInfo
来获取和设置控制台的信息。 - 异步按键检测 :使用
GetAsyncKeyState
来实时检测键盘按键的状态。 - 定时器 :使用
<chrono>
库来实现游戏的定时功能,保证蛇的移动频率。 - 动态内存管理 :使用
new
和delete[]
来分配和释放屏幕缓冲区。
通过以上文档,你应该能够清楚地了解这款游戏的工作原理以及如何开始游戏。
完整源码+注释
cpp
/*C++ 精品贪吃蛇*/
// 项目标题:C++精品贪吃蛇游戏
#include <iostream> // 包含用于输入输出的基本流库
#include <list> // 包含标准库中的list容器
#include <thread> // 包含线程库,用于延时
#include <chrono> // 包含时间库,用于获取当前时间点
#include <windows.h> // 包含Windows API,用于控制台操作
#include <conio.h> // 包含控制台输入函数,用于检测按键
using namespace std; // 使用标准命名空间,简化代码
int nScreenWidth = 120; // 初始化屏幕宽度
int nScreenHeight = 30; // 初始化屏幕高度
struct snake { // 定义贪吃蛇的身体段结构体
int x; // 蛇段的横坐标
int y; // 蛇段的纵坐标
};
void GetConsoleSize(int& width, int& height) { // 函数用于获取控制台窗口的尺寸
CONSOLE_SCREEN_BUFFER_INFO csbi; // 结构体变量,用于存储控制台屏幕缓冲区信息
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); // 获取控制台屏幕缓冲区信息
width = csbi.srWindow.Right - csbi.srWindow.Left + 1; // 计算窗口宽度
height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; // 计算窗口高度
}
int main() { // 主函数入口
HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL); // 创建一个新的控制台屏幕缓冲区
SetConsoleActiveScreenBuffer(hConsole); // 设置当前控制台的活动屏幕缓冲区
DWORD dwBytesWritten = 0; // 用于存储写入字符的数量
while (true) { // 游戏主循环
GetConsoleSize(nScreenWidth, nScreenHeight); // 获取控制台的当前尺寸
// Create display screen
wchar_t* screen = new wchar_t[nScreenHeight * nScreenWidth]; // 动态分配一个二维数组用于显示屏幕
for (int i = 0; i < nScreenHeight * nScreenWidth; i++) { // 初始化屏幕数组为全空格
screen[i] = L' ';
}
list<snake> snake1 = { {60,15},{61,15},{62,15},{63,15},{64,15},{65,15},{66,15},{67,15},{68,15},{69,15} }; // 初始化贪吃蛇的起始位置
int nFoodX = 30; // 食物的横坐标
int nFoodY = 15; // 食物的纵坐标
int nScore = 0; // 分数初始化为0
int nSnakeDirection = 3; // 贪吃蛇的方向初始化为向左
bool bDead = false; // 游戏结束标志初始化为未结束
bool bKeyLeft = false, bKeyRight = false, bKeyLeftOld = false, bKeyRightOld = false; // 键盘按键状态
while (!bDead) { // 当游戏未结束时循环
auto t1 = chrono::system_clock::now(); // 获取当前时间点
while ((chrono::system_clock::now() - t1) < ((nSnakeDirection % 2 == 1) ? 120ms : 200ms)) { // 控制游戏帧率
bKeyRight = (0x8000 & GetAsyncKeyState((unsigned char)('\x27'))) != 0; // 检测右箭头键是否按下
bKeyLeft = (0x8000 & GetAsyncKeyState((unsigned char)('\x25'))) != 0; // 检测左箭头键是否按下
if (bKeyRight && !bKeyRightOld) { // 如果右键被按下且上次不是右键
nSnakeDirection++; // 改变方向
if (nSnakeDirection == 4) nSnakeDirection = 0; // 方向循环
}
if (bKeyLeft && !bKeyLeftOld) { // 如果左键被按下且上次不是左键
nSnakeDirection--; // 改变方向
if (nSnakeDirection == -1) nSnakeDirection = 3; // 方向循环
}
bKeyRightOld = bKeyRight; // 更新按键状态
bKeyLeftOld = bKeyLeft; // 更新按键状态
}
switch (nSnakeDirection) { // 根据方向更新蛇头位置
case 0: snake1.push_front({ snake1.front().x, snake1.front().y - 1 }); break; // 向上
case 1: snake1.push_front({ snake1.front().x + 1, snake1.front().y }); break; // 向右
case 2: snake1.push_front({ snake1.front().x, snake1.front().y + 1 }); break; // 向下
case 3: snake1.push_front({ snake1.front().x - 1, snake1.front().y }); break; // 向左
}
// Check collision with food
if (snake1.front().x == nFoodX && snake1.front().y == nFoodY) { // 如果蛇头碰到食物
nScore++; // 分数增加
do { // 重新生成食物的位置直到找到一个合适的空位
nFoodX = rand() % nScreenWidth;
nFoodY = (rand() % (nScreenHeight - 3)) + 3;
} while (screen[nFoodY * nScreenWidth + nFoodX] != L' '); // 检查新位置是否为空
for (int i = 0; i < 5; i++) // 增加蛇身长度
snake1.push_back({ snake1.back().x, snake1.back().y });
}
// Check collision with walls
if (snake1.front().x < 0 || snake1.front().x >= nScreenWidth || // 检查蛇头是否撞墙
snake1.front().y < 3 || snake1.front().y >= nScreenHeight) {
bDead = true; // 游戏结束
}
// Check collision with self
for (list<snake>::iterator i = next(snake1.begin()); i != snake1.end(); ++i) // 检查蛇头是否碰到自己
if (i->x == snake1.front().x && i->y == snake1.front().y)
bDead = true; // 游戏结束
snake1.pop_back(); // 移除蛇尾
// Clear screen and draw elements
for (int i = 0; i < nScreenWidth * nScreenHeight; i++) screen[i] = L' '; // 清屏
for (int i = 0; i < nScreenWidth; i++) { // 绘制边框
screen[i] = L'='; // 上方边框
screen[2 * nScreenWidth + i] = L'='; // 下方边框
}
wsprintf(&screen[nScreenWidth + 5], L"www.OneLoneCoder.com - S N A K E ! ! SCORE: %d", nScore); // 显示分数
for (const auto& s : snake1) // 绘制蛇身
screen[s.y * nScreenWidth + s.x] = bDead ? L'+' : L'O';
screen[snake1.front().y * nScreenWidth + snake1.front().x] = bDead ? L'X' : L'@'; // 绘制蛇头
screen[nFoodY * nScreenWidth + nFoodX] = L'%'; // 绘制食物
if (bDead) // 如果游戏结束
wsprintf(&screen[15 * nScreenWidth + 40], L" PRESS 'SPACE' TO PLAY AGAIN "); // 提示玩家按空格键重新开始
WriteConsoleOutputCharacter(hConsole, screen, nScreenWidth * nScreenHeight, { 0,0 }, &dwBytesWritten); // 刷新屏幕
// Check for resize
int newWidth, newHeight; // 新的宽高
GetConsoleSize(newWidth, newHeight); // 获取新的控制台尺寸
if (newWidth != nScreenWidth || newHeight != nScreenHeight) { // 如果尺寸改变
bDead = true; // 结束游戏
}
}
delete[] screen; // 释放屏幕数组内存
// Wait for space to restart
while ((0x8000 & GetAsyncKeyState((unsigned char)('\x20'))) == 0); // 等待玩家按空格键重新开始
}
return 0; // 返回程序执行状态
}