如何在多个事件循环中正确使用 SDL_PeepEvents?

How do I use SDL_PeepEvents properly in multiple event loops?

我想做一件简单的事情 — 在具有处理 window 逻辑的单元中,我想更新 window(第一个事件循环),然后在另一个具有例如逻辑的单元键盘处理,更新其状态(第二个事件循环)。 Window 事件必须放回 SDL queue 以便在更新键盘时可见(以便能够检查 window 是否处于焦点状态)。

简而言之,有一个事件queue(SDL内部),但是有很多循环读取事件。如果一个事件需要在多个循环中可见,处理完后必须return到queue。为了使这成为可能,每个循环都应该迭代 queue 的内容,以便不处理在处理后被推回 queue 的事件。

为此,我使用 SDL_PeepEvents,其中 return 是 queued 事件的数量,并在 for 循环中迭代了这么多次。测试程序代码如下所示:

uses
  SDL2;

  procedure UpdateWindow();
  var
    Event: TSDL_Event;
    Index: Integer;
  begin
    for Index := 0 to SDL_PeepEvents(nil, -1, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) - 1 do
    begin
      SDL_PollEvent(@Event);

      if Event.Type_ = SDL_WINDOWEVENT then
        case Event.Window.Event of
          SDL_WINDOWEVENT_FOCUS_GAINED: WriteLn('focus gain - window');
          SDL_WINDOWEVENT_FOCUS_LOST:   WriteLn('focus lost - window');
        end;

      // put all events back in the queue to be seen in other event loops
      SDL_PushEvent(@Event);
    end;
  end;

  procedure UpdateKeyboard();
  var
    Event: TSDL_Event;
    Index: Integer;
  begin
    for Index := 0 to SDL_PeepEvents(nil, -1, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) - 1 do
    begin
      SDL_PollEvent(@Event);

      // reading window events a second time
      if Event.Type_ = SDL_WINDOWEVENT then
        case Event.Window.Event of
          SDL_WINDOWEVENT_FOCUS_GAINED: WriteLn('focus gain - keyboard');
          SDL_WINDOWEVENT_FOCUS_LOST:   WriteLn('focus lost - keyboard');
        end;

      SDL_PushEvent(@Event);
    end;
  end;

var
  Window: PSDL_Window;
  Done: Boolean = False;
begin
  SDL_Init(SDL_INIT_EVERYTHING);
  Window := SDL_CreateWindow('Events test', SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);

  while not Done do
  begin
    SDL_PumpEvents();

    UpdateWindow();
    UpdateKeyboard();

    // clear events queue
    SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
    SDL_Delay(20);
  end;

  SDL_DestroyWindow(Window);
  SDL_Quit();
end.

以上代码有效,但仅在某些时候有效。当程序启动并出现 window 时,有时它说 window 和键盘处于焦点状态,有时只是 window 只有焦点。这意味着 SDL_WINDOWEVENT_FOCUS_GAINED 事件在第一个循环中被处理,但在第二个循环中没有看到(即使它被添加回 queue)。

在控制台 window 和 SDL window 可见的情况下,我交替单击两者,激活和停用 SDL window。每次激活 SDL window 时,应该向控制台添加两行(一行用于事件循环),在停用 window 时也是如此。不幸的是,单击时控制台的内容如下所示:

focus gain - window
focus gain - keyboard
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus gain - keyboard
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard
focus gain - window
focus lost - window
focus lost - keyboard

{...}

如您所见,有时在激活 window 时,第二个循环看不到 SDL_WINDOWEVENT_FOCUS_GAINED 事件,因此它不会将 focus gained - keyboard 行添加到安慰。尽管第一个循环中的每个事件都使用 SDL_PushEvent.

queued 返回,但此事件在某处丢失了

问题:如何在多个循环中正确处理 SDL 事件以及如何将已处理的事件放回 queue 以便它们对多个循环可见?

Lazarus 2.2.0、FPC 3.2.2、SDL2-for-Pascal headers 和 SDL 2.0.22.0 — 均为 64 位。

如果可能的话,完全不要那样做。最好的解决方案是只获取一次事件。如果您想多次处理事件,没有什么能阻止您将感兴趣的所有事件提取到单个数组中,并根据需要多次迭代该数组。您可以通过单个 SDL_PeepEvents(SDL_GETEVENT) 调用将所有事件提取到大数组中。

您遇到的问题基于两个因素:SDL_PollEvent 的 return 值在队列耗尽时为 0;但在文档中没有任何地方指出 SDL_PushEvent 将事件放入同一处理帧 - 即 PollEvent 可能表示队列已耗尽并且 return 您新推送的事件在下一帧。如果发生这种情况,在你的第二个循环中你错误地处理了一个事件(因为 PollEvent 说 0 并且没有数据填充到 Event 结构中),并跳过最后一个事件。

但是,保留更高级别的逻辑,您有两个选择:

  1. 使用 SDL_PeepEvent 获取事件,而不是 SDL_PollEvent。 IE。将 SDL_PollEvent(@Event) 替换为 SDL_PeepEvent(@Event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT).

  2. 检查 SDL_PollEvent 的 return 值,如果它 returns 0 - 你必须再次调用它,因为这意味着没有弹出事件从队列。此解决方案容易出现竞争条件,您不能保证它不会在幕后调用 SDL_PumpEvents,并且您最后的 SDL_FlushEvents 可能会终止一些未处理的事件。