Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, Life and Mana, so a system asks for Current<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>() gives Life the next AttributeId; behaviours, routes, executions and calculations get theirs the same way. Code names each by type: reads and changes, such as value::<Life>(), Current<Life> and BaseChange::of::<Life>, find its number for you. world.attribute_id::<Life>() asks for it, as Bevy has world.component_id::<T>(), to match what an event names.
  • Content is numbered as it’s checked. Tags, curves, effects and abilities get a TagId, a CurveId, an EffectId and an AbilityId, 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, its EffectDef or AbilityDef, 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

PartWritesReads
AttributesCurrent<A>, from bases and modifiersBase<A>, Modifiers of the actor’s sources, its OwnedTags
EffectsModifiers and granted tags while they last; bases as they executeTheir registry, attributes, owned tags
AbilitiesTheir behaviour’s component; costs and cooldowns as effectsTheir registry, attributes, owned tags
Your systemsBases, OwnedTags, effects applied, abilities activatedCurrent<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:

  1. Effects: requirements weighed, periods come round, time run out.
  2. Attributes: changed bases synced in, changed modifiers folded, owned tags told, values worked out.
  3. Settle: effects whose live reads moved are worked out again, and attributes folded again, until nothing moves.
  4. 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 in Update sees what the frame’s last fixed step published.
  • A change made after mettle ran. A base changed after MettleSystems reaches Current the next time it runs.