小白小白特别白~~
c语言贪吃蛇其实在网上可以找到很多的资源。但是因为用的是mac,windows.h和conio.h找不到,所以里面很多函数用不了。在网上找了很久,才慢慢写完。在这里记录一下,欢迎感谢各位大佬指点
用的IDE是codelite。游戏是为了在mac的terminal上80x24的界面展示写的。
一些没有实现或者有待改进的地方:
注意!下面的一些函数(例如:gotoxy)是在conio.h的头文件下面的。这个头文件在mac里面是不能直接用的。我是找到了别人写的myconio 然后下载了下来。
效果展示:游戏刚开始的界面,蛇身初始向下
蛇身用了一个结构体数组。假设了蛇身长度不会超过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;
}
}
}
在网上看到过system(“cls”);但是在我的mac上还是不能用。所以自己写了一个清屏的函数
void clearBoard(){
for(int y = 3; y < 25; y++){
for(int x = 1; x < 81; x++){
gotoxy(x,y);
printf(" ");
}
}
}
蛇身按照当前的方向前进。
蛇身前进的原理:
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);
}
随机生成食物坐标,并打印。
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);//如果重合,则重新生成
}
}
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这个函数的时候,如果只写一个延迟的话(不管这个延迟是多长时间),在这个延迟的期间按键,不会被这次的循环识别到,而是下一个循环才能识别到玩家已经按键,并改变方向。从游戏最终实现的角度上来讲,就是按键之后有延迟,蛇身并不会马上改变方向,而是按照当前的方向再运动一格,然后改变方向。
解决的方法就是使用两个延迟,第一个延迟不管有多短,都可以解决这个问题。
在处理游戏界面的时候,居中打印会让界面看起来更整洁一点。
void printMiddle(char *string, int y){
gotoxy( ( COL - strlen(string) ) / 2, y);
}
记录下来玩家的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;
}
感谢阅读!!!