fjvallarino / monomer

An easy to use, cross platform, GUI library for writing Haskell applications.
BSD 3-Clause "New" or "Revised" License
588 stars 44 forks source link

Simulation/Game Update Loop #317

Open julmb opened 6 months ago

julmb commented 6 months ago

I would like to use Monomer to make a video game. The game is fairly UI-heavy with lots of menus and mouse-interactable screens, so a GUI library like Monomer seems like a good fit. However, the game does not only react to user interactions, it also requires a simulation to constantly update the game state at a reasonably fast rate (at least 60 Hz). How can I achieve something like this using Monomer?

From what I have seen, the easiest way would be to use a Producer (similar to the timeOfDayProducer from the tutorial) to generate Tick events that are then used to update the simulation state. However, what happens when either the simulation or the rendering cannot keep up? Will the generated Tick events just pile up in a growing queue?

I have also been thinking about using a Task to kick off a Tick event, which then both updates the model and kicks of a new Task, thus keeping the loop going. If EventResponses are handled sequentially, this should not cause a pileup of Tick events. Although this also seems kind of ugly, since each Task kicks off an asynchronous operation, which seems like overkill for a simple game loop.

Maybe there is a more elegant solution?

fjvallarino commented 6 months ago

Hi @julmb!

I think something similar to Example 03 - Ticker could work for your use case. The idea is that since you may receive more events than you can render in a reasonable time, you use a grouping thread that receives the messages, de-duplicates or combines them, and sends them to the main application not more frequently than desired.

To be more specific about your scenario, you could:

That example also shows a basic way to communicate from the application to the Producer, which, again, is just a channel. Depending on your needs, an MVar/TVar could be useful.

julmb commented 5 months ago

Hello and thank you for the quick reply!

Decoupling the simulation loop from the UI loop sounds like a good idea. And you are right, I can easily discard stale simulation states in favor of the most recent one.

However, I am still slightly worried that the intermediate producer might send too many events. If I want the UI to update smoothly at 60Hz, this means the intermediate producer will send 60 events per second. What happens if the UI can not keep up with this?

Or maybe there is a way to process new simulation states in a way that is tied to the update/render cycle of the UI, so that a new simulation state is only used when the UI is ready for a model update?

Disclaimer: I am probably worrying too much. I have a working example and so far there seems no issue with too many updates for the UI to handle. I am just trying to make sure I use the library correctly. So if this is fine then I just need to move on.

Thank you again!

jhrcek commented 5 months ago

The generic solution for your problem that's usually employed is debouncing. You can use for example this simple package https://hackage.haskell.org/package/auto-update-0.2.0/docs/Control-Debounce.html to make sure that producer doesn't overwhelm the UI by making sure that it doesn't send update more often than every 1/60th of a second.

julmb commented 5 months ago

I did not know about this package, thank you for mentioning it. However, my concern is not on how to implement debouncing, but rather what happens if monomer cannot perform layout and rendering at my chosen update rate of 60Hz (which might depend on the machine and background load) and whether there is a more robust way to handle this.