-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Motivation
Every gametau game currently reinvents render loops, object management, and scene setup boilerplate. A first-party scene layer would eliminate this duplication and unlock dev tools (inspector, scene tree) that read component metadata.
Feature Tiers
Tier 1: High Value, Natural Fit
1. Component System — The biggest gap. Base class with lifecycle hooks (awake, start, update, afterUpdate, dispose) attached to scene nodes.
2. SceneController — Boilerplate every Three.js game writes (renderer, camera, clock, deltaTime, pause/resume).
3. Property Decorators — @prop.num(), .color(), .select(), .vec3() etc. Enables inspector, serialization, and hot-tweaking without a visual editor.
Tier 2: Medium Value, Build After Tier 1
4. Inspector Panel — In-game devtools reading property metadata → editable UI. Framework-specific bindings (SolidJS, React).
5. Scene Serialization — JSON save/load of scene graph + component props. Needs component system first.
6. Input Manager — Action-mapped input (action → key/button). Lower priority.
Tier 3: Skip
Visual scripting, asset marketplace, standalone editor app, prefab system.
Architecture: Renderer-Agnostic Core + Three.js Bindings
The key insight: lifecycle hooks, property decorators, and the component registry don't care about the renderer. Only the "what does a component attach to" binding is renderer-specific.
Package Structure
gametau/
packages/
webtau/ (existing) — command bridge, invoke, providers
webtau-vite/ (existing) — WASM build plugin
webtau-scene/ (NEW) — renderer-agnostic core
webtau-three/ (NEW) — Three.js SceneController + bindings
webtau-devtools/ (NEW, later) — inspector, scene tree
Layer Split
webtau-scene (renderer-agnostic core):
├── Component — base class with lifecycle hooks
├── prop decorators — @prop.num(), .color(), .select(), .vec3(), etc.
├── ComponentRegistry — register/lookup components by name
├── SceneNode — minimal interface a component attaches to
└── GameLoop — requestAnimationFrame, deltaTime, pause/resume
webtau-three (Three.js bindings):
├── ThreeSceneController — wraps THREE.WebGLRenderer/Scene/Camera/Clock
├── ThreeComponent — Component subclass where node is THREE.Object3D
├── createScene() — factory: container → controller + render loop
└── ThreeSceneNode — implements SceneNode for THREE.Object3D
webtau-pixi (future, hypothetical):
├── PixiSceneController — wraps PIXI.Application/Stage
├── PixiComponent — Component where node is PIXI.Container
└── createPixiScene() — factory
API Design
Core (webtau-scene) — no Three.js import
import { Component, prop, registerComponent } from "webtau-scene";
class Orbiter extends Component {
@prop.num({ min: 0, max: 100, label: "Speed" })
speed = 1.0;
@prop.color()
tint = 0xff0000;
@prop.select(["cw", "ccw"])
direction: "cw" | "ccw" = "cw";
// Lifecycle (called by the game loop, never manually)
awake() {} // before first frame
start() {} // after assets ready
update(dt: number) {} // every frame
afterUpdate() {} // post-update
dispose() {} // on removal
// this.node — typed by the renderer binding (Object3D, Container, etc.)
}
registerComponent(Orbiter);Property decorator metadata (renderer-agnostic)
const meta = Component.getMeta(Orbiter);
// → [{ key: "speed", type: "num", min: 0, max: 100, label: "Speed" },
// { key: "tint", type: "color" },
// { key: "direction", type: "select", options: ["cw", "ccw"] }]Three.js binding (webtau-three)
import { createScene, ThreeComponent } from "webtau-three";
class OrbitalBody extends ThreeComponent {
@prop.num() radius = 1.0;
update(dt: number) {
this.object3d.rotation.y += dt * this.radius;
}
}
const ctrl = createScene({
container: document.getElementById("canvas")!,
antialias: true,
pixelRatio: window.devicePixelRatio,
});
// ctrl.scene, ctrl.camera, ctrl.renderer, ctrl.clock
// ctrl.deltaTime, ctrl.isPaused
// ctrl.pause(), ctrl.resume(), ctrl.setFullscreen()
// ctrl.renderFunc — override for post-processing
// ctrl.start() — begins RAF loop, ticks all componentsComponent attachment
const mesh = new THREE.Mesh(geometry, material);
ctrl.scene.add(mesh);
const orbiter = ctrl.addComponent(mesh, OrbitalBody);
orbiter.radius = 2.5;
// Query
const body = ThreeComponent.get(mesh, OrbitalBody);
const all = ThreeComponent.getAll(OrbitalBody);
// Remove
ctrl.removeComponent(orbiter); // calls dispose()Lifecycle Order (per frame)
RAF tick:
1. ctrl.clock.getDelta() → deltaTime
2. For each component (awake-pending): component.awake()
3. For each component (start-pending): component.start()
4. For each component: component.beforeUpdate?.(dt)
5. For each component: component.update(dt)
6. For each component: component.afterUpdate?.()
7. ctrl.renderFunc(ctrl.renderer, ctrl.scene, ctrl.camera)
Design Principles
| Aspect | Approach |
|---|---|
| Ownership | Open source, in gametau monorepo |
| Editor | In-game dev overlay (opt-in, later) |
| Renderer | Core is agnostic; Three.js/PixiJS via bindings |
| Build | Vite (webtau-vite compatible) |
| Backend | Full Rust via #[webtau::command] |
| Desktop | Tauri + Electrobun + web |
| Scene format | JSON (portable) |
| UI framework | SolidJS/React bindings (webtau-devtools-*) |
Roadmap
Phase 1: webtau-scene + webtau-three
- Component base class with lifecycle
- Property decorators with metadata registry
- ComponentRegistry (register, lookup, getAll)
- ThreeSceneController (renderer, scene, camera, clock, deltaTime)
- ThreeComponent (Component with
object3dtyped binding) createScene()factory- Game loop with lifecycle dispatch
Phase 2: webtau-devtools (later)
- Inspector panel reading prop metadata → editable UI
- Scene tree viewer
- SolidJS bindings (
webtau-devtools-solid)
Phase 3: Extras (community / as-needed)
- Input manager with action mapping
- Scene serialization (JSON save/load)
- PixiJS bindings (
webtau-pixi)
Validation Criteria
webtau-scenehas zero Three.js imports (renderer-agnostic)webtau-three's peer dependency onthreeis the only renderer coupling- Tree-shaking works: unused
webtau-threedoesn't bloat a PixiJS project - A real game's render loop can be ported to
createScene()+ThreeComponentwith identical behavior