Events
An event is something that happened, kept in a log each world holds. Every emit is one event, read once by each system and heard once by each view that listens, in the order emitted; two emits are never merged. A system reads it with readEvents, a view hears it with useEvent, spawnite play dump lists it under the dump’s events, and in a room it reaches every page that holds its entity. The engine’s own include DamagedEvent, DamageRefusedEvent, DiedEvent, HealedEvent, TriggerEnteredEvent, TriggerExitedEvent, ChaseReachedEvent, ChaseExitedEvent, TakenEvent, InteractedEvent and ItemAcceptedEvent.
A game’s own event
Section titled “A game’s own event”A game makes its own with defineEvent, which takes the name the log, the dump and the room know it by, then the record’s defaults, or a function that makes them, or none for an event with no record. Every field that holds an entity, or a list of them, goes in entities, one level into a record or a list of records included. Declare what a page that does not hold the entity receives, WhenAbsent.Null or WhenAbsent.Withhold, or the event fails to typecheck. The field travels as the entity’s key, and a page reads it back as its own copy of that entity. A list the default holds empty names what one of its records is in elements, so a page reads a streamed list by it and a vector in a record comes back a vector. For a hits: [] as { at: Vector3 }[] default, elements: { hits: () => ({ at: new Vector3() }) } says each hit holds a vector.
import type { Entity } from "@spawnite/engine/core";import { defineEvent, useEvent, WhenAbsent } from "@spawnite/engine";
/** On the monster a chain of lightning starts from: each monster it * jumped to after, in order. */export const ChainedEvent = defineEvent( "chained", (): { hits: Entity[]; damage: number } => ({ hits: [], damage: 0 }), { entities: { hits: WhenAbsent.Null } },);
export interface ChainArcsProps { /** The game's own effect: an arc from each monster to the next. */ drawArcs: (monsters: readonly Entity[]) => void;}
/** Draws each chain on this page, once, as the frame hears it. */export function ChainArcs({ drawArcs }: ChainArcsProps) { useEvent(ChainedEvent, ({ entity, record }) => { if (entity) drawArcs([entity, ...record.hits]); }); return null;}A system in the room emits it with emitEvent(step, ChainedEvent, { hits, damage }, { entity: first }) as it deals the damage to each monster. ChainArcs on every page then draws the arcs with the game’s own drawArcs, between the monsters where they stand now.
Reading it in a system
Section titled “Reading it in a system”- Each system reads every event once.
readEvents(step, event)returns every event emitted since that system last read the event, each as{ entity, record, tick }, andreadEvents(step, event, { entity })reads one entity’s alone. A system that runs before the emitter reads it the next step and one after reads it the same step.core’s reference page tables every engine system in the order the step runs them, and a game’s own system runs after the engine’s in its phase unless it anchors. A system that skips ticks, by its own counter or a schedule, reads every event since its last read. The world keeps an event for a system that has read it before for up tomaximumUnreadSteps, 60 steps or one second; a read later than that gets what is left, counted in the room’s heartbeat and named in development. A system never reads its own emits of the run that made them: it reads them on its next run. The array is valid until the system returns. - A predicted system reads no events. A replay after a correction runs predicted systems again, which would read each event again;
readEventsrefuses a predicted step by type. A reaction that changes predicted state runs in an ordinary system on the room, which runs once a step, and writes the predicted value withgrantPredicted, which the player’s page takes as one correction. - A reader can tell it missed events.
readEventsreturns its list withgap, true where the read came more thanmaximumUnreadStepsafter the system’s last read, or its page fell past the bound behind the room. After a gap, read the state the events describe rather than trust the events: whether a trigger stands occupied, not that a missed exit never happened.
Emitting one
Section titled “Emitting one”- An event is an outcome, so emitting one takes an authority. Pass the room’s step, narrowed by
step.authoritativewhere needed.findAuthority(writer, entity)gives a page authority over its own local entity, or null for a streamed one. An emit between steps reaches every system on the next step. A page cannot emit an outcome about an entity the room streams. The room’s event reaches that page after the room handles the player’s action. - Each emit is one event. Two hits on one monster in one step are two
DamagedEvents, each with its own record, aDamagedRecord.weaponis the nameregisterWeapongives, never the weapon’s entity, and the weapon page says what each field holds. A moment that touches many entities may still carry them in one event, as the chain does, which each page draws between their current positions. - An event can go to chosen players.
emitEvent(step, event, record, { to })takes a player’sPlayerEntity, or a list of them, and sends the event to their pages alone, in emit order among the rest. Every system of the world that emitted it reads it. A player who left before the emit receives nothing, an empty list reaches no page, and the room tells a command’s sender the command’scauseonly of an event it sends that sender. A game played alone hands its views, and its systems that run on the client alone, only what reaches its own player, as the player’s page in a room receives it. - A world holds at most
maximumHeldEventsevents, andmaximumHeldEventBytesbytes of records, at once. Past either, a room refuses an emit, which throws in development naming the system emitting most and is counted in the heartbeat in a deployed room. A page that has fallen behind drops its oldest, with a development warning.
Hearing it in a view
Section titled “Hearing it in a view”- A view hears each event once, after the frame. The frame loop hands each world’s events to its views once a frame, after the frame’s steps and its animation, however many steps the frame ran. A view hears each event emitted after it subscribed; unmounting drops the rest. A listener’s throw is reported and the other listeners still hear the event, and an event a listener emits is heard at the next frame. A room never draws, so it keeps nothing for views.
- A view hears every entity’s event unless it passes
{ entity }asuseEvent’s third argument, even a view mounted under one<Entity>. An effect that belongs to one entity, such as a coin’s sparks, passes{ entity: useEntity() }, as the Pickup page’s coin does. - A view that mounts as its entity arrives hears none of the events that came with it. The frame hands out its events before React mounts the views of what arrived. On a page in a room, that is every event the send that carried the entity held. In a game played alone, it is every event the step that spawned it emitted. Hear such an event in a view mounted before it, such as one with no
{ entity }. A view that mounts also mounts for a player who joins later, so draw the arrival from the entity’s traits, and play a one-off sound from the event. - A record is the reader’s to keep. The engine copies each record as it is emitted and never reuses one, so a view may keep it, in React state or anywhere else, and a reader never sees it change.
Where it goes
Section titled “Where it goes”-
Its
replicationsays which pages hear it.EventReplication.Reliable, the default, reaches every page that holds its entity, kept through a page that falls behind or hides its tab, up tomaximumHeldEvents.EventReplication.Cosmeticis dropped by a page more than a second behind or with its tab hidden, as a spark or a tracer should be.EventReplication.AuthorityOnlystays on the world that emitted it, the room or a game played alone, anduseEventrefuses it by type.EventReplication.Localis the drawing world’s own, emitted withemitEvent(world, ...)outside any step, as the engine’sClipMarkEventis as a clip passes a mark, andreadEventsrefuses it by type.scope: EventScope.Globalmakes an event about the world, with no entity, such as the engine’sShotJudgedEvent. -
An event outlives its entity.
event.entityreads null once the entity is destroyed, and so does an entity field that names it, or a list leaves it out, even after a new entity takes its slot, while{ entity }still matches it. Put what a reader needs in the record:DamagedEventcarries where the hit landed, so a damage number shows for a killing blow. On a page in a room, the entity stands until its readers are done, as the next bullet says. -
In a room, each send carries every event emitted since the last, one entry per emit, with the tick it was emitted at. A page holds them for its next step’s systems and its frame’s views. When the stream removes an entity that an event the page holds still names, as its entity or in an entity field, the entity stays in the page’s world. It stays until the page’s next step has read the event and, in a game’s frame, its views have heard it. Only then does the page remove it. So a page reads a death with the dead entity standing, even when one send carries the death and the removal, or one frame lands both sends. It stands as the page last held it: a write the room made in the tick it destroyed the entity never reaches the page.
A page’s reader may therefore find an entity standing that the room’s reader, running later in the same step as the destroy, read as null. A system that skips ticks and reads later reads null, as it would on the room. An event whose entity the room destroyed before the send arrives marked gone, so a page that never held the entity reads it as null. A player who joins hears every event emitted after the tick of the snapshot in the player’s welcome, and none before, since the snapshot holds their consequences. An exception is an event sent to the joining player by name with
to, which the joining player’s welcome carries from the step the player’s join landed in. -
A page that does not hold an entity receives what the event declares. An event on a living entity a page does not hold never reaches it. A
WhenAbsent.Withholdfield naming such an entity keeps the event from that page; aWhenAbsent.Nullfield reads null there, and a list leaves the entity out, so an event never hands a page an entity it was not sent. Anullthe list held stays, so a list beside it keeps its places. An entity destroyed since the last send counts as held by the pages that held it, so a field naming it still reaches them, null once their readers are done with it; an event on a destroyed entity reaches every page, marked gone.
Beside the other engines
Section titled “Beside the other engines”Roblox’s BindableEvent, Unreal’s delegates, Unity’s UnityEvent and Godot’s signals call a listener inside the emit, which in a room runs only where the emit runs. Here an event is a log that systems read, as Bevy’s buffered messages are, each reader from where it last read, so the dump lists it, the room streams it and a replay replays it. An agent from Bevy 0.17 reads this engine’s event as Bevy’s message, not Bevy’s observer. emitEvent takes its verb from Godot’s emit and Node’s EventEmitter, and does what Bevy’s MessageWriter::write does: it hands the event to the systems that read it, and calls no listener.