EGE基础:键盘输入篇
EGE专栏:EGE专栏
目录
- 一、按键消息
- 1. 按键消息类型
- 2. 长按与短按
- 2.1 短按
- 2.2 长按
- 3. 按键消息结构体
- 二、按键消息处理
- 1. 按键消息队列
- 1.1 flushkey() 清空按键消息队列
- 2. 按键消息处理循环
- 三、虚拟键码
- 四、按键状态检测
- 1. keystate() 函数
- 通过keystate()检测的缺点
- 2. 通过读取按键消息记录按键状态
- 五、按键控制
- 1. 按键识别
- 2. 按键按下与抬起
- 3. 辅助键
- 六、字符输入
- 1. 获取字符输入 getch()
- 2. 字符输入:由按键消息读取(EGE20.08新增)
- 2.1 字符的编码
- 2.2 编码设置
- 2.2.1 GB2312编码(国家简体中文字符集)
- 2.2.2 UTF-16编码
- 3. 字符的输入
- 3.1 UTF-16编码
- 3.2 GB2312编码
- 3.3 字符的存储与处理
- 4. GB2312编码字符的处理示例
- 示例程序
- 七、按键控制移动示例
- 1. 单次移动
- 2. 匀速移动
- 3. 平滑移动
一、按键消息
下图是笔记本键盘的一个常见键位布局,包含了大部分按键。
1. 按键消息类型
键盘上的按键在按下和抬起时,系统会发送相应的按键消息,通知程序用户使用键盘进行了哪些操作。程序可以读取这些消息,识别后做出响应。同时,键盘作为输入工具主要是用于输入文字,系统除了发送按下和抬起消息外,还会发送字符输入消息,程序可以读取来处理用户输入的字符。
EGE中的按键消息分为三种,分别是 按键按下、按键抬起 和 字符输入 消息,定义在 key_msg_e
枚举中:
// 按键消息类型枚举
typedef enum key_msg_e {key_msg_down = 1, // 按键按下key_msg_up = 2, // 按键抬起key_msg_char = 4, // 字符输入
} key_msg_e;
2. 长按与短按
按键长按与短按时,消息的发送是两种不同的情况,短按是指按键按下后很快又松开,而 长按 是指按键被按下后,保持足够长的时间后再松开。
2.1 短按
当按键按下时,系统会发送到一条 按键按下(down) 的消息,如果当前使用的是英文输入法,并且按键对应一个字符(如A键),紧接着还会发送到一条 字符输入消息(char)。
中文输入法的字符输入消息会等到确认再发送,并不是在按键按下时。例如拼音输入法,需要多个按键才能确定一个词,这时会将中文字词的编码拆分成多个字符输入消息再将它们一起发送。
松开按键时,系统会发送到一条 按键抬起(up) 消息。
2.2 长按
一般同一个按键的按下和抬起消息是成对出现的,但如果按住某个键的时间足够长,会触发键盘的自动重复功能,以一定的时间间隔重复发送 按下消息 和 字符输入消息,直到松开按键才停止重复发送。松开按键时,长按和短按都会发送一条按键抬起消息。这就是按键长按时按键消息的发送情况。
3. 按键消息结构体
在EGE中,按键消息的结构体 key_msg 定义如下:
typedef struct key_msg {unsigned int msg; //消息类型unsigned int key; //键码unsigned int flags; //辅助键标志
}key_msg;
结构体key_msg 有三个成员:
成员 | 含义 | 值 |
---|---|---|
msg | 表示消息类型:按下、抬起或字符 | key_msg_e 中的枚举值 |
key |
当是按下、抬起消息时,表示相应按键的键码。如果是字符消息,则存储 字符编码(有时一个字符编码分多个消息存储 )
|
windows 虚拟键码 或字符编码 |
flags | 辅助键标志,用二进制位表示辅助键Shift, Ctrl是否被按下,这样可以识别类似 Ctrl + A 的快捷键 | 使用 key_flag_e 中的枚举值表示辅助键是否被按下 |
二、按键消息处理
1. 按键消息队列
用户使用键盘进行输入时,系统会将产生的消息发送至活动窗口。EGE窗口接收到后按键消息后,会将消息存储到消息队列中。在程序中可以使用 kbmsg() 判断存储按键消息的消息队列是否为空,如果不为空,则可以使用 getkey() 从消息队列中取出一个按键消息。
如果消息队列为空,getkey() 会一直等待,直到有按键消息进入队列。
1.1 flushkey() 清空按键消息队列
当你觉得按键消息队列中的消息已经没有用时,可以将这些消息清空。这样下一次就能 直接处理到最新的键盘消息了。
清空键盘消息缓存区的函数为
flushkey();
2. 按键消息处理循环
在从按键消息队列中取出按键消息前,先用 kbmsg() 判断队列中是否保存有消息,如果有再调用 getkey() 取出队列中的消息,否则 getkey() 会一直等待,直到有新的按键消息产生才会继续往下执行。
// 从队列中取出所有按键消息
while (kbmsg()) {key_msg keyMsg = getkey();
}
使用 while() 循环目的是要将队列中的消息全部取出,因为队列中消息都是用户在之前操作键盘产生的,距离用户操作已经过去了一小段时间,如果现在不处理完毕,那么消息处理会被拖延得更久,更加滞后,用户可能会感觉到程序对按键的响应比较慢。
三、虚拟键码
Windows文档:虚拟键码
键盘上的按键被映射成一个值,称为 虚拟键码(Virtual-Key Codes),是系统定义的独立于设备的值,值在 1到255之间,0不是按键的键码。(例如,在系统中,不管是什么键盘,回车键都是用同一个值表示)
按键消息结构体中的key成员便是用来表示存储按键的虚拟键码,通过它我们可以知道按键消息是由哪个按键产生的。
虚拟键码的宏命名一般以 VK_
开头,如回车键 VK_RETURN
,部分可以用ASCII字符表示的按键,就用ASCII值表示,如数字键、标点符号键、字母键等(字母键是大写字母的ASCII值
),没有单独命名。如A键,虚拟键码就等于大写字母 A 的 ASCII字符的值 'A'
。
EGE为了方便表示这些键,则为这些虚拟键码另起了名称,如回车键 key_enter
, 字母A键 key_A
,数字0键 key_0
等。按键对应的值依然和虚拟键码一致,并没有更改。定义如下
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_B = 0x42,key_C = 0x43,key_D = 0x44,key_E = 0x45,key_F = 0x46,key_G = 0x47,key_H = 0x48,key_I = 0x49,key_J = 0x4a,key_K = 0x4b,key_L = 0x4c,key_M = 0x4d,key_N = 0x4e,key_O = 0x4f,key_P = 0x50,key_Q = 0x51,key_R = 0x52,key_S = 0x53,key_T = 0x54,key_U = 0x55,key_V = 0x56,key_W = 0x57,key_X = 0x58,key_Y = 0x59,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,//小键盘的符号键key_multiply = 0x6a, *key_add = 0x6b, +key_separator = 0x6c,key_subtract = 0x6d, -key_decimal = 0x6e, .key_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;
四、按键状态检测
按键有两个状态:松开状态和按下状态。
1. keystate() 函数
判断某个按键当前是否是按下状态,可以使用EGE中的 keystate()
函数,参数key是按键的虚拟键码,如果按键当前处于按下状态,那么 keystate() 函数返回 1,否则返回 0。
int keystate(int key);
需要注意的是,keystate() 函数检测按键状态是依靠窗口接收到的按键消息,如果EGE窗口处于非活动窗口,那么按下按键是无法检测到的。实际上也不需要检测,在其它窗口按下按键本身就是和本窗口无关。如果真的想要检测,可以使用win API中的 GetAsyncKeyState()。
由于一些键盘的检测电路设计问题,某些按键同时按下时,会有一些按键无法识别出按下松开,至于是哪些按键的组合会出现这种状况,要看键盘的电路如何设计。
例如,检测回车键是否是按下状态:
if (keystate(key_enter)) {}
通过keystate()检测的缺点
缺点是只能检测按键 当前的状态,如果在一段时间内很多按键快速地按下又松开,结束后你再去检测当前状态,是无法得到他们在这一段时间内按下和抬起的顺序的。
当某一帧的计算非常耗时,在这段时间内可能会出现按键操作无效的现象,因为keystate() 丢失了两次检测中间这段时间内按键的状态变化信息。两次检测之间间隔时间越久,灵敏度越低。
2. 通过读取按键消息记录按键状态
每当按键触发按下消息时,就表明按键接下来是按下状态,触发抬起消息就说明按键接下来是松开状态。因此,我们可以通过读取按键消息来得到按键的状态。得到的按键状态需要记录,否则如果后面没有接收到对应按键的按键消息的话,将无法得知按键的状态。
按键消息发送的顺序和按键按下抬起的顺序一致,因此可以通过按键消息准确地知道按键状态变化顺序,而不像 keystate()只能获取按键当前的状态。
示例代码如下所示,虚拟键码范围为1~255,因此可以直接创建一个256大小的bool数组存储按键状态。当检测到消息类型为 key_msg_down
时,设置对应按键状态为按下;当检测到消息类型为 key_msg_up
时,设置对应按键状态为松开。
当然,如果觉得占用内存比较多,可以使用二进制位来表示,需占用32个字节。
// 存储按键状态:是否被按下
bool keyIsPressed[256] = {false};
while (kbmsg()) {key_msg msg = getkey();if (msg.msg == key_msg_down) {//按键按下,变为按下状态keyIsPressed[msg.key] = true;} else if (msg.msg == key_msg_up) {//按键抬起,变为松开状态keyIsPressed[msg.key] = false;}
}
五、按键控制
用户按下松开按键时,系统会发送按键的按下、抬起消息,我们可以在程序中读取并识别出这些消息,然后对用户按键动作做出响应。
按键消息结构体中的 msg 和 key 成员分别表示消息类型和按键。对于按键控制,我们只处理 按下(key_msg_down) 和 抬起(key_msg_up) 这两个类型的消息,字符输入类型的消息我们就暂时忽略掉。
// 按键消息结构体
typedef struct key_msg {unsigned int msg; //消息类型unsigned int key; //键码unsigned int flags; //辅助键标志
}key_msg;// 按键消息类型枚举
typedef enum key_msg_e {key_msg_down = 1, // 按键按下key_msg_up = 2, // 按键抬起key_msg_char = 4, // 字符输入
} key_msg_e;
1. 按键识别
虚拟键码统一了按键的标识,我们可以通过虚拟键码来确定按键消息的触发来源。
while (kbmsm()) {key_msg msg = getkey();if ((msg.key = key_enter)) {// 由回车键触发的消息,可能按下、抬起或字符消息}
}
2. 按键按下与抬起
上面通过虚拟键码可以得到触发的按键,但对于控制来说还是不够的,因为在一个按键被按下和松开的过程中,会触发多次按键消息:按下、字符输入、抬起。
如果仅仅是根据键码判断用户是否按下了按键的话,那么按键每被按下一次,会发送多条与该按键相关的消息,程序会识别到按键多次按下,所以还需要通过消息类型将这些消息区分开来。
按键的按下和抬起消息可以通过 key_msg 结构体中的 msg 成员的值识别出来。
按键无论是长按还是短按,都只会发送一次 抬起消息(up),而按键长按时却会多次发送 按下消息(down),这是需要注意的地方。
while (kbmsg())
{key_msg msg = getkey();// 通过key_msg的成员msg来判断消息类型if (msg.msg == key_msg_down) {//按键按下消息//长按会多次触发,这个需要注意} else if (msg.msg == key_msg_up) {//按键抬起消息} else {// 字符输入消息:key_msg_char}
}
所以某个按键抬起的消息可以通过对消息类型和虚拟键码的组合判断进行确定。
while (kbmsg())
{key_msg msg = getkey();// 回车键抬起if ((msg.msg == key_msg_up) && (msg.key == key_enter)) {}
}
那对于按键按下的消息该如何进行判断呢?长按会发送多次按键按下的消息,怎么识别出按键按下时发送的第一条消息?那就要通过消息发送前按键的状态来解决。
本来Windows发送的按键消息中已经包含按键之前的状态,但由于EGE库中处理出现失误,没有加入相关位,因此无法直接由按键消息得到按键之前的状态。
由按键消息记录按键的状态,每次读取到按下消息时,先对按键之前的状态进行判断。如果是按键按下时发送的第一条消息,那它之前肯定是松开状态。
//记录按键状态:是否被按下,初始是松开状态; false:松开,true:按下
bool keyIsPressed[256] = {false};
while (kbmsg()) {key_msg msg = getkey();if (msg.msg == key_msg_down) {// 按键被按下,先判断之前的状态是否是松开if (!keyIsPressed[msg.key]) {// 按键之前是松开状态,因此是第一次发送的按下消息这里执行按键按下时进行的操作}else {// 按键长按时重复发送的按下消息}//按键按下,记录按键已变为按下状态keyIsPressed[msg.key] = true;} else if (msg.msg == key_msg_up) {这里执行按键按下时进行的操作// 按键抬起,记录按键已变为松开状态keyIsPressed[msg.key] = false;}
}
3. 辅助键
按键消息的成员变量flags中有两个位是用来指示辅助键Shift和Ctrl是否被按下的。如果对应位上为1,那就辅助键被按下。
typedef enum key_flag_e {key_flag_shift = 0x100,key_flag_ctrl = 0x200,
}key_flag_e;
while (kbmsg()) {key_msg msg = getkey();// 判断按键被按下时,辅助键是否也被按下if (msg.msg == key_msg_down) {if (msg.flags & key_flag_shift) {//判断是否按下了 Shift 辅助键}if (msg.flags & key_flag_ctrl) {//判断是否按下了 Ctrl 辅助键}}
}
六、字符输入
1. 获取字符输入 getch()
除暂停作用外,不建议使用。
如果你只是想判断是哪个按键按下的或者获取输入的字符,这里有种简单的方式,那就是 **kbhit() 和 getch() 的组合。
最常用的 getch(),这时候程序会暂停,等待用户按下按键,返回值是按键输入的字符的ASCII值或者是功能键的码值。
这个码值不是虚拟键码,如果能用ASCII表示,那就是等于ASCII码,如果不能表示,那么码值大于255。
int ch = getch();
getch() 返回的值需要用两个字节表示,部分键按下时返回值大于0xFF,所以不能使用 char
类型变量进行存储,否则会被被截断,应该用 int。
如果不想暂停,可以使用 kbhit() 检测是否有字符输入,如果没有就跳过,有就读取字符,这样就不影响程序运行了。
//判断是否有按键字符输入,有就读取字符。
while (kbhit()) {int ch = getch();...
}
2. 字符输入:由按键消息读取(EGE20.08新增)
字符类型按键消息是EGE20.08实现的功能,20.08之前的版本虽然有key_msg_char定义,但实际上并没有实现。
key_msg中的 msg 成员表示的是消息类型。
当msg等于 key_msg_char 时,就表明是字符消息,可以由 key 成员得到输入字符的编码。
2.1 字符的编码
英文输入时,那么得到的就是字符的 ASCII 码,这和普通的字符输入一致,这时候使用就很简单(相比getch()直接获取要多写几行)
。
key_msgkeyMsg = key_msg{ 0 }; //初始化while (kbmsg()) { //判断是否有按键消息,避免堵塞keyMsg = getkey(); //获取按键消息if (keyMsg.msg == key_msg_char) { //判断是否是字符输入int ch = keyMsg.key; //得到输入的字符这个ch就是得到的字符了}}
如果是中文输入,那么得到的是汉字的编码值。通常编码默认采用的是本地编码GBK或 GB2312,这个编码可以设置。
2.2 编码设置
ege可以设置窗口的字符编码方式,中文系统默认是GB2312编码,也可以设置成Unicode编码(UTF-16)
,这通过初始化模式INIT_UNICODE来设置。
下面就是将窗口字符编码设置成Unicode编码
initgraph(640, 480, INIT_UNICODE);
这里简单说一下几个概念:
- 码点:即一个字符在Unicode编码集中对应的数字值。这里注意一下,只是数学上的数值概念,并不涉及到具体在计算机中如何存储。
- 代码单元:具体的字符编码用来表示码点的基本单元,一个码点可由多个代码单元来表示。
2.2.1 GB2312编码(国家简体中文字符集)
GB2312编码的代码单元为一个字节大小。
ASCII字符占一个代码单元,中文字符占两个代码单元。
GB2312的代码单元是一个字节大小,所以ASCII字符用一个代码单元来表示,而中文字符用两个代码单元来表示。所以中文字符的输入会分成两个字节来发送。
ASCII编码字节的最高位是0,只用低7位。而GBK编码除了包含 ASCII 外,剩余的汉字部分,汉字编码固定占两个字节,并且每个字节的最高位为1。所以可以用最高位判断是字符是1个字节还是两个字节。
2.2.2 UTF-16编码
UTF-16编码的代码单元为两个个字节大小,即16位。一个码点用一个或两个代码单元来表示。
UTF-16编码的代码单元占两个字节,这时候直接用宽字符类型wchar_t 存储即可,然后就可以使用宽字符相关的函数来输出。
3. 字符的输入
我们可以通过读取按键消息,如果消息类型为字符类型,即key_msg_char,那么就读取字符,做字符消息处理。
按键字符消息处理如下所示:
while (kbmsg()) {key_msg keyMsg = getkey();//判断是否是字符类型消息if (keyMsg.msg == key_msg_char) {int ch = keyMsg.key;//这里就得到了字符的值,即ch}
}
这里注意一下,key_msg.key的类型是int类型。
因为有不同的编码,对于中文,keyMsg.key对应的不一定就是完整的一个字符。
3.1 UTF-16编码
此时keyMsg.key内的值用两个字节即可以表示,对应宽字符类型wchar_t。
key_msgkeyMsg = key_msg{ 0 }; //初始化while (kbmsg()) { //判断是否有按键消息,避免堵塞keyMsg = getkey(); //获取按键消息if (keyMsg.msg == key_msg_char) { //判断是否是字符输入wchar_t ch = (wchar_t)keyMsg.key; //得到的一个代码单元}}
用宽字符相关的函数即可处理
3.2 GB2312编码
GB2312编码的代码单元是一个字节大小,中文字符用两个字节表示,所以中文字符会连发两个字符型消息。
字符你需要判断哪些字节是属于同一个文字的编码。
这里以GB2312编码为例,ASCII码部分,字节的最高位为0,而一个汉字部分,一个汉字固定占两个字节,每个字节最高位都是1。所以可以检测最高位,发现是0就是ASCII字符,如果是1就凑够两个字节。
3.3 字符的存储与处理
读取到字符后,根据编码用char 型或wchar_t数组来存储,然后使用相应的普通字符串函数或者宽字符类型字符串函数来操作即可,像字符串拼接,字符串输出之类,系统都能自行处理。
4. GB2312编码字符的处理示例
汉字输入的时候,输入法会有候选窗口弹出,直到按下空格或数字才会选中对应的汉字。此时字符消息一起发出,一个汉字发出两个字符消息。并且是按编码中的顺序发出。所以只要按顺序存储即可。可以创建个字符缓存区,并且增加缓存区长度记录。
下面是消息处理循环,字符的处理就在while循环中。
const int buffSize = 128;
char buff[128] = {""}; //缓存区
int len = 0; //记录长度
key_msg keyMsg;while (kbmsg()) {keyMsg = getkey();/*---------------------------------*/这里处理按键消息/*---------------------------------*/
}
先判断是否为字符消息
if (keyMsg.msg == key_msg_char)
获取键值key,即 key_msg 的 key 成员,因为GB2312的代码单元为一个字节大小,所以key值用一个字节即可存储。
char ch = (char)keyMsg.key;
- 对字节的最高位判断,最高位为0则是ASCII码,否则GB2312的扩展部分。如果是想单字输出,如输出到控制台,就可以凑够两个字节输出了,而如果想一起输入,直接保存即可,后面一起输出。
单字输出:
这里凑够一个字就直接输出。(英文一个字节,中文两个字节)
if (keyMsg.msg == key_msg_char) {char ch = keyMsg.key;if ((ch & 0x80) == 0) { //ASCII字符if ((ch == '\r') || (ch == '\n'))putchar('\n');elseputchar(ch);}else{ buff[len++] = ch;if (len >= 2) { //没有存储完汉字的全部字节buff[len] = '\0'; //末尾增加结束符printf("%s", buff); //这里仅做直接输出处理len = 0; //长度清零}}
}
完整程序
#define SHOW_CONSOLE
#include <graphics.h>
#include <stdio.h>int main()
{initgraph(640, 480, 0);setcaption("按键消息类型测试");setbkcolor(WHITE);setcolor(BLACK);setfont(18, 0, "宋体");char buff[3] = { "" }; //缓存区int len = 0; //记录长度xyprintf(40, 200, "请输入中文或英文,然后查看控制台输出");for (; is_run(); delay_fps(60)) {while (kbmsg()) {key_msg keyMsg = getkey();if (keyMsg.msg == key_msg_char) {char ch = keyMsg.key;if ((ch & 0x80) == 0) { //ASCII字符if ((ch == '\r') || (ch == '\n'))putchar('\n');elseputchar(ch);}else{ buff[len++] = ch;if (len >= 2) { //没有存储完汉字的全部字节buff[len] = '\0'; //末尾增加结束符printf("%s", buff); //这里仅做直接输出处理len = 0; //长度清零}}}}}closegraph();return 0;
}
单行输出
这里等到输入一个回车符再输出,因为输入缓存区总有限制,如果一行文字过长,做直接输出处理。
这里还有个小问题,如果满了的时候,输出时最后那个汉字仅仅读取了其中一个字节,存不下了呀,没读完整呀。怎么办?那么要把这个字节保留下来,并且原来的位置设成 ‘\0’ 再输出,然后把这个字节放到存到缓存区的第一的字节,长度设为1。
那怎么读取的时候怎么知道最后那个是汉字的哪个字节?那当然是读取的过程中设立一个变量,用来对自己进行计数,如果是GB2312编码,因为固定的两个字节,在while循环外设立一个bool值,检测到汉字的一个字节取反就可以了,初始为false,最后也为 false ,则说明不是汉字的前一个字节。
字符的处理就在 charHandle() 函数中,为了防止嵌套太多,所以放到了函数里,同时也增加了清晰度。只是这样会增加一个传参的步骤。需要将缓存区长度返回,所以用了指针。
示例程序
#define SHOW_CONSOLE
#include <graphics.h>
#include <stdio.h>//字符处理函数
void charHandle(char ch, char* buff, int *len, int buffSize)
{static bool half = false;bool output = false; //输出标记bool lineCompleted = false; //行结束标记//存储buff[(*len)++] = ch;//如果是英文字符的编码if ((ch & 0x80) == 0) { //回车或满则输出标志置位if ((ch == '\r') || (ch == '\n')) {(*len)--; //回车要清掉output = true;lineCompleted = true; //行已结束} else if (*len >= buffSize - 1) {output = true; }}//如果是中文字符的编码else {half = !half; //对标志位取反//判断是否满(最后要留一个存结束符),,满则输出标志置位if ((*len >= buffSize - 1)) { output = true;}}if (output) {//当前之前汉字只读了一半,那么把这个用'\0'替换if (half)(*len)--;//输出清空buff[(*len)++] = '\0';printf("%s", buff);if (lineCompleted)putchar('\n');*len = 0;//填充之前的字节if (half)buff[(*len)++] = ch;}
}int main()
{initgraph(640, 480, 0);setcaption("按键消息类型测试");setbkcolor(WHITE);setcolor(BLACK);setfont(18, 0, "宋体");#define BUFF_SIZE 16char buff[BUFF_SIZE] = { "" }; //缓存区int len = 0; //记录长度xyprintf(40, 200, "请输入中文或英文,然后查看控制台输出");xyprintf(40, 220, "(回车主动输出或缓存区满后输出)");xyprintf(40, 240, "当前缓存区内所占字节:%3d / %d", len, BUFF_SIZE);for (; is_run(); delay_fps(60)) {while (kbmsg()) {key_msg keyMsg = getkey();if (keyMsg.msg == key_msg_char) {char keyVal = keyMsg.key;charHandle(keyVal, buff, &len, BUFF_SIZE);xyprintf(40, 240, "当前缓存区内所占字节:%3d / %d", len, BUFF_SIZE);}}}closegraph();return 0;
}
七、按键控制移动示例
1. 单次移动
单次移动: 按下一次按键只会移动一次,长按也不会再次移动。
单次移动一般是在按键按下时移动,按键按下动作可以通过按键状态切换来检测(消息类型是 key_msg_down 且之前是松开状态)。如果想响应按键抬起动作,那么只需要检测按键消息类型是否是 key_msg_up
即可,按键抬起消息并不会重复发送。
#include <graphics.h>
#include <math.h>void drawGrid(int width, int height, int totalRow, int totalCol);
void positionOffset(int key, int* xOffset, int* yOffset);
bool PositionIsValid(int totalRow, int totalCol, int row, int col);
void drawCircle(float x, float y, float radius);bool keyIsPressed[256] = { false };int main()
{const int winWidth = 600;const int COL_NUM = 7, ROW_NUM = COL_NUM;initgraph(winWidth, winWidth, INIT_RENDERMANUAL);ege_enable_aa(true);setbkcolor(EGERGB(0xFF, 0xFF, 0xFF));setfont(20, 0, "SimSun");settextjustify(RIGHT_TEXT, TOP_TEXT);setbkmode(TRANSPARENT);int curRow = ROW_NUM / 2, curCol = COL_NUM / 2;for (; is_run(); delay_fps(60)) {while (kbmsg()) {key_msg msg = getkey();if (msg.msg == key_msg_down) {// 按键按下if (!keyIsPressed[msg.key]) {//按键按下时处理int colOffset = 0, rowOffset = 0;positionOffset(msg.key, &colOffset, &rowOffset);int rowNext = curRow + rowOffset, colNext = curCol + colOffset;// 位置有效则移动至下一个位置if (PositionIsValid(ROW_NUM, COL_NUM, rowNext, colNext)) {curRow = rowNext;curCol = colNext;}}//按键按下,记录按键已变为按下状态keyIsPressed[msg.key] = true;} else if (msg.msg == key_msg_up) {// 按键抬起,记录按键已变为松开状态keyIsPressed[msg.key] = false;}}cleardevice();drawGrid(winWidth, winWidth, ROW_NUM, COL_NUM);float radius = (float)winWidth / (2.0f * COL_NUM);float x = winWidth * ((float)curCol / COL_NUM) + radius;float y = winWidth * ((float)curRow / ROW_NUM) + radius;drawCircle(x, y, radius);setcolor(BLACK);xyprintf(winWidth, 0, "当前位置:(%d, %d)", curRow, curCol);}return 0;
}// 绘制网格
void drawGrid(int width, int height, int totalRow, int totalCol)
{setlinestyle(DASHED_LINE, 0, 1);setcolor(EGERGB(0xC0, 0xC0, 0xC0));for (int i = 1; i < totalCol; i++) {int x = (int)roundf((float)width * i / totalCol);line(x, 0, x, height);}for (int i = 1; i < totalRow; i++) {int y = (int)roundf((float)height * i / totalRow);line(0, y, width, y);}
}//绘制圆
void drawCircle(float x, float y, float radius)
{setfillcolor(EGEACOLOR(0xFF, 0xA0D8EF));ege_fillellipse(x - radius, y - radius, 2 * radius, 2 * radius);setlinestyle(SOLID_LINE, 0, 2);setcolor(BLACK);ege_ellipse(x - radius, y - radius, 2 * radius, 2 * radius);
}void positionOffset(int key, int* xOffset, int* yOffset)
{int dx = 0, dy = 0;switch (key) {case 'A': case key_left: dx = -1; break; //左移case 'W': case key_up: dy = -1; break; //上移case 'D': case key_right: dx = 1; break; //右移case 'S': case key_down: dy = 1; break; //下移default: break; //其他键不移动}*xOffset = dx;*yOffset = dy;
}bool PositionIsValid(int totalRow, int totalCol, int row, int col)
{return (0 <= row) && (row < totalRow) && (0 <= col) && (col < totalCol);
}
2. 匀速移动
匀速移动 是指物体在每一帧移动相同的距离或者单位时间内移动相同的距离。
这是两种不同的移动策略,如果每一帧间隔的时间相同或者保持平均间隔时间在一定值,那么按帧匀速和按时间匀速效果差不多,
如果渲染时一些帧突然耗时严重,这时按帧匀速和按时间匀速这两种就会出现较大的位置偏差。相对来说,按帧匀速能够避免卡顿时物体移动出现跳跃,使用户反应不及,但匀速性没有按时间匀速好。
按照按键消息发送的数量来移动是无法实现的匀速移动的,因为按键消息的发送并不是匀速的,特别是长按时,从第一次发送按下消息到第二次发送会间隔较长时间,而后续自动重复发送时发送速度又比较快。按键消息的发送和帧率并不同步,如果按照是否接收到按键消息来移动,那么就会出现部分帧静止部分帧移动的情况,这样看起来物体移动动画就比较卡顿。
为了让物体每一帧都匀速移动,应该通过检测 按键当前状态 的方来确定当前帧是否需要移动物体。
每帧检测一下按键的当前状态,如果控制物体移动的按键处于按下状态,那就移动物体。按帧匀速是每一帧移动相同的距离,如果是按时间匀速,则需要计算与上一帧间隔的时间,根据间隔时间来确定移动的距离,而不是固定。
按键当前状态使用 keystate() 进行检测即可,因为不需要考虑两帧之间按键状态切换的时序问题。
#include <graphics.h>
#include <Windows.h>int main()
{const int winWidth = 600, winHeight = 600;const int radius = 64;initgraph(winWidth, winHeight, INIT_RENDERMANUAL);//为了美观,使用抗锯齿ege_enable_aa(true);setbkcolor(EGEACOLOR(0XFF, WHITE));setcolor(BLACK);setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));setfont(20, 0, "Simsun");settextjustify(RIGHT_TEXT, TOP_TEXT);setbkmode(TRANSPARENT);//浮点型,精确位置,float xCircle = winWidth / 2, yCircle = winHeight / 2;//0, 1, 2, 3分别对应方向 左 上 右 下int keys[4] = {'A', 'W', 'D', 'S'};int directionKeys[4] = { key_left, key_up, key_right, key_down };//移动速度, 浮点型能任意控制速度快慢,const float speed = 2.0f;float xSpeeds[4] = { -speed, 0, speed, 0 };float ySpeeds[4] = { 0,-speed, 0, speed };setlinewidth(2.0f);setcolor(BLACK);setfillcolor(EGEACOLOR(0xFF, 0xA0D8EF));timeBeginPeriod(1);for (; is_run(); delay_fps(60)) {//根据按键按下状态对位置增量进行累加,可八方向移动float xNext = xCircle;float yNext = yCircle;// 判断四个方向键是否被按下for (int i = 0; i < 4; i++) {if (keystate(keys[i]) || keystate(directionKeys[i])) {// 往对应方向进行位移xNext += xSpeeds[i];yNext += ySpeeds[i];}}// 如果移动则检测移动是否有效if (xNext != xCircle || yNext != yCircle) {//检测是否超出边界if (radius <= xNext && xNext <= (winWidth - radius)&& radius <= yNext && yNext <= (winHeight - radius)) {xCircle = xNext;yCircle = yNext;}}/*上面是做位置计算的,下面是根据位置绘图的部分*///清屏cleardevice();ege_fillellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);ege_ellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);xyprintf(winWidth, 0, "当前位置:(%.2f, %.2f)", xCircle, yCircle);}timeBeginPeriod(2);return 0;
}
3. 平滑移动
平滑移动 也叫 惯性移动,模仿物体具有的惯性,移动速度不会突变,而是从0逐渐增加,或者逐渐衰减至0。当然,本身显示的帧和像素本身就是离散的,不可能有连续,所以这里速度不会突变是指视觉上有一定过渡时间,能让人眼感觉到移动速度在缓慢变化。
平滑移动较为简单的实现就是模拟物理上的力,给物体一个匀加速度。另外有些实现是将物体的移动速度和距离联系。不管如何实现,让速度的变化幅度降低到人眼感觉到平滑渐变即可。
#include <cassert>
#include <cmath>
#include <graphics.h>
#include <Windows.h>float clamp(float value, float max, float min);
float distance(float* speed, float forceAcceleration, float frictionAcceleration);int main()
{SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);const int winWidth = 800, winHeight = 800;const int radius = 64;timeBeginPeriod(1);initgraph(winWidth, winHeight, INIT_RENDERMANUAL);//为了美观,使用抗锯齿ege_enable_aa(true);setbkcolor(EGEACOLOR(0XFF, WHITE));setcolor(BLACK);setfillcolor(EGEARGB(0XFF, 0XFF, 0, 0XFF));setfont(20, 0, "Simsun");settextjustify(RIGHT_TEXT, TOP_TEXT);setbkmode(TRANSPARENT);setlinewidth(2.0f);setcolor(BLACK);setfillcolor(EGEACOLOR(0xFF, 0xA0D8EF));//0, 1, 2, 3分别对应方向 左 上 右 下int keys[4] = {'A', 'W', 'D', 'S'};int directionKeys[4] = { key_left, key_up, key_right, key_down };// 物体速度及加速度参数const float MAX_SPEED = 8.0f;const float ACCELERATION = 15.0f / 60.0f; //按键按下时在对应方向上施加的加速度const float FRICTION_ACCELERATION = ACCELERATION / 2.0f; //摩擦力的加速度const float xOffset[4] = {-1.0f, 0.0f, 1.0f, 0.0f}; //指示左上右下的偏移方向const float yOffset[4] = {0, -1.0f, 0.0f, 1.0f}; //指示左上右下的偏移方向//物体位置和速度float xCircle = winWidth / 2, yCircle = winHeight / 2;float xSpeed = 0.0f, ySpeed = 0.0f;for (; is_run(); delay_fps(60)) {//根据按键计算加速度float xForceAcceleration = 0.0f, yForceAcceleration = 0.0f;// 判断四个方向键是否被按下for (int i = 0; i < 4; i++) {if (keystate(keys[i]) || keystate(directionKeys[i])) {// 根据方向键计算各方向的加速度xForceAcceleration += xOffset[i] * ACCELERATION;yForceAcceleration += yOffset[i] * ACCELERATION;}}// 根据移动方向计算摩擦力对物体的加速度,用于减速float resultantVelocity = sqrtf(xSpeed * xSpeed + ySpeed * ySpeed); //速度方向float xFrictionAccelertion, yFrictionAccelertion;if (resultantVelocity == 0.0f) {xFrictionAccelertion = 0.0f;yFrictionAccelertion = 0.0f;}else {// 摩擦力产生的加速度和速度方向相反xFrictionAccelertion = -(xSpeed / resultantVelocity) * FRICTION_ACCELERATION;yFrictionAccelertion = -(ySpeed / resultantVelocity) * FRICTION_ACCELERATION;}// 计算移动距离float xDistance = distance(&xSpeed, xForceAcceleration, xFrictionAccelertion);float yDistance = distance(&ySpeed, yForceAcceleration, yFrictionAccelertion);// 限制最大速率xSpeed = clamp(xSpeed, MAX_SPEED, -MAX_SPEED);ySpeed = clamp(ySpeed, MAX_SPEED, -MAX_SPEED);// 根据物体位移量float xNext = xCircle + xDistance;float yNext = yCircle + yDistance;if (xCircle != xNext) {xCircle = clamp(xNext, winWidth - radius, radius);if (xCircle != xNext)xSpeed = 0.0f;}if (yCircle != yNext) {yCircle = clamp(yNext, winHeight - radius, radius);if (yCircle != yNext)ySpeed = 0.0f;}/*上面是做位置计算的,下面是根据位置绘图的部分*///清屏cleardevice();ege_fillellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);ege_ellipse(xCircle - radius, yCircle - radius, 2 * radius, 2 * radius);xyprintf(winWidth, 0, "当前位置:(%.2f, %.2f)", xCircle, yCircle);}timeBeginPeriod(2);return 0;
}float distance(float* speed, float forceAcceleration, float frictionAcceleration)
{float accelertation = forceAcceleration + frictionAcceleration;float sp = *speed;// 如果出现力使物体速度反向,则需要考虑自然停止或摩擦力反向if ((sp < 0) != (sp + accelertation < 0)) {// 计算速度为0时的移动, S = -(v^2) / (2a)float dist = -sp * sp / (2.0f * accelertation);float time = fabs(sp / accelertation);// 如果施加有主动力,则根据剩余时间继续移动, S = (1/2) * 加速度 * 时间的平方// 反向移动后摩擦力也反向if (forceAcceleration != 0.0f) {dist += 0.5f * (forceAcceleration - frictionAcceleration) * (1 - time) * (1 - time);*speed = (forceAcceleration - frictionAcceleration) * (1 - time);} else {*speed = 0.0f;}return dist;}else {*speed = sp + accelertation;return (sp + sp + accelertation) * 0.5f;}
}float clamp(float value, float max, float min)
{return (value > max) ? max : ((value < min) ? min : value);
}
EGE专栏:EGE专栏
EGE基础:键盘输入篇相关推荐
- java数组元素的输入_java基础--键盘输入一个数,输出数组中指定元素
java基础--键盘输入一个数,输出数组中指定元素 java基础--键盘输入一个数,输出数组中指定元素 package com.lcn.day05; import java.util.Scanner; ...
- EGE基础入门篇(二):开始使用EGE
EGE专栏:EGE专栏 上一篇:EGE基础入门篇(一):绘图基础知识 下一篇:EGE基础入门篇(三):开场动画 EGE基础入门篇(二) 文章最后修改时间:2021年6月23日19:30:47 文章目录 ...
- java编程基础篇-- 编写一个程序,从键盘输入三个整数,求三个整数中的最小值。
编写一个程序,从键盘输入三个整数,求三个整数中的最小值. package Exam01;import java.util.Scanner;public class Topic03 {public st ...
- EGE入门基础知识学习篇
文章目录 前言 一.EGE是什么? 二.EGE使用步骤 1.基本框架 2.创建游戏窗口 3.窗口背景颜色篇 4.坐标 5.文字篇 6.音乐篇 7.图片篇 8.其余篇 前言 本文主要是学习EGE的一些基 ...
- Java从键盘输入n行字符串_Java十四天零基础入门-Java布尔类型
不闲聊!!!不扯淡!!!小UP只分享Java相关的资源干货 Java布尔类型 在Java语言中布尔类型的值只包括true和false,没有其他值,不包括1和0,布尔类型的数据在开发中主要使用在逻辑判断 ...
- c语言从键盘输入千米数,第二章 C语言编程基础.ppt
第二章 C语言编程基础 习题2 P51-7.8.13.14.16 2.4.8break 语句和continue语句 [例2.19] 输出100 - 200 之间不能被3整除的数. P44 2.4.9循 ...
- 有程序在记录你的键盘输入_12个用Java编写基础小程序amp;经典案例(收藏)
点击上方"潭州教育EDU"关注我们 如果是刚接触或者刚学习java,练习一些基础的算法还是必须的,可以提升思维和语法的使用. 1.输出两个int数中的最大值 import java ...
- C++基础知识(一) 键盘输入
不得不说已经学过C++有两年的时间了,但是之前不论是做实验还是干活,所使用的工具都不是CPP.所以现 在,基本上已经忘得差不多很可以了.现在重新开始对C++进行学习,写一些博客,对自己所学过的东西进行 ...
- java判断五位数回文数_【视频+图文】Java经典基础练习题(五):键盘输入一个五位数,判断这个数是否为回文数...
能解决题目的代码并不是一次就可以写好的 我们需要根据我们的思路写出后通过debug模式找到不足再进行更改 多次测试后才可得到能解决题目的代码! 通过学习,练习[Java基础经典练习题],让我们一起来培 ...
- EGE基础入门篇(六):基本图形
EGE专栏:EGE专栏 上一篇:EGE基础入门篇(五):窗口简单操作 下一篇:EGE基础入门篇(七):组合图形 一.EGE提供的基本图形 EGE绘制图形相关库函数文档 https://xege.org ...
最新文章
- 编写高性能的 JavaScript 程序的几个提示
- MAT之GA:GA优化BP神经网络的初始权值、阈值,从而增强BP神经网络的鲁棒性
- STL的tuple集合对象
- ubuntu18.04安装windows版本微信
- [转贴]ATOM和RSS的区别
- Android使用webview控件加载本地html,通过Js与后台Java实现数据的传递
- 服务器mysql如何添加数据库文件,如何在使用MySQL作为嵌入式服务器时创建数据库文件...
- 深入理解Scala的隐式转换系统
- HTTP1.1/2.0与QUIC协议
- 职责链模式在开发中的应用
- PHP-php://(类型)访问各个输入/输出流以及全局变量$HTTP_RAW_POST_DATA讲解
- 测试工作中常用在线小工具-初级篇
- Oracle基础查询
- 命令行操作flyway
- Vue进阶(六十八):JS 判断当前浏览器是否为 IE
- Yar 搭建 RPC 服务
- python的turtle的正六角形简洁画法
- Laravel5.4中文分词搜索-使用 Laravel Scout,Elasticsearch,ik 分词(三)
- 深度学习--采用ReLU解决消失的梯度问题(vanishing gradient problem)
- ActiveMQ(二)