@ringozz/react-godot 4.7.2-614 → 4.7.2-616
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +327 -327
- package/package.json +5 -5
- package/src/index.ts +58 -58
- package/src/react-fiber.ts +331 -320
- package/src/react-hooks.ts +164 -164
- package/src/react-jsx.ts +45 -45
package/README.md
CHANGED
|
@@ -1,327 +1,327 @@
|
|
|
1
|
-
# `@ringozz/react-godot`
|
|
2
|
-
|
|
3
|
-
Declarative, component-driven **Godot 4** scene graph management in React. Built for high-performance applications using `@ringozz/godot`.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
`@ringozz/react-godot` brings a **React Three Fiber (R3F)**-like developer experience to Godot. It provides a custom React reconciler that renders React components directly into Godot's live engine scene tree (`SceneTree`).
|
|
8
|
-
|
|
9
|
-
If you are familiar with React Three Fiber, the core paradigms will feel instantly recognizable:
|
|
10
|
-
- **Declarative Nodes**: Godot 3D/2D nodes (e.g., `<RigidBody3D>`, `<Camera3D>`, `<DirectionalLight3D>`) map directly to JSX components.
|
|
11
|
-
- **Resource Attachment**: Use the `attach` prop to assign nested Godot resources (materials, shapes, meshes, environments) directly to parent properties.
|
|
12
|
-
- **Reactive Engine Loop**: Use hooks like `useSignal` to hook into Godot signals (`processFrame`, `windowInput`) without triggering React state re-renders.
|
|
13
|
-
|
|
14
|
-
## Installation & Setup
|
|
15
|
-
|
|
16
|
-
### 1. Dependencies
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install @ringozz/react-godot react
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### 2. TypeScript Configuration (`tsconfig.json`)
|
|
23
|
-
|
|
24
|
-
To enable custom JSX element resolution for Godot nodes, configure `jsxImportSource` and module settings in `tsconfig.json`:
|
|
25
|
-
|
|
26
|
-
```json
|
|
27
|
-
{
|
|
28
|
-
"compilerOptions": {
|
|
29
|
-
"module": "esnext",
|
|
30
|
-
"target": "es2025",
|
|
31
|
-
"jsx": "react-jsx",
|
|
32
|
-
"jsxImportSource": "@ringozz/react-godot",
|
|
33
|
-
"rewriteRelativeImportExtensions": true,
|
|
34
|
-
"verbatimModuleSyntax": true
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Application Entry & Portal Rendering
|
|
40
|
-
|
|
41
|
-
### 1. Mounting with `createRoot(parent: Node)`
|
|
42
|
-
`createRoot` is the primary entry point for mounting a React component tree into a Godot target `Node` (typically `SceneTree.root`).
|
|
43
|
-
|
|
44
|
-
```tsx
|
|
45
|
-
import { Engine } from '@ringozz/godot/Engine';
|
|
46
|
-
import { SceneTree } from '@ringozz/godot/SceneTree';
|
|
47
|
-
import { createRoot } from '@ringozz/react-godot';
|
|
48
|
-
import { App } from './App';
|
|
49
|
-
|
|
50
|
-
const tree = Engine.getMainLoop() as SceneTree;
|
|
51
|
-
const { render } = createRoot(tree.root);
|
|
52
|
-
|
|
53
|
-
render(<App />);
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
- Returns `{ render, unmount }`.
|
|
57
|
-
- Enables React `StrictMode` automatically in development (`NODE_ENV === 'development'`).
|
|
58
|
-
|
|
59
|
-
### 2. Render Portals with `createPortal(children, container)`
|
|
60
|
-
`createPortal` allows rendering a React sub-tree into a different Godot `Node` in the hierarchy—such as a `CanvasLayer` for 2D UI overlays, a `SubViewport`, or an isolated container node.
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
import { createPortal } from '@ringozz/react-godot';
|
|
64
|
-
import { Label } from '@ringozz/godot/Label';
|
|
65
|
-
|
|
66
|
-
function Overlay({ overlayNode }: { overlayNode: Node }) {
|
|
67
|
-
return createPortal(
|
|
68
|
-
<Label text="HUD Overlay" position={[20, 20]} />,
|
|
69
|
-
overlayNode
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- **Container Constraint**: The target `container` must be a Godot `Node` (it cannot target `Resource` instances).
|
|
75
|
-
- **Cleanup**: Unmounting the portal safely disposes of and frees its child Godot nodes.
|
|
76
|
-
|
|
77
|
-
## Core Concepts & Patterns
|
|
78
|
-
|
|
79
|
-
### 1. Existing Objects with `object` Prop
|
|
80
|
-
|
|
81
|
-
If you already have a pre-existing Godot `Node`, `Resource`, or `PackedScene` instance (e.g. instantiated from C++ or loaded from disk), pass it via the `object` prop to adopt it into the React component tree instead of letting the reconciler instantiate a new object:
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
<Node object={myExistingGodotNode}>
|
|
85
|
-
{/* Children attached to existing node */}
|
|
86
|
-
</Node>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
- **`object` is immutable on a mounted instance.** It selects the host instance once, at creation. A re-render that changes `object` on an existing element throws (`Cannot change the \`object\` of a mounted instance`) because React cannot swap a fiber's Godot node — remount with a new `key` instead.
|
|
90
|
-
- **A `PackedScene` `object` is instantiated once per host mount** — each element gets a fresh node tree; the result is a plain `Node` (not `.reference()`'d) freed on unmount. The same `PackedScene` can feed multiple elements; give same-type siblings stable `key`s.
|
|
91
|
-
- **Dynamic same-type sibling lists need stable `key`s.** Godot host nodes are identity, not content: React reconciles keyless siblings by position, so deleting the *first* of two same-type siblings reuses its Godot node for the survivor. A plain state re-render does this too, and Bun's web dev server makes it especially likely — its React Fast Refresh re-renders the edited component **in place** (no app re-run), reusing the same fibers and Godot nodes, so a deleted sibling's node can survive on its neighbor and the neighbor's node is freed. Add `key="..."` to each dynamically toggled same-type sibling to keep nodes pinned to the right elements.
|
|
92
|
-
|
|
93
|
-
### 1b. Attaching a Script with `script`
|
|
94
|
-
|
|
95
|
-
Use the `script` prop to attach a `Script` resource (e.g. an imported GDScript `.gd` module) to a node on creation. The script is attached **before the node enters the tree**, so Godot fires `_ready` and auto-enables the script's `_process`/`_input` — no manual `set_process(true)`/`set_process_input(true)` calls needed:
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
import controller from './camera_controller.gd';
|
|
99
|
-
|
|
100
|
-
<Camera3D script={use(controller)} current={true} fov={55} />
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
- **Attach before tree entry means `_ready`/`_process`/`_input` work.** Attaching to an already-in-tree node (e.g. in a `useEffect`) never fires `_ready` and leaves `_process`/`_input` disabled — prefer `script` over an effect for scripted nodes.
|
|
104
|
-
- **`script` is immutable on a mounted instance.** A re-render that changes `script` on an existing element throws — remount with a new `key` to swap the compiled script.
|
|
105
|
-
|
|
106
|
-
### 2. Resource Nesting with `attach`
|
|
107
|
-
|
|
108
|
-
In Godot, nodes often hold references to `Resource` objects (such as shapes, meshes, materials, or environments). Use the `attach` prop to automatically assign a child resource to a specific property on its parent:
|
|
109
|
-
|
|
110
|
-
```tsx
|
|
111
|
-
/* Attaching a shape to CollisionShape3D */
|
|
112
|
-
<CollisionShape3D>
|
|
113
|
-
<BoxShape3D attach="shape" size={[1, 1, 1]} />
|
|
114
|
-
</CollisionShape3D>
|
|
115
|
-
|
|
116
|
-
/* Attaching a material override to MeshInstance3D */
|
|
117
|
-
<MeshInstance3D>
|
|
118
|
-
<BoxMesh attach="mesh" />
|
|
119
|
-
<StandardMaterial3D attach="materialOverride" albedoColor={[1, 0, 0]} />
|
|
120
|
-
</MeshInstance3D>
|
|
121
|
-
|
|
122
|
-
/* Attaching physics material to RigidBody3D */
|
|
123
|
-
<RigidBody3D>
|
|
124
|
-
<PhysicsMaterial attach="physicsMaterialOverride" bounce={0.8} />
|
|
125
|
-
</RigidBody3D>
|
|
126
|
-
|
|
127
|
-
/* Attaching environment to WorldEnvironment */
|
|
128
|
-
<WorldEnvironment>
|
|
129
|
-
<Environment attach="environment" backgroundColor={[0.1, 0.1, 0.2]} />
|
|
130
|
-
</WorldEnvironment>
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Note for physics bodies: `attach="shape"` goes on the shape **resource** element (e.g. `<BoxShape3D attach="shape" />`), never on the `CollisionShape3D` node itself — `Node.add_child` requires a `Node`, and shapes are `Resource`s, so an `attach` on `CollisionShape3D` adds the shape as a child node and fails. Likewise, mesh children of a physics body must be wrapped in a `MeshInstance3D` (a `RigidBody3D`/`StaticBody3D` has no `mesh`/`materialOverride` props to attach to).
|
|
134
|
-
|
|
135
|
-
### 3. Properties & Helper Types
|
|
136
|
-
|
|
137
|
-
Props map directly to Godot node setters and properties. Helper arrays are automatically cast to Godot types:
|
|
138
|
-
|
|
139
|
-
- **Vectors / value types**: value types are **named tuples** — flat (`Vector3`, `Color`, `Rect2`, …) are scalar-labeled tuples (`Vector3 = [x, y, z]`, `Color = [r, g, b, a?]`); nested (`AABB`/`Transform2D`/`Basis`/`Transform3D`/`Projection`) are row-labeled tuples of the flat element alias (`Transform3D = [x, y, z, origin]`, each row a `Vector3`), so `position={[0, 1, 0]}`, `transform={[[1, 0, 0], [0, 1, 0], [0, 0, 1], [10, 20, 30]]}`. Getters return the same labeled shape (`node.position` is `[x, y, z]`); direct setters accept them too (`node.position = [1, 2, 3]`). **Wrong-arity arrays fail typecheck** (`position={[1, 2]}` errors) — except `Color`'s trailing `a`, which is optional (`[r, g, b, a?]`, alpha defaults 1; it's the only value type with a partial scalar ctor). The math API lives as named functions per type module — `import * as v3 from '@ringozz/godot/Vector3'`, then `v3.normalized(v)`, `v3.dot(a, b)`; construction uses `fromXxx` factories (`basis.fromAxisAngle(axis, angle)`, `transform3d.fromBasisOrigin(basis, origin)`).
|
|
140
|
-
- **Colors**: `albedoColor={[1, 0.2, 0.2]}` (a `Color` is just `[r, g, b, a?]`).
|
|
141
|
-
- **Packed arrays**: `Packed*Array`-typed props (e.g. `Gradient.colors`/`offsets`) accept plain JS arrays — elements convert with the array's element type, so `colors={[[0.5, 0, 0, 1], [1, 1, 1, 1]]}` works; `new PackedVector2Array([[1, 2], [3, 4]])` works too. The same element-aware conversion applies to **direct setters and method args**: `gradient.colors = [[1, 0, 0, 1], [0, 1, 0, 1]]`, `addPoint(0.5, [1, 0, 0, 1])`, and `PhysicsRayQueryParameters3D.create([1, 2, 3], [4, 5, 6])` all accept tuples.
|
|
142
|
-
- **Array-typed props** (`GodotArray`, e.g. `CodeEdit.lineLengthGuidelines`, `Font.fallbacks`) accept plain JS arrays directly — they become a Godot `Array`.
|
|
143
|
-
- **Sub-properties / Dash props**: Set individual vector/color components directly using dash syntax:
|
|
144
|
-
```tsx
|
|
145
|
-
<BoxMesh attach="mesh" size-x={0.5} size-y={1.0} size-z={0.5} />
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Text content**: text nodes are unsupported, but string/number children — and arrays of all-string/all-number children — are flattened into the node's `text` prop, so `<Label>Hello</Label>` works. Mixed children (containing elements) throw; use an explicit `text` prop instead, and never pass string children to a node without a `text` prop.
|
|
149
|
-
|
|
150
|
-
### 4. Engine Signals & Event Subscription (`useSignal`)
|
|
151
|
-
|
|
152
|
-
Subscribe to Godot engine signals cleanly with `useSignal`.
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
import { Engine } from '@ringozz/godot';
|
|
156
|
-
import { SceneTree } from '@ringozz/godot/SceneTree';
|
|
157
|
-
import { useSignal } from '@ringozz/react-godot';
|
|
158
|
-
|
|
159
|
-
const tree = Engine.getMainLoop() as SceneTree;
|
|
160
|
-
const root = tree.root;
|
|
161
|
-
|
|
162
|
-
// Subscribe to frame updates (process loop)
|
|
163
|
-
useSignal(tree.processFrame, () => {
|
|
164
|
-
// Update animations, camera matrix, physics targets, etc.
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Subscribe to window input events
|
|
168
|
-
useSignal(root.windowInput, (event) => {
|
|
169
|
-
if (event instanceof InputEventMouseMotion) {
|
|
170
|
-
// Handle mouse motion
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Signal **JSX props** (`pressed`, `toggled`, `valueChanged`, `textSubmitted`, ...) are typed as the signal's callback — pass a plain function, no cast needed:
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
<Button text="Play" pressed={() => start()} />
|
|
179
|
-
<CheckButton text="Debug" toggled={(on) => setDebug(on)} />
|
|
180
|
-
<HSlider value={round} valueChanged={(v) => setRound(v)} />
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Signal handlers are connected **before** value props are applied within a commit (`Instance.assign`), so the handler a render installs is already current when a value setter fires its signal (e.g. `Range.value` emits `value_changed`). Note that programmatic value sets still emit (Godot semantics) — if you feed a slider's `value` back from your own state, guard the handler (e.g. compare against the live store) or the emission loops back into your reducer.
|
|
184
|
-
|
|
185
|
-
`ComponentProps` only exposes writable members: read-only props (e.g. `Node.multiplayer`) and methods are not settable props.
|
|
186
|
-
|
|
187
|
-
## Full Example
|
|
188
|
-
|
|
189
|
-
Below is a complete interactive 3D physics scene with custom camera controls and physics bodies:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
import { Engine, Key, MouseButton } from '@ringozz/godot';
|
|
193
|
-
import type { Color } from '@ringozz/godot';
|
|
194
|
-
import { BoxMesh } from '@ringozz/godot/BoxMesh';
|
|
195
|
-
import { BoxShape3D } from '@ringozz/godot/BoxShape3D';
|
|
196
|
-
import { Camera3D } from '@ringozz/godot/Camera3D';
|
|
197
|
-
import { CollisionShape3D } from '@ringozz/godot/CollisionShape3D';
|
|
198
|
-
import { DirectionalLight3D } from '@ringozz/godot/DirectionalLight3D';
|
|
199
|
-
import { BGMode, Environment, ToneMapper } from '@ringozz/godot/Environment';
|
|
200
|
-
import { InputEventMouseButton } from '@ringozz/godot/InputEventMouseButton';
|
|
201
|
-
import { InputEventMouseMotion } from '@ringozz/godot/InputEventMouseMotion';
|
|
202
|
-
import { MeshInstance3D } from '@ringozz/godot/MeshInstance3D';
|
|
203
|
-
import { PhysicsMaterial } from '@ringozz/godot/PhysicsMaterial';
|
|
204
|
-
import { RigidBody3D } from '@ringozz/godot/RigidBody3D';
|
|
205
|
-
import { SceneTree } from '@ringozz/godot/SceneTree';
|
|
206
|
-
import { SphereMesh } from '@ringozz/godot/SphereMesh';
|
|
207
|
-
import { SphereShape3D } from '@ringozz/godot/SphereShape3D';
|
|
208
|
-
import { StandardMaterial3D } from '@ringozz/godot/StandardMaterial3D';
|
|
209
|
-
import { StaticBody3D } from '@ringozz/godot/StaticBody3D';
|
|
210
|
-
import { WorldEnvironment } from '@ringozz/godot/WorldEnvironment';
|
|
211
|
-
import { useSignal, type ComponentProps } from '@ringozz/react-godot';
|
|
212
|
-
import { useRef, type ReactElement } from 'react';
|
|
213
|
-
|
|
214
|
-
const tree = Engine.getMainLoop() as SceneTree;
|
|
215
|
-
const root = tree.root;
|
|
216
|
-
|
|
217
|
-
function PhysBody({ color, shape, children, ...rest }: {
|
|
218
|
-
color: Color;
|
|
219
|
-
shape: ReactElement;
|
|
220
|
-
} & ComponentProps<typeof RigidBody3D>) {
|
|
221
|
-
return (
|
|
222
|
-
<RigidBody3D {...rest}>
|
|
223
|
-
<MeshInstance3D>
|
|
224
|
-
{children}
|
|
225
|
-
<StandardMaterial3D attach="materialOverride" albedoColor={color} />
|
|
226
|
-
</MeshInstance3D>
|
|
227
|
-
<CollisionShape3D>{shape}</CollisionShape3D>
|
|
228
|
-
</RigidBody3D>
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function App() {
|
|
233
|
-
const cameraRef = useRef<Camera3D>(null);
|
|
234
|
-
const isDragging = useRef(false);
|
|
235
|
-
const yaw = useRef(0.7);
|
|
236
|
-
const pitch = useRef(0.3);
|
|
237
|
-
const zoom = useRef(7);
|
|
238
|
-
|
|
239
|
-
useSignal(root.windowInput, (event) => {
|
|
240
|
-
if (event instanceof InputEventMouseMotion && isDragging.current) {
|
|
241
|
-
yaw.current -= event.relative[0] * 0.005;
|
|
242
|
-
pitch.current = Math.max(-1.4, Math.min(1.4, pitch.current + event.relative[1] * 0.005));
|
|
243
|
-
} else if (event instanceof InputEventMouseButton) {
|
|
244
|
-
if (event.buttonIndex === MouseButton.MOUSE_BUTTON_LEFT) {
|
|
245
|
-
isDragging.current = event.pressed;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
event.free();
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
useSignal(tree.processFrame, () => {
|
|
252
|
-
const camera = cameraRef.current;
|
|
253
|
-
if (!camera) return;
|
|
254
|
-
|
|
255
|
-
const cx = zoom.current * Math.cos(pitch.current) * Math.sin(yaw.current);
|
|
256
|
-
const cy = zoom.current * Math.sin(pitch.current) + 1;
|
|
257
|
-
const cz = zoom.current * Math.cos(pitch.current) * Math.cos(yaw.current);
|
|
258
|
-
camera.position = [cx, cy, cz];
|
|
259
|
-
camera.lookAt([0, 1, 0], [0, 1, 0]);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<>
|
|
264
|
-
<WorldEnvironment>
|
|
265
|
-
<Environment attach="environment" tonemapMode={ToneMapper.TONE_MAPPER_AGX} backgroundMode={BGMode.BG_COLOR} backgroundColor={[0.05, 0.1, 0.25]} />
|
|
266
|
-
</WorldEnvironment>
|
|
267
|
-
<DirectionalLight3D name="Sun" rotation={[-0.8, 0.5, 0]} />
|
|
268
|
-
<Camera3D name="Camera" ref={cameraRef} />
|
|
269
|
-
|
|
270
|
-
{/* Physics Sphere */}
|
|
271
|
-
<PhysBody name="BouncySphere" position={[0, 4, 0]} color={[0.2, 1, 0.2]}
|
|
272
|
-
shape={<SphereShape3D attach="shape" radius={0.5} />}>
|
|
273
|
-
<SphereMesh attach="mesh" radius={0.5} height={1} />
|
|
274
|
-
<PhysicsMaterial attach="physicsMaterialOverride" bounce={0.8} />
|
|
275
|
-
</PhysBody>
|
|
276
|
-
|
|
277
|
-
{/* Ground Plane */}
|
|
278
|
-
<StaticBody3D name="Ground">
|
|
279
|
-
<CollisionShape3D>
|
|
280
|
-
<BoxShape3D attach="shape" size={[10, 0.1, 10]} />
|
|
281
|
-
</CollisionShape3D>
|
|
282
|
-
<MeshInstance3D>
|
|
283
|
-
<BoxMesh attach="mesh" size-x={10} size-y={0.1} size-z={10} />
|
|
284
|
-
<StandardMaterial3D attach="materialOverride" albedoColor={[0.2, 0.2, 0.25]} />
|
|
285
|
-
</MeshInstance3D>
|
|
286
|
-
</StaticBody3D>
|
|
287
|
-
</>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
## API Summary
|
|
293
|
-
|
|
294
|
-
| Export | Type | Description |
|
|
295
|
-
| :--- | :--- | :--- |
|
|
296
|
-
| `createRoot(parent)` | Function | Mounts a React component tree into a target Godot `Node` (e.g. `tree.root`). Returns `{ render, unmount }`. |
|
|
297
|
-
| `createPortal(children, container)` | Function | Renders children into a target Godot `Node` (e.g. `CanvasLayer` or `SubViewport`). |
|
|
298
|
-
| `useSignal(signal, callback)` | Hook | Connects a callback to a Godot object signal (e.g. `tree.processFrame`, `node.treeEntered`). Disconnects on unmount. |
|
|
299
|
-
| `useMutableCallback(fn)` | Hook | Returns a `RefObject` whose `.current` always points to the latest callback implementation. |
|
|
300
|
-
| `useTween<NodeT>(create, deps)` | Hook | Reactive tween-as-prop-value. Returns `[spring, tweenRef]` — spread `spring` onto a node; `tweenRef.current` is the live native `Tween`. The optional node type (`useTween<Node3D>`) types `to`/`from` array goals as that node's value-type tuples. |
|
|
301
|
-
| `ComponentProps<T>` | Type | Utility type to infer valid React JSX props for any Godot node class `T`. |
|
|
302
|
-
|
|
303
|
-
## Animating props with `useTween`
|
|
304
|
-
|
|
305
|
-
`useTween` animates a node's properties declaratively. You describe the target values once per `to` key; spreading the returned `spring` onto a node binds the tween to it. Re-rendering with the same `deps` keeps the same tween (nothing restarts); changing `deps` creates a new `Tween` and animates from the current values.
|
|
306
|
-
|
|
307
|
-
```tsx
|
|
308
|
-
const [spring, tweenRef] = useTween<Node3D>(() => ({
|
|
309
|
-
from: { position: [0, 0, 0] },
|
|
310
|
-
to: { position: [0, 2, 0], globalRotation: [0, 90, 0] },
|
|
311
|
-
config: { duration: 0.6, transition: TransitionType.TRANS_QUAD, ease: EaseType.EASE_IN_OUT },
|
|
312
|
-
delay: 0, loops: 1, immediate: false,
|
|
313
|
-
onFinished: () => {},
|
|
314
|
-
}), [x, z]);
|
|
315
|
-
|
|
316
|
-
return <Node3D {...spring} />;
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
- **`spring`** holds one function per `to` key (spread them all onto the node; each contributes its own prop's tweener to the shared `Tween`). Array values are converted to the property's value type.
|
|
320
|
-
- **Node-typed goals**: call `useTween<Node3D>(...)` so `to`/`from` are typed against that node's props — `position: [0, 2, 0]` and nested `basis: [[…]]` infer as `Vector3`/`Basis`, with no `as Vector3` casts. Only scalar-number and value-type props are tweenable (signals, `Node` refs, bools, strings, containers are rejected); the returned `spring` is that whole prop set (all optional), so `{...spring}` stays JSX-spreadable. A bare `useTween(...)` keeps array goals loose (`number[]`).
|
|
321
|
-
- **`tweenRef.current`** is the **live native Godot `Tween`**, created when the node mounts (null before that): `tweenRef.current?.kill()`, `?.pause()`, `?.play()`, `?.setSpeedScale(n)`, `?.isRunning()`, `?.finished`…
|
|
322
|
-
- **`config`** maps to the Godot Tween API: `duration` (seconds), `transition`/`ease` (`TransitionType`/`EaseType` — the tween's defaults), `easing` (a `[0,1]→[0,1]` function → `setCustomInterpolator`), `speedScale`.
|
|
323
|
-
- **`onStart`** fires when the tween is created/bound (on mount or when `deps` change), before it starts stepping.
|
|
324
|
-
- **`onFinished`** fires on natural completion only (Godot's `finished` signal); `kill()`/unmount don't fire it, so no stale callbacks.
|
|
325
|
-
- **Unwrapping**: replacing a springed prop with a plain value (e.g. dropping `{...spring}` for `position={[9, 0, 0]}`) does not stop the running tween — it keeps animating to its target and the tween's value wins over the plain value. Stop it with `tweenRef.current?.kill()`.
|
|
326
|
-
|
|
327
|
-
|
|
1
|
+
# `@ringozz/react-godot`
|
|
2
|
+
|
|
3
|
+
Declarative, component-driven **Godot 4** scene graph management in React. Built for high-performance applications using `@ringozz/godot`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@ringozz/react-godot` brings a **React Three Fiber (R3F)**-like developer experience to Godot. It provides a custom React reconciler that renders React components directly into Godot's live engine scene tree (`SceneTree`).
|
|
8
|
+
|
|
9
|
+
If you are familiar with React Three Fiber, the core paradigms will feel instantly recognizable:
|
|
10
|
+
- **Declarative Nodes**: Godot 3D/2D nodes (e.g., `<RigidBody3D>`, `<Camera3D>`, `<DirectionalLight3D>`) map directly to JSX components.
|
|
11
|
+
- **Resource Attachment**: Use the `attach` prop to assign nested Godot resources (materials, shapes, meshes, environments) directly to parent properties.
|
|
12
|
+
- **Reactive Engine Loop**: Use hooks like `useSignal` to hook into Godot signals (`processFrame`, `windowInput`) without triggering React state re-renders.
|
|
13
|
+
|
|
14
|
+
## Installation & Setup
|
|
15
|
+
|
|
16
|
+
### 1. Dependencies
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ringozz/react-godot react
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. TypeScript Configuration (`tsconfig.json`)
|
|
23
|
+
|
|
24
|
+
To enable custom JSX element resolution for Godot nodes, configure `jsxImportSource` and module settings in `tsconfig.json`:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"compilerOptions": {
|
|
29
|
+
"module": "esnext",
|
|
30
|
+
"target": "es2025",
|
|
31
|
+
"jsx": "react-jsx",
|
|
32
|
+
"jsxImportSource": "@ringozz/react-godot",
|
|
33
|
+
"rewriteRelativeImportExtensions": true,
|
|
34
|
+
"verbatimModuleSyntax": true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Application Entry & Portal Rendering
|
|
40
|
+
|
|
41
|
+
### 1. Mounting with `createRoot(parent: Node)`
|
|
42
|
+
`createRoot` is the primary entry point for mounting a React component tree into a Godot target `Node` (typically `SceneTree.root`).
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Engine } from '@ringozz/godot/Engine';
|
|
46
|
+
import { SceneTree } from '@ringozz/godot/SceneTree';
|
|
47
|
+
import { createRoot } from '@ringozz/react-godot';
|
|
48
|
+
import { App } from './App';
|
|
49
|
+
|
|
50
|
+
const tree = Engine.getMainLoop() as SceneTree;
|
|
51
|
+
const { render } = createRoot(tree.root);
|
|
52
|
+
|
|
53
|
+
render(<App />);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- Returns `{ render, unmount }`.
|
|
57
|
+
- Enables React `StrictMode` automatically in development (`NODE_ENV === 'development'`).
|
|
58
|
+
|
|
59
|
+
### 2. Render Portals with `createPortal(children, container)`
|
|
60
|
+
`createPortal` allows rendering a React sub-tree into a different Godot `Node` in the hierarchy—such as a `CanvasLayer` for 2D UI overlays, a `SubViewport`, or an isolated container node.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { createPortal } from '@ringozz/react-godot';
|
|
64
|
+
import { Label } from '@ringozz/godot/Label';
|
|
65
|
+
|
|
66
|
+
function Overlay({ overlayNode }: { overlayNode: Node }) {
|
|
67
|
+
return createPortal(
|
|
68
|
+
<Label text="HUD Overlay" position={[20, 20]} />,
|
|
69
|
+
overlayNode
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Container Constraint**: The target `container` must be a Godot `Node` (it cannot target `Resource` instances).
|
|
75
|
+
- **Cleanup**: Unmounting the portal safely disposes of and frees its child Godot nodes.
|
|
76
|
+
|
|
77
|
+
## Core Concepts & Patterns
|
|
78
|
+
|
|
79
|
+
### 1. Existing Objects with `object` Prop
|
|
80
|
+
|
|
81
|
+
If you already have a pre-existing Godot `Node`, `Resource`, or `PackedScene` instance (e.g. instantiated from C++ or loaded from disk), pass it via the `object` prop to adopt it into the React component tree instead of letting the reconciler instantiate a new object:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Node object={myExistingGodotNode}>
|
|
85
|
+
{/* Children attached to existing node */}
|
|
86
|
+
</Node>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **`object` is immutable on a mounted instance.** It selects the host instance once, at creation. A re-render that changes `object` on an existing element throws (`Cannot change the \`object\` of a mounted instance`) because React cannot swap a fiber's Godot node — remount with a new `key` instead.
|
|
90
|
+
- **A `PackedScene` `object` is instantiated once per host mount** — each element gets a fresh node tree; the result is a plain `Node` (not `.reference()`'d) freed on unmount. The same `PackedScene` can feed multiple elements; give same-type siblings stable `key`s.
|
|
91
|
+
- **Dynamic same-type sibling lists need stable `key`s.** Godot host nodes are identity, not content: React reconciles keyless siblings by position, so deleting the *first* of two same-type siblings reuses its Godot node for the survivor. A plain state re-render does this too, and Bun's web dev server makes it especially likely — its React Fast Refresh re-renders the edited component **in place** (no app re-run), reusing the same fibers and Godot nodes, so a deleted sibling's node can survive on its neighbor and the neighbor's node is freed. Add `key="..."` to each dynamically toggled same-type sibling to keep nodes pinned to the right elements.
|
|
92
|
+
|
|
93
|
+
### 1b. Attaching a Script with `script`
|
|
94
|
+
|
|
95
|
+
Use the `script` prop to attach a `Script` resource (e.g. an imported GDScript `.gd` module) to a node on creation. The script is attached **before the node enters the tree**, so Godot fires `_ready` and auto-enables the script's `_process`/`_input` — no manual `set_process(true)`/`set_process_input(true)` calls needed:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import controller from './camera_controller.gd';
|
|
99
|
+
|
|
100
|
+
<Camera3D script={use(controller)} current={true} fov={55} />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **Attach before tree entry means `_ready`/`_process`/`_input` work.** Attaching to an already-in-tree node (e.g. in a `useEffect`) never fires `_ready` and leaves `_process`/`_input` disabled — prefer `script` over an effect for scripted nodes.
|
|
104
|
+
- **`script` is immutable on a mounted instance.** A re-render that changes `script` on an existing element throws — remount with a new `key` to swap the compiled script.
|
|
105
|
+
|
|
106
|
+
### 2. Resource Nesting with `attach`
|
|
107
|
+
|
|
108
|
+
In Godot, nodes often hold references to `Resource` objects (such as shapes, meshes, materials, or environments). Use the `attach` prop to automatically assign a child resource to a specific property on its parent:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
/* Attaching a shape to CollisionShape3D */
|
|
112
|
+
<CollisionShape3D>
|
|
113
|
+
<BoxShape3D attach="shape" size={[1, 1, 1]} />
|
|
114
|
+
</CollisionShape3D>
|
|
115
|
+
|
|
116
|
+
/* Attaching a material override to MeshInstance3D */
|
|
117
|
+
<MeshInstance3D>
|
|
118
|
+
<BoxMesh attach="mesh" />
|
|
119
|
+
<StandardMaterial3D attach="materialOverride" albedoColor={[1, 0, 0]} />
|
|
120
|
+
</MeshInstance3D>
|
|
121
|
+
|
|
122
|
+
/* Attaching physics material to RigidBody3D */
|
|
123
|
+
<RigidBody3D>
|
|
124
|
+
<PhysicsMaterial attach="physicsMaterialOverride" bounce={0.8} />
|
|
125
|
+
</RigidBody3D>
|
|
126
|
+
|
|
127
|
+
/* Attaching environment to WorldEnvironment */
|
|
128
|
+
<WorldEnvironment>
|
|
129
|
+
<Environment attach="environment" backgroundColor={[0.1, 0.1, 0.2]} />
|
|
130
|
+
</WorldEnvironment>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Note for physics bodies: `attach="shape"` goes on the shape **resource** element (e.g. `<BoxShape3D attach="shape" />`), never on the `CollisionShape3D` node itself — `Node.add_child` requires a `Node`, and shapes are `Resource`s, so an `attach` on `CollisionShape3D` adds the shape as a child node and fails. Likewise, mesh children of a physics body must be wrapped in a `MeshInstance3D` (a `RigidBody3D`/`StaticBody3D` has no `mesh`/`materialOverride` props to attach to).
|
|
134
|
+
|
|
135
|
+
### 3. Properties & Helper Types
|
|
136
|
+
|
|
137
|
+
Props map directly to Godot node setters and properties. Helper arrays are automatically cast to Godot types:
|
|
138
|
+
|
|
139
|
+
- **Vectors / value types**: value types are **named tuples** — flat (`Vector3`, `Color`, `Rect2`, …) are scalar-labeled tuples (`Vector3 = [x, y, z]`, `Color = [r, g, b, a?]`); nested (`AABB`/`Transform2D`/`Basis`/`Transform3D`/`Projection`) are row-labeled tuples of the flat element alias (`Transform3D = [x, y, z, origin]`, each row a `Vector3`), so `position={[0, 1, 0]}`, `transform={[[1, 0, 0], [0, 1, 0], [0, 0, 1], [10, 20, 30]]}`. Getters return the same labeled shape (`node.position` is `[x, y, z]`); direct setters accept them too (`node.position = [1, 2, 3]`). **Wrong-arity arrays fail typecheck** (`position={[1, 2]}` errors) — except `Color`'s trailing `a`, which is optional (`[r, g, b, a?]`, alpha defaults 1; it's the only value type with a partial scalar ctor). The math API lives as named functions per type module — `import * as v3 from '@ringozz/godot/Vector3'`, then `v3.normalized(v)`, `v3.dot(a, b)`; construction uses `fromXxx` factories (`basis.fromAxisAngle(axis, angle)`, `transform3d.fromBasisOrigin(basis, origin)`).
|
|
140
|
+
- **Colors**: `albedoColor={[1, 0.2, 0.2]}` (a `Color` is just `[r, g, b, a?]`).
|
|
141
|
+
- **Packed arrays**: `Packed*Array`-typed props (e.g. `Gradient.colors`/`offsets`) accept plain JS arrays — elements convert with the array's element type, so `colors={[[0.5, 0, 0, 1], [1, 1, 1, 1]]}` works; `new PackedVector2Array([[1, 2], [3, 4]])` works too. The same element-aware conversion applies to **direct setters and method args**: `gradient.colors = [[1, 0, 0, 1], [0, 1, 0, 1]]`, `addPoint(0.5, [1, 0, 0, 1])`, and `PhysicsRayQueryParameters3D.create([1, 2, 3], [4, 5, 6])` all accept tuples.
|
|
142
|
+
- **Array-typed props** (`GodotArray`, e.g. `CodeEdit.lineLengthGuidelines`, `Font.fallbacks`) accept plain JS arrays directly — they become a Godot `Array`.
|
|
143
|
+
- **Sub-properties / Dash props**: Set individual vector/color components directly using dash syntax:
|
|
144
|
+
```tsx
|
|
145
|
+
<BoxMesh attach="mesh" size-x={0.5} size-y={1.0} size-z={0.5} />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Text content**: text nodes are unsupported, but string/number children — and arrays of all-string/all-number children — are flattened into the node's `text` prop, so `<Label>Hello</Label>` works. Mixed children (containing elements) throw; use an explicit `text` prop instead, and never pass string children to a node without a `text` prop.
|
|
149
|
+
|
|
150
|
+
### 4. Engine Signals & Event Subscription (`useSignal`)
|
|
151
|
+
|
|
152
|
+
Subscribe to Godot engine signals cleanly with `useSignal`.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { Engine } from '@ringozz/godot';
|
|
156
|
+
import { SceneTree } from '@ringozz/godot/SceneTree';
|
|
157
|
+
import { useSignal } from '@ringozz/react-godot';
|
|
158
|
+
|
|
159
|
+
const tree = Engine.getMainLoop() as SceneTree;
|
|
160
|
+
const root = tree.root;
|
|
161
|
+
|
|
162
|
+
// Subscribe to frame updates (process loop)
|
|
163
|
+
useSignal(tree.processFrame, () => {
|
|
164
|
+
// Update animations, camera matrix, physics targets, etc.
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Subscribe to window input events
|
|
168
|
+
useSignal(root.windowInput, (event) => {
|
|
169
|
+
if (event instanceof InputEventMouseMotion) {
|
|
170
|
+
// Handle mouse motion
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Signal **JSX props** (`pressed`, `toggled`, `valueChanged`, `textSubmitted`, ...) are typed as the signal's callback — pass a plain function, no cast needed:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<Button text="Play" pressed={() => start()} />
|
|
179
|
+
<CheckButton text="Debug" toggled={(on) => setDebug(on)} />
|
|
180
|
+
<HSlider value={round} valueChanged={(v) => setRound(v)} />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Signal handlers are connected **before** value props are applied within a commit (`Instance.assign`), so the handler a render installs is already current when a value setter fires its signal (e.g. `Range.value` emits `value_changed`). Note that programmatic value sets still emit (Godot semantics) — if you feed a slider's `value` back from your own state, guard the handler (e.g. compare against the live store) or the emission loops back into your reducer.
|
|
184
|
+
|
|
185
|
+
`ComponentProps` only exposes writable members: read-only props (e.g. `Node.multiplayer`) and methods are not settable props.
|
|
186
|
+
|
|
187
|
+
## Full Example
|
|
188
|
+
|
|
189
|
+
Below is a complete interactive 3D physics scene with custom camera controls and physics bodies:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { Engine, Key, MouseButton } from '@ringozz/godot';
|
|
193
|
+
import type { Color } from '@ringozz/godot';
|
|
194
|
+
import { BoxMesh } from '@ringozz/godot/BoxMesh';
|
|
195
|
+
import { BoxShape3D } from '@ringozz/godot/BoxShape3D';
|
|
196
|
+
import { Camera3D } from '@ringozz/godot/Camera3D';
|
|
197
|
+
import { CollisionShape3D } from '@ringozz/godot/CollisionShape3D';
|
|
198
|
+
import { DirectionalLight3D } from '@ringozz/godot/DirectionalLight3D';
|
|
199
|
+
import { BGMode, Environment, ToneMapper } from '@ringozz/godot/Environment';
|
|
200
|
+
import { InputEventMouseButton } from '@ringozz/godot/InputEventMouseButton';
|
|
201
|
+
import { InputEventMouseMotion } from '@ringozz/godot/InputEventMouseMotion';
|
|
202
|
+
import { MeshInstance3D } from '@ringozz/godot/MeshInstance3D';
|
|
203
|
+
import { PhysicsMaterial } from '@ringozz/godot/PhysicsMaterial';
|
|
204
|
+
import { RigidBody3D } from '@ringozz/godot/RigidBody3D';
|
|
205
|
+
import { SceneTree } from '@ringozz/godot/SceneTree';
|
|
206
|
+
import { SphereMesh } from '@ringozz/godot/SphereMesh';
|
|
207
|
+
import { SphereShape3D } from '@ringozz/godot/SphereShape3D';
|
|
208
|
+
import { StandardMaterial3D } from '@ringozz/godot/StandardMaterial3D';
|
|
209
|
+
import { StaticBody3D } from '@ringozz/godot/StaticBody3D';
|
|
210
|
+
import { WorldEnvironment } from '@ringozz/godot/WorldEnvironment';
|
|
211
|
+
import { useSignal, type ComponentProps } from '@ringozz/react-godot';
|
|
212
|
+
import { useRef, type ReactElement } from 'react';
|
|
213
|
+
|
|
214
|
+
const tree = Engine.getMainLoop() as SceneTree;
|
|
215
|
+
const root = tree.root;
|
|
216
|
+
|
|
217
|
+
function PhysBody({ color, shape, children, ...rest }: {
|
|
218
|
+
color: Color;
|
|
219
|
+
shape: ReactElement;
|
|
220
|
+
} & ComponentProps<typeof RigidBody3D>) {
|
|
221
|
+
return (
|
|
222
|
+
<RigidBody3D {...rest}>
|
|
223
|
+
<MeshInstance3D>
|
|
224
|
+
{children}
|
|
225
|
+
<StandardMaterial3D attach="materialOverride" albedoColor={color} />
|
|
226
|
+
</MeshInstance3D>
|
|
227
|
+
<CollisionShape3D>{shape}</CollisionShape3D>
|
|
228
|
+
</RigidBody3D>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function App() {
|
|
233
|
+
const cameraRef = useRef<Camera3D>(null);
|
|
234
|
+
const isDragging = useRef(false);
|
|
235
|
+
const yaw = useRef(0.7);
|
|
236
|
+
const pitch = useRef(0.3);
|
|
237
|
+
const zoom = useRef(7);
|
|
238
|
+
|
|
239
|
+
useSignal(root.windowInput, (event) => {
|
|
240
|
+
if (event instanceof InputEventMouseMotion && isDragging.current) {
|
|
241
|
+
yaw.current -= event.relative[0] * 0.005;
|
|
242
|
+
pitch.current = Math.max(-1.4, Math.min(1.4, pitch.current + event.relative[1] * 0.005));
|
|
243
|
+
} else if (event instanceof InputEventMouseButton) {
|
|
244
|
+
if (event.buttonIndex === MouseButton.MOUSE_BUTTON_LEFT) {
|
|
245
|
+
isDragging.current = event.pressed;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
event.free();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
useSignal(tree.processFrame, () => {
|
|
252
|
+
const camera = cameraRef.current;
|
|
253
|
+
if (!camera) return;
|
|
254
|
+
|
|
255
|
+
const cx = zoom.current * Math.cos(pitch.current) * Math.sin(yaw.current);
|
|
256
|
+
const cy = zoom.current * Math.sin(pitch.current) + 1;
|
|
257
|
+
const cz = zoom.current * Math.cos(pitch.current) * Math.cos(yaw.current);
|
|
258
|
+
camera.position = [cx, cy, cz];
|
|
259
|
+
camera.lookAt([0, 1, 0], [0, 1, 0]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<>
|
|
264
|
+
<WorldEnvironment>
|
|
265
|
+
<Environment attach="environment" tonemapMode={ToneMapper.TONE_MAPPER_AGX} backgroundMode={BGMode.BG_COLOR} backgroundColor={[0.05, 0.1, 0.25]} />
|
|
266
|
+
</WorldEnvironment>
|
|
267
|
+
<DirectionalLight3D name="Sun" rotation={[-0.8, 0.5, 0]} />
|
|
268
|
+
<Camera3D name="Camera" ref={cameraRef} />
|
|
269
|
+
|
|
270
|
+
{/* Physics Sphere */}
|
|
271
|
+
<PhysBody name="BouncySphere" position={[0, 4, 0]} color={[0.2, 1, 0.2]}
|
|
272
|
+
shape={<SphereShape3D attach="shape" radius={0.5} />}>
|
|
273
|
+
<SphereMesh attach="mesh" radius={0.5} height={1} />
|
|
274
|
+
<PhysicsMaterial attach="physicsMaterialOverride" bounce={0.8} />
|
|
275
|
+
</PhysBody>
|
|
276
|
+
|
|
277
|
+
{/* Ground Plane */}
|
|
278
|
+
<StaticBody3D name="Ground">
|
|
279
|
+
<CollisionShape3D>
|
|
280
|
+
<BoxShape3D attach="shape" size={[10, 0.1, 10]} />
|
|
281
|
+
</CollisionShape3D>
|
|
282
|
+
<MeshInstance3D>
|
|
283
|
+
<BoxMesh attach="mesh" size-x={10} size-y={0.1} size-z={10} />
|
|
284
|
+
<StandardMaterial3D attach="materialOverride" albedoColor={[0.2, 0.2, 0.25]} />
|
|
285
|
+
</MeshInstance3D>
|
|
286
|
+
</StaticBody3D>
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## API Summary
|
|
293
|
+
|
|
294
|
+
| Export | Type | Description |
|
|
295
|
+
| :--- | :--- | :--- |
|
|
296
|
+
| `createRoot(parent)` | Function | Mounts a React component tree into a target Godot `Node` (e.g. `tree.root`). Returns `{ render, unmount }`. |
|
|
297
|
+
| `createPortal(children, container)` | Function | Renders children into a target Godot `Node` (e.g. `CanvasLayer` or `SubViewport`). |
|
|
298
|
+
| `useSignal(signal, callback)` | Hook | Connects a callback to a Godot object signal (e.g. `tree.processFrame`, `node.treeEntered`). Disconnects on unmount. |
|
|
299
|
+
| `useMutableCallback(fn)` | Hook | Returns a `RefObject` whose `.current` always points to the latest callback implementation. |
|
|
300
|
+
| `useTween<NodeT>(create, deps)` | Hook | Reactive tween-as-prop-value. Returns `[spring, tweenRef]` — spread `spring` onto a node; `tweenRef.current` is the live native `Tween`. The optional node type (`useTween<Node3D>`) types `to`/`from` array goals as that node's value-type tuples. |
|
|
301
|
+
| `ComponentProps<T>` | Type | Utility type to infer valid React JSX props for any Godot node class `T`. |
|
|
302
|
+
|
|
303
|
+
## Animating props with `useTween`
|
|
304
|
+
|
|
305
|
+
`useTween` animates a node's properties declaratively. You describe the target values once per `to` key; spreading the returned `spring` onto a node binds the tween to it. Re-rendering with the same `deps` keeps the same tween (nothing restarts); changing `deps` creates a new `Tween` and animates from the current values.
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
const [spring, tweenRef] = useTween<Node3D>(() => ({
|
|
309
|
+
from: { position: [0, 0, 0] },
|
|
310
|
+
to: { position: [0, 2, 0], globalRotation: [0, 90, 0] },
|
|
311
|
+
config: { duration: 0.6, transition: TransitionType.TRANS_QUAD, ease: EaseType.EASE_IN_OUT },
|
|
312
|
+
delay: 0, loops: 1, immediate: false,
|
|
313
|
+
onFinished: () => {},
|
|
314
|
+
}), [x, z]);
|
|
315
|
+
|
|
316
|
+
return <Node3D {...spring} />;
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- **`spring`** holds one function per `to` key (spread them all onto the node; each contributes its own prop's tweener to the shared `Tween`). Array values are converted to the property's value type.
|
|
320
|
+
- **Node-typed goals**: call `useTween<Node3D>(...)` so `to`/`from` are typed against that node's props — `position: [0, 2, 0]` and nested `basis: [[…]]` infer as `Vector3`/`Basis`, with no `as Vector3` casts. Only scalar-number and value-type props are tweenable (signals, `Node` refs, bools, strings, containers are rejected); the returned `spring` is that whole prop set (all optional), so `{...spring}` stays JSX-spreadable. A bare `useTween(...)` keeps array goals loose (`number[]`).
|
|
321
|
+
- **`tweenRef.current`** is the **live native Godot `Tween`**, created when the node mounts (null before that): `tweenRef.current?.kill()`, `?.pause()`, `?.play()`, `?.setSpeedScale(n)`, `?.isRunning()`, `?.finished`…
|
|
322
|
+
- **`config`** maps to the Godot Tween API: `duration` (seconds), `transition`/`ease` (`TransitionType`/`EaseType` — the tween's defaults), `easing` (a `[0,1]→[0,1]` function → `setCustomInterpolator`), `speedScale`.
|
|
323
|
+
- **`onStart`** fires when the tween is created/bound (on mount or when `deps` change), before it starts stepping.
|
|
324
|
+
- **`onFinished`** fires on natural completion only (Godot's `finished` signal); `kill()`/unmount don't fire it, so no stale callbacks.
|
|
325
|
+
- **Unwrapping**: replacing a springed prop with a plain value (e.g. dropping `{...spring}` for `position={[9, 0, 0]}`) does not stop the running tween — it keeps animating to its target and the tween's value wins over the plain value. Stop it with `tweenRef.current?.kill()`.
|
|
326
|
+
|
|
327
|
+
|