Build
Component Reference
Edit on GitHubAll component types with their properties and defaults.
Generated from the Zod schemas in packages/core/src/schema/components.ts. Run hearth inspect components --json for the machine-readable form.
Transform
Position (pixels), rotation (degrees), and scale of an entity. Almost every entity needs one.
Defaults:
{
"position": {
"x": 0,
"y": 0
},
"rotation": 0,
"scale": {
"x": 1,
"y": 1
}
}
SpriteRenderer
Renders a sprite asset (assetId) or a colored primitive (shape/color/width/height) when no asset is set.
Defaults:
{
"assetId": null,
"frame": null,
"shape": "rectangle",
"color": "#ffffff",
"width": 32,
"height": 32,
"opacity": 1,
"flipX": false,
"flipY": false,
"layer": 0,
"visible": true
}
frame names a frame on assetId’s sliced spritesheet (see
assets.md) to draw as a
sub-rectangle of the texture instead of the whole image; null (the
default) always draws the whole image, sliced or not. SpriteAnimator
sets this automatically for sheet-backed clips. See
SpriteAnimator below. An unresolvable frame logs a
warning once and falls back to the whole texture (also flagged by
hearth validate as FRAME_NOT_FOUND).
color doubles as a tint on textured sprites (an asset is set), not
just the primitive fallback’s fill. Edits take effect immediately. The
default #ffffff is a no-op tint, so existing projects render
unchanged; set it to anything else to recolor real art (a damage flash,
a grayed-out disabled state) without a second sprite asset.
Collider
Box, circle, or convex polygon collision shape (polygon uses points, local space, min 3 convex vertices). isTrigger=true reports overlaps without blocking movement.
Defaults:
{
"shape": "box",
"width": 32,
"height": 32,
"radius": 16,
"points": [
{ "x": 0, "y": -16 },
{ "x": 16, "y": 16 },
{ "x": -16, "y": 16 }
],
"offset": {
"x": 0,
"y": 0
},
"isTrigger": false,
"layer": "default",
"collidesWith": ["*"],
"oneWay": false
}
Polygon colliders are convex only: at least 3 points, no duplicate
consecutive points, convex winding (validateProject enforces all three).
Split concave shapes across multiple entities. Points are local space and
respect the entity’s scale and rotation.
layer names this collider’s own layer; collidesWith lists the layers
it will contact ("*" matches any layer). Both sides must list the
other: two colliders only interact when A’s collidesWith includes B’s
layer (or "*") and B’s collidesWith includes A’s layer (or
"*"). A one-sided list is a no-op. A solid Tilemap’s auto-generated
colliders are always layer: "default", collidesWith: ["*"]. oneWay: true makes a non-trigger collider only block movers landing on top
of it while moving downward. Approaching from below or the side always
passes through; a oneWay trigger collider is unaffected by this
(triggers already never block, and report contact from every side).
PhysicsBody
Simple physics: dynamic bodies fall with gravity and collide; kinematic bodies move by velocity only; static bodies never move.
Defaults:
{
"bodyType": "dynamic",
"velocity": {
"x": 0,
"y": 0
},
"gravityScale": 1,
"drag": 0,
"mass": 1,
"restitution": 0,
"friction": 0
}
mass only matters between two dynamic bodies pushing on each other:
the overlap resolution splits proportionally, so a heavier body shoves a
lighter one further than it gets shoved back (a static/kinematic
obstacle is treated as infinitely heavy regardless of mass).
restitution (0-1, bounciness) and friction (0-1, tangential damping on
contact) are combined per contact pair by taking the max of both
sides: a bouncy Ball (restitution: 0.85) still bounces off a plain
static floor with restitution: 0 (max(0.85, 0) = 0.85); a floor with
its own friction: 1 slows anything that touches it regardless of that
body’s own friction. A Tilemap’s solid tiles have no material of their
own. They’re always an effective (restitution: 0, friction: 0) contact
partner, so give a floor real friction by adding a separate static
entity with its own Collider + PhysicsBody{friction}, not by editing
the tilemap. Restitution is suppressed below a 20 px/s incoming
contact speed (no micro-bounce jitter as a body comes to rest). Below
that, a bounce settles into a stop instead of jittering forever.
Script
Attaches a behavior script from scripts/ (scriptPath): Lua (.lua, the default) or JavaScript (.js). params are passed to the script as ctx.params.
Defaults:
{
"scriptPath": "",
"params": {}
}
Camera
Viewpoint for the scene. One entity should have a Camera with isMain=true.
Defaults:
{
"zoom": 1,
"isMain": true,
"backgroundColor": "#1a1a2e",
"ambientLight": 1,
"postEffects": []
}
ambientLight sets scene brightness with no lights present: 1 is fully
lit (lighting is disabled entirely and the renderer skips the lightmap pass,
so existing projects with no Light2Ds render exactly as before this
feature existed), down to 0, black except inside a Light2D’s radius.
Lower it (e.g. 0.15) for a dark scene lit by torches/lanterns placed as
Light2D entities.
postEffects is a stack of up to 8 screen-space post-processing filters
(bloom, CRT, vignette, chromatic aberration, pixelate, color grade),
rendered in array order. The empty-array default is what keeps a fresh
Camera a no-op. Any non-empty entry always changes pixels, since each
effect’s own field defaults are visually reasonable starting points, not
off/neutral values. See effects.md for the full param
catalog, ranges, order semantics, and determinism notes.
Text
Renders UI/world text (content, fontSize, color).
Defaults:
{
"content": "Text",
"fontSize": 16,
"color": "#ffffff",
"align": "left",
"fontFamily": "monospace",
"layer": 10,
"visible": true
}
AudioSource
References an audio asset; autoplay plays on scene start with loop/volume. Scripts can also play any audio asset via ctx.audio.play(assetRef, { volume, loop }).
Defaults:
{
"assetId": null,
"autoplay": false,
"loop": false,
"volume": 1,
"music": false
}
Create sound assets with hearth create sound <name> --preset coin
(deterministic procedural WAVs; presets: coin, jump, hit, laser,
powerup, explosion, blip). Headless runs record every play/stop as
audioEvents in the run report.
music: true routes scene-start autoplay onto the single shared music
channel (ctx.audio.playMusic) instead of a regular one-shot/looping
playback: use it for a soundtrack entity that should start on its own
and survive ctx.scenes.load scene switches. See
assets.md for the full playMusic/stopMusic/
setMusicVolume semantics, including the streaming-vs-single-file-export
caveat.
UIElement
Makes the entity screen-space UI: positioned by anchor+offset, unaffected by the camera. Visuals come from Text/SpriteRenderer. interactive=true sends pointer events to the Script hook onUiEvent(ctx, event). focusable=true lets it join keyboard/gamepad focus navigation.
Defaults:
{
"anchor": "top-left",
"offset": {
"x": 0,
"y": 0
},
"interactive": false,
"focusable": false
}
Anchors (top-left, top, top-right, left, center, right,
bottom-left, bottom, bottom-right) are points in the game’s
buildSettings width×height space; offset is pixels from there.
Transform.position is ignored (scale and rotation still apply). UI
renders above all world layers; within UI, layer (on SpriteRenderer,
Text, UISlider, or UIToggle; UIElement has no layer of its own)
orders elements, both for rendering and for which element wins a pointer
hit under overlapping widgets. Interactive elements are hit-tested against
the SpriteRenderer’s rect and/or the measured Text bounds; pointer/focus
events are click, press, release, enter, exit, drag, change,
focus, blur. See scripting.md for the
onUiEvent payload shape and ui.md for the full reference,
including UILayout (which reflows every UIElement child parented to
it; an entity’s own anchor/offset are ignored once it has a
UILayout parent).
UILayout
A layout container: stacks its UIElement children (vertical or horizontal), reflowing their positions each frame. Has no visuals of its own.
Defaults:
{
"direction": "vertical",
"gap": 8,
"padding": 0,
"align": "start"
}
Give the container entity its own UIElement (for its anchor/offset)
and parent the widgets you want stacked to it. A UILayout entity has no
visuals; it only positions children. direction picks the stacking axis;
gap is the pixel space between consecutive children; padding insets
the whole stack from the container’s anchor point; align positions
children on the cross axis (start/center/end; e.g. for a vertical
stack, left/center/right). Hit-testing and focus navigation both use the
reflowed position, not each child’s bare anchor/offset.
UISlider
A draggable value widget the runtime renders and hit-tests directly (handle + fill track). Needs UIElement{interactive: true} on the same entity to receive pointer/keyboard input.
Defaults:
{
"min": 0,
"max": 1,
"value": 0.5,
"step": 0,
"width": 160,
"trackColor": "#3a3a3a",
"fillColor": "#f76b15",
"handleColor": "#ececec",
"layer": 0
}
Dragging the handle (pointer down + move) maps the pointer position onto
the track and writes value, snapped to step when it’s above 0
(step: 0 is the default, meaning no snapping and any value in [min, max]).
Every value change fires onUiEvent {type: 'change', value} on the same
entity, in the slider’s own min-max range (not normalized to 0..1
unless that happens to be the range). Focused via UIElement.focusable
and nudged with ctx.ui.adjust(delta) (see
scripting.md), which uses step when set, or
a tenth of max - min otherwise, so keyboard/gamepad control feels
reasonable even with step: 0.
UIToggle
A boolean checkbox widget the runtime renders and hit-tests directly. Needs UIElement{interactive: true} on the same entity to receive clicks.
Defaults:
{
"value": false,
"size": 20,
"color": "#3a3a3a",
"checkColor": "#f76b15",
"layer": 0
}
A click flips value and fires onUiEvent {type: 'change', value} with
the new boolean. Activating a focused toggle via ctx.ui.activate() goes
through the same click path, so it flips too.
Tilemap
Character-grid tilemap: tileAssets maps grid characters to assets; solid=true auto-generates colliders.
Defaults:
{
"tileSize": 32,
"tileAssets": {},
"grid": [],
"solid": true,
"layer": -10
}
(This layer is the rendering z-order, the same numeric layer every
other SpriteRenderer/Text/etc. uses, not a Collider.layer physics
layer name. A solid Tilemap’s generated colliders are always physics
layer: "default", collidesWith: ["*"]; see Collider
above.)
A tileAssets entry is either a plain asset id (a fixed sprite/tile for
that char) or an autotile rule ({ sheet, template: "blob47", mapping? }) that picks the char’s on-screen frame from its 8 neighbours
at render time instead of drawing the same tile everywhere. Set with
setTileAutotile/hearth autotile set, never with
setComponentProperty directly (it rejects the rule shape). See
editor.md for the frame-naming convention and an
ASCII diagram of the neighbour bitmask.
Light2D
Emits dynamic 2D light (radius, color, intensity) in the forward rendering pipeline.
Defaults:
{
"radius": 200,
"color": "#ffffff",
"intensity": 1,
"enabled": true
}
radius is the light’s falloff distance in world pixels (quadratic falloff,
full brightness at the center); intensity multiplies brightness at the
center (values above 1 blow out to white faster). enabled: false removes
the light from the lightmap without deleting the entity. Every enabled
Light2D in the scene is composited into one lightmap alongside Camera. ambientLight, so a dark ambientLight (e.g. 0.15) plus a handful of
lights reads as torches/lanterns punching pools of visibility out of the
dark. See architecture.md for how the
lightmap is built. Lights ignore the entity’s rotation/scale (only position
matters); parent a light to a moving entity (e.g. the player) to make a
“torch” that follows them. Children inherit only their parent’s
translation, so a fixed local offset stays put relative to the parent.
LineRenderer
Renders a polyline in local space; use for debug geometry or simple line effects.
Defaults:
{
"points": [],
"width": 2,
"color": "#ffffff",
"closed": false,
"opacity": 1,
"layer": 0,
"visible": true
}
points are Vec2s in local space. The entity’s Transform
(position/rotation/scale) applies to the whole line, so you can move or
rotate a line by moving its entity rather than rewriting every point.
closed: true connects the last point back to the first, turning a
polyline into a closed outline (a cave wall, a patrol path, a fenced-off
area). Good for level geometry that doesn’t need art: cave walls, beams,
borders, debug paths.
ParticleEmitter
Spawns and simulates particles deterministically; seed controls reproducibility.
Defaults:
{
"emitting": true,
"rate": 10,
"burst": 0,
"lifetime": 1,
"speed": 100,
"spread": 30,
"direction": 0,
"gravity": { "x": 0, "y": 0 },
"startColor": "#ffffff",
"endColor": "#ffffff",
"startSize": 8,
"endSize": 0,
"maxParticles": 256,
"layer": 0,
"seed": 0
}
Particles spawn continuously at rate per second while emitting is true,
plus a one-time burst count when the scene starts (scripts can trigger
more at runtime with ctx.particles.burst(count); see
scripting.md). Each particle interpolates from
startColor/startSize to endColor/endSize over its lifetime
seconds, launched within spread degrees of direction (0 = +x, 90 =
+y/down) at speed px/s, then accelerated by gravity px/s². maxParticles
is a hard cap: the oldest particles die first when exceeded, so a runaway
emitter can’t grow unbounded. seed makes the emitter’s RNG stream
independent of every other emitter and of ctx.random: the same seed spawns
the exact same particles (position jitter within the cone, per-particle
timing) on every run, which is what makes assertParticleCount in playtests
reliable. See scripting.md for the exact
spawn-timing rule (spawns land on whole fixed frames, not fractional ones).
A trail recipe (a short-lived, tightly-focused stream: sparks off a
grinder, water dripping from a ceiling) is a high rate, zero spread, and
endSize: 0 so each particle shrinks to nothing right at the end of its
short lifetime:
{
"rate": 30,
"lifetime": 0.3,
"speed": 60,
"spread": 0,
"direction": 90,
"startSize": 4,
"endSize": 0,
"startColor": "#9fd8ef",
"endColor": "#9fd8ef",
"gravity": { "x": 0, "y": 150 }
}
SpriteAnimator
Plays sprite animations; requires a sibling SpriteRenderer and animation asset.
Defaults:
{
"assetId": "",
"fps": 0,
"playing": true,
"loop": true
}
Create the animation asset first: either hearth create animation <name> --frames f1 f2 … (frame args are existing sprite/tile asset ids or
names, in playback order; see hearth create animation) or, for a sliced spritesheet, hearth create asset anim-from-sheet <name> --sheet <asset> --frames a,b,c (see
assets.md). Each fixed
frame, SpriteAnimator writes the current frame’s asset id into the
sibling SpriteRenderer.assetId, and for a sheet-backed clip also writes
the frame name into SpriteRenderer.frame (null for a plain
sprite-asset clip); fps: 0 uses the animation asset’s own
frameDuration, otherwise fps overrides it. loop: false stops on
the last frame and flips playing to false. Scripts switch clips (and
restart at frame 0) with ctx.animate(assetRef). See
scripting.md.
AnimationStateMachine
Drives a sibling SpriteRenderer from an animation state machine asset
(params/states/transitions) instead of a single looping clip. Wins over a
SpriteAnimator on the same entity (the runtime warns once).
Defaults:
{
"assetId": "",
"playing": true
}
assetId references a state machine asset at
assets/statemachines/*.asm.json, created with createStateMachineAsset
(CLI hearth create asset state-machine) or edited with
updateStateMachineAsset (CLI hearth set-state-machine). See
scripting.md for the asset’s
{ params, states, initial, transitions } shape and the full
ctx.animator scripting API, or the editor’s Animator
editor for a typed authoring UI.
playing: false freezes the current frame. No transitions are evaluated
and the clip doesn’t advance.
SpriteEffects
Per-sprite visual effects: outline, hit flash (ctx.effects.flash), dissolve. Every field defaults to a no-op, so attaching this component with no overrides changes nothing.
Defaults:
{
"outlineEnabled": false,
"outlineColor": "#ffffff",
"outlineWidth": 2,
"flashColor": "#ffffff",
"flashStrength": 0,
"flashDuration": 0.15,
"dissolveAmount": 0,
"dissolveSeed": 0
}
flashStrength decays linearly to 0 over flashDuration seconds once
triggered (by ctx.effects.flash(color?, seconds?) or a direct
setComponentProperty write): deterministic, no RNG, same-frame decay
applies (see effects.md for the exact first-read
value). dissolveAmount masks sprite texels by a per-texel hash of pixel
position + dissolveSeed, not randomness, so the same seed dissolves the
same texels every run. Tween it with ctx.tweens.to('SpriteEffects. dissolveAmount', 1, 0.6) for a reproducible death/spawn effect. See
effects.md for the full field reference, the ctx.effects
scripting surface, and the assertPostEffect playtest step’s sibling
coverage of Camera.postEffects.
Notes
- One component of each type per entity (format v1).
- Colors are hex strings (
#rgb,#rrggbb,#rrggbbaa). - Positions/sizes are pixels; rotation is degrees; +y is down.
SpriteRendererwithassetId: nulldraws its primitiveshape/color, so you can build a whole game before any art exists.- An entity can also carry an optional
prefab: { asset, ids, overrides }field, not a component, so it never shows up inhearth inspect componentsor the list above, but it round-trips through scenes, snapshots, and undo history like any other entity field. It marks the entity as a live-linked instance of a prefab asset;overridesrecords the instance’s own per-field edits so a later sync can re-apply them. See prefabs.md.