mosra/magnum

[Feature Request] Split mainLoopIteration() methods

AndreasLrx opened this issue · 7 comments

Currently mainLoopIteration methods (for the different platform applications) does multiple actions that could be split in 3 other methods:

  • handleEvents: any events including the window close check
  • update: tickEvent(), when available (like SDL)
  • draw: all drawing stuff (and associated drawEvent)

This would allow a better control when making custom game loop.

mosra commented

Sure, I'm not opposed to this idea. But first I'd like to know more about your use case, and what exactly is hard / impossible to achieve with the current API.

I'm starting a small game engine for personnal use (mostly as a learning project) with Magnum for rendering/physics functionalities.

Looking at the game loop of Magnum applications, you handle the update going faster than expected minimalPeriod (with a delay). By doing something like this:

while (true)
{
  double start = getCurrentTime();
  processInput();
  update();
  render();

  sleep(start + MS_PER_UPDATE- getCurrentTime());
}

However you do not handle the opposite case: update taking more time than expected. This case could be handled like this:

double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;

  processInput();

  if (lag > MS_PER_UPDATE * 10)
    lag = MS_PER_UPDATE * 10;
  while (lag >= MS_PER_UPDATE)
  {
    update();
    lag -= MS_PER_UPDATE;
  }

  render();
}

This is a possible way to manage lagging but isn't the only one. Allowing separated functions call like I suggested above would allow users to handle it the way they want.

(For more informations see https://gameprogrammingpatterns.com/game-loop.html)

mosra commented

I see, that makes a lot of sense, thanks!

How about this:

  • I break the mainLoopIteration() into three pieces, mainLoopEventIteration(), mainLoopTickEventIteration() and mainLoopDrawEventIteration()
  • Each of those would contain also the logic like "don't call into drawEvent() if nothing was drawn", "don't call into tickEvent() if it's not implemented", so you don't need to do that yourself.
  • And the mainLoopIteration() would itself call into these three and only handle the delays on top. So if you don't use it, handling the delays would be completely your responsibility.

Yes this sounds great ;)

mosra commented

Sorry for going back to the drawing board but ... while attempting to merge #580 I realized the new split workflow adds a lot of potential for errors and the set of states that need to be handled is quite hard to get right on the app side. Even documenting the process is rather complex.

So, going back to your example snippets above, why wouldn't something like this work as well, without splititng anything? Assuming you don't call setMinimalLoopPeriod() and rely just on VSync to avoid the sleep(), it's doing exactly the same thing as yours, as far as I can see:

double previous = getCurrentTime();
double lag = 0.0;

YourApplication::tickEvent() {
  if (lag > MS_PER_UPDATE * 10)
    lag = MS_PER_UPDATE * 10;
  while (lag >= MS_PER_UPDATE)
  {
    update();
    lag -= MS_PER_UPDATE;
  }
}

while(true) {
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;

  // does the following: 
  //   processInput();
  //   tickEvent();
  //   render();
  app.mainLoopIteration();
}

Or am I missing something? The point here is that if you need to perform updates at regular intervals, you do it yourself in the tickEvent().

No worries, I don't want to cause any regressions 😉

Your idea totally works, I don't know why I haven't thought of it before. It would have saved time to both of us 😅

mosra commented

Yay! I'll salvage the GLFW tickEvent() addition from #580 at least, it's not like your whole work would go to waste ;) Thanks for that!