DEMIENGINEDOCUMENTATION Everything is free
Core concepts · Start here

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, default true) — when false the entity exists but is inactive: its components and scripts do not run until it is enabled.
  • persistent (boolean, optional, default false) — when true the 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 every damageable object.
  • 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. Omitting Transform2D places the entity at the origin of its 2D domain.
  • Sprite.texture — an asset URI naming the image to draw. Sprite.size is the drawn extent in world units.
  • LuaScript.module — a script:// URI naming the Lua file attached to this entity. LuaScript.properties are 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 — a Transform3D at the world origin.
  • Child house/roof — its own Transform3D with 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/roof style 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, all Transform3D, or all IsoTransform. 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 parent fields — the transform parent field 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") and require("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. dt is the frame time in seconds.
  • Input.vector("move") — returns two numbers, the current movement axes in the range -1 to 1.
  • Transform2D.get_position("player") — returns the entity's x, y world position. Transform2D.set_position writes it back.
  • speed — a local value; the matching properties.speed field 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