游戏开发工具

C语言非阻塞式键盘监听,用户不输入数据程序也能继续执行

使用scanf()和getchar()处理输入文字很合适,它会停止后续代码执行等待用户输入,直到按回车才继续执行,这种缓冲方式称为阻塞式监听,很多时候我们利用在代码末尾添加getchar()来防止程序马上退出以看到输出结果。

但这种方式对于编写需要实时响应按键的程序简直是灾难,例如用键盘操作的游戏。

我们需要一种随时获取键盘按下状态的非缓冲机制,这个机制称为非阻塞式监听。

在windows中C库给我们准备了三个函数来干这件事情:

kbhit() 查看按键缓冲区中是否有字符,有返回true,没有返回false,非阻塞函数。

getche() 如果按键缓冲区中有字符则取出并返回该字符,没有则等待用户输入,阻塞函数

getch() 如果按键缓冲区中有字符则取出并返回该字符,没有则等待用户输入,该字符不能被显示,阻塞函数

当按下键盘上的按键时,kbhit()并不能阻止字符送入缓冲区,但kbhit()随时可以检测缓冲区是否有内容,getche()和getch()与getchar()一样也是阻塞式的,用于从按键缓冲区取出字符,如果有内容立即取出,如果没有内容则等待用户输入,不同的是输入后不必等到按回车才从缓冲区读取字符,而是立即取出,这就为创造非阻塞式监听提供了方案,请看下面代码:

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

void onKeyDown(char c)
{
    printf("pressedkey=%d\n", c);
    if (c == 27) exit(0);
}
void onKeyUp(char c)
{
    printf("releasedkey=%d\n", c);
}
int main()
{
    char key = 0;
    while (1)
    {
      if (kbhit())
	{
            key = getch();
	    onKeyDown(key);
        }    
        else if(key)
	{
            onKeyUp(key);
            key = 0;
	}
	Sleep(100);
    }
    return 0;
}

测试程序也请使用windows自身的控制台,有些IDE自带的控制台可能会工作不正常,它们跟原生控制台还是有区别的。这段代码在main()函数中使用死循环模拟了一个间隔事件调用程序,这也是基于事件驱动的高级程序的原始实现方式。

在循环体中不断检测按键缓冲区中的内容,当按下一个键盘按键时,输入缓冲区中的字符立即被hbhit()侦测到,接着立刻被getche()取出,然后调用回调函数onKeyDown()触发事件,该事件将按键字符作为事件参数发送。

在事件onKeyDown中传入键入字符的ascii码,如果输入的是ESC键退出程序。

如果不想用死循环可以修改代码如下:

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

void onKeyDown(char c)
{
    printf("\nkey=%d\n", c);
}

int main()
{
    char c=0;

    do
    {
        c = getche();
	onKeyDown(c);
    } 
    while (c != 27);
    return 0;
}

上面代码仍然使用阻塞监听,由于输入字符无需回车,效果与死循环相同,但不能检测到键盘释放事件。非缓冲按键并不是C语言定义的标准,因为有些设备和操作系统并不支持非缓冲输入,kbhit(),getche(),getch()是windows提供的3个函数,不能用于其它操作系统,kbhit()需导入windows库,getche()和getch()需导入conio.h库。


输入密码

当我们在控制台中键入字符时字符会显示出来,如果是密码就会被剽窃,这时可以使用getch()函数来获取输入字符,它不会显示在控制台中。修改上面的代码,在onKeyDown事件中获取字符来验证输入密码,如下:

#include<stdio.h>
#include<windows.h>
#include<conio.h>
#include<string.h>

char password[7] = "123456";
char pwStr[7] = "abcdef";
int i = 0;

void onKeyDown(char c)
{
    if (c == 13) //键入回车键
    {
        if (i == 6 && strcmp(pwStr, password) == 0)//键入密码位数和内容和原密码完全匹配
	{
            printf("\n");
	    printf("密码正确!");
	    exit(0);
	}
	else
	{
	     printf("\n");
             printf("密码错误!请重新输入:");
             i = 0;
	}
    }
    else if (i < 6)
    {
        pwStr[i] = c;
        i++;
    }
    else
    {
        i++;
    }
}

int main()
{
    char c = 0;
    printf("请输入密码:");
    do
    {
        c = getch();
	onKeyDown(c);
    } while (c != 27);
    return 0;
}

getche()和getch()之所以能够用于密码输入还是因为它们是阻塞式监听,只不过不必等到按回车键才返回,因此我们需要自己检测回车键。


键盘事件

使用getche()和getch()获取单个按键的按下和释放可以,但如果需要测试多个按键同时按下就不行了,这种需求包括软件中的组合功能键或游戏中的出招,getche()和getch()按顺序读取缓冲区按键序列的管道方式是不能判断多个键同时按下和释放的,而且对于功能键的支持也不好,因为功能键不一定有对应的字符编码,更重要的是它们仍然是阻塞式监听函数。

为了完全实现非阻塞监听按键,我们需要在任何情况下能直接访问键盘按键状态的函数,此函数不会暂停代码运行也不受任何条件约束,为此Window.h提供了2个函数:

    SHORT GetKeyState(int nVirtKey); 从windows消息队列中取得键盘消息

    SHORT GetAsyncKeyState(int vKey); 是直接侦测键盘的硬件中断

GetAsyncKeyState ()比GetKeyState()更直接,它不必等待windows消息产生,对于不使用事件机制的程序这两个效果相同。

GetKeyState()和GetAsyncKeyState()返回一个short值,它的最高位表示nVirtKey指定的按键是否按下,1表示按下,0表示没有按下,而最低位用于测试那些开关键,如CapsLock,Num是否切换,指示灯亮起为1,熄灭为0。

通过返回结果获取某个键按下有两种方式:

一种是判断值的正负,高位为1表示负值,高位为0表示正数或0;

另一种方式是将结果与0x8000进行&位运算,0x8000的二进制数为1000 0000 0000 0000,因此这是一个掩码操作,只暴露最高位。

第二种方法用的更多,因为可以将掩码操作定义为一个宏来简化代码,例如:

#define IsKeyPressed(nVirtKey) (GetAsyncKeyState(nVirtKey) & 0x8000)

下面代码用于测试上下左右方向键是否按下,可以通过方向组合给出8个方向,按ESC键退出:

#include <stdio.h>
#include <windows.h>
#define IsKeyPressed(nVirtKey) (GetAsyncKeyState(nVirtKey) & 0x8000)
int main(void)
{
    while (1)
    {
        if (IsKeyPressed(VK_ESCAPE)) exit(0);
        else if (IsKeyPressed(VK_UP) && IsKeyPressed(VK_LEFT)) puts(“upleft”);
        else if (IsKeyPressed(VK_UP) && IsKeyPressed(VK_RIGHT)) puts(“upright”);
        else if (IsKeyPressed(VK_DOWN) && IsKeyPressed(VK_LEFT)) puts(“downleft”);
        else if (IsKeyPressed(VK_DOWN) && IsKeyPressed(VK_RIGHT)) puts(“downright”);
        else if (IsKeyPressed(VK_UP) && !IsKeyPressed(VK_DOWN)) puts(“up”);
        else if (IsKeyPressed(VK_DOWN) && !IsKeyPressed(VK_UP)) puts(“down”);
        else if (IsKeyPressed(VK_LEFT) && !IsKeyPressed(VK_RIGHT)) puts(“left”);
        else if (IsKeyPressed(VK_RIGHT) && !IsKeyPressed(VK_LEFT)) puts(“right”);
        Sleep(100);
    }
    return 0;
}

为了方便用户记忆按键代码,Window.h中定义了很多宏,常用按键如下:


鼠标:

VK_LBUTTON             0x01 鼠标左键

VK_RBUTTON             0x02 鼠标右键

VK_CANCEL                0x03 鼠标取消键

VK_MBUTTON            0x04 鼠标中键

VK_XBUTTON1            0x05 鼠标X1键

VK_XBUTTON2            0x06 鼠标X2键


常用功能键:

VK_BACK                    0x08 BACKSPACE 键

VK_TAB                       0x09 TAB 键

VK_RETURN                0x0D ENTER 键

VK_ESCAPE                 0x1B ESC 键

VK_SPACE                   0x20 空格键

VK_LEFT                      0x25 方向左键

VK_UP                         0x26 方向上键

VK_RIGHT                   0x27 方向右键

VK_DOWN                  0x28 方向下键

VK_LWIN                     0x5B 左Windows键

VK_RWIN                    0x5C 右Windows键

VK_LSHIFT                  0xA0 左SHIFT键

VK_RSHIFT                  0xA1 右SHIFT键

VK_LCONTROL            0xA2 左ctrl键

VK_RCONTROL           0xA3 右ctrl键

VK_MENU                   0x12 ALT 键

VK_END                      0x23 END 键

VK_HOME                   0x24 HOME 键

VK_INSERT                  0x2D INS 键

VK_DELETE                  0x2E DEL 键

VK_CAPITAL                0x14 CAPS LOCK 键

VK_NUMLOCK            0x90 NUM LOCK 键


F1 ~ F12为VK_F1~ F12,小键盘数字为VK_NUMPAD1~ VK_NUMPAD9,如果是字母键就输入该字母的大写字符,数字就输入数字字符,例如:

    GetAsyncKeyState(‘A’); 按键盘上的A键

    GetAsyncKeyState(‘1’); 按键盘上的数字1

可以看到除了可以检测键盘按键,还可以检测鼠标按键,鼠标操作适合图形化操作。有了判断方向的代码,就可以以这段代码为基础实现角色在地图上移动的功能,如下:

#include <stdio.h>
#include <windows.h>
#define IsKeyPressed(nVirtKey) (GetAsyncKeyState(nVirtKey) & 0x8000)

#define ROW 28 //最大行数
#define COL 80 //最大列数
char map[ROW * COL]; //地图数据
int lastIndex = 0; //上次位置

//打印地图
void printMap()
{
    system("cls");
    for (int i = 0; i < ROW; i++)
    {
        for (int j = 0; j < COL; j++) putchar(map[i*COL+j]);
	    putchar('\n');
    }
}

void moveUpleft()
{
    int y = lastIndex / COL;
    if (y > 0 && map[lastIndex - COL] == '.')
    {
        map[lastIndex] = '.';
	lastIndex -= COL;
	map[lastIndex] = 'o';
    }
    int x = lastIndex % COL;

    if (x > 0 && map[lastIndex - 1] == '.')
    {
        map[lastIndex] = '.';
	lastIndex--;
	map[lastIndex] = 'o';
	printMap();
    }
    printMap();
}

void moveUpright()
{
    int y = lastIndex / COL;

    if (y > 0 && map[lastIndex - COL] == '.')
    {
        map[lastIndex] = '.';
	lastIndex -= COL;
	map[lastIndex] = 'o';
    }
    int x = lastIndex % COL;

    if (x < COL - 1 && map[lastIndex + 1] == '.')
    {
        map[lastIndex] = '.';
	lastIndex++;
	map[lastIndex] = 'o';
	printMap();
    }
    printMap();
}

void moveDownleft()
{
    int y = lastIndex / COL;
    if (y < ROW - 1 && map[lastIndex + COL] == '.')
    {
        map[lastIndex] = '.';
	lastIndex += COL;
	map[lastIndex] = 'o';
    }
    int x = lastIndex % COL;

    if (x > 0 && map[lastIndex - 1] == '.')
    {
        map[lastIndex] = '.';
	lastIndex--;
	map[lastIndex] = 'o';
	printMap();
    }
    printMap();
}

void moveDownright()
{
    int y = lastIndex / COL;

    if (y < ROW - 1 && map[lastIndex + COL] == '.')
    {
	map[lastIndex] = '.';
	lastIndex += COL;
	map[lastIndex] = 'o';
    }
    int x = lastIndex % COL;

    if (x < COL - 1 && map[lastIndex + 1] == '.')
    {
        map[lastIndex] = '.';
	lastIndex++;
	map[lastIndex] = 'o';
    }
    printMap();
}

void moveUp()
{
    int y = lastIndex / COL;

    if (y > 0 && map[lastIndex - COL] == '.')
    {
        map[lastIndex] = '.';
	lastIndex -= COL;
	map[lastIndex] = 'o';
	printMap();
    }
}

void moveDown()
{
    int y = lastIndex / COL;

    if (y < ROW - 1 && map[lastIndex + COL] == '.')
    {
        map[lastIndex] = '.';
	lastIndex += COL;
	map[lastIndex] = 'o';
	printMap();
    }
}

void moveLeft()
{
    int x = lastIndex % COL;

    if (x > 0 && map[lastIndex - 1] == '.')
    {
        map[lastIndex] = '.';
	lastIndex--;
	map[lastIndex] = 'o';
	printMap();
    }
}

void moveRight()
{
    int x = lastIndex % COL;

    if (x < COL - 1 && map[lastIndex + 1] == '.')
    {
        map[lastIndex] = '.';
	lastIndex++;
	map[lastIndex] = 'o';
	printMap();
    }
}

int main(void)
{
    int i, j;
    //创建地图
    for (i = 0; i < ROW * COL; i++) map[i] = '.';
    for(i=5;i<12;i++) for (j = 10; j < 30; j++) map[i*COL+j] = '#';
    for(i=16;i<23;i++) for (j = 10; j < 30; j++) map[i*COL+j] = '#';
    for(i=5;i<23;i++) for (j = 50; j < 70; j++) map[i*COL+j] = '#';

    //创建角色
    map[0] = 'o';
    printMap();
    while (1)
    {
	if (IsKeyPressed(VK_ESCAPE)) exit(0);
	else if (IsKeyPressed(VK_UP) && IsKeyPressed(VK_LEFT)) moveUpleft();
	else if (IsKeyPressed(VK_UP) && IsKeyPressed(VK_RIGHT)) moveUpright();
	else if (IsKeyPressed(VK_DOWN) && IsKeyPressed(VK_LEFT)) moveDownleft();
	else if (IsKeyPressed(VK_DOWN) && IsKeyPressed(VK_RIGHT)) moveDownright();
	else if (IsKeyPressed(VK_UP) && !IsKeyPressed(VK_DOWN)) moveUp();
	else if (IsKeyPressed(VK_DOWN) && !IsKeyPressed(VK_UP)) moveDown();
	else if (IsKeyPressed(VK_LEFT) && !IsKeyPressed(VK_RIGHT)) moveLeft();
	else if (IsKeyPressed(VK_RIGHT) && !IsKeyPressed(VK_LEFT)) moveRight();

	Sleep(50);
    }
    return 0;
}

上面代码中我们绘制了一张地图,用’.‘表示道路,用’#'表示障碍物,用’o’表示人物,如图:

1.png

虽然地图是一个二维数组,但可以和一维数组之间进行转换,在移动角色前先要进行判断,角色不能超出地图也不能越过障碍物,每次移动角色后都要重绘整个地图,这使得屏幕出现闪烁,如果用图形化编程就不会遇到这个问题。