GRIM

Editors (2/3): Editing entries (in Rust & With<Bevy>)

Last time on Untitled Game Project, we briefly covered the database and began diving into making the editor. We ended up with a registry window where we could theoretically select entries.

Spell Registry Table

Now we’ll start selecting things and figure out how to edit them.

Selecting

This is simple, all we need is to store the Id of the entry when it is clicked in the table. Thus we add the SpellEditor resource.

#[derive(Default, Resource)]
struct SpellEditor {
    selected: Option<SpellId>,
}

And thanks to Option<> we know when we have selected anything because selected is Some. Chuck that into the row code from last post and we can select entries.

if quick_row(
    row,
    &[
        id.0.to_string(),
        lang.name.clone(),
        internal_data.internal_name.clone(),
        spell.target.to_string(),
        spell.get_range().to_string(),
    ],
    spell_editor.selected == Some(*id), // If this is the selected value.
) {
    spell_editor.selected = Some(*id);
}

Editing

So we’ve covered the simple part. Before we continue, some appreciation for what came before. As I’ve been working on this I’ve had The WoW Diary by John Staats open on my desk. It is a fantastic book covering the development of WoW up until release, and it features screenshots from the internal tool “wowedit” and its spell editor.

wowedit spell editor

Image of wowedit from Staats, J. (2018). The Wow Diary: A Journal of Computer Game Development (1st ed), pp. 227.

It’s very old, and I don’t have the same properties as the WoW spell system but I like the visual reference for what has been used before. It gives an idea of what I’d like to achieve with egui. And with a goal in mind, we’re going to need another window.

The Spell Editor window.

We only need to display this window when we have a spell selected which means we can make an early exit out of the system if it doesn’t exist.

let Some(spell_id) = spell_editor.selected else {
    return;
};
let Some(raw_spell) = spell_registry.get(&spell_id) else {
    return;
};

egui::Window::new("Spell Editor").show(egui_context, |ui| {
    // Spell content!
});

Simple enough, now we need to start making a mess though. We can’t simply edit the values directly in the registry. Well, we could but then we wouldn’t be able to revert our changes or tell what we’ve changed. Instead, when we select an entry we are going to clone it into a DirtySpells set if the entry isn’t already in it.

#[derive(Default, Resource)]
struct DirtySpells(HashMap<SpellId, Spell>);

// After getting raw_spell from registry.

let dirty_spell =
    if let Some(dirty_spell) = dirty_spells.0.get_mut(&spell_id) {
        dirty_spell
    } else {
        dirty_spells.0.insert(
            spell_id,
            raw_spell.clone(),
        );
        dirty_spells.0.get_mut(&spell_id).unwrap()
    };

// Window creation follows here...

We’ll also add a separate table for dirty spells in the spell registry window.

Dirty Spell Registry Table

Reusing code from the previous post but iterating over dirty entries.

Whenever we switch selection, we will check if the entry in the dirty set is the same as the one in the registry and delete the dirty version if so.

Knobs and dials.

By now we have a spell editor window that appears when we select a spell in the registry, but it looks like this:

Empty Spell Editor

Which isn’t an editor, it’s just another window. We are now going to quite quickly move to an initial version, then I will then explain how it fits together, and the challenges with it.

Less empty Spell Editor

Styling

Most of these elements are wrapped in a egui:Grid which you could equate to a CSS grid, you get X columns, and when they are filled move on to the next row.

    egui::Grid::new("spell_main_props")
        .num_columns(3)
        .spacing([40.0, 4.0])
        .striped(true)
        .show(ui, |ui| {
            // UI Code here.

            // End the current row pre-maturely.
            ui.end_row();
        });

Enums

Far as I can tell there is no built-in way to iterate over enums in Rust. So what powers these nice drop downs that map to enums such as target? Quite simple, manually implemented static iterators.

Target Dropdown & Static Iterator

I’ve expressed on mastodon that I don’t think this is a great solution but I don’t have a better one because I need to be able to have these dropdowns handle field enums and I don’t want a specific None type on these which removes my ability to simply #[derive(Default)].

At the time of writing I have 12 of these iterator functions.

I tried to use strum_macros for this and it didn’t quite cover my use case. I can’t say more because my notes just say “DOES NOT WORK” (I believe it was due to lacking Default).

strum_macros doesn't work

Displaying the target row above takes this code:

ui.label("Target");
egui::ComboBox::from_label("")
    .selected_text(format!("{}", dirty_spell.target)) // This is the text for the currently selected element (in the above case Target Character).
    .show_ui(ui, |ui| {
        for value in SpellTarget::iterator() {
            ui.selectable_value(
                &mut dirty_spell.target, // Property to apply value to when selected.
                value.clone(), // Value to apply.
                format!("{value}") // Text to display in UI.
            );
        }
    });

SpellTarget also implements std::fmt::Display

From here on these will be condensed into a generic function:

fn combo_box<T>(
    ui: &mut egui::Ui,
    combo_box_id: &str,
    current_value: &mut T,
    values: Iter<T>,
    label: &str,
) where
    T: Display + std::cmp::PartialEq + Clone,
{
    ui.push_id(combo_box_id, |ui| {
        egui::ComboBox::from_label(label)
            .selected_text(format!("{current_value}"))
            .show_ui(ui, |ui| {
                for value in values {
                    ui.selectable_value(current_value, value.clone(), format!("{value}"));
                }
            });
    });
}

Enum fields

The Global Cooldown enum has a Custom variant that allows specifying a specific value. This means that when we select this variant we need a special case for it, this is repeated for every enum variant with fields.

Switching away from GCD

if let SpellGlobalCooldown::Custom(val) = &mut dirty_spell.global_cooldown {
    ui.label("Custom GCD:");
    ui.add(egui::DragValue::new(val).speed(100));
}

Iterators will also need to create a default version for these:

GCD Iterator

Telling if a value is dirty.

Essentially each type needs to #[derive(PartialEq)] then we can compare the value on the raw_spell to the dirty_spell, if they differ we append a * label.

if dirty_spell.target != raw_spell.target {
    ui.label("*");
} else {
    ui.end_row(); // End row pre-maturely.
}

Spell Editor with a dirty target

Numeric Value Entry

This is entirely solved by egui. It’s as simple as using egui::DragValue

// Here speed is the step size. Dragging will change the value of cast_time by 100 for each step.
ui.add(egui::DragValue::new(&mut dirty_spell.cast_time).speed(100));

Drag Value

Expanding the Window

The previous section covers everything we need for the simple version of the Spell Editor. But that version does not cover everything a spell needs.

Expanded Spell Editor

Adding onto this we have 3 more fields: Power cost (an optional value), effects (a Vec<> of SpellEffects), and flags (a bitflag property).

Handling optional values (Power cost)

Optional values are essentially the same as how we handled the Custom variant of Global Cooldown above just with a checkbox instead of a dropdown.

// Checked state.
let mut has_cost = dirty_spell.power_cost.is_some();
if ui
    // Create a checkbox with checked state and "Power Cost" as the label.
    .checkbox(&mut has_cost, RichText::new("Power Cost").strong())
    .changed() // React to if it changed.
{
    // If the checked state changed then we change the value to Some/None.
    dirty_spell.power_cost = if has_cost {
        Some((Power::Stamina, 0.0))
    } else {
        None
    };
}

if let Some((power, cost)) = &mut dirty_spell.power_cost {
    // Power cost editing! Woo!
}

Power Cost

Bitflags

We use the bitflags crate for bitflags. Like with enums, this is handled with a static iterator function.

pub fn iterator() -> Iter<'static, SpellFlags> {
    static FLAGS: [SpellFlags; 4] = [
        SpellFlags::CASTABLE_WHILE_DEAD,
        SpellFlags::MELEE_RANGE,
        SpellFlags::IGNORES_GCD,
        SpellFlags::CUSTOM_GCD,
    ];
    FLAGS.iter()
}

But instead of creating a dropdown we insert checkboxes.

for flag in SpellFlags::iterator() {
    // Checked state. This is mut because egui needs it to be.
    let mut checked = dirty_spell.flags.contains(*flag);
    // But we only need to know if it has changed.
    if ui.checkbox(&mut checked, flag.get_flag_name()).changed() { 
        // And if so toggle the flag.
        dirty_spell.flags.toggle(*flag);
    }
}

Bitflags

Vec<>s (Effects)

Effects are displayed in a grid, this isn’t optimal, with a table we could initialize it with the correct amount of rows (see listing registry entries) which would likely be faster. Within the grid we simply iterate over all effects and display a row with relevant information.

Editing effects works the same as other field enums with the addition of storing a Option<usize> in SpellEditor for the currently selected effect and a big match statement.

for (i, effect) in dirty_spell.effects.iter_mut().enumerate() {
    // Display the name of the effect.
    if ui.selectable_label(selected, format!("{effect}")).clicked() {
        spell_editor.selected_effect = Some(i);
    }

    match effect {
        SpellEffect::Damage {
            school, // Damage School
            bonus, // Base damage
            backing_attribute, // Optional backing attribute 
        } => {
            // Display the damage school if any.
            if let Some(school) = school {
                ui.label(format!("{school}"));
            } else {
                ui.label("");
            }
            // Display the damage formula.
            if let Some((attribute_multiplier, attribute)) = backing_attribute {
                ui.label(format!(
                    "{bonus} + {attribute_multiplier} * {attribute}"
                ));
            } else {
                ui.label(format!("{bonus}"));
            }
        }
        // etc for every other effect...
    }
}

Damage effect

Adding new effects is as simple as adding another entry to the effects Vec whilst to handle deleting entries we use a Option<usize> set to the current index when the delete button is clicked.

I find this to be simpler than manually handling removing mid-iteration. Your opinion may differ.

let mut to_delete = None;

{
    // Effects display code...
    if ui.button("-").clicked() {
        to_delete = Some(i);
    }
}

// After effects display loop.
if let Some(to_delete) = to_delete {
    dirty_spell.effects.remove(to_delete);
}

Effects

Wrapping up.

This is not perfectly optimized.

True. I could probably reduce the number of string copies but it doesn’t matter here. The important thing for the editor systems is that they are easy to write out and easy to expand. They don’t need to scale to the same level as the gameplay code.

E tu performance?

Could you elaborate on X?

Yes. Toot at me on mastadon or tweet at me on twitter.

Coming up next…

With this, you should be able to make an editor, and you can likely figure out saving on your own, but we will cover that and two more features in the next (& hopefully final) part; creating entries and the Selector.

Selector

See you in part 3.

Written 12 Mar 2023 by Grim