(十四)EGE键盘消息

目录

  • 键盘
    • 按键的
    • 一、按键消息
      • 按键消息说明
        • 按键消息类型 msg
        • 按键码 key
        • 辅助键类型 flags
      • 获取字符输入 getch()
      • 获取按键消息 getkey()
    • 二、检测按键状态
    • 三、检测按键消息
      • 检测单个按键消息
      • 如何由按键消息得到按键的状态
        • 通过处理键盘消息来获取字母按键状态示例
      • 只检测按键按下和松开动作
        • 状态设置流程
        • 示例程序
        • 多按键情况
          • 少量按键可使用的方法
          • 较多按键判断
      • 注意事项(初始化)
        • 不进行初始化而直接检测的错误示例
    • 四、清空键盘消息缓存区
    • 五、人物移动示例
      • (1) 普通的按键控制移动
      • (2) 按键控制格子移动
      • (3) 按键控制平滑移动
    • 六. 键盘控制四方向移动写法

键盘

按键的

一、按键消息

按键消息说明

键盘按键按下会触发按键消息

  • 键盘短按和长按是不一样的。按键轻轻按一下,就只会立刻产生一个按键按下消息,松开产生一个按键松开消息。长按时,会在一开始产生一个按键按下消息,一段时间后,开始大量产生按键按下消息,速度很快,松开时,产生一个按键松开消息。
  • 按键按下消息,如下面所示
    短按时
    按键消息:*
    长按时(每秒大概32个)
    按键消息:*   *************

EGE中,产生的键盘消息用下面的 key_msg 结构体保存,包含是什么按键,是按下还是松开,是否同时按下了辅助键的信息。

按键消息结构体

typedef struct key_msg {
    unsigned int msg;		//消息类型
    unsigned int key;		//按键码
    unsigned int flags;		//辅助键标志
}key_msg;

结构体key_msg 有三个成员,分别是按键消息类型,按键码和辅助键标志。

按键消息类型 msg

用来判断按键是按下还是松开
msg 表示消息类型,有以下几个枚举值

key_msg_down 键盘按下消息。
key_msg_up 键盘弹起消息。
key_msg_char 键盘字符输入消息。(测试时没检测到有这种类型的值)

定义如下:

typedef enum key_msg_e {
	key_msg_down    = 1,
	key_msg_up      = 2,
	key_msg_char    = 4,
}key_msg_e;

使用方法
假设keyMsg是key_msg类型的变量

  • 判断是否是按键按下消息
keyMsg.msg == key_msg_down
  • 判断是否是按键松开消息
keyMsg.msg == key_msg_up

来试试按键消息类型的检测, 多个按键按下都是可以的
不用纠结代码意思,做次按键短按,长按,松开的实验

#include 

int main()
{
	initgraph(640, 480, 0);
	setcaption("按键消息类型测试");
	
	setbkcolor(WHITE);
	setcolor(BLACK);

	int keyUpCount = 0, keyDownCount = 0, keyCharCount = 0;

	for (; is_run(); delay_fps(60)) {
		key_msg keyMsg = { 0 };

		while (kbmsg()) {
			keyMsg = getkey();
			switch(keyMsg.msg) {
				case key_msg_up: 	keyUpCount++; 	break;
				case key_msg_down:	keyDownCount++; break;
				case key_msg_char: 	keyCharCount++; break;
			}
		}
		cleardevice();
		xyprintf(100, 300, "down 计数 = %d    up 计数 = %d     char 计数 = %d",
			keyUpCount, keyDownCount, keyCharCount);
	}
	return 0;
}

可以看出,按键长按按键消息是这样的:
(1的时候长按,6的时候松开, 9的时候再长按,15的时候再松开)


消息类型 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
up   ∙ \ \bullet     ∙ \ \bullet  
down   ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet  



按键码 key

用来 判断是来自哪个按键的消息

  如果是按下和弹起的消息,key则表示按键虚拟键码,否则为gbk编码字符消息
  平时用来判断是否是键盘A键的消息, 使用 if (keyMsg.key == ‘A’) 来判断即可。

关于虚拟键码,可以自己查看定义,这是windowsAPI定义的一些宏,对应键盘上的各种按键,一般以 VK_ 开头,如
ESC键: VK_ESCAPE
上方向键:VK_UP
右方向键:VK_RIGHT

虚拟键码数不足256个, 每个键码的值用一个字节即可表示

可以在VS中输入VK_UP, 然后按快捷键F12转到定义,就可以查看各种虚拟键码。

字母键的虚拟键码就等于它的大写字母的ASCII码值

如果是想判断是否是上方向键的消息, if (keyMsg.key == VK_UP)

EGE中也自己定义的一些枚举值,与虚拟键码是相等的, 可以使用ege中的定义, 以 key_ 开头

typedef enum key_code_e {
	//鼠标左右中三键
	key_mouse_l     = 0x01,
	key_mouse_r     = 0x02,
	key_mouse_m     = 0x04,
	
	//退格,Tab,回车键
	key_back        = 0x08,
	key_tab         = 0x09,
	key_enter       = 0x0d,
	
	//辅助键
	key_shift       = 0x10,
	key_control     = 0x11,
	
	key_menu        = 0x12,
	key_pause       = 0x13,
	
	//大写锁定,esc键,空格键
	key_capslock    = 0x14,
	key_esc         = 0x1b,
	key_space       = 0x20,

	//上一页,下一页,行首,行尾
	key_pageup      = 0x21,
	key_pagedown    = 0x22,
	key_home        = 0x23,
	key_end         = 0x24,

	//方向键
	key_left        = 0x25,
	key_up          = 0x26,
	key_right       = 0x27,
	key_down        = 0x28,

	key_print       = 0x2a,
	key_snapshot    = 0x2c,
	
	//插入,删除键
	key_insert      = 0x2d,
	key_delete      = 0x2e,

	//大键盘数字键
	key_0           = 0x30,
	key_1           = 0x31,
	key_2           = 0x32,
	key_3           = 0x33,
	key_4           = 0x34,
	key_5           = 0x35,
	key_6           = 0x36,
	key_7           = 0x37,
	key_8           = 0x38,
	key_9           = 0x39,

	//字母键中的A键和Z键
	key_A           = 0x41,
	key_Z           = 0x5a,

	//windows键
	key_win_l       = 0x5b,
	key_win_r       = 0x5c,

	key_sleep       = 0x5f,

	//小键盘的数字键,就是九个数字围成九宫格那个
	key_num0        = 0x60,
	key_num1        = 0x61,
	key_num2        = 0x62,
	key_num3        = 0x63,
	key_num4        = 0x64,
	key_num5        = 0x65,
	key_num6        = 0x66,
	key_num7        = 0x67,
	key_num8        = 0x68,
	key_num9        = 0x69,
	
	//小键盘的符号键
	//EGE缺少定义,故拿虚拟键码来补充
	//VK_MULTIPLY       0x6A		*
	//VK_ADD            0x6B		+
	//VK_SEPARATOR      0x6C		
	//VK_SUBTRACT       0x6D		-
	//VK_DECIMAL        0x6E		.
	//VK_DIVIDE         0x6F		/

	//这个是键盘上方的12个功能键
	key_f1          = 0x70,
	key_f2          = 0x71,
	key_f3          = 0x72,
	key_f4          = 0x73,
	key_f5          = 0x74,
	key_f6          = 0x75,
	key_f7          = 0x76,
	key_f8          = 0x77,
	key_f9          = 0x78,
	key_f10         = 0x79,
	key_f11         = 0x7a,
	key_f12         = 0x7b,

	//小键盘数字锁
	key_numlock     = 0x90,
	
	key_scrolllock  = 0x91,

	//可能左右两边都有一个
	key_shift_l     = 0xa0,
	key_shift_r     = 0xa1,
	key_control_l   = 0xa2,
	key_control_r   = 0xa3,
	key_menu_l      = 0xa4,
	key_menu_r      = 0xa5,

	//大键盘上的符号键
	key_semicolon   = 0xba,		;	分号
	key_plus        = 0xbb,		+	加号
	key_comma       = 0xbc,		,	逗号
	key_minus       = 0xbd,		-	减号
	key_period      = 0xbe,		.	句号
	key_slash       = 0xbf,		/	右斜杠
	key_tilde       = 0xc0,		`	波浪符(下面的点)
	key_lbrace      = 0xdb,		[	左方
	key_backslash   = 0xdc,		\	反斜杠
	key_rbrace      = 0xdd,		]	右方
	key_quote       = 0xde,		'	引号

	key_ime_process = 0xe5,
}key_code_e;

从上面挑选一些比较常用的键码
方向键:上下左右
key_up, key_down, key_left, key_right
字母键:
大写字母的ASCII值, 如S键, 'S’
大键盘的数字键:
key_数字
键盘右边的小键盘数字键
key_num数字

ESC键
key_esc
空格键
key_space

辅助键类型 flags

按键参数,可能为以下值的组合(位或的方式), 如果没有那就是0了

  • key_flag_shift 表示同时按下了shift
  • key_flag_ctrl 表示同时按下了ctrl
  • key_flag_shift | key_flag_ctrl 表示同时按下了shift 键和ctrl
typedef enum key_flag_e {
	key_flag_shift  = 0x100,
	key_flag_ctrl   = 0x200,
}key_flag_e;

获取字符输入 getch()

如果你只是想判断是哪个按键按下的或者获取输入的字符,这里有种简单的方式,那就是 kbhit()getch() 的组合
  最常用的getch(),这时候程序会暂停,等待用户按下按键,返回值是按键输入的字符的ASCII值或者是功能键的码值。(这个码值不是虚拟键码,如果能用ASCII表示,那就是等于ASCII码,如果不能表示,那么码值大于255)

int ch = getch();

(特别注意)
getch() 返回的值需要用两个字节表示,部分键返回值大于0xFF, 所以不能用char存储,否则很可能被截断,应该用 int

需要注意

  • EGE中的 getch() 会返回一个码,这个码并不是虚拟键码,是输入的字符或者功能键的码值,有些按键能用shift辅助输入两种字符
  • 字母键有大小写 之分,即返回大小写对应的ASCII码
  • 返回的一般是按键输入字符对应的ASCII码。如果是功能键,返回的数值大于255
  • 用ASCII值表示的字符,可以在用 getkey() 获取按键消息后用 getch() 获取字符输入 ,但如果不是用ASCII码表示的字符,无法再获取按键消息后获取到。
  • 鼠标左键双击点击后拖动会产生按键消息,被getch()获取。
  • getch() 获取字符输入后, 无法使用 getkey() 获取到按键的按键消息

  如果不想暂停,可以使用 kbhit() 检测是否有字符输入,如果没有就跳过,有就读取字符,这样就不影响程序运行了。


//判断是否有按键字符输入,有就读取字符。
if (kbhit()) {
	int ch = getch();
	...
}

但是,一般都是用while一次读取出全部的消息


//判断是否有按键字符输入,有就读取字符。
while (kbhit()) {
	int ch = getch();
	...
}

 &emsp因为用 if 的话,如果你帧率比较小,并且按键长按产生了大量的按键消息的话,处理就没那么快了。键盘消息就会堆积起来,有种滞后的感觉。当然,如果你帧率较快,那么单键长按就什么问题。(比较快是多快? 帧率大于34)
  下面来看看滞后的情况,设置成每秒20帧, 按键长按,然后松开,就会看到。

修改 delay_fps() 中的数来改变帧率,看看滞后的情况变化

#include 

int main()
{
	initgraph(640, 480, 0);
	setcaption("按键消息处理滞后");
	setbkcolor(WHITE);
	setcolor(BLACK);

	int keyCount = 0;
	for (; is_run(); delay_fps(20)) {
		key_msg keyMsg = { 0 };

		if (kbhit()) {
			int key = getch();
			keyCount++;
		}
		cleardevice();
		xyprintf(100, 300, "按键计数 = %d ",
			keyCount);
	}
	return 0;
}

所以一般是用while来处理,这样就能很快将键盘消息清空,而不用管帧率是多少
来看看,使用了while来处理,按键长按,即使是10FPS也没事。

绘图的部分不要放在while循环中,因为绘图是需要 控制帧率 的,在里面循环是非常快的,绘图放在里面while循环里面,相当于产生多少消息就绘制多少次,会占用过多的CPU

像按键计数这种不需要控制帧率的是可以放在while循环中

字符输入检测示例:

#include 

int main()
{
	initgraph(640, 480, 0);
	setcaption("按键消息处理");
	setbkcolor(WHITE);
	setcolor(BLACK);

	int keyCount = 0;
	
	for (; is_run(); delay_fps(10)) {
		cleardevice();
		//按键处理
		while (kbhit()) {
			int key = getch();
			keyCount++;
		}
		
		xyprintf(100, 300, "按键计数 = %d ",
			keyCount);
	}
	return 0;
}

按键按下 getch() 到底返回什么,可以通过实验的方式
下面是一段测试代码,按下什么键,就会显示 getch() 返回的码值
如,通过实验,方向键左上右下分别是293, 294, 295, 296

#define SHOW_CONSOLE
#include 
#include 

int main()
{
	initgraph(640, 480,0);
	setbkcolor(WHITE);
	
	for (; is_run(); delay_fps(60)) {
		while (kbhit()) {
			int key = getch();
			printf("%c, %d, %#x\n", key, key, key);
		}
	}

	closegraph();

	return 0;
}

获取按键消息 getkey()

  这个是通过获取鼠标消息结构体的方式,结构体包含了更多的信息。
  kbmsg()getkey() 的组合, kbmsg() 判断有无按键消息,getkey() 则是获取按键消息。
  getkey() 也会暂停程序,等待用户按下按键。getkey() 返回一个key_msg 结构体。
  用法如下所示:

key_msg keyMsg = {0};
while (kbmsg()) {
	keyMsg = getkey();
	这里做简单的按键消息处理,但绘图的代码不应该放这里
}

绘图代码应该放在这里

  鼠标左键双击点击后拖动会产生按键消息,被getkey()获取。

  得到 key_msg 后,就可以读取

  • 消息类型(keyMsg.msg) : 判断是按下还是松开
  • 按键值(keyMsg.key): 判断是什么按键
  • 辅助键标志(keyMsg.flag):判断有没有辅助键同时按下

按键消息检测示例

#include 

int main()
{
	initgraph(640, 480, 0);
	setcaption("按键消息处理");
	setbkcolor(WHITE);
	setcolor(BLACK);

	int keyCount = 0;
	
	for (; is_run(); delay_fps(10)) {
		cleardevice();
		key_msg keyMsg = {0};
		//按键消息处理
		while (kbmsg()) {
			keyMsg = getkey();
			if (keyMsg.key == 'A' && keyMsg.msg == key_msg_down)
				keyCount++;
		}
		
		xyprintf(100, 300, "A键 按下计数 = %d ",
			keyCount);
	}
	return 0;
}

二、检测按键状态

  判断某个按键是否按下。这个不需要处理按键消息,直接获取按键状态。
  按下返回1,没按下返回0, 相当于 true 和 false
  由于键盘电路问题,当某些按键同时按下时,会有无法识别出按键按下松开的现象。例如S键,D键和K键这三个按键,任意两个按下后,另一个按键是检测不出来按没按下的,这是电路问题。

int keystate(int key);

  其中key是虚拟键码, 如果虚拟键码是字母键或数字键(大键盘)的,就是它的字符值(大写)。
  注意,字母键的表示使用大写,如检测A键, 是 keystate(‘A’) , 而不是keystate(‘a’)
  小键盘上的数字键则用其它宏表示, 如数字3, VK_NUMPAD3。也可以使用上面EGE自己定义的 key_code_e 枚举值
  也可以用来获取鼠标按键状态,如鼠标左键码 key_mouse_l

if (keystate(VK_ESCAPE)) {
    // ESC键按下了
}
或者
if (keystate(key_esc) {

}
检测A键
if (keystate('A') {

}

当然,检测按键也可以通过处理按键消息的方式,只不过比较麻烦

三、检测按键消息

检测单个按键消息

  • 判断是否是某个按键按下的消息
if (keyMsg.key == 键码 && keyMsg.msg == key_msg_down)
  • 判断是否是某个按键松开的消息
if (keyMsg.key == 键码 && keyMsg.msg == key_msg_up)

如何由按键消息得到按键的状态

检测按键的状态可以直接由 keystate() 检测,比较方便,但如果你想通过处理按键消息的方式来获得按键状态也是可以的
  为按键设置一个状态变量,初始为up状态。在按键消息处理时,只要检测到 该按键的down 消息,就把状态设为down 状态,若检测到该按键的 up消息,则把该按键的状态设置为 up.
  下面是示例(非完整程序),只适合单键按下,不适合多键同时按键的情况

//按键状态
enum KeyState
{
	KEY_UP = 0,
	KEY_DOWN = 1
};

//记录按键的状态
KeyState keyState = KEY_UP;	

for (; is_run(); delay_fps(60)) {
	key_msg kMsg = { 0 };

	while (kbmsg()) {
		kMsg = getkey();
		keyState = (kMsg.msg == key_msg_down) ? KEY_DOWN : KEY_UP;
	}

	在这里由keyState的值就可得出按键的状态,
	当然,只能是对一个按键来说

}
  • 如果想要正确检测多个按键,比如想检测26个字母键,就可以为每个按键都设置一个状态变量。检测到是对应的按键时,就为该按键设置状态。
//按键状态
enum KeyState
{
	KEY_UP = 0,
	KEY_DOWN = 1
};

//记录26个字母键按键的状态, KEY_UP等于0,只给出第一个初始值,那剩下的都是初始化为0,,正好是KEY_UP
KeyState keyStates[26] = {KEY_UP};	

for (; is_run(); delay_fps(60)) {
	key_msg kMsg = { 0 };

	while (kbmsg()) {
		kMsg = getkey();
		
		int keyId = kMsg.key;
		KeyState state = (kMsg.msg == key_msg_down) ? KEY_DOWN : KEY_UP;
		//如果是字母按键(键码都是用大写表示)
		if ('A' <= keyId && keyId <= 'Z')
			keyStates[keyId - 'A'] = state;
	}

	//这里可以由keyStates得出各个按键的状态
	
}

通过处理键盘消息来获取字母按键状态示例

  键盘由于键盘主板电路问题,并不能识别很多个同时按下的情况,当已经有很多个按键同时按下时,如果有其它的一些按键按下,电路就有可能不能将其识别出来, 比如S, D和K,我键盘上按下其中两个,剩下的一个按下是检测不出来的

#include 

enum KeyState
{
	KEY_UP,
	KEY_DOWN
};

const int SCREEN_WIDTH = 600, SCREEN_HEIGHT = 300;

int main()
{
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, 0);
	setcaption("字母按键状态显示");

	setbkcolor(WHITE);
	setcolor(BLACK);
	setfillcolor(EGERGB(0XFF, 0x80, 0XFF));
	//透明文字背景,不然带色块的
	setfont(20, 0, "仿宋");
	setbkmode(TRANSPARENT);

	KeyState keyStates[26] = { KEY_UP };

	for (; is_run(); delay_fps(60)) {
		key_msg kMsg = { 0 };

		while (kbmsg()) {
			kMsg = getkey();

			int keyId = kMsg.key;
			KeyState state = (kMsg.msg == key_msg_down) ? KEY_DOWN : KEY_UP;
			//如果是字母按键
			if ('A' <= keyId && keyId <= 'Z')
				keyStates[keyId - 'A'] = state;
		}

		/*上面是做位置计算的,下面是根据位置绘图的部分*/

		//清屏
		cleardevice();

		//画按钮状态示意图
		for (int i = 0; i < 26; i++) {
			int x = 100 + i % 10 * 40;
			int y = 100 + i / 10 * 40;

			//按下时画色块
			if (keyStates[i] == KEY_DOWN) {
				bar(x, y, x + 38, y + 38);
			}
			//对应位置写字母
			xyprintf(x + 10, y + 10, "%c", 'A' + i);
		}
	}
		
	return 0;
}

只检测按键按下和松开动作

  上面检测单个按键消息的判断只能单独判断一个键盘消息,但是因为实际上按住不放的时候会发出多个按下的消息
  将上面检测是否是按键按下消息的代码(即 kMsg.msg == key_msg_down ),用于人物移动判断的话,长按按键,人物会突然移动很多,就像上面按键消息计数程序一样,长按按键时,计数会快速增长。

下面是按下松开时键盘发出的 keymsg 消息:
1的时候长按,6的时候松开, 9的时候再长按,15的时候再松开


消息类型 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
up   ∙ \ \bullet     ∙ \ \bullet  
down   ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet     ∙ \ \bullet  

  所以我们可以看出,按键长按时, 过一小段时间后,会快速发出多条键盘按键按下 (down) 的消息,如果你用 kMsg.msg == key_msg_down 判断的话,长按一次按键,会检测到多次按键按下的消息。但检测到按键 松开(up) 的消息时,那就是松开的时刻,因为松开时只会发送一条消息
  所以将上面检测是否是按键按下消息的代码(即 kMsg.msg == key_msg_down ),用于人物移动判断的话,长按按键,人物会突然移动很多
  如果我们只需要按键按下一次人物移动一格,长按也不会继续移动时,可以只检测按键按下和松开的时刻。记录下按键的状态,这样按键按下和松开时状态会变化, 只对状态变化的时刻做出反应 。这样的话,长按时,即使检测到按键按下的消息,但状态没有发生变化,因此不是按键刚按下的时候。

状态设置流程

  1. lastKeyState,curKeyState 记录上次和当前的状态, 初始值必须为 KEY_UP, 否则首次按下时判断失效。
  2. 在按键消息处理循环中, 对消息类型逐个判断,是松开 (up) 的消息就将curKeyState 设置为 KEY_UP, 是按下 (down) 的消息就设置为KEY_DOWN
  3. 按下时刻的判断, 即原本是松开,现在是按下的状态变化时刻。
if (lastKeyState == KEY_UP && curKeyState == KEY_DOWN)
  1. 松开时刻的判断, 即原本是按下,现在是松开的状态变化时刻。
if (lastKeyState == KEY_DOWN && curKeyState == KEY_UP)
  1. 最后不要忘了在所有涉及按键消息判断的代码外加上 lastKeyState = curKeyState,放在按键消息处理判断之前之后都是可以的,。

示例程序

#include 

//按键状态
enum KeyState
{
	KEY_UP,
	KEY_DOWN
};

int main()
{
	initgraph(640, 480, 0);
	setcaption("按键按下松开计数");
	setbkcolor(WHITE);
	setcolor(BLACK);

	//按键按下松开计数
	int keyUpCount = 0, keyDownCount = 0;
	
	//记录按键上一次的状态和现在的状态
	int lastKeyState = KEY_UP;
	int curKeyState = KEY_UP;
		

	for (; is_run(); delay_fps(60)) {
		key_msg kMsg = { 0 };
		//这个不能省
		lastKeyState = curKeyState;
		
		while (kbmsg())
		{
			kMsg = getkey();
			if (kMsg.msg == key_msg_up)
				curKeyState = KEY_UP;
			else if (kMsg.msg == key_msg_down)
				curKeyState = KEY_DOWN;
		}

		//按键按下
		if (lastKeyState == KEY_UP && curKeyState == KEY_DOWN)
			keyDownCount++;
		else if (lastKeyState == KEY_DOWN && curKeyState == KEY_UP)
			keyUpCount++;

		
		cleardevice();
		xyprintf(200, 200, "当前按键状态:%s", (curKeyState == KEY_UP) ? "松开" : "按下");
		xyprintf(200, 300, "down 计数 = %d", keyDownCount);
		xyprintf(200, 330, "up   计数 = %d", keyUpCount);	
	}
		
	return 0;
}

  说明一下,这代码不适用于多按键同时按下的情况。多个按键同时按下并且长按时,如果有一个按键松开,状态显示会突然变成松开,然后又变回按下

多按键情况

如果需要处理多键按下的情况,可以为每个按键分别定义两个状态变量
只要消息处理时,根据 keyMsg.key 判断出是哪个按键,就设置对应按键的状态就行

少量按键可使用的方法

如果需要判断的按键只有几个,比如,判断常用的A, W, D, S四个方向键,则可以按照如下来写(非完整程序

	#define KEY_NUM 4

	//这里设置好各个键的状态变量在数组中的下标
	const int KEY_A_INDEX = 0, KEY_W_INDEX = 1, KEY_D_INDEX = 2, KEY_S_INDEX = 3;
	
	//按键按下计数
	int keyDownCount[KEY_NUM] = { 0 };
	
	//记录按键上一次的状态和现在的状态
	int lastKeyState[KEY_NUM] = { KEY_UP };
	int curKeyState[KEY_NUM] = { KEY_UP };


	for (; is_run(); delay_fps(60)) {
		key_msg kMsg = { 0 };
		
		for (int i = 0; i < KEY_NUM; i++) {
			lastKeyState[i] = curKeyState[i];
		}
		
		while (kbmsg())
		{
			kMsg = getkey();
			
			int key = -1;

			//根据kMsg.key 判断哪个键按下
			switch (kMsg.key) {
			case 'A': key = KEY_A_INDEX; break;
			case 'W': key = KEY_W_INDEX; break;
			case 'D': key = KEY_D_INDEX; break;
			case 'S': key = KEY_S_INDEX; break;
			}
			//如果是四个键其中之一,那么key就会不是-1
			if (key >= 0) {
				if (kMsg.msg == key_msg_up)
					curKeyState[key] = KEY_UP;
				else if (kMsg.msg == key_msg_down)
					curKeyState[key] = KEY_DOWN;

				//不需控制帧率的按键处理等可以放在这里
				keyDownCount[key]++;
			}
		}

		//这里通过状态来判断按下情况, 这里只检测A键
		if (lastKeyState[KEY_A_INDEX] == KEY_UP && curKeyState[KEY_A_INDEX] == KEY_DOWN) {
			这里作A键按下时的绘图处理等
		}
			
		else if (lastKeyState[KEY_A_INDEX] == KEY_DOWN && curKeyState[KEY_A_INDEX] == KEY_UP) {
			这里作A键松开时的绘图处理等
		}

		//如果想遍历的话
		for (int i = 0; i < KEY_NUM; i++) {
			//如果按下
			if (lastKeyState[i] == KEY_UP && curKeyState[i] == KEY_DOWN) {
				
			}
		}
		
较多按键判断
  • 因为键码只用一个字节就可以表示,即小于256,所以可以创建大小为256的状态变量数组。由键码直接对应下标。

  • 对于 lastKeyState == curKeyState, 由于涉及过多的按键,状态变量的改变只会在某个按键按下或松开时,所以可以用一个数组,保存本次循环中有哪些键发出消息(即保存键码),只对这些按键赋值即可,这样可以减少一些操作。 数组大小可以设置为256(虽然不可能有那么多按键在一帧里同时按下)当然,也可以直接将256个状态变量遍历赋值,会多一些操作量,不过应该也没啥问题。

  • 因为按键长按时会发出很多消息,所以不应该直接将本次的按键保存,而应该遍历之前保存的,看有没有这个按键的记录,没有就保存,按下按键的数目 + 1, 有的话就不用记录了在一帧里,同时按下的按键数也就几个,所以这个操作也不是很多

  • 记得在帧循环开始将按下按键数置为 0

#define KEY_NUM 256

//记录按键上一次的状态和现在的状态
int lastKeyState[KEY_NUM] = { KEY_UP };
int curKeyState[KEY_NUM] = { KEY_UP };

//记录按下的按键
int pressKeyStack[256];
int pressKeyCount = 0;

for (; is_run(); delay_fps(60)) {
	key_msg kMsg = { 0 };

	//上次状态转换
	for (int i = 0; i < pressKeyCount; i++) {
		lastKeyState[pressKeyStack[i]] = curKeyState[pressKeyStack[i]];
	}

	pressKeyCount = 0;


	while (kbmsg())
	{
		kMsg = getkey();

		if (kMsg.msg == key_msg_up)
			curKeyState[kMsg.key] = KEY_UP;
		else if (kMsg.msg == key_msg_down)
			curKeyState[kMsg.key] = KEY_DOWN;

		//下面是对按下的按键进行记录
		//对按下的按键进行遍历,看有没有记录
		int i = 0;
		for (i = 0; i < pressKeyCount; i++) {
			if (pressKeyStack[i] == kMsg.key)
				break;
		}
		//i == pressKeyCount 说明没有记录
		if (i == pressKeyCount)
			pressKeyStack[pressKeyCount++] = kMsg.key;
	}

	//这里通过状态来判断按下情况, 判断A键
	if (lastKeyState['A'] == KEY_UP && curKeyState['A'] == KEY_DOWN) {

	}
	else if (lastKeyState['A'] == KEY_DOWN && curKeyState['A'] == KEY_UP) {

	}

	//判断空格键
	if (lastKeyState[key_space] == KEY_UP && curKeyState[key_space] == KEY_DOWN) {

	}
	else if (lastKeyState[key_space] == KEY_DOWN && curKeyState[key_space] == KEY_UP) {

	}
}

注意事项(初始化)

  可以看到上面 keyMsg 是定义在帧循环里面的,这样每次循环,都会初始化一次。如果你定义在外面,在里面进行鼠标处理之前也要进行一次初始化,除非你想要处理上一次遗留下来的消息。因为如果当前没有键盘消息,keyMsg就不会赋值,如果没有初始化,那就是上一次检测留下来的值,这样如果直接对keyMsg进行检测的话就会造成误判

几种初始化方式

  • 定义在里面
for ( ; is_run(); delay_fps()) {
	key_msg keyMsg = {0};
}
  • 定义在外面,里面结构体赋值
key_msg keyMsg = {0};
for ( ; is_run(); delay_fps()) {
	keyMsg = key_msg(0);
}

不进行初始化而直接检测的错误示例

下面是不进行初始化的示例,由于是直接对 keyMsg 进行检测,不初始化会造成误判。

#include 

int main()
{
	initgraph(640, 480, 0);
	setbkcolor(WHITE);
	setcolor(BLACK);

	//按键按下松开计数
	int keyCount = 0;
	xyprintf(200, 200, "按键下计数:%d", keyCount);

	key_msg keyMsg = { 0 };

	for (; is_run(); delay_fps(60)) {
		while (kbmsg()) {
			keyMsg = getkey();
		}

		if (keyMsg.msg == key_msg_down) {
			keyCount++;
			xyprintf(200, 200, "按键下计数:%d", keyCount);
		}
	}

	return 0;
}

下面是进行初始化后的,比较一下

#include 

int main()
{
	initgraph(640, 480, 0);
	setbkcolor(WHITE);
	setcolor(BLACK);

	//按键按下松开计数
	int keyCount = 0;
	xyprintf(200, 200, "按键下计数:%d", keyCount);

	key_msg keyMsg = { 0 };

	for (; is_run(); delay_fps(60)) {
		keyMsg = key_msg{ 0 };	//初始化
		while (kbmsg()) {
			keyMsg = getkey();
		}

		if (keyMsg.msg == key_msg_down) {
			keyCount++;
			xyprintf(200, 200, "按键下计数:%d", keyCount);
		}
	}

	return 0;
}

四、清空键盘消息缓存区

当你觉得缓存区中的键盘消息已经没有用时,可以将这些消息清空。这样下一次就能直接处理到最新的键盘消息了。

清空键盘消息缓存区的函数为

flushkey();

使用后将清空键盘消息缓存区



五、人物移动示例

这里用小球来表示演示

(1) 普通的按键控制移动

会有突然移动很多的问题
短按和长按分别试试

  • 里面的moveCheck() 函数可以直接拿来用,判断上下左右四个方向移动的,传入的分别是按键的键码和dx, dy的引用, 得到的是格子的位置变化。
#include 


void drawGrid();

void moveCheck(int key, int& dx, int& dy);

enum KeyState
{
	KEY_UP,
	KEY_DOWN
};


const int SCREEN_WIDTH = 600, SCREEN_HEIGHT = 600;
const int radius = 20;

int main()
{
	const int COL = SCREEN_WIDTH / (2 * radius);
	const int ROW = SCREEN_HEIGHT / (2 * radius);
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, INIT_RENDERMANUAL);
	setcaption("人物格子移动");

	//为了美观,使用抗锯齿
	ege_enable_aa(true);
	setbkcolor(EGEACOLOR(0XFF, WHITE));
	setcolor(BLACK);
	setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));
	//透明文字背景,不然带色块的
	setbkmode(TRANSPARENT);


	int xCircle = COL / 2, yCircle = ROW / 2;

	for (; is_run(); delay_fps(60)) {

		key_msg kMsg = { 0 };

		while (kbmsg())
		{
			kMsg = getkey();
		}


		int dx = 0, dy = 0;
		//按键按下时
		if (kMsg.msg == key_msg_down) {
			//位置移动监测
			moveCheck(kMsg.key, dx, dy);

			//计算下一个位置在哪
			int xNext = xCircle + dx, yNext = yCircle + dy;
			//对下一个位置做边界检测,在边界内的才移动, 
			if (0 <= xNext && xNext < COL
				&& 0 <= yNext && yNext < ROW) {
				//如果里面有围墙,那么就需要再次判断要走到的位置是否是围墙
				xCircle += dx;
				yCircle += dy;
			}
		}
		/*上面是做位置计算的,下面是根据位置绘图的部分*/
		
		//清屏
		cleardevice();
		//画格子
		drawGrid();

		//高级绘图函数 画填充椭圆,可抗锯齿,这里的四个参数是(left, top, width, height)
		ege_fillellipse(xCircle * 2 * radius, yCircle * 2 * radius, 2 * radius, 2 * radius);

		xyprintf(5, 5, "当前位置:(%d, %d)", xCircle, yCircle);
	}

	return 0;
}

void drawGrid()
{
	for (int i = 0; i < SCREEN_WIDTH; i += 2 * radius) {
		line(i, 0, i, SCREEN_HEIGHT);
	}
	for (int j = 0; j < SCREEN_HEIGHT; j += 2 * radius) {
		line(0, j, SCREEN_WIDTH, j);
	}
}

void moveCheck(int key, int& dx, int& dy)
{
	switch (key) {
	case 'A': case key_left:	//左
		dx = -1; dy = 0;
		break;
	case 'W': case key_up:		//上
		dx = 0; dy = -1;
		break;
	case 'S': case key_down:	//下
		dx = 0; dy = 1;
		break;
	case 'D': case key_right:	//右
		dx = 1; dy = 0;
		break;
	default:					//其他键不移动
		dx = dy = 0;
		break;
	}
}

(2) 按键控制格子移动

按下一次只会移动一格,长按也不会多移动

#include 


void drawGrid();

void moveCheck(int key, int& dx, int& dy);

enum KeyState
{
	KEY_UP,
	KEY_DOWN
};


const int SCREEN_WIDTH = 600, SCREEN_HEIGHT = 600;
const int radius = 20;

int main()
{
	const int COL = SCREEN_WIDTH / (2 * radius);
	const int ROW = SCREEN_HEIGHT / (2 * radius);

	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, INIT_RENDERMANUAL);
	setcaption("人物格子移动");

	//为了美观,使用抗锯齿
	ege_enable_aa(true);
	setbkcolor(EGEACOLOR(0XFF, WHITE));
	setcolor(BLACK);
	setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));
	//透明文字背景,不然带色块的
	setbkmode(TRANSPARENT);


	int xCircle = COL / 2, yCircle = ROW / 2;

	KeyState lastKeyState = KEY_UP;
	KeyState curKeyState = KEY_UP;

	for (; is_run(); delay_fps(60)) {

		key_msg kMsg = { 0 };

		while (kbmsg())
		{
			kMsg = getkey();
			if (kMsg.msg == key_msg_up)
				curKeyState = KEY_UP;
			else if (kMsg.msg == key_msg_down)
				curKeyState = KEY_DOWN;
		}

		int dx = 0, dy = 0;

		//有按键按键按下
		if (lastKeyState == KEY_UP && curKeyState == KEY_DOWN) {

			//根据按键做出移动判断
			moveCheck(kMsg.key, dx, dy);

			//判断一下位置有没有移动,如果是其它键按下的话,位置就没有移动,就不用做位置计算了
			if (dx != 0 || dy != 0) {
				//计算下一个位置在哪
				int xNext = xCircle + dx, yNext = yCircle + dy;
				//对下一个位置做边界检测,在边界内的才移动, 
				if (0 <= xNext && xNext < COL
					&& 0 <= yNext && yNext < ROW) {

					//如果里面有围墙,那么就需要再次判断要走到的位置是否是围墙
					xCircle += dx;
					yCircle += dy;
				}
			}
		}

		/*上面是做位置计算的,下面是根据位置绘图的部分*/


		//清屏
		cleardevice();
		//画格子
		drawGrid();

		//高级绘图函数 画填充椭圆,可抗锯齿,这里的四个参数是(left, top, width, height)
		ege_fillellipse(xCircle * 2 * radius, yCircle * 2 * radius, 2 * radius, 2 * radius);

		xyprintf(5, 5, "当前位置:(%d, %d)", (xCircle - radius) / (2 * radius), (yCircle - radius) / (2 * radius));

		//这个不能省
		lastKeyState = curKeyState;


	}

	return 0;
}

void drawGrid()
{
	for (int i = 0; i < SCREEN_WIDTH; i += 2 * radius) {
		line(i, 0, i, SCREEN_HEIGHT);
	}
	for (int j = 0; j < SCREEN_HEIGHT; j += 2 * radius) {
		line(0, j, SCREEN_WIDTH, j);
	}
}

void moveCheck(int key, int& dx, int& dy)
{
	switch (key) {
	case 'A': case key_left:	//左
		dx = -1; dy = 0;
		break;
	case 'W': case key_up:		//上
		dx = 0; dy = -1;
		break;
	case 'S': case key_down:	//下
		dx = 0; dy = 1;
		break;
	case 'D': case key_right:	//右
		dx = 1; dy = 0;
		break;
	default:					//其他键不移动
		dx = dy = 0;
		break;
	}
}

(3) 按键控制平滑移动

  • 普通的按键移动不均匀,长按时会动一下, 隔一段时间后再连续移动,不够平滑

  • 而格子移动一次只能移动一格,显然不符合要求。

  • 现在这里借助按键当前状态进行按键控制的平滑移动。每一帧都检测当前哪个按键处于按下状态,位置就往哪个方向增,而不用判断是否有按键按下。因为只根据当前状态,所以一个按键只用一个状态变量就够了。

  • 检测按键状态使用 keystate()就可以了,自己使用鼠标消息来处理状态也可以,下面两个程序都将贴出

  • 如果是四方向移动的,只判断出一个按下的方向键就够了,不用判断其它方向。如果是八方向移动的,就把所有按下的方向键的位置增量都加起来。这样,如果左和上同时按下时,会朝左上方向移动。

  • 如果想平滑地拐弯,减速地停止,如同具有惯性,可以设置加速度, 而不是速度dx, dy 突然变化,可以慢慢变化,如本来向左时匀速 dx = -10, dy = 0, 按下上方向键,这时候每一帧 dx的减小一点,dy增加一点, 比如变成 dx = -8, dy = -2, 而不是突然变成dx = 0, dy = -10
    (十四)EGE键盘消息_第1张图片

  • 使用 keystate() 检测按键状态的方式
    这种方式并不需要处理按键消息

 #include 

//窗口大小
const int SCREEN_WIDTH = 600, SCREEN_HEIGHT = 600;

const int radius = 40;		//圆半径大小

int main()
{
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, INIT_RENDERMANUAL);
	setcaption("人物格子移动");

	//为了美观,使用抗锯齿
	ege_enable_aa(true);
	setbkcolor(EGEACOLOR(0XFF, WHITE));
	setcolor(BLACK);
	setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));
	//透明文字背景,不然带色块的
	setbkmode(TRANSPARENT);

	//浮点型,精确位置,
	float xCircle = SCREEN_WIDTH / 2, yCircle = SCREEN_HEIGHT / 2;

	
	//0, 1, 2, 3分别对应方向 左 上 右 下
	int keys[4] = {'A', 'W', 'D', 'S'};
	int directKeys[4] = { key_left, key_up, key_right, key_down };
	//移动速度, 浮点型能任意控制速度快慢,
	float speed = 2.0f;
	float xSpeeds[4] = { -speed, 0, speed, 0 };
	float ySpeeds[4] = { 0,-speed, 0, speed };

	
	for (; is_run(); delay_fps(60)) {
		//根据按键按下状态对位置增量进行累加,可八方向移动
		float xNext = xCircle;
		float yNext = yCircle;

		for (int i = 0; i < 4; i++) {
			if (keystate(keys[i]) || keystate(directKeys[i])) {
				xNext += xSpeeds[i];
				yNext += ySpeeds[i];
			}
		}

		//如果移动了
		if (xNext != xCircle || yNext != yCircle) {
			//检测是否超出边界
			if (radius <= xNext && xNext <= (SCREEN_WIDTH - radius)
				&& radius <= yNext && yNext <= (SCREEN_HEIGHT - radius)) {
				xCircle = xNext;
				yCircle = yNext;
			}
		}

		/*上面是做位置计算的,下面是根据位置绘图的部分*/

		//清屏
		cleardevice();

		//高级绘图函数 画填充椭圆,可抗锯齿.这里的四个参数是(left, top, width, height)
		ege_fillellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);

		xyprintf(5, 5, "当前位置:(%f, %f)", xCircle, yCircle);
	}

	return 0;
}

自己处理按键状态的方式

#include 

enum KeyState
{
	KEY_UP,
	KEY_DOWN
};

//窗口大小
const int SCREEN_WIDTH = 600, SCREEN_HEIGHT = 600;

const int radius = 40;		//圆半径大小

int main()
{
	initgraph(SCREEN_WIDTH, SCREEN_HEIGHT, INIT_RENDERMANUAL);
	setcaption("人物格子移动");

	//为了美观,使用抗锯齿
	ege_enable_aa(true);
	setbkcolor(EGEACOLOR(0XFF, WHITE));
	setcolor(BLACK);
	setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));
	//透明文字背景,不然带色块的
	setbkmode(TRANSPARENT);

	//浮点型,精确位置,
	float xCircle = SCREEN_WIDTH / 2, yCircle = SCREEN_HEIGHT / 2;

	//0:左, 1:上, 2:右, 3:下
	KeyState keysState[4] = { KEY_UP };

	//移动速度, 浮点型能任意控制速度快慢,
	float speed = 2.0f;
	float xSpeeds[4] = {-speed, 0, speed, 0};
	float ySpeeds[4] = {0,-speed, 0, speed };

	for (; is_run(); delay_fps(60)) {

		key_msg kMsg = { 0 };

		while (kbmsg())
		{
			kMsg = getkey();

			//如果是方向键就改变方向键状态
			int keyNum = -1;

			switch (kMsg.key) {
			case 'A': case key_left:	//左
				keyNum = 0;
				break;
			case 'W': case key_up:	//上
				keyNum = 1;
				break;
			case 'D': case key_right:	//右
				keyNum = 2;
				break;
			case 'S': case key_down:	//下
				keyNum = 3;
				break;
			}
			
			//修改对应按键的状态
			if (keyNum >= 0) {
				if (kMsg.msg == key_msg_up)
					keysState[keyNum] = KEY_UP;
				else
					keysState[keyNum] = KEY_DOWN;
			}
		}

		//根据按键按下状态对位置增量进行累加,可八方向移动
		float xNext = xCircle;
		float yNext = yCircle;
		for (int i = 0; i < 4; i++) {
			if (keysState[i] == KEY_DOWN) {
				xNext += xSpeeds[i];
				yNext += ySpeeds[i];
			}
		}

		//如果移动了
		if (xNext != xCircle || yNext != yCircle) {
			//检测是否超出边界
			if (radius <= xNext && xNext <= (SCREEN_WIDTH - radius)
				&& radius <= yNext && yNext <= (SCREEN_HEIGHT - radius)) {
				xCircle = xNext;
				yCircle = yNext;
			}
		}

		/*上面是做位置计算的,下面是根据位置绘图的部分*/

		//清屏
		cleardevice();


		//高级绘图函数 画填充椭圆,可抗锯齿.这里的四个参数是(left, top, width, height)
		ege_fillellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);

		xyprintf(5, 5, "当前位置:(%f, %f)", xCircle, yCircle);
	}

	return 0;
}

六. 键盘控制四方向移动写法

  可以先定义一个方向变量 direction,初始值为-1。在按键处理时,当检测到方向键按下时,则把它赋值为对应的值。然后再检测 direction,如果不为-1,就代表刚才有方向键按下,这时就可以根据值来做不同的变化。

int direction = -1;

//按键消息处理
while (kbmsg()) {
	key_msg keyMsg = getkey();
	if (keyMsg.msg == key_msg_down) {
		switch(keyMsg.key) {
		case 'A': case key_left:	direction = 0;	break;
		case 'W': case key_up:		direction = 1;	break;
		case 'D': case key_right:	direction = 2;	break;
		case 'S': case key_down:	direction = 3;	break;
		}
	}
}

//这里根据direction的值判断,如果不是-1,说明按下了方向键
if (direction != -1) {
	//移动处理
}

  因为四方向就是坐标 (x, y) 的变化, 可以根据四个方向不同的变化定义变化量dx, dy 数组。
  下面下标 0~3 分别对应左, 上,右, 下方向的坐标偏移

int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, -1, 0, 1};

这样,得到方向direction 后,则可以做移动

x += dx[direction];
y += dy[direction];

下面是按键控制四方向格子移动的实例(按下按键)

#include 

#define NUM_GRID 10
#define GRID_WIDTH 50

const int SCR_WIDTH = GRID_WIDTH * NUM_GRID;
const int SCR_HEIGHT = SCR_WIDTH;

int main()
{
	initgraph(SCR_WIDTH, SCR_HEIGHT, 0);
	setbkcolor(WHITE);
	setcolor(BLACK);
	setfillcolor(EGERGB(0XFF, 0X80, 0XFF));
	setfont(14, 0, "楷体");
	setbkmode(TRANSPARENT);
	
	//当前格子位置
	int xGrid = NUM_GRID / 2, yGrid = xGrid;	
	
	//计算绘制位置,绘制
	int x = xGrid * GRID_WIDTH, y = yGrid * GRID_WIDTH;
	bar(x, y, x + GRID_WIDTH, y + GRID_WIDTH);
	xyprintf(x, y, "(%d, %d)", xGrid, yGrid);

	//方向坐标偏移量,0~3分别对应左上右下
	int dx[4] = { -1, 0, 1, 0 };
	int dy[4] = { 0, -1, 0, 1 };

	for (; is_run(); delay_fps(60)) {
		int direction = -1;

		//按键消息处理
		while (kbmsg()) {
			key_msg keyMsg = getkey();
			if (keyMsg.msg == key_msg_down) {
				switch (keyMsg.key) {
				case 'A': case key_left:	direction = 0;	break;
				case 'W': case key_up:		direction = 1;	break;
				case 'D': case key_right:	direction = 2;	break;
				case 'S': case key_down:	direction = 3;	break;
				}
			}
		}

		if (direction != -1) {
			//可以计算出下一个位置坐标
			int xGridNext = xGrid + dx[direction], yGridNext = yGrid + dy[direction];
			//判断移动是否会出边界,没出边界则移动
			if (0 <= xGridNext && xGridNext < NUM_GRID && 0 <= yGridNext && yGridNext < NUM_GRID) {
				cleardevice();
				//移动
				xGrid = xGridNext;
				yGrid = yGridNext;

				//计算绘制位置,绘制
				x = xGrid * GRID_WIDTH, y = yGrid * GRID_WIDTH;
				bar(x, y, x + GRID_WIDTH, y + GRID_WIDTH);
				xyprintf(x, y, "(%d, %d)", xGrid, yGrid);
			}
		}
	}

	closegraph();

	return 0;
}

(十四)EGE键盘消息_第2张图片

你可能感兴趣的:(EGE)