大家好,又见面了,我是你们的朋友全栈君。
最近,应学校课程要求,要完成一个C语言课程设计。可以是写一个小游戏,或是写管理系统等。
所以,准备做一个改版贪吃蛇:消灭小虫虫(瞎起的名字 :D)。
之前学过Java,所以学C语言也就比较顺利。而在刚学完C语言刚着手准备做C语言的小游戏时,却发现了一个问题——闪屏。
(我在网上查找了很多关于双缓存,有关的解答很少,更少能够让一个完全不了解的小白一个明白的解释。下面我想和大家分享我使用双缓存完成了小游戏后的总结体会。希望能够一目了然。)
编辑器 —— Dev-C 5.11
先说一下,C语言来做游戏的原理: 就是在控制台打印图案,然后使用 system(“cls”); 来擦除界面,然后再打印图案的循环过程。
闪屏现象
我们正常打印输出内容的时候,是按顺序输出的。从第一个一直打印的最后一个。
当我们输出的内容十分庞大的时候,第一个和最后一个会存在输出时间差。
也就是前面先输出了,而后面你还没看到。所以会有闪屏的现象。
如何解决闪屏?
治标须治本——双缓存技术
何为双缓存?
我希望大家去看看这个网站:猛击这里
这个网站是我理解双缓存的主要网站,何为双缓存,这位作者写得还是比较易懂的。
不过怎么用?怎么能够用在我的C语言小游戏上?还是会让人一头雾水。
(下面只针对双缓存的实现分享我的总结,不对这个游戏的原理做详解。如果有同学想了解贪吃蛇的实现原理可以去看这位笔者:猛击这里 我的消灭小虫虫以及双缓存的学习也有借鉴他。)
Win32 API
#include<windows.h> 头文件引用
双缓存技术主要使用到了Win32 API
用到的函数有:CreateConsoleScreenBuffer、WriteConsoleOutputCharacter、ReadConsoleOutputCharacter、SetConsoleActiveScreenBuffer、SetConsoleCursorInfo
官方API文档:猛击这里
CreateConsoleScreenBuffer
简单来说就是 初始化新缓存,并配置新缓存参数。
代码语言:javascript复制HANDLE WINAPI CreateConsoleScreenBuffer(
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ const SECURITY_ATTRIBUTES *lpSecurityAttributes,
_In_ DWORD dwFlags,
_Reserved_ LPVOID lpScreenBufferData
);
代码语言:javascript复制dwDesiredAccess:控制台缓冲安全与访问权限,可取值:
GENERIC_READ (0x80000000L),读权限
GENERIC_WRITE (0x40000000L),写权限
dwShareMode:共享模式,可取值:
FILE_SHARE_READ:读共享
FILE_SHARE_WRITE:写共享
lpSecurityAttributes:安全属性,NULL
dwFlags:缓冲区类型,仅可选:
CONSOLE_TEXTMODE_BUFFER,控制台文本模式缓冲
lpScreenBufferData:保留,NULL
范例:
代码语言:javascript复制//具体使用范例
hOutBuf = CreateConsoleScreenBuffer(
GENERIC_WRITE, //对控制台屏幕缓冲区的访问
FILE_SHARE_WRITE, //定义缓冲区可共享写权限
NULL,//安全属性默认为NULL
CONSOLE_TEXTMODE_BUFFER,//缓冲区类型,固定参数
NULL
);
//第一个缓存区赋值为hOutBuf,一般是创建两个缓存区(我这命名第二缓存区为:hOutput)
hOutput = CreateConsoleScreenBuffer(
GENERIC_WRITE, //对控制台屏幕缓冲区的访问
FILE_SHARE_WRITE, //定义缓冲区可共享写权限
NULL,//安全属性默认为NULL
CONSOLE_TEXTMODE_BUFFER,//缓冲区类型,固定参数
NULL
);
WriteConsoleOutputCharacter
指定一个缓存区,将需要输出的内容(这规定的类型是字符数组)输出到控制台。
代码语言:javascript复制BOOL WINAPI WriteConsoleOutputCharacter(
_In_ HANDLE hConsoleOutput, //控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_WRITE访问权限。
_In_ LPCTSTR lpCharacter, //写入的字符数组指针
_In_ DWORD nLength, //写入的长度
_In_ COORD dwWriteCoord, //写入起始坐标, 一个COORD结构(后面讲)
_Out_ LPDWORD lpNumberOfCharsWritten //指向变量的指针,该变量接收实际写入的字符数。
);
范例:
代码语言:javascript复制char score_char1[] = "012345678901234567890123456789";
coord.Y = 1;//第一行位置输出
WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );
//之前全局变量定义了: COORD coord = {0,0}; DWORD bytes = 0;
COORD:
代码语言:javascript复制typedef struct _COORD {
SHORT X; // 横坐标
SHORT Y; // 纵坐标
} COORD;
//使用范例
COORD coord = {0,0};
ReadConsoleOutputCharacter
指定缓存区,读取控制台内容输出到字符数组。
用法和WriteConsoleOutputCharacterA相同,不做范例。
SetConsoleActiveScreenBuffer
双缓存,顾名思义就是有两个缓存。那么这个函数就是用来切换两个缓存的。
代码语言:javascript复制//设置控制台活动显示缓冲
BOOL WINAPI SetConsoleActiveScreenBuffer(
_In_ HANDLE hConsoleOutput //hConsoleOutput:控制台输出设备句柄
);
范例:
代码语言:javascript复制SetConsoleActiveScreenBuffer(hOutBuf);//设置hOutBuf为活动显示的缓冲区
//*...这里是设置不同缓存区的内容等操作的代码...*//
SetConsoleActiveScreenBuffer(hOutput);//设置hOutput为活动显示的缓冲区,即实现了切换缓冲区
SetConsoleCursorInfo
这是一个设置光标的函数:大小,可见度。
代码语言:javascript复制BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput, //控制台输出设备句柄
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo //光标信息(大小、可见性)
);
范例:
代码语言:javascript复制//隐藏两个缓冲区的光标
CONSOLE_CURSOR_INFO cci;
cci.bVisible = 0; // 可见度
cci.dwSize =1;// 大小
SetConsoleCursorInfo(hOutput, &cci);
SetConsoleCursorInfo(hOutBuf, &cci);
/*注: 这里的CONSOLE_CURSOR_INFO结构体如下:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;// 光标百分比厚度(1~100)
BOOL bVisible;// 可见性 FALSE,0,不可见;TRUE,1,可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO; */
总体代码:
代码语言:javascript复制#include<stdio.h>
#include<windows.h>
HANDLE hOutput,hOutBuf; //控制台屏幕缓冲区句柄
HANDLE houtpoint;
COORD coord = {5,0};//初始输出位置
DWORD bytes = 0;
int hop_flag = 0; //通过指针轮流指向两个缓冲区,实现双缓冲
void printPic();
int main(){
hOutBuf = CreateConsoleScreenBuffer(
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
hOutput = CreateConsoleScreenBuffer(
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
while(1){
printPic();
Sleep(600);
}
}
void printPic(){
hop_flag = !hop_flag;
if(!hop_flag){
char score_char1[] = "这是一个缓存区显示内容!11111111";
coord.Y = 1;
WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );
SetConsoleActiveScreenBuffer(hOutBuf);
}else{
char score_char2[] = "这是另一个缓存区显示内容!22222222";
coord.Y = 1;
WriteConsoleOutputCharacter( hOutput, score_char2, strlen(score_char2), coord, &bytes );
SetConsoleActiveScreenBuffer(hOutput);
}
}
运行结果:
代码语言:javascript复制WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );
在这里,输出的是字符数组score_char1,用strlen()获得字符数组长度。当然这个要看你想要输出的长度。如果我改成:strlen(score_char1)-10
代码语言:javascript复制WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1)-10, coord, &bytes );
那结果是这样的:
还有这里我定义了COORD coord = {5,0};也就是初始输出点是<5,0>,又因为coord.Y = 1;所以最后coord = {5,1}
在上面输出结果中,我们还能看到有光标在闪动,如果是做游戏的话,这个光标是很碍眼的。所以就可以用我上面提到过的SetConsoleCursorInfo来隐藏光标。
以上我们用的还是一维数组,只输出一行内容。当然我们可以使用二维数组,直接循环输出以二维数组横坐标和纵坐标大小的面。如下图:
主要代码:
代码语言:javascript复制……
#define _Y 15 //15行
#define _X 20 // 20列
char data[_Y][_X];//这是全局变量定义的字符数组
……
int main(){
……//这里的代码不变,和上面一样
}
void printPic(){
int i,j;
hop_flag = !hop_flag;
if(!hop_flag){ //这里是每次交替,直接把hOutput或hOutBuf赋给houtpoint
houtpoint = hOutput;
}else{
houtpoint = hOutBuf;
}
for(i = 0;i < _Y;i ){ //打印你需要的二维数组图案
for(j = 0;j < _X;j ){
if(i == 0|| i == _Y-1 || j == 0 || j == _X-1){
data[i][j] = '*';
}else{
data[i][j] = ' ';
}
}
}
coord.Y = 1;
for(i = 0;i < _Y;i ){ //循环打印每一行
coord.Y ; //每次都打印到下一行
WriteConsoleOutputCharacter( houtpoint, data[i], _X, coord, &bytes );
} //data[i]:每行的地址。 _X: 每行的长度
SetConsoleActiveScreenBuffer(houtpoint);
}
动态更新数值:
主要代码:
代码语言:javascript复制int key = 0;//计数器
……
int main(){
……//这里的代码不变,和上面一样
}
void printPic(){
hop_flag = !hop_flag;
if(!hop_flag){
houtpoint = hOutBuf;
}else{
houtpoint = hOutput;
}
key ;
char score_char1[] = "Score:";
char score_char2[10];
itoa(key,score_char2,10);//将整型key转换成字符串,存入score_char2,10为十进制转换
strcat(score_char1,score_char2);//合并两个字符数组
coord.Y = 1;
for(int i=0;i<20;i ){//这里循环只是为让大家能看出真的不闪屏
coord.Y ;
WriteConsoleOutputCharacter( houtpoint, score_char1, strlen(score_char1), coord, &bytes );
}
SetConsoleActiveScreenBuffer(houtpoint);
}
看了这么多我相信你们也可以使用C语言写出一个小游戏咯~
在这也感谢其他博主的经验,希望大家一起加油~
如有错误之处,虚心接受~