Using egui for Bevy ECS Introspection

👀 Using egui for Bevy ECS Introspection

In this post, we look at using egui for Bevy Introspection with Macroquad rendering. This continues the series of recent posts, which started with adding egui DevTools to Macroquad, then added Rapier physics, units of measurement and an Entity Component system (ECS).

Here, we bring everything together, adding DevTools-like introspection to the Bevy ECS demo from the previous post. As before, the complete project code is on GitHub (link further down).

🧱 What are we Building Here?

Using egui for Bevy ECS Introspection: A number of blue, yellow and orange bubbles are trapped at the top of the screen.  The Developer tools panel appears to contain details for all of them, with the details for just 2 or 3 scrolled into view.  The headings Entity Generation:ID 1:8 and Entity Generation:ID 1:9 are visible.  These top sections with Position, Velocity, Colour, and Radius data for their respective bubble.

The demo simulation, built in Rust, shows a series of coloured, floating bubbles release and float to top of the window. The bubbles subsequently get trapped at the top of the screen; filling the window, from the top down.

I employed the following tools to build the demo:

  • Macroquad — a fast, quick-to-get going on Rust tool built with game prototyping in mind;
  • Rapier — mature, pure Rust physics engine for game dev;
  • uom — a crate with applications in Aerospace for maintaining consistency in physical quantity units and helping to avoid unit errors;
  • Bevy ECS — ECS implementation from the Bevy game engine, added to this project as the stand-alone bevy_ecs crate; and
  • egui — an immediate Graphics User Interface (GUI) library for Rust (inspired, in fact, by the C++ Dear ImGui library).

I focus on the egui integration in this post, so feel free to jump back through the series to learn more about how I put the other elements together.

⚙️ Crate Setup

You might need to use slightly older (than the latest) versions of some crates to get everything working together. Here is my Cargo.toml, which you can use as a starting point for finding compatible versions of egui, egui-macroquad and macroquad:

[package]
name = "macroquad-bevy-ecs-introspection"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
repository = "https://github.com/rapier-example"
# bevy_ecs 0.13 requires MSRV 1.76
rust-version = "1.76"
description = "Macroquad Rapier ECS 🦀 Rust game dev — using bevy's 🧩 Entity Component System in a Macroquad game with Rapier physics."

[dependencies]
bevy_ecs = "0.13.2"
crossbeam = "0.8.4"
egui = "0.21.0"
egui-macroquad = "0.15"
macroquad = { version = "0.3.26", default-features = false }
rand = "0.8.5"
rand_distr = "0.4.3"
rapier2d = { version = "0.19.0", features = ["simd-stable"] }
uom = "0.36.0"

🔄 Update egui Introspection System

I introduced Bevy ECS to the project in a previous Macroquad Rapier ECS post. Continuing, we need to add a couple of ECS systems for updating and rendering the egui DevTools. First up is the update_dev_tools_system in src/systems.rs:

pub fn update_dev_tools_system(query: Query<(Entity, &Position, &Velocity, &CircleMesh)>) {
    egui_macroquad::ui(|egui_ctx| {
        egui_ctx.set_pixels_per_point(4.0);
        egui::Window::new("Developer Tools").show(egui_ctx, |ui| {
            ScrollArea::vertical().show(ui, |ui| {
                CollapsingHeader::new("Bubbles")
                    .default_open(false)
                    .show(ui, |ui| {
                        for (entity, position, bubble_velocity, circle_mesh) in &query {
                            draw_ball_ui_data(ui, entity, position, bubble_velocity, circle_mesh);
                        }
                    });
            });
        });
    });
}

Bevy ECS Queries

Typically, for Bevy ECS, this function takes a query over ECS components as its argument (line 107). Here, the query generates a collection of all ECS entities that have Position, Velocity and CircleMesh components (essentially the bubbles). Additionally:

  • In line 109, I call set_pixels_per_point this scales up the UI. 4.0 worked for screen captures, though for general use, you will probably want something a touch lower.
  • We need a ScrollArea in the UI (line 111) to make viewing data for the dozens of balls in the simulation more practical.
  • You can iterate over the ECS entities satisfying the initial query, with a simple for loop (line 115).

Next, we look at the draw_ball_ui_data function called within that last loop.

🫧 Individual Bubble Updates

Using egui for Bevy ECS Introspection: An orange bubble is trapped in the top, right corner of the window.  Towards the left, is the Developer Tools Panel.  This show properties for the bubble, reading Entity Generation:ID 1:0, Position x: 26.26 m, y: -0.65m, Velocity x: -0.00 m/s, y: 0.00 m/s, Colour (orange dot is displayed), Radius 0.6m

Another advantage of using the uom crate for handling physical quantities is formatting of those values for output, which we use in the draw_ball_ui_data function (src/systems):

fn draw_ball_ui_data(
    ui: &mut Ui,
    entity: Entity,
    position: &Position,
    bubble_velocity: &Velocity,
    circle_mesh: &CircleMesh,
) {
    let m = Length::format_args(length::meter, Abbreviation);
    let m_s = VelocityUnit::format_args(velocity::meter_per_second, Abbreviation);
    CollapsingHeader::new(format!(
        "Entity Generation:ID {}:{}",
        entity.generation(),
        entity.index()
    ))
    .default_open(false)
    .show(ui, |ui| {
        ui.horizontal(|ui| {
            ui.label("Position");
            ui.label(format!(
                "x: {:.2}, y: {:.2}",
                m.with(position.0.x),
                m.with(position.0.y)
            ))
        });
        // TRUNCATED...
    });
}

Quantity Formatting

To select preferred formatting, in lines 55 and 56, we add unit formatting for metre and metre per second quantities. These uom format args incorporate type checking, so you would get a compile-time error if you tried to format a length quantity using metres per second, for example.

Notice, in for example, in lines 67-70 that we can use standard Rust formatting arguments with the uom format arg to specify the precision we want position output data with. Here, the {:.2} format specifiers indicates we want to round the position values to two decimal places.

ECS Entity Generations and IDs

ECS entities are not a lot more than an integer ID and a generation. The generation provides a mechanism for reusing IDs. For example, the first bubble spawned might have ID 0 and generation 1. If we then despawned that bubble from the ECS, the next bubble can re-use ID 0, which is now free. However, the ECS would give it a generation of 2, so it can be distinguished from the earlier one.

We access, both generation an id from the Bevy ECS Entity struct calling .generation() and .index() in lines 59 and 60.

✍🏽 Draw UI System

The system for actually drawing the DevTools panel, using egui is a little simpler — we just need to call egui_macroquad::draw:

pub fn draw_dev_tools_system() {
    egui_macroquad::draw();
}

📆 Bringing it all together: ECS Schedule

I added the two new systems to the Bevy ECS schedule, to give us some control over the order Bevy ECS runs them in (src/main.rs):

let mut playing_schedule = Schedule::default();
playing_schedule
        .configure_sets(
                (
                        ScheduleSet::BeforeSimulation,
                        ScheduleSet::Simulation,
                        ScheduleSet::AfterSimulation,
                )
                        .chain(),
        )
        .add_systems(
                (
                        create_ball_physics_system,
                        (
                                update_dev_tools_system,
                                draw_balls_system,
                                draw_dev_tools_system,
                        )
                                .chain(),
                )
                        .chain()
                        .in_set(ScheduleSet::BeforeSimulation),
        )
        .add_systems(step_physics_engine_system.in_set(ScheduleSet::Simulation))
        .add_systems(
                (
                        update_balls_system,
                        spawn_new_ball_system,
                        end_simulation_system,
                )
                        .in_set(ScheduleSet::AfterSimulation),
        );

With Bevy ECS, schedule systems grouped into a tuple will run in parallel. Tacking .chain() onto the end of the tuple tells the ECS that you want the systems to run in series, in the order of appearance.

🏁 What Next?

I have the basic, minimal DevTools panel working now. In terms of extensions and where to go next, I am considering:

  • adding a wireframe view mode, with toggle, to show/hide the Rapier colliders;
  • buttons for pausing, stepping and restarting the simulation from the dev panel tool; and
  • adding the collider properties to the panel.

Interested to know if you have some ideas on and also, what else might be a good direction to take this in. Drop a comment below, or reach out on other channels.

🙌🏽 Using egui for Bevy ECS Introspection: Wrapping Up

In this post on using egui for Bevy ECS introspection, we looked at displaying Rapier physical properties with egui and Macroquad rendering. In particular, we saw:

  • code for adding egui update and draw ECS systems;
  • how you can add an egui ScrollArea to display large data collections; and
  • how you can format uom quantities for display, including rounding to a fixed number of decimal places.

I hope you found this useful. As promised, you can get the full project code on the Rodney Lab GitHub repo. I would love to hear from you, if you are also new to Rust game development. Do you have alternative resources you found useful? How will you use this code in your own projects?

🙏🏽 Using egui for Bevy ECS Introspection: Feedback

If you have found this post useful, see links below for further related content on this site. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on X (previously Twitter) and also, join the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Game Dev as well as Rust and C++ (among other topics). Also, subscribe to the newsletter to keep up-to-date with our latest projects.

0
Subscribe to my newsletter

Read articles from Rodney Lab - Game Developer with “Eternal Student” mindset. directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rodney Lab - Game Developer with “Eternal Student” mindset.
Rodney Lab - Game Developer with “Eternal Student” mindset.

I build accessible, fast and secure sites, currently focussing on frontend using JAMStack. My consultancy spans the gamut of developing SEO-friendly sites from scratch to developer advocacy for a range of technologies. If I like it I can write and talk about it! I enthusiastically generate blog and video content from how-tos helpful to anyone finding their feet with the technology to more in-depth case studies helping developers generate c-suite interest in projects. Get in touch to find out more.