Now we’ll start selecting things and figure out how to edit them.
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);
}
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.
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.
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.
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.
By now we have a spell editor window that appears when we select a spell in the registry, but it looks like this:
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.
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();
});
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.
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
).
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}"));
}
});
});
}
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.
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:
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.
}
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));
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.
Adding onto this we have 3 more fields: Power cost (an optional value), effects (a Vec<>
of SpellEffects
), and flags (a bitflag property).
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!
}
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);
}
}
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...
}
}
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);
}
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?
Yes. Toot at me on mastadon or tweet at me on twitter.
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.
See you in part 3.
Written 12 Mar 2023 by Grim