Skip to content

GameObjects & Components

Everything in a Awaken3D scene is a GameObject built from Components - small bundles of typed data. This page explains the entity-component model underneath, and how one reflection system drives both the Inspector and saving.

The model: entities + components

Awaken3D uses a data-oriented Entity-Component-System (ECS). There are three moving parts:

  • an Entity is a lightweight handle - just a number,
  • a Component is a plain typed object of data attached to an entity,
  • Systems (physics, animation, scripts) run each frame over entities that have the components they care about.

A GameObject is the friendly, object-oriented face of an entity you interact with in the editor and in scripts. It carries no data of its own - it's a thin wrapper over (world, entity) that forwards to the World.

Entities are handles

An entity is a single integer that packs an id and a generation counter. The generation is what makes stale references safe: when an entity is destroyed its id is recycled, but the generation ticks up, so an old handle to that slot no longer matches and reads as not alive. Handle 0 is reserved as NULL_ENTITY (meaning "none" / "no parent"). You rarely see these numbers - the editor shows names - but it's why deleting an object never corrupts other references.

Components are typed data

Each component type has its own sparse-set store in the World, keyed by entity. Adding, reading, checking, and removing a component are all direct operations, and you can query for every entity that has a given set of components. A GameObject exposes the same operations:

ts
const go = createGameObject(world, "Crate");
go.addComponent(MeshRenderer, { mesh: "cube", color: [0.8, 0.6, 0.4] });
const mr = go.getComponent(MeshRenderer);   // read
go.hasComponent(RigidBody);                 // check
go.removeComponent(Collider);               // remove

createGameObject always ensures a Transform exists, so every object has a place in space and shows up in the hierarchy.

The registered component types

The editor knows about a fixed set of component types - these appear in the Add-Component menu, get an Inspector, and are serialized:

ComponentPurpose
TransformPosition, rotation, scale - every object has one. See Transforms.
MeshRendererDraws a mesh with a color/texture and PBR values.
MeshMaterialBinds a reusable material asset by id.
LightDirectional or point light.
CameraA viewpoint: perspective (uses fov) or orthographic (uses orthoSize, a half-height in world units), with near/far clip planes. The first active, enabled camera renders. See Cameras.
AnimatorPlays skeletal animation clips (crossfade, root motion). See Animation.
AnimStateMachineDrives the Animator from a param-driven state graph, with per-character clip bindings. See Animation.
RagdollHands a skinned character to physics so it goes limp on death/impact (via api.ragdoll). Experimental.
BoneAnchorMakes an entity mirror a named skeleton bone so attached props (sword, cape) ride the animation.
RigidBodyPhysics body - mass, velocity. See Rigidbody.
ColliderOne or more collision shapes. See Colliders.
AudioSourceEmits a sound. See Audio.
ScriptAttaches one or more script behaviours + their parameters. See Scripting.
TagA named label for finding objects from scripts.
UINodeA screen-space UI element - text, panel, button, or image - drawn as a DOM overlay over the canvas. See UI.
UIAnimatorPlays a UIClip on this widget's UINodes (fade / slide / pulse / spin), driven by api.ui.play/stop. See UI.
ParticleEmitterPlays a ParticleEffect asset from this object; the asset owns the look, the component picks which one. See Particles.

Built-in object kinds

The Add menu (the + button in the Hierarchy, or a right-click on an object to add a child) spawns a ready-made GameObject - each kind is just a preset bundle of the components above:

MenuKindComponents it adds
Cube / Sphere / Plane / Cylinder / CapsuleprimitiveTransform + MeshRenderer using the matching built-in mesh.
Emptygrouping nodeTransform only - a pivot to parent other objects under.
Directional Light / Point LightlightTransform + Light of that kind.
CameracameraTransform + Camera (perspective).
2D → Sprite2D spriteTransform + MeshRenderer on the built-in quad mesh (a flat card in the XY plane, facing +Z), created transparent with shadow casting off. Assign a texture in the Inspector.
2D → 2D Camera (Orthographic)2D cameraTransform + Camera with projection: "orthographic" and orthoSize 5 (10 world units tall), placed at z = 10 looking down -Z. Pair it with Sprites for a 2D or isometric view.
UI → Text / Panel / Button / ImageUI elementTransform + UINode of that kind. See UI.

A top-level object lands where the camera is looking (surface-snapped under the drop point); adding a child places it at the parent's local origin. Every add is one undoable step.

The enabled flag

Every component carries a per-component enabled toggle, in the style of Unity's Behaviour.enabled. It is driven by the checkbox in each component's Inspector header. When a component is disabled, systems skip it - a disabled Script stops updating, a disabled Collider stops colliding - without removing the component or losing its settings.

This is distinct from the GameObject's own active flag, which enables or disables the whole object:

  • Component enabled - turns one component off, leaving the rest of the object working.
  • GameObject active - turns the object off. An object is only truly visible/simulated if it and every ancestor are active (active-in-hierarchy), so disabling a parent disables the whole subtree.

Both are undoable edits (see Editing Operations).

📸 Screenshot - save as img/world-component-enabled.png

The Inspector for a selected object showing several components stacked, with one component's header checkbox unticked (disabled) and the GameObject active checkbox at the top.

Reflection: one schema, three jobs

Components aren't hand-wired into the UI or the save format. Each is declared once with defineComponent(name, fields, create), where fields is a list of FieldSpec entries describing each property's name, type, and default:

ts
export const Transform = defineComponent<Transform>(
  "Transform",
  [
    { name: "position", type: "vec3", default: { x: 0, y: 0, z: 0 } },
    { name: "rotation", type: "quat", default: { x: 0, y: 0, z: 0, w: 1 } },
    { name: "scale",    type: "vec3", default: { x: 1, y: 1, z: 1 } },
  ],
  () => ({ position: vec3(), rotation: quat(), scale: vec3(1, 1, 1) }),
);

That single declaration is read by three subsystems:

The available field types are number, bool, string, vec3, quat, color, enum, assetRef, entityRef, plus composite types (colliderShapes, scriptList, scriptParams, materialParams, entityRefMap, clipMap) that render as richer Inspector blocks. Because the schema is the single source of truth, adding a field to a component makes it appear in the Inspector and be saved - no separate wiring. A field can also declare showIf to hide itself when it's irrelevant (for example, a Collider's radius is hidden for a mesh shape).

defineComponent also auto-appends the enabled field to every component, so the enabled toggle serializes like any other property. In memory an untouched component leaves enabled unset (which reads as enabled) and only records false once you disable it; when a game is exported, fields equal to their default are dropped from the shipped scene to shrink the file, and the player backfills them.

Reference-typed fields (entityRef, entityRefMap, and a Script's refs) are special-cased at save time: entity handles are rewritten to stable save-ids and remapped back on load, so a script that points at another object keeps pointing at it across save/reopen and across prefab instancing.

See also

Awaken — browser-native WebGPU game engine.