简介
一、学习交流
这个代码当时使用DevC++编写,后用vs也能编译通过,如果想先赋值代码,请直接点目录的完整代码 ,然后复制到自己的电脑上。
这个程序时我上高中时写出来的,现在已经大一了。回过头来再看这个程序,发现好多地方都可以改进,代码太过啰嗦,效率不是很高。代码仅供学习交流。
二、适合刚学C语言的朋友
因为当时没有学习太多的数据结构和算法,数据都是用线性结构存储(数组),只要你会数组、结构体和函数,这个代码对你来说就没问题,因为当时我就学到了数组,所以这个适合刚学c的初学者。
三、游戏设计思路
计算机与人交互的三个步骤:输入、计算机处理输入数据、输出。
所以游戏也是基于这个思想来实现人和游戏的互动的。
实现游戏互动,有种叫做MVC的思维模式,主要分为四部分:
User(用户):游戏玩家;
Controller(控制器):处理游戏交互;
Model(模型):游戏功能的实现;
View(视图):设计图形界面,给玩家呈现游戏场景;
首先,由 User(用户) 输入数据,然后由 Controller(控制器) 接受并处理,然后转发给 Model(模型) 进行处理与运算,最后由 View(视图) 把成果呈现在屏幕上。
我这个小程序就是:
输入 -> 处理输入数据 -> 处理数据 -> 更新屏幕显示
这个小程序并没有严格按照 MVC的思维模式 ,因为当时思维没有这么清晰。不过这个模式是非常重要的,它很好的描述出了游戏交互的主要步骤,我们在代码实现时,尽量按照这个模式来,这样会使你的代码可读性高,整体结构好。
代码核心
主要有两个:
一、数据的存储和表示(Data):
这里有一些定义的常量,目前不需要知道它们的具体用途,主体代码明白后,这些常量的用途也自然明白了,先把它们放这:
//游戏信息
#define GAMENAME "快乐贪吃蛇"
#define DEVLOPER "这里填作者信息"
#define QQ "这里填作者QQ号"
#define TEL "电话号码:10086"
#define WRITEDATE "最后一次编写日期:2019年7月19号"
//活动空间大小
#define I 20
#define J 40 //J>=length
//菜单框大小
#define MI 15
#define MJ 50
//方向常量
#define Key_Up 72
#define Key_Down 80
#define Key_Right 77
#define Key_Left 75
#define UP 'W'
#define DOWN 'S'
#define LEFT 'A'
#define RIGHT 'D'
//颜色常量
#define FONTCOLOR 0x0E //字体颜色
#define SPACECOLOR 0x80 //空区域颜色
#define WALLCOLOR 0xC0 //墙颜色
#define DATACOLOR 0x8B //控制数据颜色
#define SNAKECOLOR 0x8B //蛇颜色
//游戏初始数据
#define LEN 4 //LEN>=3 and LEN<=J/2
#define DIRECTION LEFT//最开始时,蛇的前进方向
#define WAITTIME 10//蛇的速度,越小越快
下面是数据的存储方式:
struct Position
{
uint_fast8_t i, j;
};
//全局变量区
struct Position S[I * J] = {0}, FOOD; //蛇座标,食物坐标
uint_fast8_t M[I + 2][J + 2]; //操作数据
uint_fast8_t length, waittime, foodcolor; //长度,输入等待时间,食物颜色
char direction; //方向
bool alive; //存活状态
char playerName[21]; //玩家名字
这里用一个Position结构体表示点的坐标,因为我们一个地图是一个直角坐标系。
我编写gotoxy函数用来定位坐标轴上面的点,并根据flag参数更改坐标数据:
void gotoxy(uint_fast8_t i, uint_fast8_t j, char c, uint_fast8_t flag);
//移动光标并更改控制数据
这里的uint_fast8_t表示的是无符号8位整型,你可以理解为int,只不过它的数据范围是0~255,在这个程序中坐标不会超过255(不然地图太大了)。
我用数组M存地图上的数据,其中:
4表示食物,屏幕上面用 ‘*’ 打印;
3表示头部,屏幕上面用 ‘@’ 打印;
所有表示如下代码所示:
#define GF(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, '*', 4) //食物
#define GH(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, '@', 3) //头
#define GN(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, 'o', 2) //颈
#define GB(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, 'o', 1) //身
#define GT(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, '+', 1) //尾
#define GW(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, ' ', 1) //墙
#define GS(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, ' ', 0) //空
游戏使用这个函数初始化数据:
void get_really(void); //游戏数据初始化
二、游戏场景(Scene):
小程序比较简单:只有两个场景:菜单场景和游戏场景。
菜单场景主要由下路函数实现:
void welcome(void); //游戏开始界面
void title(uint_fast8_t i, uint_fast8_t j); //打印标题
void menu(void); //打印菜单
void rule_of_game(void); //游戏介绍
void devloper(void); //开发者信息
void aboutgame(void); //关于游戏
void choice(void); //选择菜单选项
游戏场景主要由下列函数组成:
void game(void); //游戏开始
void draw_map(void); //绘制地图
void put_control_data(void); //打印控制数据
void put_game_data(void); //打印游戏数据
void press(void); //玩家输入函数,用来接收玩家的输入指令
void count_nexthead(void); //计算移动一次后,蛇头的位置
bool countstate(void); //判定移动一次后,游戏处于什么状态(暂定、变长、死亡等等)
void renew_food(void); //重新生成食物
void renew_S(void); //更新蛇的坐标
bool death(void); //死亡时调用
void move_control_data(void); //刷新屏幕上面的控制数据
void move(void); //刷新屏幕上面蛇的位置
char select(void); //游戏结束时调用,选择
void game_over(void); //游戏结束
完整代码
下面整个小程序的C语言代码,当时使用DevC++编写,后用vs也能编译通过。
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <conio.h>
#include <windows.h>
#include <time.h>
#include <string.h>
#include <ctype.h>
#include <inttypes.h>
//游戏信息
#define GAMENAME "快乐贪吃蛇"
#define DEVLOPER ""
#define QQ ""
#define TEL "电话号码:10086"
#define WRITEDATE "最后一次编写日期:2019年7月19号"
//活动空间大小
#define I 20
#define J 40 //J>=length
//菜单框大小
#define MI 15
#define MJ 50
//方向常量
#define Key_Up 72
#define Key_Down 80
#define Key_Right 77
#define Key_Left 75
#define UP 'W'
#define DOWN 'S'
#define LEFT 'A'
#define RIGHT 'D'
//颜色常量
#define FONTCOLOR 0x0E //字体颜色
#define SPACECOLOR 0x80 //空区域颜色
#define WALLCOLOR 0xC0 //墙颜色
#define DATACOLOR 0x8B //控制数据颜色
#define SNAKECOLOR 0x8B //蛇颜色
//游戏初始数据
#define LEN 4 //LEN>=3 and LEN<=J/2
#define DIRECTION LEFT//最开始时,蛇的前进方向
#define WAITTIME 10//蛇的速度,越小越快
//快捷使用蛇坐标
#define NEXTHEAD S[length]
#define HEAD S[length - 1]
#define NECK S[length - 2]
#define BODY S[length - 3]
#define TAIL S[0]
//界限坐标常量
#define UPFRAME 10 //地图上边界线 (横坐标)
#define DOWNFRAME (UPFRAME + I + 2) + 1 //地图下边界线 (横坐标)
#define LEFTFRAME 10 //地图左边界线(纵坐标)
#define RIGHTFRAME (LEFTFRAME + J + 2) + 3 //地图右边界线(纵坐标)
#define TITLE(x) LEFTFRAME + (x - strlen(GAMENAME)) / 2 //标题(横坐标)
//移动光标
#define GO(i, j) gotoxy(i, j, ' ', -1) //无特殊定位,原始数据移动光标
#define GU(i, j) gotoxy(i, LEFTFRAME + j, ' ', -1) //上框初始坐标
#define GD(i, j) gotoxy(DOWNFRAME + i, LEFTFRAME + j, ' ', -1) //下框初始坐标
#define GL(i, j) gotoxy(UPFRAME + i, j, ' ', -1) //左框初始坐标
#define GR(i, j) gotoxy(UPFRAME + i, RIGHTFRAME + j, ' ', -1) //右框初始坐标
#define GM(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, ' ', -1) //中框初始坐标
//移动光标并打印
#define GF(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, '*', 4) //食物
#define GH(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, '@', 3) //头
#define GN(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, 'o', 2) //颈
#define GB(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, 'o', 1) //身
#define GT(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, '+', 1) //尾
#define GW(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, ' ', 1) //墙
#define GS(i, j) gotoxy(UPFRAME + i, LEFTFRAME + j, ' ', 0) //空
//快捷使用函数
#define PRIN(x) printf("%d", x) //输出数值
#define COLOR(x) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), x) //改色
//
struct Position
{
uint_fast8_t i, j;
};
//全局变量区
struct Position S[I * J] = {0}, FOOD; //蛇座标,食物坐标
uint_fast8_t M[I + 2][J + 2]; //操作数据
uint_fast8_t length, waittime, foodcolor; //长度,输入等待时间,食物颜色
char direction; //方向
bool alive; //存活状态
char playerName[21]; //玩家名字
//函数声明区
void gotoxy(uint_fast8_t i, uint_fast8_t j, char c, uint_fast8_t flag); //移动光标并更改控制数据
void colorfulfont(char *font); //打印花里胡哨的彩色字体
//
void get_really(void); //游戏数据初始化
void welcome(void); //游戏开始界面
void title(uint_fast8_t i, uint_fast8_t j); //打印标题
void menu(void); //打印菜单
void rule_of_game(void); //游戏介绍
void devloper(void); //开发者信息
void aboutgame(void); //关于游戏
void choice(void); //选择菜单选项
//
void game(void); //游戏开始
void draw_map(void); //绘制地图
void put_control_data(void); //打印控制数据
void put_game_data(void); //打印游戏数据
void press(void); //玩家输入函数,用来接收玩家的输入指令
void count_nexthead(void); //计算移动一次后,蛇头的位置
bool countstate(void); //判定移动一次后,游戏处于什么状态(暂定、变长、死亡等等)
void renew_food(void); //重新生成食物
void renew_S(void); //更新蛇的坐标
bool death(void); //死亡时调用
void move_control_data(void); //刷新屏幕上面的控制数据
void move(void); //刷新屏幕上面蛇的位置
char select(void); //游戏结束时调用,选择
void game_over(void); //游戏结束
//程序入口
int main(void)
{
FILE *fp_score = fopen("score.txt", "ab+"); //打开分数文件
FILE *fp_temp = fopen("temp", "w+"); //新建一个临时文件
char gameTime[50]; //用来存放时间字符串
if (fp_score == NULL)
{
printf("打开分数记录文件失败!!!");
getch();
exit(1);
}
start:
welcome();
game:
get_really();
game();
game_over();
//结算游戏数据
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
//通过文件进行类型转换
fprintf(fp_temp, "%d年%d月%d号 %d:%d分%d秒", //将时间存到临时文件
1900 + timeinfo->tm_year, 1 + timeinfo->tm_mon, timeinfo->tm_mday, timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
rewind(fp_temp);
fgets(gameTime, 30, fp_temp); //从临时文件读取时间
rewind(fp_temp);
fprintf(fp_score, "| %-30s| %-50s| %-10d\n", playerName, gameTime, length - 4); //将结算数据保存
//
switch (select()) //游戏结束,判断用户的选择
{
case '1':
goto start;
break;
case '2':
goto game;
break;
default:
break;
}
//显示历史分数
system("cls");
int size = ftell(fp_score);
rewind(fp_score);
char *data = (char *)malloc(size + 1);
fread(data, size, 1, fp_score);
data[size] = 0;
puts(data);
fclose(fp_score);
fclose(fp_temp);
remove("temp");
getch();
return 0;
}
void gotoxy(uint_fast8_t i, uint_fast8_t j, char c, uint_fast8_t flag)
{
COORD position = {j, i};
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), position); //移动光标
CONSOLE_CURSOR_INFO cursor_info = {1, 0};
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info); //隐藏光标
if (flag == 0 || flag == 1 || flag == 2 || flag == 3 || flag == 4)
M[i - UPFRAME][j - LEFTFRAME] = flag, printf("%c", c); //改写
}
void colorfulfont(const char *font)
{
uint_fast8_t i;
for (i = rand() % 6 + 0x0A; *font; font++, i++)
{
if (i == 0x10)
i = rand() % 6 + 0x0A;
COLOR(i);
putchar(*font);
}
}
void get_really(void)
{
uint_fast8_t i;
length = LEN; //蛇的初始长度
for (i = 0; i < length; i++)
{
S[i].i = I / 2;
S[i].j = J / 2 + (length - 1 - i); //一条蛇的初始坐标
}
alive = true; //活着
direction = DIRECTION; //初始方向
}
void welcome(void)
{
system("mode con cols=120 lines=45");
srand((unsigned)time(NULL));
COLOR(0x00);
system("cls");
title(UPFRAME * 0.8, TITLE(49));
menu();
choice();
}
void game(void)
{
COLOR(0x0F);
system("cls");
GO(0, 0);
puts("请输入你的名字(最多20个字符,汉字算两个):");
gets(playerName);
system("cls");
title(UPFRAME * 0.8, TITLE(J + 2));
draw_map();
renew_food();
put_control_data();
put_game_data();
Sleep(2000);
while (alive)
{
press(); //输入
count_nexthead(); //计算下一步蛇头的位置
alive = countstate(); //刷新存活状态
move();
move_control_data();
put_game_data();
}
}
char select(void)
{
char choice;
GD(6, 10);
COLOR(0x0A);
printf("游戏结束,请键入选择【 】");
GD(7, 1);
printf("1.返回主菜单 2.重新开始 3.退出游戏");
GD(6, 32);
for (choice = getche(); choice != '1' && choice != '2' && choice != '3'; choice = getche())
GD(6, 32);
return choice;
}
void game_over(void)
{
uint_fast8_t i;
COLOR(SNAKECOLOR);
for (i = 0; i < 5; i++)
{
Sleep(200);
GM(HEAD.i, HEAD.j);
putchar('x');
Sleep(200);
GM(HEAD.i, HEAD.j);
putchar('@');
}
GM(HEAD.i, HEAD.j);
putchar('x');
uint_fast16_t a;
}
void title(uint_fast8_t i, uint_fast8_t j)
{
GO(i, j);
colorfulfont(GAMENAME);
}
void menu(void)
{
uint_fast8_t i;
COLOR(WALLCOLOR);
for (i = 0; i < MI; i++)
GW(i, 0), GW(i, MJ - 1);
for (i = 1; i < MJ - 1; i++)
GW(0, i), GW(MI - 1, i);
COLOR(FONTCOLOR);
GM(3, MJ / 6);
colorfulfont("1.开始游戏");
GM(9, MJ / 6);
colorfulfont("2.游戏规则");
GM(3, MJ / 2);
colorfulfont("3.作者信息");
GM(9, MJ / 2);
colorfulfont("4.关于游戏");
}
void rule_of_game(void)
{
uint_fast8_t i, j;
COLOR(0x00);
for (i = 0; i < 10; i++)
{
for (j = MJ; j < MJ + 60; j++)
{
GM(i, j);
putchar(' ');
}
}
COLOR(0x0A);
GM(0, MJ + 20);
printf("游戏规则");
GM(2, MJ + 6);
colorfulfont("操作一:控制方向-----\"W\"、\"S\"、\"A\"、\"D\"");
GM(3, MJ + 14);
colorfulfont("分别对应上下左右(大小写皆可)");
GM(4, MJ + 14);
colorfulfont("也可使用键盘的上下左右键");
GM(6, MJ + 6);
colorfulfont("操作二:暂停游戏。往反方向走可以暂停游戏");
GM(8, MJ + 13);
colorfulfont("(比如你正在向左走,按一下右键即可暂停游戏)");
GM(10, MJ + 6);
colorfulfont("规则三:撞墙和吃到自己都会死");
}
void devloper(void)
{
uint_fast8_t i, j;
COLOR(0x00);
for (i = 0; i < 10; i++)
{
for (j = MJ; j < MJ + 60; j++)
{
GM(i, j);
putchar(' ');
}
}
COLOR(0x0A);
GM(0, MJ + 20);
printf("作者介绍");
GM(2, MJ + 10);
colorfulfont(DEVLOPER);
GM(4, MJ + 10);
colorfulfont(QQ);
GM(6, MJ + 10);
colorfulfont(TEL);
}
void aboutgame(void)
{
uint_fast8_t i, j;
COLOR(0x00);
for (i = 0; i < 10; i++)
{
for (j = MJ; j < MJ + 60; j++)
{
GM(i, j);
putchar(' ');
}
}
COLOR(0x0A);
GM(0, MJ + 15);
printf("关于游戏");
GM(2, MJ + 6);
colorfulfont(WRITEDATE);
}
void choice(void)
{
COLOR(0x0A);
char chance;
const char *notice = "请键入选择【 】";
GM(MI + 3, MJ / 6);
colorfulfont(notice);
GM(MI + 3, MJ / 6 + strlen(notice) - 3);
chance = getche();
while (chance != '1')
{
if (chance == '2')
rule_of_game();
else if (chance == '3')
devloper();
else if (chance == '4')
aboutgame();
//
GM(MI + 3, MJ / 6 + strlen(notice) - 3);
chance = getche();
}
}
void draw_map(void)
{
uint_fast8_t i, j;
//
COLOR(WALLCOLOR);
for (i = 1; i < I + 2; i++)
GW(i, 0), GW(i, J + 1);
for (j = 0; j < J + 2; j++)
GW(0, j), GW(I + 1, j); //打印墙
//
COLOR(SPACECOLOR);
for (i = 1; i <= I; i++)
for (j = 1; j <= J; j++)
GS(i, j); //打印活动空间
//
COLOR(SNAKECOLOR);
GH(HEAD.i, HEAD.j); //打印蛇头
GN(NECK.i, NECK.j); //打印蛇颈
for (j = BODY.j; j < TAIL.j; j++)
GB(BODY.i, j); //打印蛇身
GT(TAIL.i, TAIL.j); //打印蛇尾
}
void put_control_data(void)
{
uint_fast8_t i, j;
const char *notice = "控制数据";
GO(UPFRAME * 0.8, RIGHTFRAME + (J + 2 - strlen(notice)) / 2);
colorfulfont(notice);
COLOR(DATACOLOR);
for (i = 0; i < I + 2; i++)
{
GR(i, 0);
for (j = 0; j < J + 2; j++)
PRIN(M[i][j]);
}
}
void put_game_data(void)
{
COLOR(FONTCOLOR);
GD(0, 0);
printf("Food:(%3d,%3d)", FOOD.i, FOOD.j);
GD(1, 0);
printf("Head:(%3d,%3d)", HEAD.i, HEAD.j);
GD(2, 0);
printf("Neck:(%3d,%3d)", NECK.i, NECK.j);
GD(3, 0);
printf("Tail:(%3d,%3d)", TAIL.i, TAIL.j);
GD(4, 0);
printf("Length:%3d", length);
}
void press(void)
{
char key;
for (waittime = WAITTIME; waittime && !kbhit(); waittime--)
{
GD(1, (J + 2) / 2);
COLOR(FONTCOLOR);
printf("等待时间:%3d", waittime);
Sleep(1);
}
if (waittime)
{
key = getch();
if (key == -32) //方向键转换
{
key = getch();
switch (key)
{
case Key_Up:
key = UP;
break;
case Key_Down:
key = DOWN;
break;
case Key_Right:
key = RIGHT;
break;
case Key_Left:
key = LEFT;
break;
}
}
strupr(&key); //把输入的字母同意转换成大写
if (key == UP || key == DOWN || key == LEFT || key == RIGHT)
direction = key;
}
GD(2, (J + 2) / 2);
COLOR(FONTCOLOR);
printf("方向:%c", direction);
}
void count_nexthead(void)
{
NEXTHEAD = HEAD;
switch (direction)
{
case UP:
NEXTHEAD.i--;
break;
case DOWN:
NEXTHEAD.i++;
break;
case LEFT:
NEXTHEAD.j--;
break;
case RIGHT:
NEXTHEAD.j++;
break;
}
}
bool countstate(void)
{
switch (M[NEXTHEAD.i][NEXTHEAD.j])
{
case 1: //撞墙
{
//清除尾巴的控制数据
COLOR(DATACOLOR);
GR(TAIL.i, TAIL.j);
PRIN(0);
//清除蛇尾
COLOR(SPACECOLOR);
GS(TAIL.i, TAIL.j);
//
renew_S();
return false;
}
case 2: //回头
{
GD(0, (J + 2) / 2);
COLOR(FONTCOLOR);
printf("暂停中");
break;
}
case 4: //吃到食物
{
GD(0, (J + 2) / 2);
COLOR(FONTCOLOR);
printf("奔跑中");
length++;
renew_food();
break;
}
default: //正常前进
{
GD(0, (J + 2) / 2);
COLOR(FONTCOLOR);
printf("奔跑中");
//清除控制数据
COLOR(DATACOLOR);
GR(TAIL.i, TAIL.j);
PRIN(0);
//清除蛇尾
COLOR(SPACECOLOR);
GS(TAIL.i, TAIL.j);
//
renew_S();
break;
}
}
return true;
}
void renew_food(void)
{
FOOD.i = rand() % I + 1;
FOOD.j = rand() % I + 1;
while (M[FOOD.i][FOOD.j])
{
FOOD.i = rand() % I + 1;
FOOD.j = rand() % I + 1;
}
foodcolor = rand() % 6 + 0x8A;
COLOR(foodcolor);
GF(FOOD.i, FOOD.j);
}
void renew_S(void)
{
uint_fast8_t i;
for (i = 1; i <= length; i++)
S[i - 1] = S[i];
}
bool death(void)
{
bool die = 0;
if (M[HEAD.i][HEAD.j] == 1)
die = 1;
return die;
}
void move_control_data(void)
{
COLOR(DATACOLOR);
GR(HEAD.i, HEAD.j);
PRIN(M[HEAD.i][HEAD.j]);
GR(NECK.i, NECK.j);
PRIN(M[NECK.i][NECK.j]);
GR(BODY.i, BODY.j);
PRIN(M[BODY.i][BODY.j]);
GR(TAIL.i, TAIL.j);
PRIN(M[TAIL.i][TAIL.j]);
}
void move(void)
{
COLOR(SNAKECOLOR);
GH(HEAD.i, HEAD.j);
GN(NECK.i, NECK.j);
GB(BODY.i, BODY.j);
GT(TAIL.i, TAIL.j);
}
看以后能不能再重新优化或者重新写一个,欢迎各位一起交流,一起进步!