【SFML】事件管理

SFML 能够处理 PC 程序的几乎所有事件,包括窗口关闭事件、窗口大小改变事件、失去/获得焦点事件、字符输入事件、键盘事件、鼠标事件、游戏手柄事件。另外,对象全局的输入设备,比如鼠标、键盘、游戏手柄,SFML 还提供对应的静态类,用于获取设备的各个状态,能够实现事件无法很好实现的功能。

事件

SFML 的事件使用联合(union)实现,即所有类型的事件使用同一个数据类型,使用的时候先通过 event.type 拿到事件类型,再去拿相应的成员数据,去拿不属于当前事件类型的成员数据是非法的,通常会拿到一个随机值或非法值。事件由窗口的 pollEventwaitEvent 生产,通常是在主循环中迭代处理每一个事件。下面是一个典型的事件循环,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void dealEvent()
{
sf::Event evt;
while (window.pollEvent(evt))
{
switch (evt.type)
{
case sf::Event::Closed:
window.close();
break;
case sf::Event::Resized:
window.display();
break;
case sf::Event::KeyPressed:
//...
break;
//...
default:
break;
}
}
}

窗口关闭事件

当用户关闭窗口时,窗口关闭事件 sf::Event:Closed 会触发,比如点击关闭按钮,按下键盘快捷键等;这时候只是收到一个关闭请求,窗口并没有真正关闭,要在监听到关闭事件的时候手动调用 window.close() 才能关闭窗口。

1
2
if (event.type == sf::Event::Closed)
window.close();

调整大小事件

当窗口大小改变时,调整大小事件 sf::Event:Resized 会触发,无论是用户手动去调整窗口大小或者程序通过接口 window.setSize() 来调整大小。这个事件通常用于调整渲染设置,比如 OpenGL 的渲染视口大小或者 SFML 图形的显示视图,即使窗口没什么显示的,也能调用 window.display() 来刷新视图。

1
2
3
4
5
if (event.type == sf::Event::Resized)
{
glViewport(0, 0, evt.size.width, evt.size.height);
window.display();
}

获得/失去焦点事件

当窗口获得焦点时会触发事件 sf::Event::GainFocus,失去焦点时会触发事件 sf::Event::LostFocus,这两个事件没有额外的数据,通常用于做一些暂停/恢复操作。当用户切换到其它窗口时,当前窗口会失去焦点,切换回当前窗口时会获得焦点,失去焦点的窗口将不再监听键盘事件。

1
2
3
4
5
if (event.type == sf::Event::LostFocus)
myGame.pause();

if (event.type == sf::Event::GainedFocus)
myGame.resume();

有个特别的点,监听到失去焦点事件时,如果对窗口做一些操作,比如设置窗口大小,则窗口会立即重新获得焦点,相当于使用代码来自动激活窗口。

字符输入事件

注意把这个事件和键盘事件区分开,sf::Event::TextEntered 不关心键盘操作,只关心键盘操作的结果,即最后产生的字符。特别是同时按下多个键的情况,比如同时按下 shift+1,键盘事件会产生两个 KeyPressed 事件,但只会产生一个 TextEntered,输入的结果为 !。这个事件能接收键盘输入的任意字符,不仅限于 ASCII 字符,所以其事件数据为一个 Unicode 码,如果其值小于 128,可将其转成 ASCII 字符。另外,输入不可打印的字符也会触发字符输入事件,比如在键盘上敲退格,会产生一个 TextEntered 事件,其数据为 evt.text.unicode==8,但无法转成可打印字符。

1
2
3
4
5
6
if (evt.type == sf::Event::TextEntered)
{
std::cout << evt.text.unicode << std::endl;
if (evt.text.unicode < 128)
std::cout << static_cast<char>(evt.text.unicode) << std::endl;
}

键盘

键盘按下事件

当键盘按下一个键时,会触发键盘按下事件 KeyPressed,产生一个 code,用于标识按下的是哪个键。要注意区分这个 code 和字符输入事件的 unicodeevt.key.codesf::Keyboard::Key 中定义,而 evt.text.unicode 是产生的字符对应的 unicode 码;键盘事件不区分大小写字母,而字符输入事件会区分大写字母和小写字母,比如分别按下键盘的 a 键和 A 键,键盘事件都只会产生 code==0,而字符输入事件分别会产生 unicode==97unicode==65

如果按下一个键不放,会按照系统默认延迟不断地产生 KeyPressed 事件,如果只想产生一次,可以设置 window.setKeyRepeatEnabled(false)

键盘按下时可以检测修饰键(ctrl, alt, shift, system)是否有按下,如果有按下 ctrl 键 evt.key.ctrl==1,反之 evt.key.ctrl==0,注意这四个键按下时也会触发 KeyPressed 事件,即使我们常说同时按下多个键,但事实上手速再快也会有先后顺序。如果我们同时按下 ctrl+alt+shift+system+a 键,按照按下的顺序会产生五次 KeyPressed 事件,假设按下顺序为 ctrl-->shift-->alt-->system-->a,则五次事件的 code 分别为 37,38,39,40,0。

另外,一些功能键可能会被系统拦截,导致 SFML 程序无法正常处理其按键事件,这是个 bug,后续版本应该会修复。

1
2
3
4
5
6
7
8
9
window.setKeyRepeatEnabled(false);
if (evt.type == sf::Event::KeyPressed)
{
std::cout << evt.key.code << std::endl;
std::cout << evt.key.control << std::endl;
std::cout << evt.key.alt << std::endl;
std::cout << evt.key.shift << std::endl;
std::cout << evt.key.system << std::endl;
}

键盘松开事件

当松开一个键时,键盘松开事件 KeyReleased 会触发,其数据和键盘按下事件完全一样,有一个标识哪个键的 code 以及检测四个修饰键是否按下。很明显,不同于按下事件,同一个键是不可能会重复产生松开事件的。

1
2
3
4
5
6
7
8
if (evt.type == sf::Event::Released)
{
std::cout << evt.key.code << std::endl;
std::cout << evt.key.control << std::endl;
std::cout << evt.key.alt << std::endl;
std::cout << evt.key.shift << std::endl;
std::cout << evt.key.system << std::endl;
}

键盘状态类

如何访问全局输入设备:键盘?sf::Keyboard 提供访问全局键盘状态的方法,这个类只提供一个静态方法 isKeyPressed,检查某个键是否处理按下状态。

长按某个键的时候,键盘按下事件虽然可以重复触发,但其触发间隔是由系统决定的,如果用键盘按下事件来实现不断移动,则移动过程是不平滑的。要实现平滑移动,一个做法是将键盘按下重复给关了,然后在键按下的时候设置一个标志,在键松开的时候取消标志,然后以自定义的间隔(比如主循环)中检查这个标志,如果是标志为按下则移动。而另一种更方便的做法就是使用键盘类的 isKeyPressed 接口。

1
2
3
4
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
{
character.move(1.f, 0.f);
}

鼠标

鼠标按下事件

SFML 支持鼠标的五个按钮:左键、右键、中键(滚轮)以及两个侧边按钮。
鼠标按钮按下事件 MouseButtonPressed 关联的成员是 mouseButton,包含三个数据,button 标识按下的是哪个按钮,在 sf::Mouse::Button 中定义,x, y 分别表示当前鼠标的位置,这个位置是相对窗口原点的,也就是相对于窗口左上角。

1
2
3
4
5
6
7
8
9
if (event.type == sf::Event::MouseButtonPressed)
{
if (event.mouseButton.button == sf::Mouse::Right)
{
std::cout << "the right button was pressed" << std::endl;
std::cout << "mouse x: " << event.mouseButton.x << std::endl;
std::cout << "mouse y: " << event.mouseButton.y << std::endl;
}
}

鼠标松开事件

鼠标按钮松开事件 MouseButtonReleased 关联的成员是 mouseButton,同鼠标按钮按下事件。

鼠标进入事件

当鼠标从窗口外移入窗口内时,鼠标进入事件 MouseEntered 触发,这个事件没有关联数据。有两点要注意的,一是进入窗口的内部区域才算进入窗口,也就是说标题栏和边框不算;二是即使窗口当前失去焦点,这个事件也能够正常触发。

鼠标离开事件

当鼠标从窗口内部移出时,鼠标离开事件 MouseLeft 触发。同鼠标进入事件。

鼠标移动事件

当鼠标在窗口内部移动时,鼠标移动事件 MouseMoved 触发,关联的成员是 mouseMove,包含两个数据,x, y 分别表示当前鼠标相对于窗口原点的位置。 同鼠标进入事件、鼠标离开事件一样,鼠标移动事件在窗口失焦的情况下也能触发,且只有在窗口内部区域移动时才触发,标题栏和边框不算。

1
2
3
4
5
if (event.type == sf::Event::MouseMoved)
{
std::cout << "new mouse x: " << event.mouseMove.x << std::endl;
std::cout << "new mouse y: " << event.mouseMove.y << std::endl;
}

鼠标滚动事件

当鼠标滚轮滚动时,鼠标滚动事件 MouseWheelScrolled 触发,关联的成员是 mouseWheelScroll,包括 4 个数据。wheel 表示滚轮滚动的方向,在 sf::Mouse::Wheel 中定义,包括水平滚动和垂直滚动,虽然我们平常用到的鼠标都是垂直滚动,但确实有滚轮水平滚动的鼠标。delta 是滚动的距离,滚轮向后滚是负值,向前滚是正值,一般是 -11x, y 还是鼠标当前的位置。

1
2
3
4
5
6
7
8
9
10
11
12
if (event.type == sf::Event::MouseWheelScrolled)
{
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
std::cout << "wheel type: vertical" << std::endl;
else if (event.mouseWheelScroll.wheel == sf::Mouse::HorizontalWheel)
std::cout << "wheel type: horizontal" << std::endl;
else
std::cout << "wheel type: unknown" << std::endl;
std::cout << "wheel movement: " << event.mouseWheelScroll.delta << std::endl;
std::cout << "mouse x: " << event.mouseWheelScroll.x << std::endl;
std::cout << "mouse y: " << event.mouseWheelScroll.y << std::endl;
}

鼠标状态类

如何访问全局输入设备:鼠标?同键盘一样,SFML 提供一个类 sf::Mouse,用于访问全局鼠标状态,这个类提供三个静态方法:

  • isButtonPressed 用于检测鼠标某个按钮是否处于按下状态;
  • getPosition 获取鼠标位置,默认是相对于桌面的全局坐标,可以把窗口作为参数传入来获得相对于窗口的坐标;
  • setPosition 强制设置鼠标位置,设置之后会立即将鼠标光标移动到对应位置,同样默认是设置全局坐标,可以把窗口传入来设置相对于窗口的坐标。
  • 没有对应的方法来获取鼠标滚轮的状态,因为滚轮的操作都是相对的,鼠标按钮有是否按下状态,键盘有是否按下状态,鼠标自身有位置,唯独鼠标滚轮没有全局状态。注意,这里说的滚轮状态是指滚轮的滚动状态,滚轮本身也是个按钮,有是否按下状态的,归于鼠标按钮是否按下状态 isButtonPressed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
{
// left mouse button is pressed: shoot
gun.fire();
}

// get the global mouse position (relative to the desktop)
sf::Vector2i globalPosition = sf::Mouse::getPosition();
// get the local mouse position (relative to a window)
sf::Vector2i localPosition = sf::Mouse::getPosition(window); // window is a sf::Window

// set the mouse position globally (relative to the desktop)
sf::Mouse::setPosition(sf::Vector2i(10, 50));
// set the mouse position locally (relative to a window)
sf::Mouse::setPosition(sf::Vector2i(10, 50), window); // window is a sf::Window

游戏杆

SFML 支持多个游戏杆,这对于多人对战的单机游戏十分实用,最多支持 8 个游戏杆和 32 个按钮,每个游戏杆会分配到一个 id,用于唯一标识这个游戏杆,这个唯一 id 其实就是其编号,即 0-7,每个按钮也有编号,这个编号是全局的,即所有游戏杆的所有按钮放在一起编号,取值 0-31。

游戏杆连接事件

当游戏杆成功连接后,事件 JoystickConnected 触发,关联的成员为 joystickConnect,只有一个数据 joystickId,即给这个游戏杆分配的 id。

1
2
if (event.type == sf::Event::JoystickConnected)
std::cout << "joystick connected: " << event.joystickConnect.joystickId << std::endl;

游戏杆断开连接事件

当游戏杆断开连接后,事件 JoystickDisconnected 触发,关联的成员为 joystickConnect,只有一个数据 joystickId,即给这个游戏杆分配的 id。

1
2
if (event.type == sf::Event::JoystickDisconnected)
std::cout << "joystick disconnected: " << event.joystickConnect.joystickId << std::endl;

游戏杆按钮按下事件

当游戏杆按钮按下时,事件 JoystickButtonPressed 会触发,关联的成员为 joystickButton,包含两个数据,joystickId 是游戏杆的编号,button 是按钮的编号。

1
2
3
4
5
6
if (event.type == sf::Event::JoystickButtonPressed)
{
std::cout << "joystick button pressed!" << std::endl;
std::cout << "joystick id: " << event.joystickButton.joystickId << std::endl;
std::cout << "button: " << event.joystickButton.button << std::endl;
}

游戏杆按钮松开事件

当游戏杆按钮松开时,事件 JoystickButtonReleased 触发,关联的成员为 joystickButton,同按下事件。

游戏杆操纵轴移动事件

SFML 支持 8 个游戏杆操纵轴,X, Y, Z, R, U, V, POV X, POV Y,这些轴如何和游戏杆映射取决于它的驱动程序。

当操作杆移动时,事件 JoystickMoved 触发,关联的成员为 joystickMove,包含三个数据,joystickId 游戏杆唯一标识,axis 是当前操纵的轴,在 sf::Joystick::Axis 中定义,position 是当前操纵杆轴的位置,其取值范围为 [-100, 100]

操纵杆轴通常非常敏感,所以 SFML 设置了一个阈值避免大量的 JoystickMoved 事件发向事件循环,小于这个阈值时将不产生 JoystickMoved 事件。可以通过 setJoystickThreshold 来调整这个阈值,默认是 0.1,可设置范围为 [0, 100],阈值越大,操纵杆越不灵敏,产生的事件越少。

1
2
3
4
5
6
7
8
9
10
window.setJoystickThreshold(0.1);
if (event.type == sf::Event::JoystickMoved)
{
if (event.joystickMove.axis == sf::Joystick::X)
{
std::cout << "X axis moved!" << std::endl;
std::cout << "joystick id: " << event.joystickMove.joystickId << std::endl;
std::cout << "new position: " << event.joystickMove.position << std::endl;
}
}

游戏杆状态类

如何访问全局输入设备:游戏杆?同鼠标键盘一样,SFML 提供一个类 sf::Joystick,用于访问全局游戏杆状态,这个类提供三个静态方法:

  • isConnected 检查某个游戏杆有没有连接;
  • getButtonCount 获取某个游戏杆的按钮数;
  • hasAxis 检查某个游戏杆是否有某个轴;
  • getAxisPosition 获取某个游戏杆的某个轴的位置;
  • isButtonPressed 检查某个按钮是否按下。

要注意的是,按钮是独立于游戏杆编号的,所以检查按钮的时候不需要传入游戏杆编号,而操纵轴是依赖于具体的游戏杆的,所以检查操纵轴的时候需要传入游戏杆编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (sf::Joystick::isConnected(0))
{
// joystick number 0 is connected
// ...
}

// check how many buttons joystick number 0 has
unsigned int buttonCount = sf::Joystick::getButtonCount(0);
// check if joystick number 0 has a Z axis
bool hasZ = sf::Joystick::hasAxis(0, sf::Joystick::Z);

// is button 1 of joystick number 0 pressed?
if (sf::Joystick::isButtonPressed(0, 1))
{
// yes: shoot!
gun.fire();
}

// what's the current position of the X and Y axes of joystick number 0?
float x = sf::Joystick::getAxisPosition(0, sf::Joystick::X);
float y = sf::Joystick::getAxisPosition(0, sf::Joystick::Y);
character.move(x, y);