-
Notifications
You must be signed in to change notification settings - Fork 0
Matter
π Matter is the base carrier of event data. Every matter has a
Guid(its unique identity) and a list ofCircumstances(a collection of matter that led to the emission of this one).
Extend Matter to define your own event types:
class PlayerDied : Matter
{
public string Cause { get; }
public PlayerDied(string cause) => Cause = cause;
}
class EnemyDefeated : Matter
{
public int XpReward { get; }
public EnemyDefeated(int xpReward) => XpReward = xpReward;
}ππ§ Mark engine-native fields on matter with [JsonIgnore] (from System.Text.Json.Serialization).
- Game matter routinely carries engine handles -
PackedScenereturned from a scene load, aNodereference, aTexture2D, etc. - These belong on matter, they just shouldn't go over the dev wire to Eris, because
System.Text.Jsoncan't serialize them.
public class LoadSceneResponse : Response<LoadSceneRequest>
{
[JsonIgnore] public PackedScene PackedScene { get; }
public LoadSceneResponse(LoadSceneRequest request, PackedScene packedScene)
: base(request, wasSuccessful: true)
{
PackedScene = packedScene;
}
}- Strings, lists, nested
IMatter, and your own plain data classes serialize fine and need no annotation. -
Forgetting
[JsonIgnore]on an engine-native field breaks Eris casuality and downstream matter occurences will displayMissingcircumstances. - When that happens Eris emits a Horror about that. Your game runs unchanged, only the debugger enters trouble-land when that happens.
π𧨠Try to keep your Matter instances immutable - avoid mutable reference-type fields and set accessors. Mutating matter after emission produces three specific failure modes:
- Snapshot warping: subscribers that process matter on different frames see different values for what is supposedly the same event.
-
Circumstance pollution: matter lives in multiple
Circumstanceslists, mutationas mess up the causal DAG by causing rewrites within its nodes. - Eris timeline confusion: mutations between emission and serialization show up as drift in the recorded history.
Two common ways the temptation appears, and the rzeka-native answer is:
-
"I want this to update over time and have everyone see the new value." That's state, not an event. Mark the type
[HasState]and evolve it through a reducer Loom - see Attributes. - "I want consumers to be able to call X on this matter." X is a service the consumer depends on, not data that belongs on matter. Inject it using Microsoft.Extensions.DependencyInjection (or your preferred equivalent) and keep matter as data.
ππ± I understand the temptation is sometimes too great, and that's okay. When it stops feeling occasional and starts feeling structural, one of the above two patterns is almost always the better fit.
π Circumstances describe context of a given matter emission.
They are usually attached automatically, but there are situations where you have to do it manually, to read more on that see Circumstance rules. Nothing horrible happens if you forget it, game runs fine, but Eris, rzeka's debugger will lose casuality tracking in such situations.
For example, a DamageDealt matter could carry the AttackExecuted that triggered it as a circumstance.
- This allows you to track the causality chains through Eris.
- Or to check in your game logic if a given matter is caused by another, anywhere up its circumstance chain, using
someMatter.IsCircumstancedBy(anotherMatter).
// pseudocode, just to show relations between matter, see API section for where you will actually instantiate matter
var attack = new AttackExecuted(attacker: "dragon");
var damage = new DamageDealt(amount: 40).WithCircumstances<DamageDealt>(attack); // manual circumstances
// Later, check if a piece of matter is causally linked to something specific
bool causedByDragon = damage.IsCircumstancedBy(attack); // trueName matter in the past tense - matter represents facts that already happened (GameStarted, PlayerMoved, EnemyDefeated). Reading a circumstance chain like DamageReceived β AttackLanded β FireballCast tells a coherent story.
Two shapes to avoid:
-
Present continuous (
GameLoading) usually wants to be state instead - use[HasState]with aStatesuffix. Otherwise you end up re-emitting the same matter to mean "still loading," which is exactly what[HasState]exists to replace. -
Imperative (
StartGame) usually wants to be a request instead - use a Shuttle Request/Response pair. The fact emitted after the request resolves is the past-tense matter (GameStarted).
π§π»ββοΈ Fairy infestation test: "X happened, and as a result Y also happened." If the sentence doesn't parse with your matter names, it's probably state or a request wearing the wrong shape.
ππ§
RequestedvsRequest. Use past-tenseRequested(ScreenShakeRequested,HealthBarUpdateRequested) for regular matter that asks for something fire-and-forget, but reserve the nounRequestfor theIRequesthalf of a Shuttle pair (SaveGameRequest). The-edvs-tending doubles as a call-site cue:Loom<β¦, FooRequested>is one-way,Shuttle<FooRequest, FooResponse>is round-trip.