当前位置:   article > 正文

C语言实现贪吃蛇【完整版】_贪吃蛇代码c语言简单

贪吃蛇代码c语言简单

贪吃蛇

文章目录

  • 贪吃蛇
    • 使用到的WIN32一些接口简单介绍
      • 控制台窗口大小
      • 隐藏光标
      • 控制光标的位置
      • 获取键盘的值的情况
      • 字符问题
    • 游戏逻辑
      • 开始游戏
        • 打印地图
        • 初始化贪吃蛇
        • 创建食物
      • 运行游戏
        • 控制蛇的移动
      • 运行结束


贪吃蛇实现出来的效果如下:

贪吃蛇小游戏录屏

完整代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

#define POS_X 24
#define POS_Y 5

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

enum DIRECTION//蛇头方向
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

enum GAEM_STATUS//蛇运行状态
{
	RUNNING,//运行状态
	EXIT_NORMAL,//正常退出状态
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//撞到自己
};

typedef struct SnakeNode
{
	//位置坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

typedef struct Snake
{
	pSnakeNode _psnake;//贪吃蛇头结点
	pSnakeNode _pFood;//指向食物的节点

	int _score;//目前得分情况
	int _FoodWeight;//一个食物的分数
	int _SleepTime;//定义休眠时间

	enum DIRECTION _Dir;//蛇的方向

	enum GAEM_STATUS _Status;//游戏状态
}Snake, * pSnake;

int SetPos(short x, short y)//重定位光标位置
{
	COORD pos = { x, y };
	HANDLE output = NULL;
	//获取标准输出的句柄,表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上的光标位置为pos位置
	SetConsoleCursorPosition(output, pos);
	return 1;
}

void StartUI()
{
	//定位光标
	SetPos(36, 13);
	printf("Welcome to Snacks Game!\n");

	SetPos(36, 25);//重定位终端提示符,把终端提示信息放在上面那句话下面
	system("pause");
	system("cls");

	SetPos(36, 13);
	printf("using ↑,↓,←,→,\
	\n\t\t\tControl the direction in which the snake moves,\
	\n\t\t\t\tF3 is hasten, F4 is decelerate");

	SetPos(36, 25);
	system("pause");
	system("cls");
	return;
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;

	for (int i = 0; i < 5; i++)//蛇身五个节点
	{
		cur = (pSnakeNode)malloc(sizeof(pSnakeNode));
		if (cur == NULL)//检测空指针
		{
			perror("InitSnake::alloc()");
			return;
		}
		cur->x = POS_X + 2 * i;//初始化蛇,让蛇身在开始的时候是横着放的,一个蛇身字符是两个字节,所以要乘2
		cur->y = POS_Y;//横着的蛇身,y不用变
		cur->next = NULL;//将指针域置为空

		//节点头插
		if (ps->_psnake == NULL)
		{
			ps->_psnake = cur;
		}
		else
		{
			cur->next = ps->_psnake;//头插向后走
			ps->_psnake = cur;
		}
	}

	//打印蛇身
	cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//设置蛇的各种状态
	ps->_Status = RUNNING;//游戏状态置为运行中
	ps->_score = 0;//游戏分数置为0
	ps->_pFood = NULL;//食物节点先设置为空
	ps->_SleepTime = 200;//刷新时间
	ps->_FoodWeight = 10;//食物重量设置10
	ps->_Dir = RIGHT;//蛇头方向
	return;
}

void CreateMap()
{
	//上
	SetPos(0, 0);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 0; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 0; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

	return;
}

void CreateFood(pSnake ps)
{
	int x = 0, y = 0;

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		if (cur->x == x || cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(pSnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood::alloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	ps->_pFood = pFood;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	return;
}

void GameStart(pSnake ps)
{
	//设置终端窗口大小
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//把光标隐藏
	HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标具体操作(调用微软提供的C接口)
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(output, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;//将光标显示设置为false
	SetConsoleCursorInfo(output, &CursorInfo);//设置控制台光标状态

	//欢迎界面
	StartUI();

	//打印地图
	CreateMap();

	//初始化贪吃蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
	return;
}

void PrintHelpInfo()
{
	SetPos(62, 9);
	printf("1、you can't hit the wall");
	SetPos(62, 10);
	printf("or bite yourself");
	SetPos(62, 12);
	printf("2、using ↑,↓,←,→");
	SetPos(62, 13);
	printf(",Control snake moving");
	SetPos(62, 15);
	printf("3、F3 is hasten, F4 is decelerate");
	SetPos(62, 17);
	printf("4、ESC-Exit Geme, space-time out");
	return;
}

void TimeOut()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_psnake;
	ps->_psnake = pnext;

	//打印蛇
	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	free(ps->_pFood);
	ps->_score += ps->_FoodWeight;
	CreateFood(ps);
	return;
}

void Space(pSnake ps, pSnakeNode pnext)//表示前面位置为空地
{
	//头插
	pnext->next = ps->_psnake;
	ps->_psnake = pnext;

	//打印蛇
	pSnakeNode cur = ps->_psnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	/*pSnakeNode tmp = cur->next;
	free(tmp);*/

	cur->next = NULL;
	return;
}

//是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_psnake->x == 00 ||
		ps->_psnake->x == 56 ||
		ps->_psnake->y == 0 ||
		ps->_psnake->y == 26)
		ps->_Status = KILL_BY_WALL;
	return;
}

//是否咬到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_psnake->next;
	while (cur)
	{
		if (ps->_psnake->x == cur->x && ps->_psnake->y == cur->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		cur = cur->next;
	}
}

void SnakeMove(pSnake ps)
{
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(pSnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove::alloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->_Dir)
	{
	case UP:
		pNext->x = ps->_psnake->x;
		pNext->y = ps->_psnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_psnake->x;
		pNext->y = ps->_psnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_psnake->x - 2;
		pNext->y = ps->_psnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_psnake->x + 2;
		pNext->y = ps->_psnake->y;
		break;
	}

	//判断下个位置是不是食物
	if (NextIsFood(ps, pNext))
	{
		//吃食物
		EatFood(ps, pNext);
	}
	else
	{
		//不是食物
		Space(ps, pNext);
	}

	//是否撞墙
	KillByWall(ps);

	//是否咬到自己
	KillBySelf(ps);
	return;
}

void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(64, 3);
		printf("Score: %05d", ps->_score);
		SetPos(64, 4);
		printf("Every food's score:%2d", ps->_FoodWeight);

		if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = EXIT_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			TimeOut();
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}

		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == RUNNING);//游戏运行时各个信息设置打印等
	return;
}

void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case EXIT_NORMAL:
		printf("Exit the game\n");
		break;
	case KILL_BY_WALL:
		printf("Kill yourself, gameover\n");
		break;
	case KILL_BY_SELF:
		printf("Hit the wall, you are die\n");
		break;
	}
	SetPos(0, 27);

	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		//free(del);
	}
	ps->_psnake = NULL;
	system("pause");
	return;
}

void Test()
{
	char ch = 0;
	do
	{
		Snake snake = { 0 };//创建贪吃蛇对象
		//开始游戏
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//游戏结束
		GameEnd(&snake);

		SetPos(20, 10);
		printf("Another round?(Y/N)\n");
		SetPos(40, 10);
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');

	SetPos(0, 26);
	return;
}

int main()
{
	//设置本地中文字符
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	Test();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491
  • 492
  • 493
  • 494
  • 495
  • 496
  • 497
  • 498
  • 499
  • 500
  • 501
  • 502
  • 503
  • 504
  • 505
  • 506
  • 507
  • 508
  • 509
  • 510

使用到的WIN32一些接口简单介绍


实现过程使用了WIN32的一些API,这里简单介绍一下这些API的功能。

控制台窗口大小

  设置控制台窗口大小,在windows界面的cmd中我们可以输入这样的指令来控制窗口的大小:

mode con cols=100 lines=30 #控制窗口,cols为行长度,lines为列行数
  • 1

打开win的终端输入该指令,就可以调整窗口的大小了,效果如下:

在这里插入图片描述

  命令行窗口的名称也可以通过命令的方式来更改:

title 贪吃蛇#更改命令行窗口的名称
  • 1

同样,打开windows的cmd输入指令,效果如下:

在这里插入图片描述

  在C语言中,我们需要使用system接口来改变终端 窗口的大小 以及 窗口名称,使用system接口需要包含 stdlib.h 头文件,例如下面代码:

#include<stdio.h>
#include<stdlib.h//使用system接口的头文件

int main()
{
	system("title 贪吃蛇");//将命令行窗口的名字更改为需要的名字
	system("mode con cols=100 lines=30");//设置命令行窗口的大小
	//其他操作
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

隐藏光标

  通常,我们的终端也可看作坐标系,左上角为坐标原点,向右为x轴,向下位y轴,如下图所示:

在这里插入图片描述
  我们在windows窗口上描述一个坐标需要使用一个windows API中定义的一个结构体 COORD,表示一个字符在控制台屏幕缓冲区上的坐标,在C语言中,我们需要包含 windows.h 头文件才能使用,使用实例如下:

#include<stdio.h>
#include<windows.h>//调用该api需要的头文件
#include<stdlib.h>

int main()
{
	COORD pos = { 20, 20 };//使用第一个参数为行,第二参数为列
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  实现光标隐藏,我们需要先调用 GetStdHandle 函数来获取标准输出句柄(什么是句柄可以看这个blogger的文章:戳我跳转),使用这个句柄可以操作设备。

HANDLE output = NULL;//HANDLE为结构体指针类型
//获取标准输出句柄来表示不同设备的数值
output = GetStdHandle(STD_OUTPUT_HANDLE);
  • 1
  • 2
  • 3

  要隐藏光标,我们就先要获得一个光标信息,上面我们已经获取了标准输出相关设备的句柄,接下来我们创建 CONSOLE_CORSOR_INFO 结构体对象(接收有关主机光标信息的结构体),再调用 GetConsoleCursorInfo 函数来获得光标信息:

#include<stdio.h>
#include<windows.h>//调用win32 api所需要的头文件

int main()
{
	HANDLE output = NULL;
	//获取标准输出句柄来表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);

	CONSOLE_CURSOR_INFO cursor_info;
	GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

CONSOLE_CURSOR_INFO这个结构体包含了控制台光标信息:

typedef struct _CONSOLE_CURSOLE_INFO {
	DWORD dwSize;
	BOOL bVisible;
}CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • 1
  • 2
  • 3
  • 4
  • dwSize 参数,由光标填充的字符单元格的百分比。值范围为1到100。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
  • bVisible 参数,设置光标的可见性,如果光标不可见,设置为false。

  我们调用结构体的第二个参数设置为false(C语言要包含 stdbool.h 头文件才能使用布尔类型),然后再调用 SetConsoleCursorInfo 函数来设置更改的光标信息。

#include<stdio.h>
#include<stdbool.h>
#include<windows.h>

int main()
{
	HANDLE output = NULL;
	//获取标准输出句柄来表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);

	CONSOLE_CURSOR_INFO cursor_info;
	GetConsoleCursorInfo(output, &cursor_info);//获取光标的信息
	cursor_info.bVisible = false;

	SetConsoleCursorInfo(output, &cursor_info);//设置更改信息
	int ch = getchar();
	putchar(ch);
	
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

使用getchar putchar来输入输出信息,检测是否隐藏光标成功:

在这里插入图片描述

控制光标的位置

  设置终端光标输出位置,我们首先要获取想要输出位置的坐标,上面我们介绍了COORD结构体,用来设置位置坐标。获取完坐标之后,我们可以调用 SetConsoleCorsorPosition 函数将光标位置设置到获取的坐标位置。

BOOL SetConsoleCorsorPosition{
	HANDLE output;//句柄
	COORD pos;//位置
};
  • 1
  • 2
  • 3
  • 4

有了这个接口我们就可以将光标输出的信息放在想要的位置上了:

#include<stdio.h>
#include<stdbool.h>
#include<windows.h>

int main()
{
	HANDLE output = NULL;
	//获取标准输出句柄来表示不同设备的数值
	output = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { 20, 20 };
	SetConsoleCursorPosition(output, pos);

	int ch = getchar();
	putchar(ch);
	
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

效果如下:

在这里插入图片描述
  向上面这样写未免有些麻烦,我们可能会多次改变光标的输出位置,因此我们不妨把其封装成一个函数,使其一条语句也能完成光标定位:

void SetPos(int x, int y)
{
	HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { 20, 20 };
	SetConsoleCursorPosition(output, pos);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样我们在定位光标的时候就简单多了:

#include<stdio.h>
#include<stdbool.h>
#include<windows.h>

void SetPos(int x, int y)
{
	HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { 20, 20 };
	SetConsoleCursorPosition(output, pos);
}

int main()
{
	SetPos(20, 20);
	int ch = getchar();
	putchar(ch);
	
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

执行结果如下:

在这里插入图片描述
这样就能实现控制光标输出位置了。

获取键盘的值的情况

  完贪吃蛇我们一定需要用键盘来控制一些功能,我们可以使用 GetAsyncKeyState 函数来获取按键情况,此函数函数原型如下:

SHORT GetAsyncKeyState(int vKey);
  • 1

  将键盘上的键值传给函数,通过函数返回值来判断按键的状态。GetAsyncKeyState 返回值是short类型,在上一次调用此函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1,则说明该按键被按过,否则位0。

  如果我们要判断按键是否被按过,只需要判断返回值最低值是否为1即可,我们可以按位与上0x1来获取最低位的值,那么我们就可这样来编写函数:

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//返回1表示按过,返回0表示没有按过
  • 1

我们可以通过虚拟键码(虚拟键码:戳我查看)来判断是不同按键的不同状态,这样就可以实现一些按键响应的功能了。

字符问题

  我们在打印蛇身和墙体的时候,是需要特殊字符——宽字符 宽字符的长度为2字节,因为不同地区的语言不同,计算机中描述的方式也不太一样,普通的单字节字符并不适合我们的地区,因此C语言加入了宽字符(字符类型wchar_t 需要包含 locale.h 头文件)允许程序员针对特定地区调整程序行为函数。

类项: 通过修改地区,程序可以改变它的行为来适应世界的不同区域。但是地区改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的,所以C语言针对不同类型的类项进行修改,下面的一个宏指定一个类项:

  • LC_COLLATE:影响字符串比较函数
  • LC_CTYPE:影响字符处理函数行为
  • LC_MONETARY:影响货币格式
  • LC_NUMERIC:影响printf()函数
  • LC_TIME:影响时间格式
  • LC_ALL针对所有类项修改,将以上所有类别设定为给定的语言环境

  我们使用 setlocale 函数修改类项:

char* setlocale(int category, const char* locale);
  • 1

函数的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。C标准给第二个参数定义了2种可能取值:“C”(正常模式)和“”(本地模式) 在任意程序执行开始,默认隐式调用:

setlocale(LC_ALL, "C");
  • 1

我们需要切换到本地环境输出字符,所以:

setlocale(LC_ALL, " ");//切换为本地环境
  • 1

  我们想要打印宽字符也是与普通打印不同的,宽字符字面量前必须加上L,否则C语言就会将其当为窄字符,且占位符应当为"%lc",和"%ls",才可正常打印宽字符。

#include<stdio.h>
#include<locale.h>

int main()
{
	setlocale(LC_ALL, "");
	wchar_t ch = L'蛇';
	wprintf(L"%lc\n", ch);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

效果如下:

在这里插入图片描述


游戏逻辑

  我们采用链式结构类表示贪吃蛇,所以我们需要一个结构体来描述蛇的节点以及蛇的一些属性等:

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

#define POS_X 24
#define POS_Y 5

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

enum DIRECTION//蛇头方向
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

enum GAEM_STATUS//蛇运行状态
{
	RUNNING,//运行状态
	EXIT_NORMAL,//正常退出状态
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//撞到自己
};

typedef struct SnakeNode //蛇的节点
{
	//位置坐标
	int x;
	int y;
	struct SnakeNode* next;//指针域
}SnakeNode, *pSnakeNode;

typedef struct Snake 
{
	pSnakeNode _psnake;//贪吃蛇头结点
	pSnakeNode _pFood;//指向食物的节点

	int _score;//目前得分情况
	int _FoodWeight;//一个食物的分数
	int _SleepTime;//定义休眠时间

	enum DIRECTION _Dir;//蛇的方向

	enum GAEM_STATUS _Status;//游戏状态
}Snake, *pSnake;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

  描述完蛇的所有情况之后,我们可以把游戏拆分成三部分:游戏开始前,游戏运行时,游戏结束时

void test()
{
	Snake snake = { 0 };//创建贪吃蛇对象
	//开始游戏
	GameStart(&snake);
	//运行游戏
	GameRun(&snake);
	//游戏结束
	GameEnd(&snake);
	return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

开始游戏

  游戏开始的时候我们隐藏光标、设置窗口大小及名称,随后我们打印欢迎界面,打印地图,初始化贪吃蛇以及创建食物等操作。

打印地图

  需要注意的是,打印地图的时候,其实我们终端x轴的密度约是y轴的两倍,也就是说x轴两个单位约等于y轴一个单位,这里我给了一个合适的值:

void CreateMap()
{
	//上
	SetPos(0, 0);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 0; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 0; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

	return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

地图效果:

在这里插入图片描述

初始化贪吃蛇

  我这里初始化采用链表的头插法来进行插入,设置最初蛇的长度为5。节点创建完了之后打印出蛇身,最后再设置蛇的一系列初始状态:

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;

	for (int i = 0; i < 5; i++)//蛇身五个节点
	{
		cur = (pSnakeNode)malloc(sizeof(pSnakeNode));
		if (cur == NULL)//检测空指针
		{
			perror("InitSnake::alloc()");
			return;
		}
		cur->x = POS_X + 2 * i;//初始化蛇,让蛇身在开始的时候是横着放的,一个蛇身字符是两个字节,所以要乘2
		cur->y = POS_Y;//横着的蛇身,y不用变
		cur->next = NULL;//将指针域置为空

		//节点头插
		if (ps->_psnake == NULL)
		{
			ps->_psnake = cur;
		}
		else
		{
			cur->next = ps -> _psnake;//头插向后走
			ps->_psnake = cur;
		}
	}

	//打印蛇身
	cur = ps->_psnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
	//设置蛇的各种状态
	ps->_Status = RUNNING;//游戏状态置为运行中
	ps->_score = 0;//游戏分数置为0
	ps->_pFood = NULL;//食物节点先设置为空
	ps->_SleepTime = 200;//刷新时间
	ps->_FoodWeight = 10;//食物重量设置10
	ps->_Dir = RIGHT;//蛇头方向
	return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
创建食物

  因为蛇是采用链式结构,所以我们的食物也采用节点的方式来存储,首先贪吃蛇的食物是随机刷新的,并且在蛇吃完后才会刷新另一个。而我们地图大小是56 * 27的,食物也是一个宽字符,所以需要保证不能越界,且创建食物时,不能将食物创建在蛇身上:

	int x = 0, y = 0;

again:
	do 
	{
		x = rand() % 53 + 2;//横坐标2-54范围刚好不会越界
		y = rand() % 25 + 1;//纵坐标1-25范围也不会越界
	} while (x % 2 != 0);//食物位置正确打印,保证与蛇在一条线上

	pSnakeNode cur = ps->_psnake;
	while (cur)//保证创建食物不在蛇身上
	{
		if (cur->x == x || cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

  所以这里采用goto还是很合适的,最后再创建出正确的食物节点,将节点赋值给ps的食物节点,在对应位置打印出食物:

在这里插入图片描述

运行游戏

  游戏运行时,首先打印帮助信息,再打印食物的分数信息,然后根据按键按下的状态执行下一步的操作

  这里要注意的是,如果是要控制蛇的方向,如果当前蛇头的位置朝右,那我们就不能向左走,同理,蛇头位置朝上,我们不能朝下走…

  除此之外,还需要判断当前按键是不是退出、暂停、加速、减速等状态,如果对应了状态就做对用的事情,并且这些信息是需要不断刷新的,因此,将其放在循环中在合适不过,当游戏状态为RUNNING时,就一直循环

	do
	{
		SetPos(64, 3);
		printf("Score: %05d", ps->_score);
		SetPos(64, 4);
		printf("Every food's score:%3d", ps->_FoodWeight);

		if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = EXIT_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			TimeOut();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;//刷新时间减少
				ps->_FoodWeight += 2;//食物重量增加
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime < 320)
			{
				ps->_SleepTime += 30;//刷新时间增加
				ps->_FoodWeight -= 2;//食物重量减少
			}
		}

		Sleep(ps->_SleepTime);//休眠时间
		SnakeMove(ps);//控制蛇的移动
	} while (ps -> _Status == RUNNING);//游戏运行时各个信息设置打印等
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
控制蛇的移动

  控制蛇的移动,可以根据按键的状态来对蛇的坐标进行定位,上面我们已经将蛇的_Dir状态置为了对应的宏,再根据这个宏来进行方向选择,将坐标变换。

  如果是向右,就将向右的方向x位置+2(密度原因所以+2),y轴方向不变。同理,向下时y+1,x不变…

  新位置用一个临时节点来接收,因为需要判断下面的位置是不是食物,以及蛇到底是不是撞墙或者咬到自己了,判断食物与死亡比较简单,大家可以看源码来分析,这里就不具体展示了。

void SnakeMove(pSnake ps)
{
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(pSnakeNode));//新节点接收
	if (pNext == NULL)
	{
		perror("SnakeMove::alloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->_Dir)//判断蛇头朝向,以及下一步该如何走
	{
		case UP:
			pNext->x = ps->_psnake->x;
			pNext->y = ps->_psnake->y - 1;
			break;
		case DOWN:
			pNext->x = ps->_psnake->x;
			pNext->y = ps->_psnake->y + 1;
			break;
		case LEFT:
			pNext->x = ps->_psnake->x - 2;
			pNext->y = ps->_psnake->y;
			break;
		case RIGHT:
			pNext->x = ps->_psnake->x + 2;
			pNext->y = ps->_psnake->y;
			break;
	}

	//判断下个位置是不是食物
	if (NextIsFood(ps, pNext))
	{
		//吃食物
		EatFood(ps, pNext);
	}
	else
	{
		//不是食物,为空地
		Space(ps, pNext);
	}
	
	//是否撞墙
	KillByWall(ps);
	//是否咬到自己
	KillBySelf(ps);
	return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

运行结束

  结束的逻辑也比较简单,判断退出状态,如果为正常退出,打印一句提示信息,如果为撞墙或者咬到自己,也各自打印一句提示信息。最后游戏结束,将蛇的链式节点全部释放即可。

void GameEnd(pSnake ps)
{
	SetPos(20, 12);//在合适的位置打印提示信息
	switch (ps->_Status)
	{
	case EXIT_NORMAL:
		printf("Exit the game\n");
		break;
	case KILL_BY_WALL:
		printf("Hit the wall, you are die\n");
		break;
	case KILL_BY_SELF:
		printf("Kill yourself, gameover\n");
		break;
	}
	SetPos(0, 27);//将进程退出信息放到最下面

	pSnakeNode cur = ps->_psnake;
	while (cur)//将蛇的节点全部销毁
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_psnake = NULL;
	system("pause");
	return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

  游戏结束后,我们可以安排是否再来一局,只需要在外层套上循环即可,输入一个提示信息,如果信息正确,则再来一局,否则退出。

void Test()
{
	char ch = 0;
	do
	{
		Snake snake = { 0 };//创建贪吃蛇对象
		//开始游戏
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//游戏结束
		GameEnd(&snake);

		SetPos(20, 10);
		printf("Another round?(Y/N)\n");
		SetPos(40, 10);
		ch = getchar();
		getchar();
	} while (ch == 'Y' || ch == 'y');

	SetPos(0, 26);
	return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

  用单链表的形式写贪吃蛇还是挺简单的,这也可以检测你C语言到底学的扎不扎实,如果我写的有些问题,欢迎各位佬在评论区里指出更正~~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/365873
推荐阅读
相关标签
  

闽ICP备14008679号