SDL简单教程
第三话: SDL-事件处理
前言
SDL2(Simple DirectMedia Layer 2)是一个跨平台的多媒体库。它提供了对音频、键盘、鼠标、游戏控制器和图形硬件(通过 OpenGL、Vulkan 等)的低级访问接口,主要用于开发游戏和其他交互式多媒体应用程序。本章介绍SDL的事件处理。
第三话: 事件处理
3.1 理解SDL2事件系统
-
事件队列的概念与工作原理
- 事件的产生与存储:在SDL2中,当用户与应用程序交互(如按下键盘按键、移动鼠标、调整窗口大小等)或者系统发生某些相关情况(如窗口获得或失去焦点)时,会产生相应的事件。这些事件会被存储在一个称为事件队列的缓冲区中。事件队列是一种先进先出(FIFO)的数据结构,新产生的事件会被添加到队列的末尾。
- 事件的获取顺序:应用程序需要按照事件产生的顺序来处理它们,所以从事件队列中获取事件时,会先获取最早产生但尚未处理的事件。这种顺序确保了应用程序对用户操作的响应是符合逻辑的,例如,用户先按下一个键,然后移动鼠标,程序会先处理按键事件,再处理鼠标移动事件。
-
事件类型的多样性
- 用户输入事件:
- 键盘事件:包括
SDL_KEYDOWN
(键盘按键按下)和SDL_KEYUP
(键盘按键松开)。当用户按下或松开键盘上的任意键时,会产生相应的键盘事件。这些事件包含了按键的信息,如按下的是哪个键(通过SDL_Keysym
结构体中的sym
成员来标识,例如SDLK_a
表示字母‘a’键)以及是否有修饰键(如Ctrl、Shift、Alt等,通过SDL_Keysym
结构体中的mod
成员来判断)同时按下。 - 鼠标事件:涵盖
SDL_MOUSEMOTION
(鼠标移动)、SDL_MOUSEBUTTONDOWN
(鼠标按键按下)、SDL_MOUSEBUTTONUP
(鼠标按键松开)和SDL_MOUSEWHEEL
(鼠标滚轮滚动)。对于鼠标移动事件,会包含鼠标当前的位置坐标(相对坐标或绝对坐标,取决于鼠标模式)。鼠标按键按下和松开事件会指出按下或松开的是哪个鼠标按键(如SDL_BUTTON_LEFT
表示鼠标左键)。鼠标滚轮滚动事件则包含滚轮滚动的方向和幅度信息。
- 键盘事件:包括
- 窗口事件:
SDL_WINDOWEVENT_SHOWN
:当窗口从隐藏状态变为显示状态时触发。SDL_WINDOWEVENT_HIDDEN
:与SDL_WINDOWEVENT_SHOWN
相反,当窗口从显示状态变为隐藏状态时触发。SDL_WINDOWEVENT_EXPOSED
:当窗口的全部或部分内容需要重新绘制时触发,例如当窗口被其他窗口遮挡后重新显示时。SDL_WINDOWEVENT_MOVED
:当窗口的位置发生改变时触发,事件中包含新的窗口位置信息。SDL_WINDOWEVENT_RESIZED
:当窗口大小被调整时触发,同时会传递新的窗口宽度和高度信息,程序可以根据这些信息来重新调整窗口内的内容布局。SDL_WINDOWEVENT_MINIMIZED
:当窗口被最小化时触发。SDL_WINDOWEVENT_MAXIMIZED
:当窗口被最大化时触发。SDL_WINDOWEVENT_RESTORED
:当窗口从最小化或最大化状态恢复到正常状态时触发。
- 系统事件:
SDL_QUIT
:这是一个非常重要的事件,表示应用程序应该退出。通常在用户点击窗口的关闭按钮或者操作系统要求应用程序关闭时触发。SDL_APP_TERMINATING
:当操作系统决定终止应用程序时触发,这可能是由于系统关机、资源不足等原因。SDL_APP_LOWMEMORY
:当系统内存不足时触发,应用程序可以在这个时候尝试释放一些不必要的资源。
- 用户输入事件:
3.2 获取和处理基本事件
-
SDL_Event
结构体详解- 结构体成员介绍:
SDL_Event
结构体用于存储一个事件的所有信息。它是一个联合体(union),其中不同的成员对应不同类型的事件。例如:type
成员:这是一个Uint32
类型的值,用于标识事件的类型。它可以是上述提到的各种事件类型常量(如SDL_KEYDOWN
、SDL_MOUSEMOTION
等)。通过检查这个值,可以确定当前事件是哪种类型,然后进一步处理相应的成员数据。- 对于键盘事件相关成员(当
type
为SDL_KEYDOWN
或SDL_KEYUP
时):key
成员:这是一个SDL_KeyboardEvent
结构体,其中包含了键盘事件的详细信息。key.keysym.sym
表示按下或松开的键码(如SDLK_ESCAPE
表示Esc键),key.keysym.mod
表示修饰键的状态(如KMOD_CTRL
表示Ctrl键按下),key.state
表示是按下(SDL_PRESSED
)还是松开(SDL_RELEASED
)状态。
- 对于鼠标事件相关成员(当
type
为SDL_MOUSEMOTION
、SDL_MOUSEBUTTONDOWN
、SDL_MOUSEBUTTONUP
或SDL_MOUSEWHEEL
时):motion
成员(当type
为SDL_MOUSEMOTION
时):这是一个SDL_MouseMotionEvent
结构体,包含鼠标的位置信息。motion.x
和motion.y
表示鼠标当前的坐标(坐标的参考系取决于鼠标模式,可能是相对于窗口或屏幕),motion.xrel
和motion.yrel
表示鼠标相对上一次位置的偏移量(在相对鼠标模式下使用)。button
成员(当type
为SDL_MOUSEBUTTONDOWN
或SDL_MOUSEBUTTONUP
时):这是一个SDL_MouseButtonEvent
结构体,其中button.button
表示按下或松开的鼠标按键(如SDL_BUTTON_LEFT
、SDL_BUTTON_MIDDLE
、SDL_BUTTON_RIGHT
),button.x
和button.y
表示鼠标按键操作时的位置坐标。wheel
成员(当type
为SDL_MOUSEWHEEL
时):这是一个SDL_MouseWheelEvent
结构体,wheel.x
和wheel.y
分别表示水平和垂直滚轮的滚动量,正值表示向前(上)滚动,负值表示向后(下)滚动。
- 对于窗口事件相关成员(当
type
为各种窗口事件类型时):window
成员:这是一个SDL_WindowEvent
结构体,包含窗口相关的信息。例如,window.data1
和window.data2
在不同的窗口事件中有不同的含义,对于SDL_WINDOWEVENT_RESIZED
事件,window.data1
是新的窗口宽度,window.data2
是新的窗口高度。
- 结构体成员介绍:
-
SDL_PollEvent
函数深入剖析- 函数原型:
int SDL_PollEvent(SDL_Event* event);
- 参数含义:
event
是一个指向SDL_Event
结构体的指针。这个函数会从事件队列中取出一个事件,并将其信息填充到event
所指向的结构体中。 - 返回值分析:如果事件队列中有事件,函数返回1,并将事件信息存储到
event
结构体中;如果事件队列中没有事件,函数返回0;如果发生错误,函数返回 - 1,可以通过SDL_GetError
函数获取错误信息。以下是一个简单的示例,展示如何使用SDL_PollEvent
函数来获取和处理不同类型的事件:
- 函数原型:
#include <SDL.h>
#include <iostream>
int main(int argc, char* argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return 1;
}
SDL_Window* window = SDL_CreateWindow("My Window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);
if (window == nullptr)
{
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
SDL_Quit();
return 1;
}
SDL_bool running = SDL_TRUE;
SDL_Event e;
while (running)
{
while (SDL_PollEvent(&e))
{
switch (e.type)
{
case SDL_QUIT:
running = SDL_FALSE;
break;
case SDL_KEYDOWN:
if (e.key.keysym.sym == SDLK_ESCAPE)
{
running = SDL_FALSE;
}
else if (e.key.keysym.sym == SDLK_UP)
{
std::cout << "Up arrow key pressed." << std::endl;
}
break;
case SDL_MOUSEMOTION:
std::cout << "Mouse moved to (" << e.motion.x << ", " << e.motion.y << ")" << std::endl;
break;
case SDL_MOUSEBUTTONDOWN:
if (e.button.button == SDL_BUTTON_LEFT)
{
std::cout << "Left mouse button pressed at (" << e.button.x << ", " << e.button.y << ")" << std::endl;
}
break;
case SDL_WINDOWEVENT_RESIZED:
std::cout << "Window resized to width: " << e.window.data1 << ", height: " << e.window.data2 << std::endl;
break;
default:
break;
}
}
}
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
SDL_WaitEvent
函数介绍与对比- 函数原型:
int SDL_WaitEvent(SDL_Event* event);
- 功能特点:与
SDL_PollEvent
函数不同,SDL_WaitEvent
函数会阻塞程序执行,直到有事件发生。它会一直等待,直到事件队列中有事件可用,然后将该事件信息填充到event
所指向的结构体中,并返回1。如果发生错误,函数返回 - 1。这种阻塞式的事件获取方式适用于一些简单的应用程序,或者在某些特定的情况下,比如在程序启动时等待用户的初始操作。然而,在需要同时进行其他操作(如实时更新游戏画面、后台处理等)的复杂应用程序中,SDL_PollEvent
函数更为常用,因为它不会阻塞程序的其他部分执行。以下是一个使用SDL_WaitEvent
函数的简单示例:
- 函数原型:
#include <SDL.h>
#include <iostream>
int main(int argc, char* argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return 1;
}
SDL_Window* window = SDL_CreateWindow("My Window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);
if (window == nullptr)
{
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
SDL_Quit();
return 1;
}
SDL_Event e;
if (SDL_WaitEvent(&e))
{
switch (e.type)
{
case SDL_QUIT:
std::cout << "Received quit event." << std::endl;
break;
case SDL_KEYDOWN:
if (e.key.keysym.sym == SDLK_ESCAPE)
{
std::cout << "Escape key pressed." << std::endl;
}
break;
default:
break;
}
}
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
- 事件过滤机制(高级用法)
SDL_SetEventFilter
函数:- 函数原型:
void SDL_SetEventFilter(SDL_EventFilter filter, void* userdata);
- 功能描述:这个函数用于设置一个事件过滤器。事件过滤器是一个用户自定义的函数,它会在每个事件被处理之前被调用。
filter
参数是一个指向事件过滤函数的指针,userdata
参数是一个用户自定义的数据指针,可以传递给事件过滤函数。事件过滤函数的原型如下:
- 函数原型:
typedef int (*SDL_EventFilter)(void* userdata, SDL_Event* event);
- 使用示例:假设我们只想处理特定窗口内的鼠标事件,可以通过以下方式实现事件过滤。首先定义事件过滤函数:
int myEventFilter(void* userdata, SDL_Event* event)
{
SDL_Window* targetWindow = (SDL_Window*)userdata;
if (event->type == SDL_MOUSEMOTION || event->type == SDL_MOUSEBUTTONDOWN || event->type == SDL_MOUSEBUTTONUP)
{
if (event->motion.windowID!= SDL_GetWindowID(targetWindow))
{
return 0; // 过滤掉不是目标窗口的鼠标事件
}
}
return 1; // 允许事件通过
}
然后在程序中设置事件过滤器:
SDL_SetEventFilter(myEventFilter, window);
这样,只有在目标窗口内产生的鼠标事件才会被进一步处理。
SDL_AddEventWatch
和SDL_DelEventWatch
函数(类似功能):SDL_AddEventWatch
函数原型:void SDL_AddEventWatch(SDL_EventFilter filter, void* userdata);
SDL_DelEventWatch
函数原型:void SDL_DelEventWatch(SDL_EventFilter filter, void* userdata);
- 功能描述:
SDL_AddEventWatch
函数用于添加一个事件观察器,它的工作原理与事件过滤器类似,但是事件观察器不会阻止事件的传递,而是在事件处理之前和之后都可以执行一些额外的操作。SDL_DelEventWatch
函数用于删除之前添加的事件观察器。例如,可以使用事件观察器来记录所有事件的发生情况,或者在特定事件发生时执行一些调试操作:
int eventWatcher(void* userdata, SDL_Event* event)
{
std::cout << "Event type: " << event->type << " occurred." << std::endl;
return 1;
}
// 添加事件观察器
SDL_AddEventWatch(eventWatcher, nullptr);
// 在程序的某个地方,可以删除事件观察器
// SDL_DelEventWatch(eventWatcher, nullptr);
- 自定义事件(高级用法)
SDL_RegisterEvents
函数:- 函数原型:
int SDL_RegisterEvents(int numevents);
- 功能描述:这个函数用于向SDL2事件系统注册自定义事件类型。
numevents
参数表示要注册的事件数量。它返回一个起始的事件类型值,这个值大于所有预定义的SDL2事件类型。例如,如果要注册3个自定义事件,可以这样使用:
- 函数原型:
int customEventBase = SDL_RegisterEvents(3);
if (customEventBase >= 0)
{
// 自定义事件类型定义
const int CUSTOM_EVENT_1 = customEventBase;
const int CUSTOM_EVENT_2 = customEventBase + 1;
const int CUSTOM_EVENT_3 = customEventBase + 2;
}
SDL_PushEvent
函数:- 函数原型:
int SDL_PushEvent(SDL_Event* event);
- 功能描述:用于将一个自定义事件添加到事件队列中。需要先创建一个
SDL_Event
结构体,并设置其type
成员为自定义的事件类型,然后填充其他相关信息(如果有),最后使用SDL_PushEvent
函数将其添加到队列中。例如,假设我们有一个自定义事件表示游戏中的某个特殊情况:
- 函数原型:
#include <SDL.h>
#include <iostream>
#include <chrono>
#include <thread>
// 自定义游戏事件结构体
struct CustomGameEvent
{
int type;
int data;
};
int main(int argv, char* argc[])
{
// 注册自定义事件的数量
const int NUM_CUSTOM_EVENTS = 1;
int customEventBase;
// 初始化 SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl;
return -1;
}
// 创建 SDL 窗口
SDL_Window* window = SDL_CreateWindow("My Window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
if (window == nullptr)
{
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl;
SDL_Quit();
return -1;
}
SDL_bool running = SDL_TRUE; // 主循环控制变量
SDL_Event e; // 事件对象
bool some_condition = false; // 条件控制变量
std::chrono::steady_clock::time_point last_time = std::chrono::steady_clock::now(); // 上次时间记录
// 注册自定义事件
customEventBase = SDL_RegisterEvents(NUM_CUSTOM_EVENTS);
if (customEventBase == ((Uint32)-1)) {
std::cerr << "SDL_RegisterEvents failed: Unable to allocate custom event type" << std::endl;
SDL_DestroyWindow(window);
SDL_Quit();
return -1;
}
const int CUSTOM_GAME_EVENT = customEventBase; // 自定义事件类型
while (running)
{
std::chrono::steady_clock::time_point current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(current_time - last_time);
// 检查是否经过了5秒
if (elapsed.count() >= 5)
{
some_condition = true;
last_time = current_time;
}
// 如果满足某个条件,则触发自定义事件
if (some_condition)
{
CustomGameEvent custom_event;
custom_event.type = CUSTOM_GAME_EVENT;
custom_event.data = 52;
SDL_Event sdl_custom_event;
sdl_custom_event.type = custom_event.type;
// 设置自定义事件的数据
sdl_custom_event.user.type = CUSTOM_GAME_EVENT;
sdl_custom_event.user.code = custom_event.data;
// 推送事件到事件队列
if (SDL_PushEvent(&sdl_custom_event) < 0)
{
std::cerr << "SDL_PushEvent failed: " << SDL_GetError() << std::endl;
}
some_condition = false;
}
// 处理事件队列中的事件
while (SDL_PollEvent(&e))
{
switch (e.type)
{
case SDL_QUIT:
running = SDL_FALSE; // 退出程序
break;
case SDL_USEREVENT:
if (e.user.type == CUSTOM_GAME_EVENT)
{
std::cout << "Custom game event occurred with data: " << e.user.code << std::endl;
}
break;
default:
break;
}
}
// 这里可以添加渲染代码,例如清除屏幕等
}
// 清理资源
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
在上述代码中:
-
首先通过 SDL_RegisterEvents 函数注册了一个自定义事件类型。如果注册成功,获得一个新的事件类型编号。
-
在主循环中,每经过 5 秒,条件 some_condition 被设为 true。此时,创建一个 CustomGameEvent 结构体,填充相关数据后,将其转换为 SDL_Event 结构体,并通过 SDL_PushEvent 函数添加到事件队列中。
-
在事件处理部分,使用 SDL_PollEvent 检查事件队列。当捕获到 SDL_QUIT 时,程序退出。当捕获到自定义事件 (SDL_USEREVENT) 时,根据事件类型检查,输出自定义数据。
程序退出时,清理资源并关闭 SDL。
通过自定义事件,可以更灵活地设计应用程序的逻辑,尤其是在处理一些特定于应用程序内部的交互或状态变化情况时非常有用。同时,要注意合理使用自定义事件,避免过度复杂的事件系统导致代码难以维护。
总结
本章围绕 SDL2 事件处理展开。首先阐述了事件系统相关概念,包括事件队列。事件队列是先进先出结构,用户交互(如键盘、鼠标操作)和系统相关情况(如窗口状态改变)产生的事件按顺序存储其中,程序按此顺序获取处理,保证逻辑响应正确。