贪吃蛇实现
还记得小时候那种按键的游戏机,经常被我拿到学校偷偷玩,上面有个贪吃蛇的游戏。除了坦克大战,和俄罗斯方块就是只剩他了。通过吃食物来增加蛇长。简陋的界面却也承载我小时候的快乐。在接触到计算机语言后,一直希望我可以做出一个贪吃蛇。现在,这个愿望终于实现了。
一、运行环境
使用编译器为turc2.0。这个是DOS系统下的编译器,只支持键盘操作。距离现在已经很早了。(至于我为什么使用他呢?哦!我亲爱的朋友,只有上帝知道!)
值得注意的是:
1.在tc中,int 只有两字节。
2.tc不会清除上一次的运行结果,所以这里需要使用清屏函数clrscr();此函数包含在头文件<stdlib.h>中。
二、实现前需要解决的问题
1.获取用户按键
这里介绍bioskey()函数。
bioskey()函数使用
引用一下别人的详细介绍,我咋这里简单介绍一下具体用法:
键盘的每一个键,都有一个具体的键值与之对应。获取键,其实就是获取键按下时的键值。
bioskey(0):等待用户从键盘输入,将用户输入的键值放入缓冲区,然后将值赋值给某个变量,这个键值从缓冲区中弹出。
bioskey(1):如果没有键按下,函数返回NULL,当有键按下时,将用户输入的键值放入缓冲区,然后将值赋值给某个变量,这个键值仍存在在缓冲区。当下次从缓冲区取值时,仍是这个值,无法获得后面的键值。这是一个很大的麻烦。所以当使用bioskey(1)时,若有键按下时,需要接受这个键值,使他从缓冲区中清除。
if(bioskey(1)){
ch = bioskey(1);//得到按键值
getch();//清楚缓冲区中按下的键值
}
注意:getch();包含在头文件<conio.h>中。
2.输出蛇
蛇不可能从(0,0)点开始出发,这是不符合日常的认知的。我们需要将光标定位到某一点,从这一点进行输出。
当然我的蛇肯定没有现在贪吃蛇那么花里胡哨,只是很简单的图标。
那现在来介绍gotoxy(int x, int y)函数,此函数的作用就是将光标定位到(x,y)处,然后进行输出。(在tc中可直接调用,需添加头文件<conio.h>)
3.蛇的移动
在蛇的移动中,其实只发生了三件事,头的移动和变化,尾的删除。
从上图可以清楚的看到,删除尾,将原来的头变为身体,根据方向,将原来的头的后一个坐标变为头。
4.食物的生成
整个屏幕使用地图来进行操作。只在地图中值为0的地方随机生成食物。避免食物生成到墙上和蛇的身体上。
static int map[MAP] = {0};
这里定义食物的结构体。将食物定义为全局变量,方便对食物的管理。
typedef struct FOOD{
int x;
int y;
int exist;//判断屏幕中是否存在食物
}FOOD;
三、代码实现
#include <stdio.h>
#include <bios.h>
#include <conio.h>
#include <stdlib.h>
#define UP_KEY 18432
#define DOWM_KEY 20480
#define LEFT_KEY 19200
#define RIGHT_KEY 19712
#define ESC 283
#define W_KEY 4471
#define S_KEY 8051
#define A_KEY 7777
#define D_KEY 8292
#define MAP 1840
#define MAX_BODY 200
#define TRUE 1
#define FALSE 0
typedef unsigned char boolean;
typedef struct COORDINATE{
int x;
int y;
}COORDINATE;
typedef struct FOOD{
int x;
int y;
int exist;
}FOOD;
typedef struct VARIATION {
int x_variation;//方向改变时,x的改变量
int y_variation;//方向改变时,y的改变量
}VARIATION;
typedef struct SNAKE{
int direction;//此时蛇头方向
int lastdirection;//上一次蛇头方向
int head;//指向蛇的头
int tail;//指向蛇的尾
int length;//蛇的限制长度
int real_length;//蛇真正的长度
char headform;//蛇头形状
long time;//限制时间
COORDINATE body[MAX_BODY];//记录蛇身的数组
}SNAKE;
static int map[MAP] = {0};
static FOOD food = {0};
void initSnake();
boolean Gomeover(SNAKE *snake);
void score(SNAKE *snakeone, SNAKE *snakeother);
void snakefood();
void eatfood(SNAKE *snake);
void Direction(SNAKE *snakeone, SNAKE *snakeother);
int judesnake_key(int key);
void judesnake_dir(SNAKE *snakeone, SNAKE *snakeother, int i, int newkey);
void movesnake(SNAKE *snake);
void changesnake(SNAKE *snakeone, SNAKE *snakeother);
int judgesign(SNAKE *snake);
void deltail(int x, int y);
void draw(int x, int y);
void speed(SNAKE *snake);
void GameStare();
void snakescore(SNAKE *snakeone, SNAKE *snakeother);
//游戏结束时输出两条蛇的得分。
void snakescore(SNAKE *snakeone, SNAKE *snakeother) {
clrscr();
gotoxy(35,10);
printf("A : %d B : %d",snakeone->real_length,snakeother->real_length);
getch();
}
//游戏开始函数
void GameStare() {
//定义第一条蛇的初始信息
SNAKE snakeone = {0, RIGHT_KEY, RIGHT_KEY, 0, 0, 3, 1, '>', 50000,
{41,10}};
//定义第二条蛇的初始信息
SNAKE snakeother = {0, A_KEY, A_KEY, 0, 0, 3, 1, '<', 50000,
{39,10}};
clrscr();
initSnake();
//只要有一条蛇死亡,游戏结束
while(!(Gomeover(&snakeone) || Gomeover(&snakeother))){
score(&snakeone,&snakeother);
snakefood();
eatfood(&snakeone);
eatfood(&snakeother);
Direction(&snakeone, &snakeother);
movesnake(&snakeone);
movesnake(&snakeother);
speed(&snakeone);
}
snakescore(&snakeone, &snakeother);
}
//限制蛇移动速度,拓展:以后可根据蛇身的增长来增加移动速度
void speed(SNAKE *snake){
long i;
for(i = 0; i < snake->time; i++){
}
}
//将坐标移至指定点,画出蛇的节点
void draw(int x, int y) {
gotoxy(x, y);
printf("%c",4);
}
//将坐标移至指定点,用' '来覆盖尾节点
void deltail(int x, int y) {
gotoxy(x, y);
printf(" ");
}
//判断蛇头此时方向,返回相应的值。
int judgesign(SNAKE *snake){
if(UP_KEY == snake->direction || W_KEY == snake->direction){
return 0;
}
if(DOWM_KEY == snake->direction || S_KEY == snake->direction){
return 1;
}
if(LEFT_KEY == snake->direction || A_KEY == snake->direction){
return 2;
}
if(RIGHT_KEY == snake->direction || D_KEY == snake->direction){
return 3;
}
}
//移动蛇(画蛇)
void movesnake(SNAKE *snake) {
int i;
//根据蛇头变化定义改变量
VARIATION change[4] = { {0, -1}, {0, 1}, {-1, 0}, {1, 0} };
i = judgesign(snake);
//判断限制长度和真实长度之间的关系:当真实长度大于等于限制长度时,进行删除尾结点的操作,否则增加真实长度。
if(snake->length < snake->real_length) {
deltail(snake->body[snake->tail].x, snake->body[snake->tail].y);
map[(snake->body[snake->tail].x)*80 + (snake->body[snake->tail].y)] = 0;//将地图中的删除的尾节点重新置为0
snake->tail = (snake->tail + 1) % MAX_BODY;//运用循环数组,取下一个尾
} else {
snake->real_length++;
map[(snake->body[snake->tail].x)*80 + (snake->body[snake->tail].y)] = 1;
}
//将原来的蛇头变成蛇身
draw(snake->body[snake->head].x, snake->body[snake->head].y);
//取得下一个蛇头的坐标和指向蛇头的指针
snake->body[(snake->head + 1) % MAX_BODY].x = snake->body[snake->head].x + change[i].x_variation;
snake->body[(snake->head + 1) % MAX_BODY].y = snake->body[snake->head].y + change[i].y_variation;
snake->head = (snake->head + 1) % MAX_BODY;
gotoxy(snake->body[snake->head].x, snake->body[snake->head].y);
printf("%c",snake->headform);
map[(snake->body[snake->head].x)*80 + (snake->body[snake->head].y)] = 1;//在地图中记录这个位置不为空
}
//判断是哪个用户输入的键值
int judesnake_key(int key) {
if(key == UP_KEY || key == DOWM_KEY || key == LEFT_KEY || key == RIGHT_KEY) {
return 1;
} else if(key == W_KEY || key == S_KEY || key == A_KEY || key == D_KEY) {
return 2;
} else {
return 0;
}
}
//判断此时的方向和将键值赋值给相应的蛇。拓展:在一个蛇进行移动时,只需要删除函数关于第二条蛇的相关代码即可。
void judesnake_dir(SNAKE *snakeone, SNAKE *snakeother, int i, int newkey) {
if(i == 1){
if(UP_KEY == newkey && DOWM_KEY != snakeone->lastdirection) {
snakeone->direction = newkey;
snakeone->headform = '^';
} else if(DOWM_KEY == newkey && UP_KEY != snakeone->lastdirection) {
snakeone->direction = newkey;
snakeone->headform = 'v';
} else if(LEFT_KEY == newkey && RIGHT_KEY != snakeone->lastdirection) {
snakeone->direction = newkey;
snakeone->headform = '<';
} else if(RIGHT_KEY == newkey && LEFT_KEY != snakeone->lastdirection) {
snakeone->direction = newkey;
snakeone->headform = '>';
}
} else if(i == 2) {
if(W_KEY == newkey && S_KEY != snakeother->lastdirection) {
snakeother->direction = newkey;
snakeother->headform = '^';
} else if(S_KEY == newkey && W_KEY != snakeother->lastdirection) {
snakeother->direction = newkey;
snakeother->headform = 'v';
} else if(A_KEY == newkey && D_KEY != snakeother->lastdirection) {
snakeother->direction = newkey;
snakeother->headform = '<';
} else if(D_KEY == newkey && A_KEY != snakeother->lastdirection) {
snakeother->direction = newkey;
snakeother->headform = '>';
}
} else {
if(ESC == newkey) {
snakeone->direction = newkey;
snakeother->direction = newkey;
} else{
//若是其他无关键值,保持原来方向。
snakeone->direction = snakeone->lastdirection;
snakeother->direction = snakeother->lastdirection;
}
}
//记录上一次方向
snakeone->lastdirection = snakeone->direction;
snakeother->lastdirection = snakeother->direction;
}
//改变蛇方向
void changesnake(SNAKE *snakeone, SNAKE *snakeother) {
int i;
int newkey;
//无按键时,不进入该语句
if(bioskey(1)) {
newkey = bioskey(1);
getch();
i = judesnake_key(newkey);
judesnake_dir(snakeone, snakeother,i, newkey);
}
}
//集成调用函数,调用两次,因为要满足两条蛇同时移动
void Direction(SNAKE *snakeone, SNAKE *snakeother) {
changesnake(snakeone, snakeother);
changesnake(snakeone, snakeother);
}
//蛇吃食物
void eatfood(SNAKE *snake){
if(food.x == snake->body[snake->head].x && food.y == snake->body[snake->head].y){
snake->length++;
food.exist = 0;
}
}
//生成食物具体代码
void snakefood() {
int i;
int count = 0;
int tmp;
int t;
int z;
int tmpmap[MAP] = {0};//临时地图,记录此时地图为空的坐标
if(food.exist == 0) {
srand((unsigned int)time(NULL));//播散随机种子
for(i = 0; i < MAP; i++) {
if(map[i] == 0 ) {
tmpmap[count++] = i;//将空地图的下标赋值给临时地图
}
}
//洗牌算法,从临时地图的最后一个有效空间开始,随机取临时地图前面的下标,与临时地图最后一个有效空间进行交换,有效空间值count--,重复之前的过程,直到count = 1
for(i = count - 1 ; i > 1; i--) {
z = rand() % i;//获得随机数
tmp = tmpmap[z];
tmpmap[z] = tmpmap[i];
tmpmap[i] = tmp;
}
t = tmpmap[rand() % count];//从临时地图中取得随机的空地图下标
//地图值一维化二维
food.x = t % 80;
food.y = t / 80 + 1;
gotoxy(food.x, food.y);
printf("%c",3);
food.exist = 1;
}
}
void score(SNAKE *snakeone,SNAKE *snakeother) {
gotoxy(20,24);
printf("onescore %d otherscore %d",snakeone->real_length,snakeother->real_length);
}
//判断游戏结束
boolean Gomeover(SNAKE *snake) {
if(ESC == snake->direction || (snake->body[snake->head]).x < 2 || (snake->body[snake->head]).x > 79
|| (snake->body[snake->head]).y < 2 || (snake->body[snake->head]).y > 22) {
return TRUE;
}
return FALSE;
}
// 初始化游戏
void initSnake() {
int x;
int y;
clrscr();
gotoxy(30,10);
printf("Esc : Geme over\n");
gotoxy(27,13);
printf("Press any key to continue\n");
x = bioskey(0);
clrscr();
//打印墙体
for(x = 0; x < 80; x++){
printf("#");
map[x] = 2;
}
for(y = 2; y <= 22; y++){
for(x = 0; x < 80; x++) {
if(x == 0){
printf("#");
map[y*80 + x] = 2;
} else if(x == 79) {
printf("#");
map[y*80 + x] = 2;
} else {
printf(" ");
}
}
}
for(x = 0; x < 80; x++){
printf("#");
map[22*80 + x] = 2;
}
printf("\n");
}
int main( )
{
GameStare();
getch();
return 0;
}
四、总结
这是我做的第一个可以玩的游戏,尽管内容十分粗糙,很多功能也没有添加,但是基本框架已经搭建完成,以后只需要添加功能即可。
这次编写贪吃蛇,感觉到从0到1的难度,只要踏出了这一步,后面起码不是睁眼瞎了。在编写的过程中,很多想法用代码来实现确实有很大的难度,需要一次次的更改代码,一次次的纠错,这真的是一个很痛苦的过程。这也是需要学习的能力。
总之,加油吧!
20200729于机房