bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.12k stars 3.56k forks source link

A way to run the "update" stage potentially many times per render? #3600

Closed rodya-mirov closed 2 years ago

rodya-mirov commented 2 years ago

What problem does this solve or what need does it fill?

I'm writing a game which is turn-based, with one player and (potentially many) NPC "players." In terms of control flow, think Civilization (but simpler), so a player's turn actions may impact what the next player does. But, in Civ, an NPC civilization will often take a full second to take their turn; while in my game a turn will usually take much less than a millisecond. So I'd like to be able to process as many as possible per tick and give control back to the player as quickly as possible so they experience smooth gameplay in the "normal" case.

What solution would you like?

The most obvious solution is to somehow run a stage (or system set) "as many times as possible" in the tick, then just render whatever's there. There may be better solutions; I'm open to ideas.

What alternative(s) have you considered?

Nothing great.

Most obviously I tried a proper turn-based system -- the entity whose turn it is can take an action (or not), then a bunch of followup systems run, then it passes control to the next person at the end. I don't see a way to make that not be the entire update stage, so this is the whole stage, and so if you have (e.g.) 1 player and 59 NPCs then the player can take exactly 1 action per second, which doesn't feel great. At 500 NPCs it would be completely unplayable, so it starts to inform game design in a bad way.

I've also tried writing the code in a way that handles everybody's turn in one tick, but when I have NPCs in a loop, the nice separation of concerns ECS provides go out the window -- I really need [NPC 1]'s actions to be fully resolved before NPC 2 even considers their choices. So I can't launch an event "NPC 1 chooses this" and have a bunch of followup systems run afterward, before "NPC 2 chooses this" occurs.

I've also tried just not caring about where (e.g.) NPC 1's actions may invalidate NPC 2's actions, but this is starting to drive my game design ("oh, this is going to make bugs, let's do something else") in a way that I don't like.

Additional context

Nothing obvious comes to mind

alice-i-cecile commented 2 years ago

So, the best way to do this right now is to make an exclusive system that runs a schedule in a tight loop. Add your game logic systems to this schedule, rather than the schedule owned by the App. After each pass of the loop, check if the elapsed time has exceeded your time budget, and break back into the App's schedule if it has.

This will do exactly what you need, and is actually quite ergonomic and powerful!

The fact that you can do this is not at all obvious however, nor is it properly documented in an example. IMO we should have a game example that does this, but I'm struggling to think of a simple game where this pattern would fit well.

rodya-mirov commented 2 years ago

@alice-i-cecile Thanks! If I can get it working I can try and contribute a small example back to the repo. I think this is useful even without a "real, well-motivated" example, because for people like me, the examples are the only useful documentation of most of bevy's actual API.

alice-i-cecile commented 2 years ago

That would be very much appreciated! I was thinking about this last night, and realized that this pattern may be useful for predictive AI. Perhaps a tic-tac-toe game example?

mockersf commented 2 years ago

if you don't want to go all the way to having your own schedule, you could also use run criteria, as they allow you to rerun systems inside the same frame. This is used for example in fixed time step to rerun systems in the same frame if the time step is smaller than the frame duration.

You can see it in action in the fixed_timestep.rs example by setting it to a small time step (less than 0.016 on 60 fps)

alice-i-cecile commented 2 years ago

Yep, looping run criteria will also do the trick. They won't play nice with other run criteria (like states) though, and are likely to get cut as part of #2801 due to their sharp corners and excessive complexity :)

rodya-mirov commented 2 years ago

I think an exclusive system is probably what I want. I've already had to hack bevy more than I'd like to force all the systems to run in a consistent sequence (single threaded, apparently, is not enough) and manually clear events at the end of the tick so they're not processed twice. Clearly what I want is not what bevy is designed for but I think I've got the tools I need to make it work (still love the engine).

Which is to say, at this point I'd rather just assume direct control than try to solve my problems with configuration.

rodya-mirov commented 2 years ago

I'm going to close this issue out since the question had a solid answer. I'm not sure I feel qualified to contribute a good example but I'll think about it.

Thanks for all your help, and your work on this engine!

alice-i-cecile commented 2 years ago

Sounds good :) We should be getting a solid example in for this as part of the https://github.com/bevyengine/rfcs/pull/45 work.