Architecture#
OneJS connects JavaScript to Unity through several layers.
Architecture Overview#
┌─────────────────────────────────────────────────┐
│ Your React Code │
├─────────────────────────────────────────────────┤
│ onejs-react Reconciler │
├─────────────────────────────────────────────────┤
│ QuickJS JavaScript Engine │
├─────────────────────────────────────────────────┤
│ QuickJSUIBridge (Events, Scheduling) │
├─────────────────────────────────────────────────┤
│ UI Toolkit Elements │
└─────────────────────────────────────────────────┘The JavaScript Engine#
OneJS uses QuickJS, a small embeddable JavaScript engine. It runs your code in an isolated context with:
- Full ES2020 support
- Async/await and Promises
- Modules (ESM)
The React Reconciler#
onejs-react is a custom React reconciler. When you write:
<View style={{ padding: 20 }}>
<Label text="Hello" />
</View>The reconciler:
- Creates a
VisualElementfor the View - Creates a
Labelelement - Sets the style properties
- Adds the Label as a child of the View
Component Mapping#
| React Component | UI Toolkit Element |
|---|---|
<View> | VisualElement |
<Text> | TextElement |
<Label> | Label |
<Button> | Button |
<TextField> | TextField |
<Toggle> | Toggle |
<Slider> | Slider |
<ScrollView> | ScrollView |
<Image> | Image |
<ListView> | ListView |
<FrostedGlass> | FrostedGlassElement |
| Raw text | TextElement |
<View>Hello</View>) automatically create TextElement instances. Use <Text> for primary text display and <Label> for form labels.
C# Interop#
The CS global proxy uses reflection to access C# types:
CS.UnityEngine.Debug.Log("Hello")This resolves to:
- Look up the
UnityEngine.Debugtype - Find the
Logmethod - Convert arguments to C# types
- Invoke the method
- Convert the return value back to JavaScript
Event System#
UI Toolkit events are captured and routed to JavaScript:
- C# registers a TrickleDown callback on the root element
- When an event fires, it's serialized to JSON
__dispatchEvent(handle, eventType, data)is called- JavaScript looks up handlers and invokes them
UI Event → C# Capture → JSON Serialize → JS Dispatch → Your HandlerScheduling#
OneJS provides familiar timing APIs:
requestAnimationFrame(): Called every Unity UpdatesetTimeout()/setInterval(): Timer queue processed each framequeueMicrotask(): For Promise resolution
Tick() method drives all scheduling from Unity's Update loop.
Memory Management#
C# objects referenced from JavaScript are tracked with integer handles:
- C# object → Handle registered in table
- JavaScript proxy wraps the handle
- When JS object is garbage collected → FinalizationRegistry releases handle
- C# object becomes eligible for GC
releaseObject() but rarely needed.
Application Lifecycle#
OneJS runs your code in two contexts: edit-mode preview (renders UI without Play mode) and play mode. Export lifecycle hooks to control when game logic runs:
Edit-mode preview:
InitializeBridge → RunScript (module-level code runs) → EditModeTick at 30Hz
Play mode:
InitializeBridge → RunScript → onPlay()
→ Update every frame
→ File change → onStop() → teardown → rebuild → onPlay()
→ Exit Play mode → onStop()- Module-level code (including
render()) runs in both modes — safe for UI onPlay()fires only in play mode, after the bundle loadsonStop()fires before teardown (play mode exit or live reload)__isPlayingglobal:truein play mode,falsein edit-mode preview
WebGL Differences#
On WebGL, JavaScript runs in the browser's V8/SpiderMonkey engine:
- JIT compilation (faster execution)
- Browser's native RAF loop
- No QuickJS overhead