How it fits together
The parts of bevy_mettle meet on the actor. This chapter shows what is data and what is code, who writes what, and in which order.
Data and code
Mettle splits a game’s gameplay in two:
- Code is what your systems read: attributes are types of yours,
LifeandMana, so a system asks forCurrent<Life>. What an ability does is a component of yours, run by your systems. - Data is content: gameplay tags, curves, effects and abilities. Each
is named by a
Key, such as"burning"or"fireball", so a designer or a mod can add one without code.
Code names things by type, never by string. A tag code uses is a
#[derive(Tag)] type, an effect it applies a #[derive(Effect)] type, an
ability it grants a #[derive(Ability)] type. A misspelled one doesn’t
compile. Each type carries its content’s name, checked once, as content
loads.
Content built in code can name by type too. A Key comes from a string,
or from a type with Key::of::<Burning>(), keyed by the name its derive
gives it. Builders take either:
use bevy_mettle::prelude::*;
#[derive(Effect)]
struct Burning;
let named: Key<AnyEffect> = "burning".into();
assert_eq!(Key::of::<Burning>(), named);
let burning = EffectData::lasting(Key::of::<Burning>(), 4.0);
assert_eq!(burning.name, named);
A type is of one kind, its key’s kind inferred from it: Key::of::<Burning>()
is a Key<AnyEffect>, and Burning::KEY its name. An ability and the effect
it costs are two types; deriving both on one doesn’t compile.
Code is added to the app: attributes with add_attribute, behaviours with
add_behaviour, effect code with add_execution, add_calculation and
add_route, and the tags, effects and abilities code names with add_tag,
add_effect and add_ability.
Data is handed over as one resource, MettleContent, and checked as a
whole against the code and itself: an effect naming a tag that isn’t
defined, an attribute that wasn’t added, a curve that doesn’t exist, or an
effect code names that content doesn’t define is an error, all of them at
once. Content that holds becomes a TagRegistry, a
CurveRegistry, an EffectRegistry and an AbilityRegistry; content
that doesn’t is rejected, and the content before stays. Mettle doesn’t load
files. Your game reads them however it likes, and hands the content over:
see Loading data.
Names and numbers
Everything mettle runs on is known by a number, as Bevy knows a component by
its ComponentId. Names are for people: what data files are written in,
what’s shown in debug output, and what a client keys icons and translations
by.
- Code is numbered as it’s added.
add_attribute::<Life>()givesLifethe nextAttributeId; behaviours, routes, executions and calculations get theirs the same way. Code names each by type: reads and changes, such asvalue::<Life>(),Current<Life>andBaseChange::of::<Life>, find its number for you.world.attribute_id::<Life>()asks for it, as Bevy hasworld.component_id::<T>(), to match what an event names. - Content is numbered as it’s checked. Tags, curves, effects and
abilities get a
TagId, aCurveId, anEffectIdand anAbilityId, in order of name. A reload keeps the numbers things had, and numbers new ones after them. - Data names code by name. An effect names the attribute
"life", an ability the behaviour"cast_fireball". Checking the content resolves each name to its number, once: an effect or an ability as the app runs it, itsEffectDeforAbilityDef, holds numbers, not names. Code names content by type, and the type is resolved to its number the same way. - The network carries numbers. A server and its clients run the same code and the same content. Adding the same attributes, behaviours and routes in the same order numbers them alike, and content numbered in order of name is numbered alike wherever it’s checked. Replication already asks the same of components.
Each kind of thing has a registry, a resource that keeps the names beside
the numbers: AttributeRegistry, BehaviourRegistry,
ExecutionRegistry, CalculationRegistry and RouteRegistry for code;
TagRegistry, CurveRegistry, EffectRegistry and
AbilityRegistry for content. The registries of code, effects and
abilities read the same way: get(id) for what’s numbered id,
id("name") for a name’s number, id_of::<T>() for a type’s, name(id)
for a number’s name, and iter() for all of them. TagRegistry goes
between a tag and its number with id and key, and CurveRegistry with
id and name.
use bevy::prelude::*;
use bevy_mettle::prelude::*;
#[derive(Attribute)] #[attribute(base = 100.0)] struct Life;
let mut app = App::new();
app.add_plugins((MinimalPlugins, MettlePlugins)).add_attribute::<Life>();
let attributes = app.world().resource::<AttributeRegistry>();
let life = attributes.id_of::<Life>().unwrap();
assert_eq!(attributes.id("life"), Some(life));
assert_eq!(attributes.name(life), Some("life"));
Who writes what
| Part | Writes | Reads |
|---|---|---|
| Attributes | Current<A>, from bases and modifiers | Base<A>, Modifiers of the actor’s sources, its OwnedTags |
| Effects | Modifiers and granted tags while they last; bases as they execute | Their registry, attributes, owned tags |
| Abilities | Their behaviour’s component; costs and cooldowns as effects | Their registry, attributes, owned tags |
| Your systems | Bases, OwnedTags, effects applied, abilities activated | Current<A>, abilities’ components, what’s told |
For the mage, casting the fireball applies fireball.cost and
fireball.cooldown. The cost lowers Base<Mana>, the cooldown tags its
owner cooldown.fireball, and attributes work out Current<Mana> from the new
base.
One step
Mettle runs in its own schedule, Mettle, once each fixed step, in
FixedPostUpdate. Its steps, the MettleSets, run in order:
- Effects: requirements weighed, periods come round, time run out.
- Attributes: changed bases synced in, changed modifiers folded, owned tags told, values worked out.
- Settle: effects whose live reads moved are worked out again, and attributes folded again, until nothing moves.
- Publish: changed values written to
Current<A>, and told.
So what your systems do in a tick’s FixedUpdate is ticked, folded and
published before the tick ends. A rollback replays the tick, and mettle
with it. Abilities stand apart: in AbilitySystems, in PreUpdate, new
definitions are given out and, over a network, requests served and replies
resolved.
The system running Mettle is in MettleSystems. Order your systems
against it:
use bevy::prelude::*;
use bevy_mettle::prelude::*;
fn regenerate() {}
fn health_bars() {}
let mut app = App::new();
app.add_systems(FixedUpdate, regenerate)
.add_systems(FixedPostUpdate, health_bars.after(MettleSystems));
Applying an effect or activating an ability doesn’t wait for a schedule.
It happens as the command is applied, and what it changes is folded the
next time Mettle runs. Installation
shows how to run it elsewhere.
Owned tags
An actor’s OwnedTags are what it’s going through: state.burning,
state.stunned, cooldown.fireball. Effects grant them while they last,
abilities while they’re active, and your systems add their own, such as
state.moving. Every part reads them: a modifier can count only while
its owner owns a tag, an effect can need one to apply, and an ability is
blocked by one. Gameplay tags covers them.
Pitfalls
- Ordering across schedules.
.after(MettleSystems)orders systems only within the schedule mettle runs in. A system inUpdatesees what the frame’s last fixed step published. - A change made after mettle ran. A base changed after
MettleSystemsreachesCurrentthe next time it runs.