原文链接:C语言 贪吃蛇游戏
文章目录
- 一、说明
- 二、效果
- 2.1 欢迎界面
- 2.2 游戏规则
- 2.3 得分排行
- 2.4 退出游戏
- 2.5 游戏界面
- 2.6 游戏结束
- 三、源码
- 3.1 cmd.h
- 3.2 cmd.c
- 3.3 io.h
- 3.4 io.c
- 3.5 model.h
- 3.6 service.h
- 3.7 service.c
- 3.8 ui.h
- 3.9 ui.c
- 3.10 utils.h
- 3.11 utils.c
- 3.12 main.c
一、说明
笔者使用 C 语言实现经典贪吃蛇游戏,其中开发环境为 Windows 平台下的 VisualStudio2019
本文在 原文 的基础上将原文源码进行模块化拆分,以面向过程的自顶向下模块化思想进行编程,代码可读性高、系统健壮性强、游戏界面友好美观,功能做出适当调整并修复了一系列已知问题
二、效果
2.1 欢迎界面
2.2 游戏规则
2.3 得分排行
2.4 退出游戏
2.5 游戏界面
2.6 游戏结束
三、源码
3.1 cmd.h
#pragma once
// 设置光标
void setPox(int x, int y);
// 设置文本颜色
void setTextColor(unsigned short color);
3.2 cmd.c
#include<windows.h>
// 设置光标
void setPox(int x, int y) {
COORD pox = {x, y};
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOut, pox);
}
// 设置文本颜色
void setTextColor(unsigned short color) {
HANDLE hCon = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hCon, color);
}
3.3 io.h
#pragma once
// 游戏排行榜,读取游戏数据
void rankingList();
// 保存成绩,游戏存档
void saveGrade(int score);
3.4 io.c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include<time.h>
#include "model.h"
#include "utils.h"
// 游戏排行榜,读取游戏数据
void rankingList() {
system("cls");
// 打开游戏排行榜文件
FILE *fp = fopen("rank.txt", "rb");
if (fp == NULL) {
setPox(56, 12);
printf("游戏排行榜文件不存在");
setPox(0, 0);
return;
}
rewind(fp);
// 读取文件中的游戏数据,最多读取 1000 条游戏记录
record gameRecord[1000];
int i = 0;
// feof 检查文件是否结束,遇到结束符,返回非零
while (!feof(fp)) {
fread(&gameRecord[i], sizeof(struct record), 1, fp);
i++;
}
// 按游戏得分排序
qsort(gameRecord, i - 1, sizeof(record), compare);
// 输出得分最高的 10 次游戏记录
i = i > 10 ? 10 : i;
// 输出游戏排行榜信息
setPox(55, 3);
setTextColor(12);
printf("排行榜");
setPox(42, 5);
setTextColor(14);
printf("得分\t\t\t时间\n");
setTextColor(15);
int j = 0;
for (; j < i - 1; j++) {
setPox(43, 7 + j * 2);
printf("%d\t\t", gameRecord[j].grade);
printf("%d/%02d/%02d ", gameRecord[j].year + 1900, gameRecord[j].mon + 1, gameRecord[j].day);
printf("%02d:%02d:%02d\n", gameRecord[j].hour, gameRecord[j].min, gameRecord[j].sec);
}
setPox(43, 7 + j * 2);
setTextColor(1);
printf("注:按任意键继续···");
fclose(fp);
}
// 保存成绩,游戏存档
void saveGrade(int score) {
// 获取系统时间
time_t timestamp;
time(×tamp);
struct tm *ti = localtime(×tamp);
// 为当前游戏数据分配空间
record *gameRecord = (record *) malloc(sizeof(record));
// 保存年月日时分秒以及分数
gameRecord->year = ti->tm_year;
gameRecord->mon = ti->tm_mon;
gameRecord->day = ti->tm_mday;
gameRecord->hour = ti->tm_hour;
gameRecord->min = ti->tm_min;
gameRecord->sec = ti->tm_sec;
gameRecord->grade = score;
// 打开文件并追加写入本次游戏数据
FILE *fp = fopen("rank.txt", "ab");
if (fp == NULL)
fp = fopen("rank.txt", "wb");
fwrite(gameRecord, sizeof(record), 1, fp);
fclose(fp);
free(gameRecord);
}
3.5 model.h
#pragma once
// 蛇
typedef struct snake {
int x;
int y;
struct snake *next;
} snake;
// 游戏记录
typedef struct record {
int grade;
int year;
int mon;
int day;
int hour;
int min;
int sec;
} record;
// 游戏数据
typedef struct data {
// 分数
int score;
// 速度,值越小蛇的速度越快
int speed;
// 速度等级,值越大小蛇速度越快
int speedLevel;
// 每个食物分数,加速一次 foodFraction 翻倍,减速一次 foodFraction 减半
int foodFraction;
// 速度变化前吃到的食物数,用于判断是否开启自动加速
int eatenFoods;
} data;
3.6 service.h
#pragma once
#include "model.h"
// 初始化蛇
snake *initSnake();
// 随机食物
snake *randomFood(snake *q);
// 打印蛇身
void snakeBody(snake *p, int speed);
// 边界碰撞判定
int collision(snake *q);
// 释放蛇身
void destroy(snake *p);
// 游戏暂停
void suspendGame(snake *q);
// 蛇身传递
void snakeInherit(snake *p);
// 吃到食物
snake *foodInMouth(snake *snakeHead, snake *food, data *curGameData);
// 自动加速
void autoAccelerate(data *curGameData, int isAutoAccelerate);
// 开始游戏
void startGame();
3.7 service.c
#define _CRT_SECURE_NO_WARNINGS
#include<windows.h>
#include "model.h"
// 初始化蛇
snake *initSnake() {
snake *q = (snake *) malloc(sizeof(snake));
q->next = NULL;
for (int i = 6; i < 19; i = i + 2) {
snake *tmp = (snake *) malloc(sizeof(snake));
tmp->x = i;
tmp->y = 5;
tmp->next = q->next;
q->next = tmp;
}
return q;
}
// 随机食物
snake *randomFood(snake *q) {
snake *p, *k;
k = (snake *) malloc(sizeof(snake));
k->next = NULL;
gotoHere:
p = q->next;
srand((unsigned) time(NULL));
// 确保食物显示在游戏地图范围内
while ((k->x = rand() % 57 + 4) % 2 != 0) { ;
}
k->y = rand() % 24 + 3;
while (p != NULL) {
// 如果新食物与蛇身重合,则重新生成
if ((k->x == p->x && k->y == p->y))
goto gotoHere;
p = p->next;
}
setTextColor(12);
setPox(k->x, k->y);
printf("●");
return k;
}
// 打印蛇身
void snakeBody(snake *p, int speed) {
snake *r, *k = p->next;
setTextColor(14);
while (p->next != NULL) {
r = p->next;
p = r;
setPox(r->x, r->y);
printf("★");
}
if (k->x != p->x || k->y != p->y) {
// 覆盖尾迹
setPox(p->x, p->y);
setTextColor(3);
printf("■");
}
setPox(0, 0);
Sleep(speed);
}
// 边界碰撞判定
int collision(snake *q) {
snake *p = q->next, *r = p->next;
// 撞墙
if (p->x == 2 || p->x == 62 || p->y == 1 || p->y == 27) {
return 1;
}
while (r->next != NULL) {
// 咬到自己
if (p->x == r->x && p->y == r->y)
return 1;
r = r->next;
}
return 0;
}
// 释放蛇身
void destroy(snake *p) {
snake *q = p, *r;
while (q->next != NULL) {
r = q;
q = q->next;
free(r);
}
free(q);
}
// 游戏暂停
void suspendGame(snake *q) {
setPox(0, 0);
while (1) {
// kbhit函数,非阻塞地响应键盘输入事件
if (kbhit() && getch() == ' ')
return;
}
}
// 蛇身传递
void snakeInherit(snake *p) {
// p 为第一个结点,即蛇首
snake *r = p->next;
if (r->next != NULL)
snakeInherit(r);
// 把前一个结点的坐标传递给后一个结点(跟随)
r->x = p->x;
r->y = p->y;
}
// 吃到食物
snake *foodInMouth(snake *snakeHead, snake *food, data *curGameData) {
// 蛇身长度加 1
food->next = snakeHead->next;
snakeHead->next = food;
// 新的随机食物
food = randomFood(snakeHead);
// 更新分数
curGameData->score += curGameData->foodFraction;
scoreHint(curGameData->score);
// 吃到的食物数加 1
curGameData->eatenFoods++;
return food;
}
// 自动加速
void autoAccelerate(data *curGameData, int isAutoAccelerate) {
if (curGameData->eatenFoods % 3 == 0 && isAutoAccelerate == 1) {
// 速度减半、速度等级增一、每个食物分数翻倍
curGameData->speed /= 2;
curGameData->speedLevel++;
curGameData->foodFraction *= 2;
curGameData->eatenFoods = 0;
speedHint(curGameData->speedLevel);
}
}
// 开始游戏
void startGame() {
// 初始化本次游戏数据
data *curGameData = (data *) malloc(sizeof(data));
curGameData->score = 0;
curGameData->speed = 300;
curGameData->speedLevel = 1;
curGameData->foodFraction = 2;
curGameData->eatenFoods = 0;
// 是否开启自动加速:[0]不开启 [1]开启
int isAutoAccelerate = 1;
system("cls");
// 游戏地图
gameMap();
// 初始速度展示
speedHint(curGameData->speedLevel);
// 初始分数展示
scoreHint(curGameData->score);
// 初始化蛇,初始长度为 6 个节点
snake *q = initSnake();
// 初始化随机食物
snake *food = randomFood(q);
// 当前敲下的按键,默认为 d 即蛇往右移动
char hitKey = 'd';
// 上一次敲下的按键
char preKey = 'd';
while (1) {
// 打印蛇身
snakeBody(q, curGameData->speed);
// 撞墙或咬到自己,游戏结束
if (collision(q)) {
// 游戏存档
saveGrade(curGameData->score);
// 销毁蛇身结点,释放存储空间
destroy(q);
// 结束游戏
gameOver(curGameData->score);
break;
}
// 键盘监听,kbhit函数,非阻塞地响应键盘输入事件
if (kbhit()) {
hitKey = getch();
}
// 敲击空格,暂停游戏
if (hitKey == ' ') {
suspendGame(q);
// 恢复上一次的操作,继续游戏后按原方向爬行
hitKey = preKey;
continue;
}
// 兼容大写字母
hitKey = hitKey < 91 ? hitKey + 32 : hitKey;
// 蛇首撞击蛇身,游戏结束(上至下、下至上、左至右、右至左)
if ((hitKey == 'd' && preKey == 'a') || (hitKey == 's' && preKey == 'w') || (hitKey == 'a' && preKey == 'd') ||
(hitKey == 'w' && preKey == 's')) {
saveGrade(curGameData->score);
destroy(q);
gameOver(curGameData->score);
break;
}
// 按 q 加速
if (hitKey == 'q') {
// 速度减半、速度等级增一、每个食物分数翻倍、不再自动加速
curGameData->speed /= 2;
curGameData->speedLevel++;
curGameData->foodFraction *= 2;
curGameData->eatenFoods = 0;
isAutoAccelerate = 0;
speedHint(curGameData->speedLevel);
// 恢复上一次的操作,加速后按原方向爬行
hitKey = preKey;
continue;
}
// 按 e 减速
if (hitKey == 'e') {
// 速度翻倍、速度等级减一、每个食物分数减半、不再自动加速
curGameData->speed *= 2;
curGameData->speedLevel--;
curGameData->foodFraction /= 2;
curGameData->eatenFoods = 0;
isAutoAccelerate = 0;
speedHint(curGameData->speedLevel);
// 恢复上一次的操作,减速后按原方向爬行
hitKey = preKey;
continue;
}
// 上
if (hitKey == 'w') {
// 吃到食物,蛇身长度加 1,创建新食物,更新分数
if (q->next->x == food->x && q->next->y - 1 == food->y) {
food = foodInMouth(q, food, curGameData);
autoAccelerate(curGameData, isAutoAccelerate);
} else {
// 未吃到食物,传递上一次的舍身
snakeInherit(q->next);
q->next->y -= 1;
}
}
// 下
if (hitKey == 's') {
if (q->next->x == food->x && q->next->y + 1 == food->y) {
food = foodInMouth(q, food, curGameData);
autoAccelerate(curGameData, isAutoAccelerate);
} else {
snakeInherit(q->next);
q->next->y += 1;
}
}
// 左
if (hitKey == 'a') {
if (q->next->x - 2 == food->x && q->next->y == food->y) {
food = foodInMouth(q, food, curGameData);
autoAccelerate(curGameData, isAutoAccelerate);
} else {
snakeInherit(q->next);
q->next->x -= 2;
}
}
// 右
if (hitKey == 'd') {
if (q->next->x + 2 == food->x && q->next->y == food->y) {
food = foodInMouth(q, food, curGameData);
autoAccelerate(curGameData, isAutoAccelerate);
} else {
snakeInherit(q->next);
q->next->x += 2;
}
}
// 记录上一次的操作
preKey = hitKey;
}
}
3.8 ui.h
#pragma once
// 界面边框
void frame(int type);
// 欢迎页
void welcomePage();
// 游戏规则
void gameRules();
// 游戏结束
void gameOver(int score);
// 游戏地图
void gameMap();
// 速度提示
void speedHint(int speedLevel);
// 输出分数
void scoreHint(int score);
3.9 ui.c
// 界面边框
void frame(int type) {
// 上边框
setPox(17, 5);
setTextColor(11);
printf("⊙--------------------------");
setTextColor(14);
printf("oOOo");
setTextColor(11);
printf("----------");
setTextColor(14);
printf("(_)");
setTextColor(11);
printf("----------");
setTextColor(14);
printf("oOOo");
setTextColor(11);
printf("--------------------------⊙");
// 左右竖边框
for (int i = 6; i <= 19; i++) {
setPox(17, i);
printf("§");
setPox(102, i);
printf("§");
}
// 下边框
setPox(17, 20);
printf("⊙---------------------------------------");
setTextColor(14);
printf("☆☆☆");
setTextColor(11);
printf("--------------------------------------⊙");
setPox(53, 23);
printf("∵ˇˇˇˇˇˇˇ∵");
setPox(53, 26);
printf("∴^^^^^^^∴");
// 启动页面字符图案
if (type == 0) {
setPox(57, 2);
setTextColor(6);
printf("\\\\\\|///");
setPox(54, 3);
printf("\\\\");
setPox(58, 3);
setTextColor(15);
printf(".-.-");
setPox(65, 3);
setTextColor(6);
printf("//");
setPox(55, 4);
setTextColor(14);
printf("(");
setPox(58, 4);
setTextColor(15);
printf(".@.@");
setPox(65, 4);
setTextColor(14);
printf(")");
} else {
// 游戏结束字符图案
setPox(57, 1);
setTextColor(6);
printf("∧ ∧");
setPox(55, 2);
printf(" / \\ / \\");
setPox(54, 3);
printf("( ︹ ˇ ︹ )");
setPox(54, 4);
printf("く ");
setTextColor(15);
printf("⊙ ⊙");
setTextColor(14);
printf(" / ");
setPox(55, 5);
printf("く い /");
setPox(57, 6);
printf("く 々 √");
setPox(60, 7);
printf("ˇ");
}
}
// 欢迎页
void welcomePage() {
system("cls");
setPox(53, 8);
setTextColor(14);
printf("贪 吃 蛇 大 作 战");
setPox(26, 14);
printf("1.开始游戏");
setPox(46, 14);
printf("2.游戏规则");
setPox(66, 14);
printf("3.得分排行");
setPox(86, 14);
printf("4.退出游戏");
// 绘制界面边框
frame(0);
setPox(56, 24);
setTextColor(14);
printf("前往:");
}
// 游戏规则
void gameRules() {
system("cls");
setPox(55, 5);
printf("游戏规则");
setTextColor(12);
setPox(34, 8);
printf("1. 'W''S''A''D' 控制上、下、左、右方向,空格键控制游戏暂停与继续");
setPox(34, 10);
printf("2. 按 Q 键可加速,按 E 键可减速,速度越快,单个食物分数越高");
setPox(34, 12);
printf("3. 在没有人为加速或减速的情况下,每吃到 3 个食物将会进行一次自动加速");
setPox(34, 14);
printf("4. 初始化每个食物 2 分,每加速一次单个食物分数翻倍,减速一次分数减半");
setPox(34, 16);
printf("5. 初始化小蛇长度为 6,每吃到一个食物长度就会加 1,分数相应地增加");
setPox(34, 18);
printf("6. 小蛇的初始速度为 300MS/格,速度等级为 1,等级值越高速度越快");
setPox(34, 20);
printf("7. 当蛇首撞墙或咬到蛇身时游戏结束");
setPox(34, 22);
printf("8. 以上按键皆不区分大小写");
setPox(34, 24);
setTextColor(1);
printf("注:按任意键继续···");
}
// 游戏结束
void gameOver(int score) {
int choice = 1;
gotoHere:
system("cls");
setTextColor(12);
setPox(45, 8);
printf("游戏结束,蛇首撞墙或咬到蛇身\n");
setPox(58, 12);
setTextColor(14);
printf("得分:%d", score);
setTextColor(14);
setPox(40, 16);
printf("1.重新开始");
setPox(56, 16);
printf("2.返回主页");
setPox(70, 16);
printf("3.退出游戏");
// 绘制界面边框
frame(1);
setPox(56, 24);
printf("前往:");
scanf("%d", &choice);
switch (choice) {
case 1:
system("cls");
startGame();
break;
case 2:
return;
case 3:
system("cls");
setPox(50, 15);
printf("游戏结束,欢迎再次登录");
setPox(0, 0);
exit(0);
default:
setPox(0, 0);
printf("您的输入有误,请重新输入!按任意键继续···");
getch();
goto gotoHere;
}
}
// 游戏地图
void gameMap() {
setTextColor(11);
// 游戏界面边框:菱形边界,方块背景
for (int i = 2; i < 27; i++) {
// 左边框
setPox(2, i);
printf("◆");
// 方块背景
setTextColor(3);
for (int j = 0; j < 29; j++)
printf("■");
// 右边框
setTextColor(11);
printf("◆");
}
// 上边框
setPox(2, 1);
for (int i = 0; i < 31; i++) {
printf("◆");
}
// 下边框
setPox(2, 27);
for (int i = 0; i < 31; i++) {
printf("◆");
}
// 游戏界面与游戏提示间隔框
setTextColor(10);
for (int i = 0; i < 30; i++) {
setPox(70, i);
printf("§");
}
// 得分提示边框
setTextColor(6);
setPox(82, 4);
printf("∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞");
setPox(82, 6);
printf("∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞");
setPox(82, 5);
printf("φ");
setPox(110, 5);
printf("φ");
// 操作提示
setTextColor(12);
setPox(94, 9);
printf("操作提示");
setPox(95, 11);
printf("上:W");
setPox(95, 12);
printf("下:S");
setPox(95, 13);
printf("左:A");
setPox(95, 14);
printf("右:D");
setPox(90, 16);
printf("暂停/继续: 空格");
// 速度提示框
setTextColor(14);
setPox(82, 19);
printf("∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞");
setPox(82, 21);
printf("∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞");
setPox(81, 20);
printf("φ");
setPox(111, 20);
printf("φ");
setPox(81, 23);
printf("注:按 Q 键可加速,按 E 键可减速");
}
// 速度提示
void speedHint(int speedLevel) {
setTextColor(11);
setPox(97, 20);
printf("%d", speedLevel);
}
// 输出分数
void scoreHint(int score) {
setPox(97, 5);
setTextColor(13);
printf("%d", score);
}
3.10 utils.h
#pragma once
// 比较两个元素
int compare(const void *a, const void *b);
3.11 utils.c
// 比较两个元素
int compare(const void *a, const void *b) {
return (*(int *) b - *(int *) a);
}
3.12 main.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include "model.h"
#include "cmd.h"
#include "utils.h"
#include "ui.h"
#include "io.h"
#include "service.h"
/*
* 程序入口
*/
int main() {
int choice;
gotoHere:
// 启动页面
welcomePage();
scanf("%d", &choice);
switch (choice) {
case 1:
system("cls");
startGame();
goto gotoHere;
case 2:
gameRules();
getch();
goto gotoHere;
case 3:
rankingList();
getch();
goto gotoHere;
case 4:
system("cls");
setPox(50, 15);
printf("游戏结束,欢迎再次登录");
setPox(0, 0);
exit(0);
default:
setPox(0, 0);
printf("您的输入有误,请重新输入!按任意键继续···");
getch();
goto gotoHere;
}
return 0;
}