2
\$\begingroup\$

I wrote some games years ago, and they were always in sync with the main thread. So, for example if the monitor refresh rate is 60 Hz it would look like below.

OnPaint =
  Advance game 1/60th of a second
  Render at 60 FPS

This works well for arcade type games, but not necessarily for other game types.

I want to run something more like this:
  Game logic at 10 FPS
  Render at 60 FPS

This is mainly because the "game logic" takes much longer than 16 ms now.

I still want to render at 60 FPS, because the animations run faster than 10 FPS, and also the moving of sprites needs to be smooth. I don't see the point in moving a sprite from 0 to 10 to 20, etc. It should move at a higher constant rate, e.g. from 0 to 1 to 2, etc.

So, I was thinking of adding a game logic thread:

Game thread =
  Setup animations
  Setup physics, e.g. move object to x/y at some speed, and direction, etc

OnPaint =
  Update animations and sprite positions
  Render

This then means there has to be some kind of sprite object between the two threads.

ISprite = interface
  x/y: integer
  width/height: integer;
  image: PImage;
end;

So, the game logic then sets up the x/y, the image (or animation), and the OnPaint then renders it to the screen. The OnPaint also advances the x/y according to the direction and speed, which makes for smooth moving.

So, the game logic might say "animate sequence X on sprite Z", or "move sprite Z to x/y at speed/direction" (the physics), and the OnPaint then takes whatever frames to complete that smoothly.

I would need to add a lock for accessing the sprite data.

ISprite = interface
  BeginUpdate(); // acquire lock
  x/y: integer
  width/height: integer;
  image: PImage;
  EndUpdate(); // release lock
end;

The idea is to lock the entire object, so there won't be any glitches where the sprite appears at the wrong location temporarily, because the render came after setting X, but before setting Y, etc.

I will also have to be extremely careful with other objects, e.g. the list of sprites needed in both threads, etc.

Is all that even right?!? Is there an easier way to achieve all this? Am I missing something obvious here? :)

Maybe I should just compromise and run the game logic, and the rendering all at say 40 FPS. Or 30. At least then, I won't need any threads...

Any thoughts on this? :)

\$\endgroup\$
3
  • \$\begingroup\$ How about both the logic frame and the render frame are in the same thread? The logic frame is executed every 6 renders. This will avoid communication between threads. \$\endgroup\$
    – Mangata
    Commented Apr 24, 2022 at 8:16
  • \$\begingroup\$ @Mangata: I'm fairly certain that the game logic will be more than the time for one frame, e.g. more than 16 ms. I'll have a think about it. Thanks. \$\endgroup\$
    – Jarno
    Commented Apr 24, 2022 at 8:40
  • 1
    \$\begingroup\$ In my experience, using lock directly will be slow,maybe you should create a channel between threads. : ) And maybe this question will be helpful? \$\endgroup\$
    – Mangata
    Commented Apr 24, 2022 at 9:54

1 Answer 1

1
\$\begingroup\$

You may want to consider triple buffering your game state (or at least the display state), instead of locking individual sprites. This gives you several advantages:

  • It's faster, because you don't have the overhead of taking and releasing locks hundreds or thousands of times in every update/draw.

  • It's simpler, because the thread management logic can stay in one place external to your game entities, rather than its tendrils reaching down into every entity you might want to update.

  • It ensures you never draw an inconsistent state, where some sprites have been updated to the next time step while others haven't yet, which could lead to visual contradictions and glitches.

  • Having multiple copies of the game data helps you implement interpolation, to smooth out the rendering of movement between simulation updates.

  • It might even help you get more parallelism in your simulation tick, to improve your update rate. You could spread the work of updating each component or entity across multiple threads that can simultaneously read the unmodified previous state, without risk of them stepping on each other and overwriting data that another part of the sim process is trying to read.

It would work something like this: you have three copies of a data structure giving the current position/orientation/size/image of each active sprite, and a counter or pointers to identify them as "previous" "current" and "next".

Your simulation thread writes to "next" while "previous" and "current" remain read-only for the rendering thread to use. Each time your simulation thread finishes an update, it swaps the pointers/counter so that "next" becomes "current", "current" becomes "previous", and "previous" gets marked as "next", to be overwritten in its next tick. This is the only place in your code you need thread synchronization — or you could even add an extra buffer so you can start your next update lock-free without overwriting the "previous" buffer that might still be in use for interpolating the renderer state.

Your rendering thread then just takes the current and previous buffers at the start of the frame and computes an interpolation factor between them, based on the elapsed time. It loops over each sprite, looking up its corresponding info in the previous and current data structures, and rendering it at an interpolated position/orientation/size between the two states. If its image changes between the two states, you choose one image or the other to draw.

A sprite that exists in only one buffer or the other can be skipped — so newly spawned sprites appear and destroyed sprites disappear at frame corresponding to the start of the next sim tick.

\$\endgroup\$
1
  • \$\begingroup\$ Hi DMGregory, thanks for taking the time to write your suggestions (I have printed them). It has given me much to think about. :) I don't think it will solve all problems, but I'll give it some more thought over the next few days. The idea of having next, current, and previous, makes a lot of sense, but multi-tasking is hard... ;) \$\endgroup\$
    – Jarno
    Commented Apr 27, 2022 at 5:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .