Scene documents
A scene document is a *.scene.json file that describes ECS entities as data. Load one into a World, run it in an app, validate it, and save a world back to one.
modules: com.awakekt.awake.scene:document · com.awakekt.awake.scene:runtime
Desktop · Android · iOS · Web
A scene document and the scene DSL are two ways to describe the same ECS content, and AwakeKt Studio
saves scene documents. awake:scene:document owns the format and SceneLoader;
awake:scene:runtime turns a document into entities and runs it. Both come with
com.awakekt.awake.scene:authoring.
Write a scene document
A document has a schema version, an optional name, and a list of nodes. Each node becomes one
entity with a Name and a Transform; its components are listed by component id, and its
children are child entities.
{
"version": 1,
"name": "Harbor Town",
"nodes": [
{
"name": "dock",
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 4.0 },
"scale": { "x": 6.0, "y": 0.2, "z": 2.0 }
}
},
{
"name": "lighthouse",
"transform": {
"position": { "x": 3.0, "y": 0.0, "z": -2.0 }
},
"children": [
{
"name": "beacon",
"transform": {
"position": { "x": 0.0, "y": 6.0, "z": 0.0 }
},
"components": [
{ "component": "spin_control", "speed": 0.5 }
]
}
]
}
]
}
- File > Open Scene... opens a scene document; File > New Scene starts an empty one.
- Edit entities in the Hierarchy and Inspector.
- File > Save Scene or File > Save Scene As... writes the scene document.
Studio validates the scene before it saves. If a rule is broken it shows a Scene Not Saved message with the first problem and its node, and writes nothing.
Every key, type and default is in the scene document schema, and every component id in scene document components.
Load it
Install the built-in components, decode the text, then instantiate it. instantiate checks the
document first and creates a new World unless you pass one.
DefaultSceneComponentResolvers.install()
val document = SceneLoader.decode(json)
val scene = SceneLoader.instantiate(document)
val lighthouse = scene.roots.first { it.name == "lighthouse" }
val beacon = lighthouse.children.single().entity
val spin = scene.world.get<SpinControl>(beacon)
scene.roots mirrors the document's nodes, each with its entity and children, so you can find
an entity by node name without a query.
| Call | Module | What it does |
|---|---|---|
SceneLoader.decode(text) |
document |
Parses JSON into a SceneDocument. Also decodeBytes. |
SceneLoader.loadFromResource(path) |
document |
Suspends; reads a bundled resource and decodes it. |
SceneLoader.load(path, source) |
document |
Suspends; reads through an AssetSource and decodes it. |
SceneLoader.instantiate(document, world, registry) |
runtime |
Validates, then creates the entities. Returns a Scene. |
document.instantiate(world, registry) |
runtime |
The same, as an extension on the document. |
SceneLoader.fromWorld(world, name) |
runtime |
Builds a document from a live world. |
SceneLoader.encode(document) |
document |
Writes JSON. Also encodeBytes. |
SceneValidator.validate(document) |
document |
Returns every rule the document breaks. |
Run a document in an app
Inside app { }, pass the decoded document to scene(document). The scene runtime installs the
built-in components itself, creates the entities when the renderer is ready, and runs your systems
over them:
val game = app {
ecs {
scene(SceneLoader.decode(json))
frameSystem("beacon-spin") { BeaconSpin() }
frameSystem("spin") { SpinSystem() }
}
}
A mesh_renderer names its mesh and material; declare them with assets { mesh(…); material(…) }
in the same block. The app's name for the scene is the document's name.
Replace the current scene
SceneManager owns one scene at a time in a World. switchTo destroys the current scene's entities
before it loads the next, so there is no step to forget:
val world = World()
val scenes = SceneManager(world)
scenes.switchTo(SceneLoader.decode(json)) // Harbor Town's entities are now in world
scenes.switchTo(other) // Harbor Town is destroyed first, then other loads
scenes.close() // destroys whatever is current
Inside an app, the runtime's own manager is sceneManager, available in onReady and system
factories.
switchTo keeps the systems the app registered. A project played with runProject goes to another of
its scenes through its SceneSwitch instead, which also closes the old scene's systems and physics
and plans the next scene's own: a system asks with services.sceneSwitch?.request("scenes/level-2.scene.json"),
and a blueprint with its Load Scene node. See
awake:project:runtime.
Save a world
fromWorld walks the entities that have a Transform, follows their parent links to rebuild the
tree, and writes each component that has a registered binding:
val saved: SceneDocument = SceneLoader.fromWorld(scene.world, name = "Harbor Town")
val text = SceneLoader.encode(saved)
Pass extraComponents = { entity -> listOf(…) } to write components that have no binding of their
own.
Components from other modules
DefaultSceneComponentResolvers.install() registers the ids Core knows about: camera, light,
ambient_light, fog, skybox, mesh_renderer, pbr_material, spin_control, keyframe_animation, particle_emitter, terrain,
canvas_element and prefab_link. custom always decodes. Other ids belong to the module that
owns the component, and must be registered before a document that uses them is decoded:
| Register with | Module | Ids |
|---|---|---|
SceneComponentRegistry().registerControls() |
scene:controls |
movement_control, camera_rig, input_actions |
SceneComponentRegistry.registerGlobal(binding) |
any | that binding's id, for every load |
loadProject(files, capabilities = …) |
project:runtime |
each scene capability's ids, for that project |
A registry you build is passed to instantiate. Inside an app, where scene(document) uses the
global registry, register with registerGlobal; Cameras and controls
shows both.
Decoding refuses an id nothing registered. An editor or a tool that may not have a game's code, such
as a validator or an asset pipeline, decodes with registry.sceneJson(keepUnknownComponents = true)
instead, or SceneSerializers.createJson(keepUnknownComponents = true) without a registry. Each
component it can't decode becomes a SceneUnknownComponent holding its JSON. The rest of the scene
validates and instantiates; the unknown component runs nothing and waits on its entity as data, and
fromWorld writes it back, so saving keeps it as it was. SceneValidator.unknownComponentIssues
names each one. A game keeps the default, so a component it doesn't link still fails its load.
Validate a document
SceneValidator.validate returns every issue with the path of the node it is on. instantiate runs
it first and throws SceneValidationException listing them all.
val document = SceneLoader.decode(
"""
{
"nodes": [
{ "name": "dock" },
{ "name": "dock", "components": [ { "component": "spin_control", "speed": -1.0 } ] }
]
}
""",
)
val issues = SceneValidator.validate(document)
issues.forEach { println("${it.path}: ${it.message}") }
dock: duplicate node name 'dock' already used at dock
dock: spinControl.speed must not be negative
Node names must be unique across the whole document, because components such as camera_rig refer
to nodes by name. A prefab instance is its own scope: the nodes a prefab_link brings in need
unique names only within that instance, so one prefab can be placed many times. A link inside an
instance resolves to the instance's own node first, then to the scopes around it.
How it works
Loading has three steps. decode parses the JSON and rewrites the old camelCase ids (meshRenderer,
spinControl, pbrMaterial, prefabLink) to their snake_case form. instantiate validates the
document, then walks the nodes: each becomes an entity with a Name and a Transform, parented to
its node's parent, and each component id is handed to the binding registered for it, which adds the
ECS component. Links to other nodes by name are resolved after every node exists.
A mesh_renderer is the exception: it does not create GPU resources while loading. It records a
request, and the app's assets { } turns each request into a MeshRenderer.
An unregistered component id does not load
decode fails on a component id with no registered binding. Register the owning module's
components first, as in the table above.
Some content does not survive fromWorld
fromWorld only writes entities that have a Transform, and only components whose binding can
export them. mesh_renderer and custom components are not written back: pass them through
extraComponents if you need them in the saved document.
A newer document is refused
A document whose version is higher than this engine's (SCENE_SCHEMA_VERSION, currently 1)
throws SceneSchemaVersionException from decode.
Unknown data is kept, not applied
A custom component keeps its type and payload in the document, but no ECS component is added
for it unless you register a resolver.
Prefabs
A node with prefab_link { path } places a prefab file there. Run withPrefabs on the decoded
document before instantiate; loadProject does. Each file is read once, however many
nodes link it, and a saved world keeps the link instead of a copy.
Debugging
Call SceneValidator.validate(document) and print each issue's path and message to see
everything that would stop a document from loading. A custom component with no resolver logs a
warning under the scene-binding logger and is skipped.
See also
- Scene document schema for every key.
- Scene document components for every component id.
- Scene DSL for the same content in Kotlin.
- Load a scene document, the tutorial.