hecrj / coffee

An opinionated 2D game engine for Rust
https://docs.rs/coffee
MIT License
1.08k stars 55 forks source link

Question around `Game` and `UserInterface` trait design #102

Closed akappel closed 4 years ago

akappel commented 4 years ago

Hello! First off, thanks for creating another lively new engine for us to try in Rust!

I have a question around the current architecture, namely after seeing this example:

...
<Tour as UserInterface>::run(WindowSettings {
        title: String::from("User Interface - Coffee"),
        size: (1280, 1024),
        resizable: false,
        fullscreen: false,
    })
...

Is there a reason why we have to cast our Game as a UserInterface? I understand that they both have ::run methods. But could we not change it around so that the Game trait owns a type UserInterface item, similar to type Input and type LoadingScreen? That way, we no longer have to worry about the casting, and can continue to implement the UserInterface trait, but provide it to the Game struct.

Thanks! Adrian

hecrj commented 4 years ago

First off, thanks for creating another lively new engine for us to try in Rust!

Thank you for giving it a shot!

Is there a reason why we have to cast our Game as a UserInterface?

We may be able to remove the cast if/when specialization lands!

The main reason for the current design is that the event loops of Game and UserInterface are slightly different. Particularly, UserInterface needs to keep track of a cache to avoid recomputing the UI layout every frame and also adds some logic to input event processing.

While we could achieve this by using composition, it would not be as straightforward, flexible, and type-safe as simply having two different entrypoints. Let's see why.

But could we not change it around so that the Game trait owns a type UserInterface item, similar to type Input and type LoadingScreen?

The current associated types of Game are decoupled from the state of the game. Input and LoadingScreen know nothing at all about a Game trait. However, a UserInterface depends on the state of the Game. For instance, we probably want a different UI for our main menu, gameplay, and game over screens, or we may simply want to display the current health of the player, etc.

We could indeed remove this dependency and make UserInterface independent. But this introduces a problem! Our users are now forced to keep the Game and UserInterface states in sync, opening a door to a lot of pesky bugs! Think what would happen if the player dies in Game::update but we forget to transition our UserInterface into a game over screen...

To avoid this, we invert the dependency and remove the need of an additional type: UserInterface decorates or extends a Game.

Hopefully this answers your question! Let me know if you need me to clarify anything!