RTS prototype update #1

2020-11-02

Some stuff has changed since the first post. The latest commit is now this.

The major advancements that I've worked on have been a simple "physics" system so units don't overlap, an overly complex UI system and a bunch of abilities to go with it.

"Physics" system

Calling this a physics system is being extremely generous. What it does is implement the Separation rule of Flocking, so that two units are always a set distance apart. This is implemented in the unit_movement system in the systems/unit.rs file.

At first I had tried using the bevy_rapier plugin, and it worked great for the most part, but it completely broke my camera rotations systems, which worked weirdly using physics. I decided to go the easy route instead, reverting everything back, and taking inspiration from flock-rs to implement separation.

Here's a small sketch to explain how Separation works:

Separation sketch

We take a list of all the units nearby the one we want to apply separation to, in this case, the nearby units are A, B and C, while the unit we're applying separation to is X. Now, we take the vectors from X to A, B, and C: XA = X - A, XB = X - B, XC = X - C, and add them all together: M = XA + XB + XC. This gives us the direction in which to move X that will make it be as far away as possible to A, B and C. We want to move X more the closer A, B and C are, so instead of directly using M, we first normalize it, to get a vector with length 1, and then scale it by (D - |M|^2) / D, where D is the minimum distance we want to keep, and |M|^2 is the length squared of M. This makes sure X moves further away the closer the other units are.

This separation algorithm turned out to be exactly the behavior I was looking for, without adding all of the other physics systems I didn't really care about. Here's a GIF of it in action:

Separation gif

As you can see, units get pushed around and behave as if there were collisions.

UI system

I fell into the temptation of over-complication, and ended up making a complex system to handle showing the different abilities from the currently selected units. It works by keeping a Resource with a vector of all the buttons that should be shown. It looks like this:

pub type AbilityChangeCallback = fn(Commands, ResMut<CurrentAbility>, ResMut<AvailableButtons>, CallbackData);
pub type ButtonIdentifier = String;
pub type ButtonTuple = (
    String,                // Name
    ButtonIdentifier,      // Identifier
    AbilityChangeCallback, // Callback called on click
    CallbackData,          // Extra data to pass to the callback
);

pub struct AvailableButtons {
    buttons: Vec<ButtonTuple>,
}

The Tuple keeps all the button information, like the Name, Identifier, a Callback and some extra data to pass to it. The identifier is used to be able to remove a specific button, for example when we deselect a unit. Then in systems/ui.rs I have a system called change_displayed_buttons, which takes care of changing the buttons whenever the buttons list changes. For that it first despawns all of the existing buttons and spawns new ones according to the list.

The buttons are spawned with a component that takes the AbilityChangeCallback and the CallbackData, so that when the button is pressed we can call the callback and pass it the data. This is done in button_system.

Then, in systems/ability.rs there's the add_ability_buttons_for_selected_units system, which takes care of adding and removing the Unit abilities to the list in the Resource according to if they're selected or not.

All of this probably needs a refactor, but it works quite alright for now.

Abilities

After implementing the fantastic ability UI system, I needed some abilities to go with it. I decided to implement some that would force me to ensure the abilities system is flexible and allows for many different types of actions. The ones I've implemented at the moment are: selecting, changing camera, a single unit teleport, a single unit heal, and an area heal.

The ability system is implemented with a Resource that keeps track of the current ability, which is a Rust enum.

pub struct CurrentAbility {
    pub ability: Ability,
}

#[derive(PartialEq, Debug)]
pub enum Ability {
    Select,
    SwitchCamera,
    SwitchBack,
    Teleport(Entity),
    HealUnit,
    HealArea,
}

Most of the time the ability is Select, which is the ability that lets the user select units and assign a target position to move them. Systems that are specific to an Ability check the current value from the Resource, and exits if it's not the correct one.

For example, here is the system for the Heal Unit ability:

fn heal_unit_ability(
    pick_state: Res<PickState>, // From bevy_mod_picking
    mouse_button_inputs: Res<Input<MouseButton>>,
    mut ability: ResMut<CurrentAbility>,
    query: Query<(&mut Health, &Unit)>,
) {
    // Only run if we're the current ability is HealUnit
    if Ability::HealUnit != ability.ability {
        return;
    }

    // Check if the user clicked
    if mouse_button_inputs.just_pressed(MouseButton::Left) {
        // Get the unit under the cursor
        if let Some(top_pick) = pick_state.top(PickGroup::default()) { // From bevy_mod_picking
            if let Ok(mut health) = query.get_mut::<Health>(top_pick.entity()) {
                health.heal(20);
            }
            
            // Change ability back to Select
            ability.ability = Ability::Select;
        }
    }
}

The system will always run, but will exit early unless the current ability is HealUnit. Then if the user presses the Left mouse button, it will heal the unit under the cursor, and change the ability back to Select.

Some abilities, like Teleport will also take a parameter, like an entity, so we can know which unit to teleport.

Damage numbers

The last important change I've worked on is Damage numbers.

Damage numbers demo

Damage number demo in Diablo 3. Source

Damage numbers are those floating colored numbers that appear whenever you deal damage in a game. They're not the most important thing to implement at this stage, but they provide feedback to the player and debugging information for me, to make sure that units do get healed or damaged when they're supposed to.

The whole implementation is relatively simple: there are 3 systems that handle spawning, rotation/movement, and despawning. All of this happens in the systems/health_numbers.rs.

The first system, spawn_health_numbers, checks if any unit has had a change in Health, both positive or negative, and if so creates a Texture with the difference value in written on it, using the Font::render_text function. It then spawns a Sprite on top of the Unit, and assigns it the Texture and the HealthDifferenceNumber component.

The move_numbers_up_and_rotate system just rotates the numbers to always face the camera, while moving them up slightly.

The third and last system, despawn_numbers, takes care of despawning the numbers after their specified lifetime has passed.

Here's how it looks all together:

Damage numbers

Next steps

For my next steps I want to make units that are bigger, because in some places I've assumed that units are size of 1, and I want to be able to have all sizes of units. This is actually a pretty good development practice, restrict your requirements and get something working, and then work on generalizing. This way, after having done a "naïve" implementation, you'll have a lot more knowledge about the problem and your general solution will probably be much better.

I also want to get some models and start getting off the programmer art stage. I don't want to have super nice looking assets straight ahead, but getting something going would be nice, so I don't look at cubes all day.

Another thing on the list is to add hotkeys for the ability buttons, to be able to run abilities fast without having to move the mouse over to the buttons.