logo of the comfy engine Comfy

Context

A core design philosophy in comfy is that the engine should get out of your way as much as possible. This includes things like not having to specify 10+ different parameters in order to read input from the keyboard, look at mouse position, and play a sound.

This is a controversial topic, and many prefer as much decoupling as possible, but we've found that as far as building games goes, there is little value in such separation. Almost all game code will want to play some sounds, draw something, change animation state and run some game logic.

Note: If you've read this section for Comfy v0.1 a few things have moved from EngineContext into globals. See v0.2 release announcement for more details.

Comfy's solution to the above is exposing most things you would want through global functions, and keeping the rest in an EngineContext, which is a single struct that can be passed around throughout your code and references all that you may want.

Since Comfy v0.2 almost everything can now be done without the EngineContext, specifically ECS world/commands access, cooldowns, mouse position, game config, egui and image loading are all available through global functions. This is in addition to all the drawing, sound playing and input functions that were available previously.

We've found that it is extremely common to be in the middle of writing gameplay logic and realize "oh crap I need to make an egui window to debug this better". Being able to just write egui() saves the developer from breaking their thought process, and more importantly it makes cleanup significantly easier.

Game specific Context🔗

While the EngineContext idea is great, we've found that going a bit further is also extremely useful for game code. This section is by no means prescriptive, and you can structure your comfy games however you prefer. But it is also a pattern we've been working on for well over a year, and something we've found increasingly more useful throughout building our games.

Firstly, let's introduce the idea of GameState, which is simply a struct that stores game-related state that is global across the game. For very complicated games one might want to use finite state machines and store state in a more complicated way, but let's leave out AAA-size games for a while and focus on smaller indie games, and assume we can just have our state in one place. ECS users might find this to be the place where they store some of their Entity handles if they don't want to use tag components.

Next up this is where we introduce the GameContext, which is a struct we'll be using in all of our game code. It will provide us access to EngineContext for when we need it, but it'll also expose parts of GameState and whatever else we might need.

The rest of this section roughly follows the physics example.

Let's say our game state looks something like this:

pub struct GameState {
    pub spawn_timer: f32,
    pub physics: Physics,
    pub ball_spawning_speed: BallSpawningSpeed,
}

We store a simple timer in f32, a physics engine state, and a flag telling us how fast we want to spawn balls.

Now what should our GameContext look like? A simple variant should be something along the lines of the following (lifetimes removed for now):

pub struct GameContext {
    pub state: &mut GameState,
    pub engine: &mut EngineContext,
}

The benefit is we don't have to write a lot of code, but the downside is now all our code will have to do c.state.physics or c.state.spawn_timer. This is not a huge deal, and some might prefer taking a small hit in ergonomics in game code to save a bit of writing.

But we've found that the hardest part of making a game is actually making the game, that is having good gameplay code, fun features, lots of iteration on the game itself, etc. This is why we prefer the significantly more verbose approach that will look ugly and redundant in the place where we define the GameContext, but as will also simplify all of our game code.

pub struct GameContext<'a, 'b: 'a> {
    pub delta: f32,
    pub spawn_timer: &'a mut f32,
    pub ball_spawning_speed: &'a mut BallSpawningSpeed,
    pub physics: &'a mut Physics,
    pub engine: &'a mut EngineContext<'b>,
}

This time we've also included all of the necessary lifetimes. If these look scary (especially since there's two), don't worry as these are the only two lifetimes you'll see while using comfy :) There are a few ways around this, and many potential tradeoffs, but again, it should be noted that a GameContext is only defined once per game.

Note that we also added delta, which while being available on the EngineContext is something that almost every bit of game code will want to touch, and thus having a simple c.delta instead of c.engine.delta is a nice quality of life improvement, at the cost of a few extra lines of code. Since Comfy v0.2 you can also call a global delta() function. Note that this does have a small amount of overhead, so it may be desirable to re-export this into your GameContext for use in tight loops.

The next part could in theory be handled by a procedural macro, but comfy wants to remain simple. As such we'll have to write a function which actually creates this context object, which means we have to specify each field again. If you're reading this and passing out from the amount of boilerplate, note that this is entirely optional. Your game does not have to do any of this. But it is a pattern we've been using for a while and found incredibly useful in terms of our productivity.

Let's define the make_context function that will take GameState and EngineContext and create a GameContext.

// An unfortunate re-apparance of the two lifetimes.
// This is the last time I promise.
fn make_context<'a, 'b: 'a>(
    state: &'a mut GameState,
    engine: &'a mut EngineContext<'b>,
) -> GameContext<'a, 'b> {
    GameContext {
        spawn_timer: &mut state.spawn_timer,
        delta: engine.delta,
        ball_spawning_speed: &mut state.ball_spawning_speed,
        physics: &mut state.physics,
        engine,
    }
}

As a last step, comfy provides a comfy_game!(...) macro which can take you the rest of the way and generate the necessary boilerplate.

comfy_game!(
    "Contexts are fun :)",
    GameContext,
    GameState,
    make_context,
    setup,
    update
);

Feel free to look inside the macro definition, or explore the full_game_loop example to see what this actually does, but note there is no extra magic hidden behind it.