intro

I finally got player interactions working.

Well, kinda.

why is that PR so big…

This has been a big learning process.

In order to understand what’s happening in the game, I need a way to inspect the world. Thankfully, there are plenty of crates in this space. I decided to use the well-supported bevy_inspector_egui as the basis of my inspector because of its integrations with the general egui ecosystem. That being said, I’m keeping an eye on haalka, mevy, and dairy-free BSN as bevy-native alternatives. I have a life outside of gamedev, otherwise I’d already be trying them out and contributing back.

General codebase architecture

In addition to building out a UI, I’ve been playing around trying to figure out the best way to organize my code. Games are big projects and I have got to be clear about what I’m doing, especially given time and attention constraints. I’ve settled on a fairly simple architecture. Click on that link to learn more, it’s got a tl;dr and everything :)

a tale of two UI systems

bevy_inspector_egui

bevy_inspector_egui has been pretty much a necessary evil. It is the most robust currently existing inspector solution, if only because egui has a lot of plugins. I’ve been making use of egui_dock, among others.

However, egui and bevy don’t always play well together. Egui has its own render cycle completely separate from the main loop, so it’s difficult to get interactions between the UI and the game world. I’ve settled on states as my go-to mechanism for implementing this functionality. This may seem strange - why not use an event? Simply, most of my use-cases have been stateful, e.g. whether or not the cursor is over the gameview or whether or not the main camera is enabled. In order to implement these boolish states, I’ve made a simple macro.

crates/utils/.../boolish.rs
#[derive(
	Default, States, Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect
)]
#[reflect(State)]
pub enum $name {
	/// awaiting setup
	/// glosses to false
	#[default]
	Init,
	/// implements Into/From<bool> (true)
	Enabled,
	/// implements Into/From<bool> (false)
	Disabled,
}
crates/inspector/.../inspector_state.rs
use q_utils::boolish_states;
boolish_states!(InspectorState, GameViewState);
//...
impl Plugin for InspectorStatePlugin {
    fn build(&self, app: &mut App) {
        app.setup_boolish_states()
        //...
    }
}

All of my states are independent. This is pretty flexible, as it allows me to query for individual states and to update them without affecting any other state. If I were to lump them all together into a big InspectorState struct then this wouldn’t allow me to do things like conditionally run systems, which is the whole point. State changes are also tracked like events, so we can run one-shot systems whenever states change. Best of both worlds!

crates/inspector/.../inspector_state.rs
impl Plugin for InspectorCamPlugin {
    fn build(&self, app: &mut App) {
        app.setup_boolish_states()
            .add_systems(OnExit(InspectorCamState::Init), Self::spawn_camera)
            .add_systems(
                Update,
                (Dolly::<InspectorCam>::update_active, Self::update_camera)
                    .run_if(in_state(InspectorCamState::Enabled)),
            )
            .add_systems(
                OnEnter(InspectorCamState::Disabled),
                Self::set_cam_active::<false>,
            )
            .add_systems(
                OnEnter(InspectorCamState::Enabled),
                Self::set_cam_active::<true>,
            );
    }
}

You may also notice that I’m running initialization code when the state exits $state::Init. This way we don’t unnecessarily populate the game world with junk components, and we have finer control over when a system set is run. This has generalized really well, and I’m using it all over the codebase. The only major drawback is that because states are handled individually you have to query lots of them to get complicated information. But, this is not too different from general queries, and may even be a good thing. In general, a system should know only what it needs to, and nothing more. This is the single-responsibility principle in practice.

ok, state is cool but what about the ui?

I need all this state to send interactions between the egui UI system and bevy. It’s a hassle! Though I’m glad I did it, because I’m using it in the main application as well. As far as the actual UI goes, I’m pretty much just copying the egui_dock example from the repo. The main exception to this is the play/pause state, which simply de/activates physics, the player’s controller state, and the player camera. Optionally, you can play the game while keeping the inspector camera running. I’m trying to capture a Unity-like feel here.

I’ve also added an InspectorIgnore component for filtering out entities I don’t want to see, for example the inspector cam and the gizmo entities.

…and the noise editor?

This is not yet implemented. I intend for it to be a generalized terrain / procgen stuff editor which utilizes noise_egui, though I will probably end up making this into a separate application, if not copying the expression library and implementing a bevy-native clone…

bevy_ui and gizmos

The real meat of the editor is in the gizmos! Unfortunately I haven’t made too many yet! The primary gizmos are those for viewing the player camera (the red sphere and arrow in the screenshot above), and the XYZ axes in the top-right corner. I’m going for a Blender-style feel for that one. The axes were achieved by rendering to a texture, which is a common practice, but I still feel proud for learning it! Future work on this includes things like jump-to-entity, transform gizmos, and scene integration.

Cameras, Physics, Controls - The actual game stuff!

So far all of this is still pretty basic. For cameras, I’m using bevy_dolly, which is a wonderfully flexible and powerful camera controller. It was a bit of a struggle to get it to do what I want, which is a big part of why this PR took so long. I’m happy with it though! It turns out I had just gotten some coordinate spaces mixed up - gamedev shit. I wouldn’t have figured that out without the inspector though.

As far as physics goes, I’m using avian3d. As I’m writing, I’m seeing that v0.3.0 is out! Avian was locking me to bevy 0.15 (one version behind), so I’m very glad to hear this has been released. That being said, it looks like bevy_dolly’s 0.16 migration is still WIP as of writing.

…and for the player I’m using tnua. I might end up replacing this if I need to, though the library seems flexible enough to do all the things I need it to. Right now the player is bumping about on the uneven terrain, which looks pretty ugly. I’m sure I can tweak the settings to improve this though. I also haven’t implemented proper camera controls yet! Right now the camera just follows behind the player, no zooming or rotating. I’m aiming for the feel of the player controls to be something akin to 3D Zelda games; Breath of the Wild is a big inspiration.

I’d really like to upgrade my dependencies. The bevy 0.16 update included a lot of API changes (such as ECS relationships) which aim toward a better devex. This is all headed toward the next-gen scene/ui system and the official editor - very big and exciting stuff! Generally 0.16 has been a huge leap forward for the engine and I’m excited to see it grow. Unfortunately, I’m still stuck in 0.15 land until my dependencies migrate.

Conclusion

This was a lot of work!! Way too much for a single PR. In the future I’m going to try to split things up a bit more cleanly. This has been good though, I’ve learned a lot about how Bevy works, and in general feel far more confident to scaffold out the project.

As far as future work goes, I need to:

  • Migrate to bevy 0.16
  • Implement better player camera controls
  • Implement better player movement
  • Do visual things like adding a skybox and textures to the world
  • Implement the terrain editor / general procgen tool.

Thank you for reading!