用c語言+單向鏈表實現一個貪喫蛇

一、效果:



二、實現步驟:(我寫代碼是就是按着下面的步驟一步步實現的,順帶在紙上畫一畫思路)


三、功能:
1.按上下左右方向鍵運動
2.按+或-加速或減速
3.撞牆或咬到蛇身時遊戲失敗
4.記錄喫食物的數量,即得分

四、難點:如何實現蛇身的移動
在while循環裏設置個定時器(Sleep函數),這樣每隔0.5秒程序執行一次,實現蛇身的移動。

一般思路:蛇身移動會遇到兩種情況:
1.蛇頭的下一個節點是食物 :喫掉食物,不釋放尾節點內存,重新生成食物
2.蛇頭的下一個節點不是食物:在蛇頭malloc內存生成一個新節點,將蛇尾節點的內存free掉

我的思路:將食物節點在棧分配內存,重新生成食物只是修改食物節點的x/y值,而蛇身節點是動態分配內存的。

那麼實現思路是:
每次循環時,把蛇頭第一個節點的x/y座標相應+1,並用頭插法在蛇頭第一個節點後再分配內存插入一個節點。
將蛇頭第一個節點的x/y座標與食物節點比較
1.如果判斷下一個節點是食物,不釋放尾節點內存,重新生成食物。
2.如果判斷下一個節點不是食物,釋放蛇最後一個節點。

插入新節點圖解:


五、代碼如下(編譯環境:vs2017):
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
#include <time.h>
#include <stdlib.h>

//蛇的狀態,U:上 ;D:下;L:左 R:右
#define U 1
#define D 2
#define L 3 
#define R 4 

#define EMPTY 0
#define BIT_SELF 1 //咬到蛇身
#define TOUCH_WALL 2 //碰到牆

#define TRUE 1
#define FALSE 0 

typedef struct SNAKE //蛇身的一個節點 
{
	int x;
	int y;
	struct SNAKE *next;
}snake;

//head用於指向蛇的第一個節點,標識整條蛇,但不屬於蛇的節點
snake head, *temp_ptr, food_node;

//direction						鍵入方向
//game_over_reason				記錄遊戲失敗原因
//speed							移動速度
int direction = R, game_over_reason = EMPTY,  speed = 100;

void locateAndPrint(int x, int y);						//在光標位置輸出方塊
void locateAndClear(int x, int y);						//在光標位置清除方塊
void creatMap();										//創建地圖
void createFood();										//創建食物
void intSnake(int snake_len, int start_x, int start_y); //初始化蛇身
void endGame();											//結束遊戲
void getEnteredDirection();								//獲取鍵入的方向
void snakeMove();										//蛇移動
void startGame();										//遊戲循環

/*在光標位置輸出方塊*/
void locateAndPrint(int x, int y)
{
	//定位
	COORD pos;
	HANDLE hOutput;
	pos.X = x;
	pos.Y = y;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);

	//輸出
	printf("■");
}

/*在光標位置清除方塊*/
void locateAndClear(int x, int y)
{
	//定位
	COORD pos;
	HANDLE hOutput;
	pos.X = x;
	pos.Y = y;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);

	//輸出
	printf(" ");
}

/*創建地圖*/
void creatMap()
{
	//設定黑窗的大小
	system("mode con cols=100 lines=30");

	//注意這裏x+2 y+1,在黑框中就是這樣約定的,沒辦法
	int i;
	for (i = 0; i < 58; i += 2)//打印上下邊框
	{
		locateAndPrint(i, 0);
		locateAndPrint(i, 26);
	}
	for (i = 1; i < 26; i++)//打印左右邊框
	{
		locateAndPrint(0, i);
		locateAndPrint(56, i);
	}
}

/*創建食物*/
void createFood()
{
	//創建食物成功,跳出循環
	int create_food_success_flag = TRUE;

	//如果創建不成功,則重新重建
	while (1) {
		int rand_x, rand_y;
		do {
			//x隨機數 2-55  y隨機數 3-23
			srand((int)time(NULL));
			rand_x = rand() % 53 + 2;
			rand_y = rand() % 21 + 3;
		} while (rand_x % 2 != 0);//x需要是2的偶數,黑框中x和y的方塊有侷限性

		//判斷生成的食物是否跟蛇身重疊
		temp_ptr = head.next;
		while (temp_ptr != NULL) {
			if (temp_ptr->x == rand_x && temp_ptr->y == rand_y) {
				//重疊,需要重新創建
				create_food_success_flag = FALSE;
				break;
			}
			temp_ptr = temp_ptr->next;
		}

		//food的座標跟蛇身重疊,重新生成food_node
		if (FALSE == create_food_success_flag) {
			continue;
		}

		//創建成功,在黑框中打印
		create_food_success_flag = TRUE;
		food_node.x = rand_x;
		food_node.y = rand_y;
		locateAndPrint(rand_x, rand_y);

		break;
	}
}

/*初始化蛇身*/
void intSnake(int snake_len, int start_x, int start_y) {

	for (int i = 0; i < snake_len; i++) {
		temp_ptr = (snake *)malloc(sizeof(snake));
		temp_ptr->x = start_x;
		temp_ptr->y = start_y;
		start_x += 2;
		//頭插法
		temp_ptr->next = head.next;
		head.next = temp_ptr;
		//輸出初始蛇身
		locateAndPrint((head.next)->x, (head.next)->y);
	}
}

/*遊戲結束*/
void endGame()
{
	//記錄得分
	int snake_length = 0;

	//釋放蛇身 malloc分配的資源
	snake * temp_ptr2;//這裏爲了思路清晰,引入第三個變量temp_ptr2,輔助變量temp_ptr2可以用head.next來替代
	temp_ptr = head.next;
	while (temp_ptr != NULL) {
		temp_ptr2 = temp_ptr->next;
		free(temp_ptr);
		temp_ptr = temp_ptr2;
		snake_length++;
	}

	//清屏並輸出遊戲失敗原因
	system("cls");

	if (BIT_SELF == game_over_reason) {

		printf("蛇頭與蛇身相碰,失敗!\n得分:%d\n", snake_length - 9);
	}
	else if (TOUCH_WALL == game_over_reason) {
		printf("碰到牆壁,失敗!\n得分:%d\n", snake_length - 9);

	}

	getchar();
}

/*獲取鍵入的方向*/
void getEnteredDirection()
{

	if (GetAsyncKeyState(VK_OEM_PLUS))
	{
		speed -= 25;
	}
	else if (GetAsyncKeyState(VK_OEM_MINUS))
	{
		speed += 25;
	}
	else if (GetAsyncKeyState(VK_UP) && direction != D)
	{
		direction = U;
	}
	else if (GetAsyncKeyState(VK_DOWN) && direction != U)
	{
		direction = D;
	}
	else if (GetAsyncKeyState(VK_LEFT) && direction != R)
	{
		direction = L;
	}
	else if (GetAsyncKeyState(VK_RIGHT) && direction != L)
	{
		direction = R;
	}
}

/*蛇移動*/
void snakeMove()
{
	//把原始的head.next的座標值存起來
	int temp_x = head.next->x, temp_y = head.next->y;

	//判斷方向,修改第一個節點的值
	if (direction == R)
	{
		head.next->x += 2;
	}
	else if (direction == L) {
		head.next->x -= 2;
	}
	else if (direction == U) {
		head.next->y -= 1;
	}
	else if (direction == D) {
		head.next->y += 1;
	}

	//case1.下一節點爲食物: 蛇身第一節點跟食物節點重合,在蛇身第二個節點新建節點
	//case2.下一節點不爲食物:在蛇身第二個節點處新建節點
	//故可合併
	snake * temp = (snake *)malloc(sizeof(snake));
	temp->x = temp_x;
	temp->y = temp_y;
	temp->next = head.next->next;
	head.next->next = temp;

	//判斷是否撞牆
	if (head.next->x >= 58 || head.next->x <= 0 || head.next->y <= 0 || head.next->y >= 26) {
		game_over_reason = BIT_SELF;
		endGame();
	}

	//判斷下一個節點是否食物
	if (
		food_node.x == head.next->x &&
		food_node.y == head.next->y
		) {
		//下一個節點是食物
		//重新新建食物
		createFood();
	}
	else {
		//下一個節點不是食物,則移動蛇身

		//在蛇前進的第一個位置打印方塊
		locateAndPrint(head.next->x, head.next->y);
		
		temp_ptr = head.next;
		while (temp_ptr->next->next != NULL) {

			//判斷是否咬到自己
			if (
				temp_ptr->next->next != NULL &&
				temp_ptr->next->next->x == (head.next)->x &&
				temp_ptr->next->next->y == (head.next)->y
				) {
				game_over_reason = BIT_SELF;
				endGame();
			}

			temp_ptr = temp_ptr->next;
		}

		//清除蛇尾的節點
		locateAndClear(temp_ptr->next->x, temp_ptr->next->y);
		free(temp_ptr->next);//釋放蛇尾節點內存
		temp_ptr->next = NULL;
	}
}

/*遊戲循環*/
void startGame()
{
	//初始方向,默認方向
	direction = R;

	while (1) {
		//判斷鍵盤輸入的方向鍵,但注意“移動方向爲上時按下鍵不起作用”,左右方向同理
		getEnteredDirection();

		//顯示蛇身
		snakeMove();//蛇身移動

		Sleep(speed);
	}
}
 
int main()
{
	//創建地圖
	creatMap();

	//初始化蛇身
	intSnake(8, 4, 3);

	//創建食物
	createFood();

	//開始遊戲 循環不停的判斷鍵入的方向
	startGame();

	return 0;
}

六、總結:
寫代碼不是一步到位的。
我寫貪喫蛇經歷了幾個步驟:
1.第一次是每次蛇身移動就改變所有蛇身節點的座標,並清屏,重新輸出蛇身所有節點。這樣的問題就是界面一直閃爍。
2.第二次是每次蛇身移動分下一節點是否食物寫兩大段代碼。針對兩種情況。
3.發現第二次的兩種情況的代碼可以在第一個頭結點後插入新節點進行合併,纔有現在的版本。
4....可能以後會有其他改進
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章