关于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;
}
讲解这个程序不妨先从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;
}
基于这个框架可以实现很多控制台游戏,不妨发挥想象力丰富这个简陋的框架吧