Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Floating text

<FloatingText> holds a pool of texts that stand at a point in the world. Mount one in a scene, inside a World beside its views. Any view in the scene then shows a text with useFloatingText().show({ at, text }): a damage number over what was hit, a coin’s worth where it was picked up. at takes a vector, { x, y, z }, or an entity’s [x, y, z] position as it is.

Its props are in FloatingTextProps, and a text’s in FloatedText.

Each text rises and fades over its seconds, then hides. The pool writes its elements in place each frame, so a hit mounts nothing and a crowd of hits renders nothing in React. The pool holds capacity texts, 48 unless changed. One more drops the oldest.

The pool draws small bold white text with a dark edge. Pass className to the pool to change the font, the size or the shadow of every text. Pass color with a text to colour that one, and className with a text for its own size, colour or animation: a crit larger and gold, a burn small and orange. The text’s classes sit on the text itself, apart from the element that moves and fades it, so a class that animates the text’s own transform runs.

FloatingText exposes { shown }, the texts on the screen, to the tools under the page’s floating text. spawnite play dump prints it, and spawnite play eval "views.page['floating text']" reads it.

Roblox shows a BillboardGui over a part, Unity a world-space TextMeshPro, and Godot a Label3D: each is an object in the scene, one per text, which a game pools by hand. Unreal draws a widget component over an actor. Phaser pools text objects. PlayCanvas has an element component.

This engine pools the texts itself and takes one call per text, because every game with damage numbers writes the same pool. The text is DOM over the canvas, not a mesh in the scene, so it stays sharp at any distance and takes the game’s fonts.

A number over a target each time it is hit:

import {
DamagedEvent,
FloatingText,
useEvent,
useFloatingText,
World,
} from "@spawnite/engine";
function DamageNumbers() {
const floatingText = useFloatingText();
// Each hit once, where it landed, a killing blow's too.
useEvent(DamagedEvent, ({ record: hit }) => {
floatingText.show({
at: { x: hit.at.x, y: hit.at.y + 2, z: hit.at.z },
text: String(Math.round(hit.amount)),
color: hit.amount >= 50 ? "#ffe08a" : undefined,
});
});
return null;
}
export function Arena() {
return (
<World map="meadow">
<FloatingText />
<DamageNumbers />
</World>
);
}

A text that belongs to one entity, such as a coin’s “+1”, comes from a view under that entity’s <Entity> that passes { entity: useEntity() } to useEvent. Without it, the view hears every entity’s event, so every coin shows a “+1” whenever any coin is taken. The coin’s own position goes straight to at:

packages/engine/test/outside/Coin.tsx
import {
Entity,
Pickup,
TakenEvent,
useEntity,
useEvent,
useFloatingText,
useParticles,
type ParticleEffect,
type Position,
} from "@spawnite/engine";
export interface CoinProps {
position: Position;
/** The burst where the coin is taken, one of the scene's
* `<Particles effects>`. */
sparks: ParticleEffect;
}
export function Coin({ position, sparks }: CoinProps) {
return (
<Entity position={position}>
<Pickup reward={1} />
<CoinTaken position={position} sparks={sparks} />
<mesh>
<cylinderGeometry args={[0.4, 0.4, 0.1, 24]} />
<meshStandardMaterial color="gold" />
</mesh>
</Entity>
);
}
// Under the coin's Entity, so `useEntity()` is this coin. Without
// `{ entity }` the view hears every coin's TakenEvent, and each coin
// would burst whenever any coin is taken.
function CoinTaken({ position, sparks }: CoinProps) {
const floatingText = useFloatingText();
const particles = useParticles();
useEvent(
TakenEvent,
() => {
floatingText.show({ at: position, text: "+1", color: "gold" });
particles.spawn(sparks, { at: position, color: "#ffd166" });
},
{ entity: useEntity() },
);
return null;
}

Client. A text changes nothing in the world; each player’s browser draws its own. Headless and on a room’s server, FloatingText renders nothing and show does nothing.