mac下的c语言贪吃蛇

小白小白特别白~~
c语言贪吃蛇其实在网上可以找到很多的资源。但是因为用的是mac,windows.h和conio.h找不到,所以里面很多函数用不了。在网上找了很久,才慢慢写完。在这里记录一下,欢迎感谢各位大佬指点

目录

  • 笔记
  • 游戏大纲
  • 代码
    • 1. 初始化界面和蛇身
    • 2. 清屏
    • 3. 蛇身运动
    • 4. 食物
    • 5. 游戏循坏&方向键的控制&判断
      • 判断是否撞墙或者撞到蛇身
      • 判断是否吃到食物
      • 游戏循环&方向键控制
    • 6. 居中打印
    • 7. 玩家用户名和score记录
  • 全部代码

笔记

用的IDE是codelite。游戏是为了在mac的terminal上80x24的界面展示写的。
一些没有实现或者有待改进的地方:

  • 当游戏结束时,整个程序就结束了。没有按某个键,重新开始的功能。
  • terminal上的光标有点妨碍最终的显示效果。
  • 如果玩家在蛇身运动的中途按键,系统依然会读取而且存储下来。

下面是游戏最终实现结果:
mac下的c语言贪吃蛇_第1张图片

游戏大纲

Created with Raphaël 2.2.0 提醒用户输入用户名 展示游戏页面 根据方向键控制蛇身 吃到食物,蛇身加长,score加一 碰到蛇身或者墙,life减一 life为0时,游戏结束 记录玩家username和score

代码

注意!下面的一些函数(例如:gotoxy)是在conio.h的头文件下面的。这个头文件在mac里面是不能直接用的。我是找到了别人写的myconio 然后下载了下来。

1. 初始化界面和蛇身

效果展示:游戏刚开始的界面,蛇身初始向下
mac下的c语言贪吃蛇_第2张图片
蛇身用了一个结构体数组。假设了蛇身长度不会超过20.

//terminal的大小:80x24
#define ROW 24 
#define COL 80
#define SROW 4//SROW: starting row. 这里是用来标记界面最上面那一行
#define maxSnakeLen 20//蛇身长度最大值

struct coordinates{
	int x;
	int y;
}snake[maxSnakeLen];

使用gotoxy就可以选择在terminal的哪一个具体位置打印,省去了全部清屏的麻烦。

 void initialize(int score, int life, int snakeLen){

	//打印游戏边界
	//横向边界
	for(int j = 1; j <= COL; j++){
		gotoxy(j, SROW);
		printf("|");
		gotoxy(j,ROW);
		printf("|");
	}
	
	//竖向边界
	for(int i = SROW+1; i < ROW; i++){
		gotoxy(1, i);
		printf("|");
		gotoxy(COL, i);
		printf("|");
	}
	
	//打印score和life
	//在网上找到了怎么在mac terminal更改色彩的办法。
	gotoxy(4, SROW-1);
	printf("\e[32mscore: %2d\e[0m",score);//绿色
	gotoxy(COL-9, SROW-1);
	printf("\e[32mlife: %d\n\e[0m", life);

	//初始化蛇身
	//蛇尾
	snake[0].y = 12;
	snake[0].x = 39;
	//初始化蛇身为竖向
	for(int i = 0; i < snakeLen; i++){
		gotoxy(snake[0].x, snake[0].y+i);
		if(i == snakeLen - 1){//蛇头
			printf("■");
		}
		else{
			printf("\e[35m■\e[0m");//蛇身
		}
		
		if(i != 0){//记录蛇身的坐标
			snake[i].x = snake[0].x;
			snake[i].y = snake[0].y+i;
		}
	}

}

2. 清屏

在网上看到过system(“cls”);但是在我的mac上还是不能用。所以自己写了一个清屏的函数

void clearBoard(){
	for(int y = 3; y < 25; y++){
		for(int x = 1; x < 81; x++){
			gotoxy(x,y);
			printf(" ");
		}
	}
}

3. 蛇身运动

蛇身按照当前的方向前进。
蛇身前进的原理:

  • 创建新的蛇头(向当前的方向,在原来的蛇头位置向前一格)
  • 原来的蛇头现在变为蛇身
  • 原来的蛇尾消失
struct coordinates{
	int x;
	int y;
}prevTail;//previous tail
void snakeMoving(char direction, int snakeLen){
	
	//清除原来的蛇尾
	gotoxy(snake[0].x, snake[0].y);
	printf(" ");
	//先记录下来蛇尾原来所在的坐标,方便后面使用
	prevTail.x = snake[0].x;
	prevTail.y = snake[0].y;
	
	//原来的蛇头变成现在的蛇身
	gotoxy(snake[snakeLen-1].x, snake[snakeLen-1].y);
	printf("\e[35m■\e[0m");
	
	//更新蛇身的坐标,原来蛇尾的坐标不再需要,倒数第二位置的蛇身现在变成蛇尾。
	for(int i = 0; i < snakeLen-1; i++){
		snake[i].x = snake[i+1].x;
		snake[i].y = snake[i+1].y;
	}
	//更新新的蛇头的位置
	if(direction == 'U'){
		snake[snakeLen-1].y -= 1;

	}
	else if(direction == 'D'){
		snake[snakeLen-1].y += 1;
		
	}
	else if(direction == 'L'){
		snake[snakeLen-1].x -= 1;
	
	}
	else{
		snake[snakeLen-1].x += 1;
		
	}
	//打印新的蛇头
	gotoxy(snake[snakeLen-1].x, snake[snakeLen-1].y);
	printf("■");

	//暂停10ms
	Sleep(10);
	
}

4. 食物

随机生成食物坐标,并打印。

struct coordinates{
	int x;
	int y;
}food;
void showFood(int snakeLen){

	int xcoordinate, ycoordinate;
	bool coincide = false;
	
	//行:[5, 23]
	ycoordinate = rand() % 18 + 5;
	//列:[2, 79]
	xcoordinate = rand() % 77 + 2;
	
	//检测生成的食物坐标,是否和蛇身坐标重合
	for(int i = 0; i < snakeLen; i++){
		if(xcoordinate == snake[i].x && ycoordinate == snake[i].y)
			coincide = true;
	}
	
	if(coincide == false){
		food.x = xcoordinate;
		food.y = ycoordinate;
		gotoxy(food.x, food.y);
		printf("\e[01;33mF\e[0m");
	}
	else{
		showFood(snakeLen);//如果重合,则重新生成
	}
}

5. 游戏循坏&方向键的控制&判断

判断是否撞墙或者撞到蛇身

void kill(int *life, int snakeLen){
	//检测当前蛇身的坐标是否位于边界位置。
	//不管多少蛇身位于边界,一次循环,只减1分
	for(int i = 0; i < snakeLen; i++){
		if(snake[i].x == 1 || snake[i].x == 80 || snake[i].y == 4 || snake[i].y == 24){
			*life -= 1;//生命值-1
			gotoxy(COL-9, SROW-1);
			printf("\e[32mlife: %d\n\e[0m", *life);//更新生命值
			break;
		}
		
	}
	//检测蛇身坐标是否重合
	for(int i = 0; i < snakeLen; i++){
		for(int j = i+1; j < snakeLen; j++){
			if(snake[i].x == snake[j].x && snake[i].y == snake[j].y){
				*life -= 1;
				gotoxy(COL-9, SROW-1);
				printf("\e[32mlife: %d\n\e[0m", *life);
				break;
			}
		}
	}

}

判断是否吃到食物

通过检测蛇身坐标是否与食物坐标重合。如果吃到食物,蛇身加长,score加一

void isFoodEaten(int *snakeLen, int *score){
	for(int i = 0; i < *snakeLen; i++){
		if(food.x == snake[i].x && food.y == snake[i].y){//食物被吃
			//将光标移到之前被清空的蛇尾,重新打印,实现蛇身加长
			gotoxy(prevTail.x, prevTail.y);
			printf("\e[35m■\e[0m");
			//更改蛇身坐标
			for(int i = *snakeLen; i > 0; i--){
				snake[i].x = snake[i-1].x;
				snake[i].y = snake[i-1].y;
			}
			snake[0].x = prevTail.x;
			snake[0].y = prevTail.y;
			*snakeLen += 1;
			//score加一,并打印
			*score += 1;
			gotoxy(4, SROW-1);
			printf("\e[32mscore: %2d\e[0m", *score);
			//重新生成食物
			showFood(*snakeLen);
			break;
		}
	}
}

游戏循环&方向键控制

贪吃蛇最重要的部分就是实现方向键的控制。这个函数根据当前用户按键,来更改方向变量,并把这个变量代入到之前的snakeMoving函数里面。

用windows的一般会用kbhit和getch两个函数。
scanf的话会导致用户输入的方向键会在terminal上面回显,而且需要用户按下回车才会被读取,所以不可以用。getch的话不需要回车,也不会回显。kbhit则是用来判断用户当前有没有按键,这在循环中是一个非常重要的步骤。

虽然我下载的myconio里面也包含了这两个函数。但是,kbhit在读取到一次按键之后,会一直显示当前用户按键,即使在没有按键的情况下。这就导致了循坏进行不下去,蛇身只有在用户按下键的时候才会运动(正常情况下,在用户没有按下方向键的话,蛇身会按照当前的方向继续运动。用户按键后,蛇身会改变方向继续运动)

最后在stackoverflow上面看到也有人遇到这个问题,然后从别人的回答里找到了解决方法。reference:(来自user3386109的答案)在这里插入图片描述
具体是为什么,我其实没有特别看懂。。。但是使用了答案里的代码,完全可以代替kbhit和getch。注意⚠️,下面的代码有一部分是答案里的函数,在上面的网址上可以找到。

void kbRespond( char *direction, int *snakeLen, int *life, int *score){
    int c;
    kbsetup();//在stackoverflow的答案上可以找到这个函数。
    for (;*life > 0;){  
		Sleep(700);//暂停700毫秒,让蛇身暂停运动,并且留给玩家时间操控。
		if ((c = getkey()) != '\0' ){//玩家按键
		//⚠️getkey函数也是来自stackoverflow
		//玩家按下一次方向键,会读取到三个数字。只有第三个数字可以用来区分不同的方向键。
			c = getkey();
			c = getkey();
			
			if(c == 65 && *direction != 'D' ){//玩家不可以输入和当前方向完全相反的指令
				*direction = 'U';
			}
			else if(c == 68 && *direction != 'R' ){
				*direction = 'L';
			}
			else if(c == 67 && *direction != 'L' ){
				*direction = 'R';
			}
			else if(c == 66 && *direction != 'U'){
				*direction = 'D';
			}
		}
		//将新的方向传递给snakeMoving,让蛇身按照这个方向移动一次。
		snakeMoving(*direction, *snakeLen);
		//蛇身移动之后
		//检测蛇身是否接触墙或者蛇身自己
		kill(life, *snakeLen);
		//检测是否吃到食物
		isFoodEaten(snakeLen, score);
    }
}

注意,在kbRepsond和snakeMoving这两个函数里面都有调用延迟。kbRespond每次循环延迟700ms,调用snakeMoving一次。snakeMoving每调用一次,延迟10ms。这两个延迟看起来可以合成一个延迟,写在任何一个函数里面就可以。

但是在使用stackoverflow里面的getkey这个函数的时候,如果只写一个延迟的话(不管这个延迟是多长时间),在这个延迟的期间按键,不会被这次的循环识别到,而是下一个循环才能识别到玩家已经按键,并改变方向。从游戏最终实现的角度上来讲,就是按键之后有延迟,蛇身并不会马上改变方向,而是按照当前的方向再运动一格,然后改变方向。
解决的方法就是使用两个延迟,第一个延迟不管有多短,都可以解决这个问题。

6. 居中打印

在处理游戏界面的时候,居中打印会让界面看起来更整洁一点。

void printMiddle(char *string, int y){
	gotoxy(  ( COL - strlen(string) ) / 2, y);
}

效果:
mac下的c语言贪吃蛇_第3张图片

7. 玩家用户名和score记录

记录下来玩家的username和score
下面的代码是写在main里面的,并不是单独的函数。

#define maxUsername 20
//使用符号数组来存储用户名,假设用户名不会超过20

struct playerInfo{
	char username[maxUsername];
	int Score;
}player;

尝试过直接读取字符串的函数,最后都不知道为什么没办法用。所以就一个个字符的读取,存储到数组里。

	printMiddle("Enter your username to keep your record", 12);//居中打印
	printf("\e[33mEnter your username to keep your record\n\e[0m");

	char c;
	int i = 0;
	for(; i < maxUsername - 1 && (c = getchar()) != '\n'; i++){
		player.username[i] = c;
	}
	player.username[i] = '\0';//将\n更改为\0
	FILE *players;
	if( ( players = fopen("players", "wt") ) == NULL){//创建一个叫做players的文件,并写入这个文件。
		printf("Error in creating file named players. Exiting the program...");
		return 0;
	}
	fwrite(&player, sizeof(struct playerInfo), 1, players);//写入一个player结构体。
	
	fclose(players);//关闭文件

全部代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ROW 24 
#define COL 80
#define SROW 4//SROW: starting row. 这里是用来标记界面最上面那一行
#define maxSnakeLen 20//蛇身长度最大值
#define maxUsername 20

struct coordinates{
	int x;
	int y;
}snake[maxSnakeLen], food, prevTail;

struct playerInfo{
	char username[maxUsername];
	int Score;
}player;


void initialize(int score, int life, int snakeLen){

	//打印游戏边界
	//横向边界
	for(int j = 1; j <= COL; j++){
		gotoxy(j, SROW);
		printf("|");
		gotoxy(j,ROW);
		printf("|");
	}
	
	//竖向边界
	for(int i = SROW+1; i < ROW; i++){
		gotoxy(1, i);
		printf("|");
		gotoxy(COL, i);
		printf("|");
	}
	
	//打印score和life
	//在网上找到了怎么在mac terminal更改色彩的办法。
	gotoxy(4, SROW-1);
	printf("\e[32mscore: %2d\e[0m",score);//绿色
	gotoxy(COL-9, SROW-1);
	printf("\e[32mlife: %d\n\e[0m", life);

	//初始化蛇身
	//蛇尾
	snake[0].y = 12;
	snake[0].x = 39;
	//初始化蛇身为竖向
	for(int i = 0; i < snakeLen; i++){
		gotoxy(snake[0].x, snake[0].y+i);
		if(i == snakeLen - 1){//蛇头
			printf("■");
		}
		else{
			printf("\e[35m■\e[0m");//蛇身
		}
		
		if(i != 0){//记录蛇身的坐标
			snake[i].x = snake[0].x;
			snake[i].y = snake[0].y+i;
		}
	}

}



void showFood(int snakeLen){

	int xcoordinate, ycoordinate;
	bool coincide = false;
	
	//行:[5, 23]
	ycoordinate = rand() % 18 + 5;
	//列:[2, 79]
	xcoordinate = rand() % 77 + 2;
	
	//检测生成的食物坐标,是否和蛇身坐标重合
	for(int i = 0; i < snakeLen; i++){
		if(xcoordinate == snake[i].x && ycoordinate == snake[i].y)
			coincide = true;
	}
	
	if(coincide == false){
		food.x = xcoordinate;
		food.y = ycoordinate;
		gotoxy(food.x, food.y);
		printf("\e[01;33mF\e[0m");
	}
	else{
		showFood(snakeLen);//如果重合,则重新生成
	}
}



void snakeMoving(char direction, int snakeLen){
	
	//清除原来的蛇尾
	gotoxy(snake[0].x, snake[0].y);
	printf(" ");
	//先记录下来蛇尾原来所在的坐标,方便后面使用
	prevTail.x = snake[0].x;
	prevTail.y = snake[0].y;
	
	//原来的蛇头变成现在的蛇身
	gotoxy(snake[snakeLen-1].x, snake[snakeLen-1].y);
	printf("\e[35m■\e[0m");
	
	//更新蛇身的坐标,原来蛇尾的坐标不再需要,倒数第二位置的蛇身现在变成蛇尾。
	for(int i = 0; i < snakeLen-1; i++){
		snake[i].x = snake[i+1].x;
		snake[i].y = snake[i+1].y;
	}
	//更新新的蛇头的位置
	if(direction == 'U'){
		snake[snakeLen-1].y -= 1;

	}
	else if(direction == 'D'){
		snake[snakeLen-1].y += 1;
		
	}
	else if(direction == 'L'){
		snake[snakeLen-1].x -= 1;
	
	}
	else{
		snake[snakeLen-1].x += 1;
		
	}
	//打印新的蛇头
	gotoxy(snake[snakeLen-1].x, snake[snakeLen-1].y);
	printf("■");

	//暂停10ms
	Sleep(10);
	
}

void isFoodEaten(int *snakeLen, int *score){
	for(int i = 0; i < *snakeLen; i++){
		if(food.x == snake[i].x && food.y == snake[i].y){//食物被吃
			//将光标移到之前被清空的蛇尾,重新打印,实现蛇身加长
			gotoxy(prevTail.x, prevTail.y);
			printf("\e[35m■\e[0m");
			//更改蛇身坐标
			for(int i = *snakeLen; i > 0; i--){
				snake[i].x = snake[i-1].x;
				snake[i].y = snake[i-1].y;
			}
			snake[0].x = prevTail.x;
			snake[0].y = prevTail.y;
			*snakeLen += 1;
			//score加一,并打印
			*score += 1;
			gotoxy(4, SROW-1);
			printf("\e[32mscore: %2d\e[0m", *score);
			//重新生成食物
			showFood(*snakeLen);
			break;
		}
	}
}

void kill(int *life, int snakeLen){
	//检测当前蛇身的坐标是否位于边界位置。
	//不管多少蛇身位于边界,一次循环,只减1分
	for(int i = 0; i < snakeLen; i++){
		if(snake[i].x == 1 || snake[i].x == 80 || snake[i].y == 4 || snake[i].y == 24){
			*life -= 1;//生命值-1
			gotoxy(COL-9, SROW-1);
			printf("\e[32mlife: %d\n\e[0m", *life);//更新生命值
			break;
		}
		
	}
	//检测蛇身坐标是否重合
	for(int i = 0; i < snakeLen; i++){
		for(int j = i+1; j < snakeLen; j++){
			if(snake[i].x == snake[j].x && snake[i].y == snake[j].y){
				*life -= 1;
				gotoxy(COL-9, SROW-1);
				printf("\e[32mlife: %d\n\e[0m", *life);
				break;
			}
		}
	}

}
//注意⚠️,这里应该写的是stackoverflow答案里面的函数,请自行查阅。

void kbRespond( char *direction, int *snakeLen, int *life, int *score){
    int c;
    kbsetup();//在stackoverflow的答案上可以找到这个函数。
    for (;*life > 0;){  
		Sleep(700);//暂停700毫秒,让蛇身暂停运动,并且留给玩家时间操控。
		if ((c = getkey()) != '\0' ){//玩家按键
		//⚠️getkey函数也是来自stackoverflow
		//玩家按下一次方向键,会读取到三个数字。只有第三个数字可以用来区分不同的方向键。
			c = getkey();
			c = getkey();
			
			if(c == 65 && *direction != 'D' ){//玩家不可以输入和当前方向完全相反的指令
				*direction = 'U';
			}
			else if(c == 68 && *direction != 'R' ){
				*direction = 'L';
			}
			else if(c == 67 && *direction != 'L' ){
				*direction = 'R';
			}
			else if(c == 66 && *direction != 'U'){
				*direction = 'D';
			}
		}
		//将新的方向传递给snakeMoving,让蛇身按照这个方向移动一次。
		snakeMoving(*direction, *snakeLen);
		//蛇身移动之后
		//检测蛇身是否接触墙或者蛇身自己
		kill(life, *snakeLen);
		//检测是否吃到食物
		isFoodEaten(snakeLen, score);
    }
}

void clearBoard(){
	for(int y = 3; y < 25; y++){
		for(int x = 1; x < 81; x++){
			gotoxy(x,y);
			printf(" ");
		}
	}
}

void printMiddle(char *string, int y){
	gotoxy(  ( COL - strlen(string) ) / 2, y);
}



int main(void){
	srand(time(NULL));

	printMiddle("Enter your username to keep your record", 12);
	printf("\e[33mEnter your username to keep your record\n\e[0m");
	

	char c;
	int i = 0;
	for(; i < maxUsername - 1 && (c = getchar()) != '\n'; i++){
		player.username[i] = c;
	}
	player.username[i] = '\0';
	
	Sleep(500);
	clearBoard();
	
	//初始化界面和蛇身
	char direction = 'D';
	int snakeLen = 4, life = 3, score = 0;
	initialize(score, life, snakeLen);
	
	//游戏开始
	showFood(snakeLen);
	kbRespond(&direction, &snakeLen, &life, &score);
	
	//当生命值为0时
	clearBoard();
	printMiddle("Game Over!", 8);
	printf("\e[31mGame Over!\e[0m");
	printMiddle("Your Score is: 12", 10);
	printf("\e[31mYour score is: %2d\n\e[0m", score);
	player.Score = score;//存储下来score
	
	
	FILE *players;
	if( ( players = fopen("players", "wt") ) == NULL){//创建一个叫做players的文件,并写入这个文件。
		printf("Error in creating file named players. Exiting the program...");
		return 0;
	}
	fwrite(&player, sizeof(struct playerInfo), 1, players);//写入一个player结构体。
	
	fclose(players);//关闭文件

	
	return 0;
}

感谢阅读!!!

你可能感兴趣的:(c语言,游戏)