GRIM

Untitled Game Project (in Rust & With<Bevy>)

I have not felt this productive in months. Where to start with this?

Since mid august of this year, I’ve toyed with the Bevy game engine. Since then, I have racked up 59 commits (not counting the ones from my experiments a year prior) or about 1/day. 71 commits and a broken test checkmark. Of course, this number is inflated by the ~7 commits dedicated to making the Github workflow run. Update rust.yml repeated 7 times.

But all those commits had a purpose! I present for you, dear reader: “Bevy Töst Game” (name not final) Two desktop windows showing the same scene of two blue a-posing characters facing each other.

A prototype for a multiplayer RPG.

Okay, but how did you end up here?

Excellent question. I got bored and started experimenting with Bevy. Bevy is, according to their website: “a refreshingly simple data-driven game engine built in Rust.” a statement I agree with. It’s a great engine built around ECS. I believe that a large part of the reason I have felt so productive recently is due to Bevy being very ergonomic. There’s not much friction to get you started with Bevy (at least for me, I doubt I represent everyone).

The Movement that sorta could.

All that existed in my experiment project at the time was movement (and it wasn’t that good).

Early movement gif

But I didn’t worry about that and instead did the obvious next thing. I added a server and another client to the mix.

The REPLICATED Movement that could.

Early server testing After some movement replication code fixes… Slightly less early server movement testing.

An issue here should quickly be obvious: There aren’t two clients in the right window. That’s because we don’t tell clients about existing clients yet. We could work on that or work on refining movement replication. I chose the latter.

So, at this point, we have Replicated Movement & Server prediction of movement. It’s not good. When a player stops or switches direction, the server snaps into position. This is noticeable if you look closely at the video.

Quick Camera break.

Around here, I implemented a player-attached third-person camera based on MMOs. We still don’t initialize old clients on new ones.

Camera System

Back to movement…

The day after making the camera system, I finally gave up on Server side prediction.

Ok we only sim on client.

And then finally implemented network relevance, new clients are now sent existing entities around them.

Replication relevance!

The Beginnings of Combat & Spells

We have reached the middle of September by now. The month of movement gives way to blood. The question is: “How do you develop a combat system?”.

Well, you need things to modify and other things that modify them or Statistics and Abilities. Statistics are things like; Health, Stamina, Intellect, and Resistances. Abilities are the spells & actions you can take to modify statistics or the world: Fireball, Limit Break, Lay on Hands, as well as spells like Teleport, are all abilities that would be representable in a flexible enough spell system.

Healthy & hurtful living.

Let’s start with the Statistics part. Here’s how I represent health:

pub struct StatHealth {
    // Not including active effects.
    pub base_health: f32,
    pub base_health_max: f32,

    // With timed-effects & multipliers applied.
    pub current_health: f32,
    pub current_health_max: f32,

    // Additional health buffer / shield.
    pub absorption: f32,
}

Current (Max) Health is what we display to players. It takes into account multipliers & additions from timed effects. Base (Max) Health is what we apply direct damage & healing to.

With a representation of health implemented, we need a way to modify it. We could theoretically get an entity’s StatHealth in every place where we want to modify it and go health -= damage but that quickly becomes messy when resistances come into the mix. Directly changing health is bad (at least in this context).

So how exactly do we modify health?

Bevy has a concept of Events where we can pass data between systems. We could for example, pass an event such as this when we wish to deal damage:

pub struct TargetedDamageOperation {
    pub source: Entity, // The entity from which this operation originated.
    pub target: Entity, // The entity this operation is targeting.

    pub school: Option<SpellSchool>, // Optional Damage School, used for resistances, for example: Physical.
    pub bonus: f32, // Base damage value.
    pub attribute: Option<(f32, Attribute)>, // Optional attribute backed damage, for example: 5% of Strength 
}

Into a system that handles the damage calculations and takes into account resistances. My system works as follows:

Where each step is a separate system.

  1. Read TargetedDamageOperation/TargetedHealingOperation events and push them into a list on the target entity.
  2. Calculate the sum of bonus damage/healing & attribute damage/healing, and push it into another list.
  3. Apply resistances to damage operations that have a school.
  4. Sum all damage & healing (total_healing - total_damage) operations from this frame and apply to StatHealth::base_health.
  5. Recalculate current_health based on base_health & timed-effects.

All damage & healing feeds into this, whether it’s a rogue’s stab or your healer’s Rejuvenating effects, both generate TargetedDamageOperation or TargetedHealingOperation events.

Damage effect

Spell Slingin’

And then there were abilities somehow we gotta supply those health modifications. Players need fireballs that hurt and holy fireballs that heal and spatial fireballs that–

What does a spell look like? Well, a spell needs; Effects that execute when cast, which I represent with enums:

pub enum SpellEffect {
    Damage {
        school: Option<SpellSchool>,
        bonus: f32,
        attribute: Option<(f32, Attribute)>,
    },
    Heal {
        bonus: f32,
        attribute: Option<(f32, Attribute)>,
    },
    // etc...
}

The Damage & Heal effects match to the TargetedDamageOperation & TargetedHealingOperation events mentioned above. If we slot one into a spell, it will push out an event into the health systems when the spell is executed.

We also need a target, again represented as an enum.

pub enum SpellTarget {
    Caster, // Self
    Character, // Targeted character
}

The spell structure then looks like this:

pub struct Spell {
    pub target: SpellTarget,
    pub effects: Vec<SpellEffect>, // Effects applied by this spell.
}

And that’s all we need for now. Now we need to execute the spell, apply the effects, etc. And at this point in the project, that process looks like this for a direct damage spell:

The flow of spell execution

Healing

This is technically a different gif from the last one. This one has a healing spell! It's not very visual though.. We'll have to solve that eventually.

Over-time and time again

Poison, a favored tool of the good rogue. We don’t support a poison yet. We need a way to execute effects over time. We will refer to these over-time effects as Auras from now on. Auras do what spells do but over time (and not everything spells do & also things spells don’t do because it doesn’t make sense as an over-time or direct thing).

For example, we could have an aura like this:

Poisonous Intellect
Deals 10 damage every 0.5 seconds & increases the Intellect attribute by 100 points.
30 seconds remaining.

Which is applied to a target by a spell with the ApplyAura effect.

Aura effects are defined similarly to spell effects:

pub enum AuraEffect {
    PeriodicDamage { // A damage effect applied every X units of time.
        school: Option<SpellSchool>,
        bonus: f32,
        attribute: Option<(f32, Attribute)>,
    },
    PeriodicHeal { // A healing effect applied every X units of time.
        bonus: f32,
        attribute: Option<(f32, Attribute)>,
    },
    // etc...
}

And the structure for an aura looks like this:

pub struct Aura {
    pub effects: [AuraEffect; 3], // Effects this aura apply.
    pub amplitudes: [Option<u32>; 3], // Time between execution of periodic effects in milliseconds. This is the unit of time referenced in the previous codeblock.
    pub duration_millis: u32, // How long the aura is applied for.
}

Every frame, the server then goes through the following process:

An aura frame.

A periodic heal in action

Cast Times

Not all abilities execute instantly. Some may take a bit of time to execute. We just figured out how to do auras. They’re not quite cast times, but the duration works similarly.

We can slot the ideas from aura duration into the spell system:

Cast Time flow

Casting spells!

We also got ourselves a cast bar and a health bar. That would've been useful a few paragraphs back..

Spell cooler

Cooldown Tick

Cooldowns are essentially another timer. When a spell is executed, we send a cooldown event then we slap a cooldown check into the cast spell system.

Cast Time flow

This is very hard to visualise without UI.

The End… for now.

From the screenshot that started this post, it is probably clear that there is more to tell after this, but this post is already very long, and we’ve caught up with most of my work. I’ve skipped over refactors and headbanging so far, as well as more in-depth explanations of how everything works.

Bevy is a fantastic engine. My favorite feature so far is change detection, where we can do things only on entities where relevant data has changed. I love it. I use it quite heavily in things that haven’t happened yet in the time frame of this post. We’ll get to that eventually.

Fear tim self for they are shaped

Continue to next post.

Written 13 Oct 2022 by Grim