What an entity is, how components attach, and hierarchy rules.
Entities & components
Every object you see or interact with in a DemiEngine game is built from three ideas: a scene lists entities, and each entity carries components. This page explains each idea from scratch, walks a complete JSON example, and covers hierarchy and the small entity fields you will use every day.
A scene is a list of entities
A scene is one file describing everything that loads together — a level, a menu screen, an arena. Scenes live under scenes/ as *.scene.json and are referenced by stable ID, for example scene://game/main. When the engine loads that scene, it creates every entity the file lists and runs their scripts. Full document shape: Scene files.
An entity is an ID plus components
An entity is one object in the world: a player, a crate, a camera, a trigger volume. In JSON it is a small object with a stable id and a components map. The ID is how everything else refers to this object — scripts, tests, overrides, other entities. IDs are unique across the whole document.
A component is one capability's data
A component is a named block of data describing exactly one capability of an entity. Position is one component (Transform2D or Transform3D), drawing a picture is another (Sprite or MeshRenderer), taking damage from a script is another (LuaScript). An entity with no components does nothing; add only the capabilities it needs.
Common components include Sprite, SpriteAnimator2D, Tilemap2D, Camera2D, Rigidbody2D, MeshRenderer, AnimationPlayer3D, CharacterController3D, Fracture3D, AudioSource, and LuaScript.
A full entity, walked field by field
{
"id": "player",
"name": "Player",
"enabled": true,
"persistent": false,
"tags": ["hero", "damageable"],
"layer": "actors",
"components": {
"Transform2D": { "position": [4, 8] },
"Sprite": {
"texture": "asset://sprites/player",
"size": [1, 1]
},
"LuaScript": {
"module": "script://scripts/player.lua",
"properties": { "speed": 6.0 }
}
},
"children": []
}
id(string, required) — the stable identity of this entity inside the document. Other files and scripts refer to it as"player". Never derived from position in an array.name(string, optional) — a human-readable label shown in tools. Not used as identity.enabled(boolean, optional, defaulttrue) — whenfalsethe entity exists but is inactive: its components and scripts do not run until it is enabled.persistent(boolean, optional, defaultfalse) — whentruethe entity survives a scene change instead of being torn down with the old scene.tags(array of strings, optional) — free-form labels for grouping and queries, for example finding everydamageableobject.layer(string, optional) — a named grouping used for visibility and filtering rules alongside physics layers.components(object, required) — map of component name to that component's fields. Keys are component names; values are the fields that capability accepts.children(array, optional) — nested entities that move and exist with this one. Covered below.
Inside components:
Transform2D.position—[x, y]coordinates in world units. OmittingTransform2Dplaces the entity at the origin of its 2D domain.Sprite.texture— an asset URI naming the image to draw.Sprite.sizeis the drawn extent in world units.LuaScript.module— ascript://URI naming the Lua file attached to this entity.LuaScript.propertiesare typed values the script can read and tools can edit.
Every entity needs a transform in one dimensional domain — Transform2D, Transform3D, or IsoTransform — because that is what places it in the world.
Hierarchy with children
Related objects should nest under their owner using children. A sword that follows the player is a child of the player; a roof is a child of the house. Children inherit their parent's placement, so moving the parent moves everything under it.
{
"id": "house",
"components": { "Transform3D": { "position": [0, 0, 0] } },
"children": [
{
"id": "house/roof",
"components": { "Transform3D": { "position": [0, 2, 0] } }
}
]
}
- Parent
house— aTransform3Dat the world origin. - Child
house/roof— its ownTransform3Dwith a local offset two units up. In world space the roof sits at[0, 2, 0]because the parent is at the origin; if the house moves, the roof moves with it.
Hierarchy rules to keep in mind:
- Document-wide IDs — nesting is not a separate ID namespace. Every ID in the file must be unique, parent and child alike. The
house/roofstyle is a naming convention, not a syntax the engine parses. - Matching transform domains — a parent and its children must use the same domain: all
Transform2D, allTransform3D, or allIsoTransform. Mixing domains is a validation error. - Maximum depth 128 — you may nest at most 128 levels of children. Deeper trees are rejected during validation.
- Explicit
parentfields — the transformparentfield is only for relationships that cannot be expressed by nesting, such as across prefabs or scenes. It also names a stable entity ID, and the same domain rule applies.
Omitted fields use defaults
You only write the fields that matter. Anything you leave out takes that component's default value, which keeps authored JSON short and readable. In the example above Sprite sets only texture and size; color, flip, and render-order fields are all at their defaults. Values you do write are kept exactly as authored.
Attaching gameplay with LuaScript
Components give an entity capabilities; a LuaScript component gives it behavior. The script file is a Lua module that imports engine services and defines lifecycle functions:
local Input = require("demi.input")
local Transform2D = require("demi.transform2d")
local speed = 6.0
function on_create()
-- runs once when the entity is created
end
function on_update(dt)
local x, y = Input.vector("move")
local px, py = Transform2D.get_position("player")
Transform2D.set_position("player", px + x * speed * dt, py + y * speed * dt)
end
require("demi.input")andrequire("demi.transform2d")— import engine services explicitly. Each returns a table of functions for that service.on_create()— lifecycle hook run once when the entity is created.on_update(dt)— lifecycle hook run every frame.dtis the frame time in seconds.Input.vector("move")— returns two numbers, the current movement axes in the range-1to1.Transform2D.get_position("player")— returns the entity'sx,yworld position.Transform2D.set_positionwrites it back.speed— a local value; the matchingproperties.speedfield in JSON is how tools expose an editable value for it.
Other lifecycle hooks are on_start, on_fixed_update, and on_destroy. The full pattern is in Script lifecycle.
Next steps
- Prefabs & instances — reuse an entity composition across scenes.
- Core concepts — how scenes, assets, and packages fit together.
- Entity API — finding and changing entities from Lua.