关于C语言控制台游戏开发的些许经验

170 阅读4分钟

关于C语言控制台游戏开发的些许经验

最近兴趣使然,开发了一些基于控制台的小游戏框架。这个框架可以实现用户交互和视觉反馈功能,当然这也是最基本的功能。就此来浅聊一下开发经验。立项初期就要明确要开发什么,并且坚定地完成第一版开发,这样很大程度上避免了开发时的迷茫。言归正传,这篇文章主要讲解这个游戏框架。

框架源码

#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <stdbool.h>
#define width 40
#define height 40
#define player 'P'
#define wall '#'
#define air ' '

typedef struct P {
	int x;
	int y;
} Player;

Player p1;
char map[height][width];

int readMap();
void hookUp();
void gotoxy(int x, int y);
void showMap();
void usr_Control();
bool isLegel(int x, int y);
void playerGoto(int x, int y);

int main(void) {
	hookUp();
	while(1) {
		showMap();
		usr_Control();
		Sleep(85);
	}
	return 0;
}
void hookUp() {
	p1.x = width / 2;
	p1.y = height / 2;
//	HANDLE StdHandle = GetStdHandle(STD_OUTPUT_HANDLE);
//	CONSOLE_CURSOR_INFO cci;
//	GetConsoleCursorInfo(StdHandle, &cci);
//	cci.bVisible = 0;
//	SetConsoleCursorInfo(StdHandle, &cci);
	printf("\033[?25l");
	SetConsoleTitle(TEXT("GameFrame"));
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			if (i == p1.y && j == p1.x) {
				map[i][j] = player;
			} else if (i == 0 || i == height - 1 || j == 0 || j == width - 1) {
				map[i][j] = wall;
			} else {
				map[i][j] = air;
			}
		}
	}
}
void gotoxy(int x, int y) {
	COORD cd;
	cd.X = x;
	cd.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cd);
}
void showMap() {
	gotoxy(0, 0);
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			printf("%c ", map[i][j]);
		}
		printf("\n");
	}
}
void usr_Control() {
	if (kbhit()) {
		char temp = getch();
		if (temp == 'W' || temp == 'w') {
			if (isLegel(p1.x, p1.y - 1)) {
				playerGoto(p1.x, p1.y - 1);
			}
		} else if (temp == 'S' || temp == 's') {
			if (isLegel(p1.x, p1.y + 1)) {
				playerGoto(p1.x, p1.y + 1);
			}
		} else if (temp == 'A' || temp == 'a') {
			if (isLegel(p1.x - 1, p1.y)) {
				playerGoto(p1.x - 1, p1.y);
			}
		} else if (temp == 'D' || temp == 'd') {
			if (isLegel(p1.x + 1, p1.y)) {
				playerGoto(p1.x + 1, p1.y);
			}
		}
	}
}
bool isLegel(int x, int y) {
	return map[y][x] != wall;
}
void playerGoto(int x, int y) {
	char temp = map[p1.y][p1.x];
	map[p1.y][p1.x] = map[y][x];
	map[y][x] = temp;
	p1.x = x;
	p1.y = y;
}

屏幕截图 2023-12-12 130132.png

讲解这个程序不妨先从main函数入手,它包含一个HookUp()函数以及一个死循环。

游戏初始化

HookUp()函数是初始化方法,初始了玩家坐标以及临时生成的地图数据。并且将光标设置为隐藏,这里有两个方案:

  • 方案1:使用转译字符设置控制台光标,例如:
printf("\033[?25l"); // 控制台光标隐藏
printf("\033[?25h"); // 控制台光标显示
  • 方案2:设置控制台光标属性,例如:
HANDLE StdHandle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cci;
GetConsoleCursorInfo(StdHandle, &cci);
cci.bVisible = 0;
SetConsoleCursorInfo(StdHandle, &cci);

游戏主死循环

这个循环包含了showMap()显示画面,usr_Control()用户控制以及Sleep()延时函数。在此循环中实现了实时用户交互的功能。

showMap()函数

这个函数主要用于屏幕输出,不过按照一般思路可能会使用system("cls")来清屏这个操作,但实际上我们通过这个方法来清空控制台会导致游戏的频闪问题,严重影响体验。以下是解决方法:

  • 方案1:使用覆写的方法避免频闪,这里首先就要设置控制台光标坐标,例如:

    void gotoxy(int x, int y) {
        COORD coord;
        coord.X = x;
        coord.Y = y;
        SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
    }
    
  • 方案2:手动设置视频缓冲区大小,例如使用setvbuf()函数:

    函数定义:

    int setvbuf(FILE *stream, char *buffer, int mode, size_t size);
    

    参数说明:

    • stream:要设置缓冲区的文件流指针。

    • buffer:指向用户提供的缓冲区的指针,如果为 NULL,则由函数自动分配缓冲区。

      mode:缓冲区类型,有以下三种取值:

      • _IOFBF:完全缓冲,即当缓冲区满时才进行实际的 I/O 操作。
      • _IOLBF:行缓冲,即遇到换行符时进行 I/O 操作。
      • _IONBF:无缓冲,即无缓冲区,直接进行 I/O 操作。
    • size:缓冲区大小(字节数),如果 buffer 不为 NULL,则此参数表示缓冲区大小;如果 buffer 为 NULL,则此参数表示函数自动分配的缓冲区大小。

    函数返回值为 0 表示成功,非零值表示失败。

    setvbuf 可以用于改变文件流的缓冲类型,从而控制 I/O 操作的特性。

    在这里我们进行如下设置:

    setvbuf(stdout, NULL, _IOFBF, 4096);
    

    在使用时:

    system("cls");
    // 你的帧内容
    fflush(stdout);
    Sleep(100);
    

    但在显示复杂画面或者延时过短时会导致显示不全问题

  • 方案3:提高延时时间,但是会导致游戏速度变慢。

usr_Control()用户控制

这个函数完成了用户输入检测以及对玩家的操作,这里需要借助于conio.h库中的kbhit()函数以及getch()函数,kbhit()函数可以完成对键盘是否键入的检测,getch()函数能够自动获取字符相较于getchar()无需输入回车,更符合现代游戏操作方式,以下是一个简单结构:

if(kbhit()) {
    char input = getch();
    if (input == 某个键位) {
        相应键位操作
    }
    .
    .
    .
}

之后就是玩家移动的合法性问题,这个架构地图是基于二堆数组,为了避免数组越界以及提供基本的地图,我们提供了墙这个不可越过方块,并且布置在地图最外围以免数组访问越界,每当移动目标为墙时则为无效移动。

#define wall '#'
#define width 40
#define height 40
char map[height][width];

int isLegel(int x, int y) {
    return map[y][x] != wall;
}

基于这个框架可以实现很多控制台游戏,不妨发挥想象力丰富这个简陋的框架吧