SFML 能够处理 PC 程序的几乎所有事件,包括窗口关闭事件、窗口大小改变事件、失去/获得焦点事件、字符输入事件、键盘事件、鼠标事件、游戏手柄事件。另外,对象全局的输入设备,比如鼠标、键盘、游戏手柄,SFML 还提供对应的静态类,用于获取设备的各个状态,能够实现事件无法很好实现的功能。
事件
SFML 的事件使用联合(union)实现,即所有类型的事件使用同一个数据类型,使用的时候先通过 event.type
拿到事件类型,再去拿相应的成员数据,去拿不属于当前事件类型的成员数据是非法的,通常会拿到一个随机值或非法值。事件由窗口的 pollEvent
或 waitEvent
生产,通常是在主循环中迭代处理每一个事件。下面是一个典型的事件循环,
1 | void dealEvent() |
窗口关闭事件
当用户关闭窗口时,窗口关闭事件 sf::Event:Closed
会触发,比如点击关闭按钮,按下键盘快捷键等;这时候只是收到一个关闭请求,窗口并没有真正关闭,要在监听到关闭事件的时候手动调用 window.close()
才能关闭窗口。
1 | if (event.type == sf::Event::Closed) |
调整大小事件
当窗口大小改变时,调整大小事件 sf::Event:Resized
会触发,无论是用户手动去调整窗口大小或者程序通过接口 window.setSize()
来调整大小。这个事件通常用于调整渲染设置,比如 OpenGL 的渲染视口大小或者 SFML 图形的显示视图,即使窗口没什么显示的,也能调用 window.display()
来刷新视图。
1 | if (event.type == sf::Event::Resized) |
获得/失去焦点事件
当窗口获得焦点时会触发事件 sf::Event::GainFocus
,失去焦点时会触发事件 sf::Event::LostFocus
,这两个事件没有额外的数据,通常用于做一些暂停/恢复操作。当用户切换到其它窗口时,当前窗口会失去焦点,切换回当前窗口时会获得焦点,失去焦点的窗口将不再监听键盘事件。
1 | if (event.type == sf::Event::LostFocus) |
有个特别的点,监听到失去焦点事件时,如果对窗口做一些操作,比如设置窗口大小,则窗口会立即重新获得焦点,相当于使用代码来自动激活窗口。
字符输入事件
注意把这个事件和键盘事件区分开,sf::Event::TextEntered
不关心键盘操作,只关心键盘操作的结果,即最后产生的字符。特别是同时按下多个键的情况,比如同时按下 shift+1
,键盘事件会产生两个 KeyPressed
事件,但只会产生一个 TextEntered
,输入的结果为 !
。这个事件能接收键盘输入的任意字符,不仅限于 ASCII 字符,所以其事件数据为一个 Unicode
码,如果其值小于 128,可将其转成 ASCII 字符。另外,输入不可打印的字符也会触发字符输入事件,比如在键盘上敲退格,会产生一个 TextEntered
事件,其数据为 evt.text.unicode==8
,但无法转成可打印字符。
1 | if (evt.type == sf::Event::TextEntered) |
键盘
键盘按下事件
当键盘按下一个键时,会触发键盘按下事件 KeyPressed
,产生一个 code
,用于标识按下的是哪个键。要注意区分这个 code
和字符输入事件的 unicode
,evt.key.code
在 sf::Keyboard::Key
中定义,而 evt.text.unicode
是产生的字符对应的 unicode 码;键盘事件不区分大小写字母,而字符输入事件会区分大写字母和小写字母,比如分别按下键盘的 a 键和 A 键,键盘事件都只会产生 code==0
,而字符输入事件分别会产生 unicode==97
和 unicode==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 | window.setKeyRepeatEnabled(false); |
键盘松开事件
当松开一个键时,键盘松开事件 KeyReleased
会触发,其数据和键盘按下事件完全一样,有一个标识哪个键的 code
以及检测四个修饰键是否按下。很明显,不同于按下事件,同一个键是不可能会重复产生松开事件的。
1 | if (evt.type == sf::Event::Released) |
键盘状态类
如何访问全局输入设备:键盘?sf::Keyboard
提供访问全局键盘状态的方法,这个类只提供一个静态方法 isKeyPressed
,检查某个键是否处理按下状态。
长按某个键的时候,键盘按下事件虽然可以重复触发,但其触发间隔是由系统决定的,如果用键盘按下事件来实现不断移动,则移动过程是不平滑的。要实现平滑移动,一个做法是将键盘按下重复给关了,然后在键按下的时候设置一个标志,在键松开的时候取消标志,然后以自定义的间隔(比如主循环)中检查这个标志,如果是标志为按下则移动。而另一种更方便的做法就是使用键盘类的 isKeyPressed
接口。
1 | if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) |
鼠标
鼠标按下事件
SFML 支持鼠标的五个按钮:左键、右键、中键(滚轮)以及两个侧边按钮。
鼠标按钮按下事件 MouseButtonPressed
关联的成员是 mouseButton
,包含三个数据,button
标识按下的是哪个按钮,在 sf::Mouse::Button
中定义,x, y
分别表示当前鼠标的位置,这个位置是相对窗口原点的,也就是相对于窗口左上角。
1 | if (event.type == sf::Event::MouseButtonPressed) |
鼠标松开事件
鼠标按钮松开事件 MouseButtonReleased
关联的成员是 mouseButton
,同鼠标按钮按下事件。
鼠标进入事件
当鼠标从窗口外移入窗口内时,鼠标进入事件 MouseEntered
触发,这个事件没有关联数据。有两点要注意的,一是进入窗口的内部区域才算进入窗口,也就是说标题栏和边框不算;二是即使窗口当前失去焦点,这个事件也能够正常触发。
鼠标离开事件
当鼠标从窗口内部移出时,鼠标离开事件 MouseLeft
触发。同鼠标进入事件。
鼠标移动事件
当鼠标在窗口内部移动时,鼠标移动事件 MouseMoved
触发,关联的成员是 mouseMove
,包含两个数据,x, y
分别表示当前鼠标相对于窗口原点的位置。 同鼠标进入事件、鼠标离开事件一样,鼠标移动事件在窗口失焦的情况下也能触发,且只有在窗口内部区域移动时才触发,标题栏和边框不算。
1 | if (event.type == sf::Event::MouseMoved) |
鼠标滚动事件
当鼠标滚轮滚动时,鼠标滚动事件 MouseWheelScrolled
触发,关联的成员是 mouseWheelScroll
,包括 4 个数据。wheel
表示滚轮滚动的方向,在 sf::Mouse::Wheel
中定义,包括水平滚动和垂直滚动,虽然我们平常用到的鼠标都是垂直滚动,但确实有滚轮水平滚动的鼠标。delta
是滚动的距离,滚轮向后滚是负值,向前滚是正值,一般是 -1
和 1
。x, y
还是鼠标当前的位置。
1 | if (event.type == sf::Event::MouseWheelScrolled) |
鼠标状态类
如何访问全局输入设备:鼠标?同键盘一样,SFML 提供一个类 sf::Mouse
,用于访问全局鼠标状态,这个类提供三个静态方法:
isButtonPressed
用于检测鼠标某个按钮是否处于按下状态;getPosition
获取鼠标位置,默认是相对于桌面的全局坐标,可以把窗口作为参数传入来获得相对于窗口的坐标;setPosition
强制设置鼠标位置,设置之后会立即将鼠标光标移动到对应位置,同样默认是设置全局坐标,可以把窗口传入来设置相对于窗口的坐标。- 没有对应的方法来获取鼠标滚轮的状态,因为滚轮的操作都是相对的,鼠标按钮有是否按下状态,键盘有是否按下状态,鼠标自身有位置,唯独鼠标滚轮没有全局状态。注意,这里说的滚轮状态是指滚轮的滚动状态,滚轮本身也是个按钮,有是否按下状态的,归于鼠标按钮是否按下状态
isButtonPressed
。
1 | if (sf::Mouse::isButtonPressed(sf::Mouse::Left)) |
游戏杆
SFML 支持多个游戏杆,这对于多人对战的单机游戏十分实用,最多支持 8 个游戏杆和 32 个按钮,每个游戏杆会分配到一个 id,用于唯一标识这个游戏杆,这个唯一 id 其实就是其编号,即 0-7,每个按钮也有编号,这个编号是全局的,即所有游戏杆的所有按钮放在一起编号,取值 0-31。
游戏杆连接事件
当游戏杆成功连接后,事件 JoystickConnected
触发,关联的成员为 joystickConnect
,只有一个数据 joystickId
,即给这个游戏杆分配的 id。
1 | if (event.type == sf::Event::JoystickConnected) |
游戏杆断开连接事件
当游戏杆断开连接后,事件 JoystickDisconnected
触发,关联的成员为 joystickConnect
,只有一个数据 joystickId
,即给这个游戏杆分配的 id。
1 | if (event.type == sf::Event::JoystickDisconnected) |
游戏杆按钮按下事件
当游戏杆按钮按下时,事件 JoystickButtonPressed
会触发,关联的成员为 joystickButton
,包含两个数据,joystickId
是游戏杆的编号,button
是按钮的编号。
1 | if (event.type == sf::Event::JoystickButtonPressed) |
游戏杆按钮松开事件
当游戏杆按钮松开时,事件 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 | window.setJoystickThreshold(0.1); |
游戏杆状态类
如何访问全局输入设备:游戏杆?同鼠标键盘一样,SFML 提供一个类 sf::Joystick
,用于访问全局游戏杆状态,这个类提供三个静态方法:
isConnected
检查某个游戏杆有没有连接;getButtonCount
获取某个游戏杆的按钮数;hasAxis
检查某个游戏杆是否有某个轴;getAxisPosition
获取某个游戏杆的某个轴的位置;isButtonPressed
检查某个按钮是否按下。
要注意的是,按钮是独立于游戏杆编号的,所以检查按钮的时候不需要传入游戏杆编号,而操纵轴是依赖于具体的游戏杆的,所以检查操纵轴的时候需要传入游戏杆编号。
1 | if (sf::Joystick::isConnected(0)) |