@ringozz/react-godot 1.0.0-8 → 4.7.2-570
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 +37 -3
- package/package.json +5 -2
- package/src/react-fiber.ts +5 -2
- package/src/react-hooks.ts +124 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ If you already have a pre-existing Godot `Node`, `Resource`, or `PackedScene` in
|
|
|
88
88
|
|
|
89
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
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
|
|
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
92
|
|
|
93
93
|
### 2. Resource Nesting with `attach`
|
|
94
94
|
|
|
@@ -117,18 +117,24 @@ In Godot, nodes often hold references to `Resource` objects (such as shapes, mes
|
|
|
117
117
|
</WorldEnvironment>
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
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).
|
|
121
|
+
|
|
122
|
+
### 3. Properties & Helper Types
|
|
121
123
|
|
|
122
124
|
Props map directly to Godot node setters and properties. Helper arrays are automatically cast to Godot types:
|
|
123
125
|
|
|
124
126
|
- **Vectors**: `position={[0, 1, 0]}`, `scale={[2, 2, 2]}`
|
|
125
127
|
- **Colors**: `albedoColor={[1, 0.2, 0.2]}` or `Color` objects
|
|
128
|
+
- **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.
|
|
129
|
+
- **Array-typed props** (`GodotArray`, e.g. `CodeEdit.lineLengthGuidelines`, `Font.fallbacks`) accept plain JS arrays directly — they become a Godot `Array`.
|
|
126
130
|
- **Sub-properties / Dash props**: Set individual vector/color components directly using dash syntax:
|
|
127
131
|
```tsx
|
|
128
132
|
<BoxMesh attach="mesh" size-x={0.5} size-y={1.0} size-z={0.5} />
|
|
129
133
|
```
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
**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.
|
|
136
|
+
|
|
137
|
+
### 4. Engine Signals & Event Subscription (`useSignal`)
|
|
132
138
|
|
|
133
139
|
Subscribe to Godot engine signals cleanly with `useSignal`.
|
|
134
140
|
|
|
@@ -161,6 +167,8 @@ Signal **JSX props** (`pressed`, `toggled`, `valueChanged`, `textSubmitted`, ...
|
|
|
161
167
|
<HSlider value={round} valueChanged={(v) => setRound(v)} />
|
|
162
168
|
```
|
|
163
169
|
|
|
170
|
+
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.
|
|
171
|
+
|
|
164
172
|
`ComponentProps` only exposes writable members: read-only props (e.g. `Node.multiplayer`) and methods are not settable props.
|
|
165
173
|
|
|
166
174
|
## Full Example
|
|
@@ -275,4 +283,30 @@ export function App() {
|
|
|
275
283
|
| `createPortal(children, container)` | Function | Renders children into a target Godot `Node` (e.g. `CanvasLayer` or `SubViewport`). |
|
|
276
284
|
| `useSignal(signal, callback)` | Hook | Connects a callback to a Godot object signal (e.g. `tree.processFrame`, `node.treeEntered`). Disconnects on unmount. |
|
|
277
285
|
| `useMutableCallback(fn)` | Hook | Returns a `RefObject` whose `.current` always points to the latest callback implementation. |
|
|
286
|
+
| `useTween(create, deps)` | Hook | Reactive tween-as-prop-value. Returns `[spring, tweenRef]` — spread `spring` onto a node; `tweenRef.current` is the live native `Tween`. |
|
|
278
287
|
| `ComponentProps<T>` | Type | Utility type to infer valid React JSX props for any Godot node class `T`. |
|
|
288
|
+
|
|
289
|
+
## Animating props with `useTween`
|
|
290
|
+
|
|
291
|
+
`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.
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
const [spring, tweenRef] = useTween(() => ({
|
|
295
|
+
from: { position: [0, 0, 0] },
|
|
296
|
+
to: { position: [0, 2, 0], globalRotation: [0, 90, 0] },
|
|
297
|
+
config: { duration: 0.6, transition: TransitionType.TRANS_QUAD, ease: EaseType.EASE_IN_OUT },
|
|
298
|
+
delay: 0, loops: 1, immediate: false,
|
|
299
|
+
onFinished: () => {},
|
|
300
|
+
}), [x, z]);
|
|
301
|
+
|
|
302
|
+
return <Node3D {...spring} />;
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- **`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.
|
|
306
|
+
- **`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`…
|
|
307
|
+
- **`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`.
|
|
308
|
+
- **`onStart`** fires when the tween is created/bound (on mount or when `deps` change), before it starts stepping.
|
|
309
|
+
- **`onFinished`** fires on natural completion only (Godot's `finished` signal); `kill()`/unmount don't fire it, so no stale callbacks.
|
|
310
|
+
- **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()`.
|
|
311
|
+
|
|
312
|
+
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ringozz/react-godot",
|
|
3
3
|
"author": "Vladimir Davidovich",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.7.2-570",
|
|
5
5
|
"description": "A React renderer for Godot Engine via @ringozz/godot",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
6
9
|
"keywords": [
|
|
7
10
|
"react",
|
|
8
11
|
"godot"
|
|
@@ -20,7 +23,7 @@
|
|
|
20
23
|
"*.md"
|
|
21
24
|
],
|
|
22
25
|
"dependencies": {
|
|
23
|
-
"@ringozz/godot": "^4.7.
|
|
26
|
+
"@ringozz/godot": "^4.7.2-570",
|
|
24
27
|
"@types/react-reconciler": "^0.33.0",
|
|
25
28
|
"react-reconciler": "^0.33.0"
|
|
26
29
|
},
|
package/src/react-fiber.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
***********************************************************************/
|
|
4
4
|
|
|
5
5
|
import { ClassDB } from '@ringozz/godot/ClassDB';
|
|
6
|
-
import
|
|
6
|
+
import { Node } from '@ringozz/godot/Node';
|
|
7
7
|
import { Object as Instance } from '@ringozz/godot/Object';
|
|
8
8
|
import { PackedScene } from '@ringozz/godot/PackedScene';
|
|
9
9
|
import { RefCounted } from '@ringozz/godot/RefCounted';
|
|
@@ -178,7 +178,10 @@ export function getInstanceFromScope(scopeInstance: unknown): Instance | null {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
export function detachDeletedInstance(node: Instance): void {
|
|
181
|
-
node
|
|
181
|
+
if (node instanceof Node)
|
|
182
|
+
node.queueFree();
|
|
183
|
+
else
|
|
184
|
+
node.free();
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
export function maySuspendCommit(type: Type, props: Props) {
|
package/src/react-hooks.ts
CHANGED
|
@@ -3,8 +3,19 @@
|
|
|
3
3
|
***********************************************************************/
|
|
4
4
|
|
|
5
5
|
import type { Signal } from '@ringozz/godot';
|
|
6
|
+
import type { Node } from '@ringozz/godot/Node';
|
|
7
|
+
import type { Object as Instance } from '@ringozz/godot/Object';
|
|
8
|
+
import { PropertyTweener } from '@ringozz/godot/PropertyTweener';
|
|
9
|
+
import type { GodotVar } from '@ringozz/godot/runtime';
|
|
10
|
+
import { Tween, type EaseType, type TransitionType } from '@ringozz/godot/Tween';
|
|
6
11
|
import type React from 'react';
|
|
7
|
-
import { useEffect, useRef } from 'react';
|
|
12
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
|
|
14
|
+
// Tween/PropertyTweener are only type-used here; value-reference them so their
|
|
15
|
+
// `_R` class registration isn't tree-shaken (otherwise createTween's wrapper
|
|
16
|
+
// falls back to an ancestor class and loses pause/play/tweenProperty).
|
|
17
|
+
void Tween;
|
|
18
|
+
void PropertyTweener;
|
|
8
19
|
|
|
9
20
|
export function useMutableCallback<T>(fn: T): React.RefObject<T> {
|
|
10
21
|
const ref = useRef<T>(fn);
|
|
@@ -23,3 +34,115 @@ export function useSignal<T extends (...args: any[]) => any>(signal: Signal<T>,
|
|
|
23
34
|
/* oxlint-disable-next-line react-hooks/exhaustive-deps */
|
|
24
35
|
}, [signal.getObjectId(), signal.getName()]);
|
|
25
36
|
}
|
|
37
|
+
|
|
38
|
+
// ---- useTween: tween-as-prop-value ----
|
|
39
|
+
//
|
|
40
|
+
// useTween(create, deps) returns [spring, tweenRef]. `spring` holds one
|
|
41
|
+
// value-assigner per `to` key — a plain function spread onto the node
|
|
42
|
+
// (`<Node3D {...spring} />`). Instance.assign (C++) treats any function on a
|
|
43
|
+
// non-signal prop as a value-assigner and invokes it with (node, nativeName);
|
|
44
|
+
// each contributes its own prop's tweener to one shared native Tween created on
|
|
45
|
+
// the first invocation. `tweenRef.current` is that Tween (null until mounted).
|
|
46
|
+
// Re-rendering with the same deps keeps the same assigners (the reconciler's
|
|
47
|
+
// identity diff drops them, so nothing restarts); changing deps creates a new
|
|
48
|
+
// tween (the previous one is killed).
|
|
49
|
+
|
|
50
|
+
export type TweenValue = number | number[] | GodotVar;
|
|
51
|
+
|
|
52
|
+
export interface TweenConfig {
|
|
53
|
+
/** Seconds (default 1). */
|
|
54
|
+
duration?: number;
|
|
55
|
+
/** Tween.setTrans — the default for the tween's tweeners. */
|
|
56
|
+
transition?: TransitionType;
|
|
57
|
+
/** Tween.setEase — the default for the tween's tweeners. */
|
|
58
|
+
ease?: EaseType;
|
|
59
|
+
/** PropertyTweener.setCustomInterpolator — a [0,1]→[0,1] easing function. */
|
|
60
|
+
easing?: (t: number) => number;
|
|
61
|
+
/** Tween.setSpeedScale. */
|
|
62
|
+
speedScale?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TweenProps<T extends Record<string, TweenValue> = Record<string, TweenValue>> {
|
|
66
|
+
/** Start values, applied on the first build (mount) before animating to `to`. */
|
|
67
|
+
from?: Partial<T>;
|
|
68
|
+
/** Target values — one assigner is returned per key. */
|
|
69
|
+
to: T;
|
|
70
|
+
config?: TweenConfig;
|
|
71
|
+
/** Seconds; per-tweener PropertyTweener.setDelay. */
|
|
72
|
+
delay?: number;
|
|
73
|
+
/** Tween.setLoops (0 = infinite). */
|
|
74
|
+
loops?: number;
|
|
75
|
+
/** Snap to `to` instead of animating. */
|
|
76
|
+
immediate?: boolean;
|
|
77
|
+
onStart?: () => void;
|
|
78
|
+
/** Fires on natural completion only (Godot's `finished`); kills/unmount don't fire it. */
|
|
79
|
+
onFinished?: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type Spring<T extends Record<string, TweenValue>> = { [K in keyof T]: T[K] };
|
|
83
|
+
|
|
84
|
+
// tweenProperty/set reject JS arrays, so array goals are reconstructed as the
|
|
85
|
+
// property's value type (read via get(native).constructor).
|
|
86
|
+
function toValueType(node: Instance, native: string, value: TweenValue): unknown {
|
|
87
|
+
if (!Array.isArray(value)) return value;
|
|
88
|
+
const Ctor = (node.get(native) as any)?.constructor;
|
|
89
|
+
if (typeof Ctor === 'function') return new Ctor(...value);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeSpring<T extends Record<string, TweenValue>>(
|
|
94
|
+
tweenRef: { current: Tween | null },
|
|
95
|
+
props: TweenProps<T>,
|
|
96
|
+
): Spring<T> {
|
|
97
|
+
const config = props.config ?? {};
|
|
98
|
+
const spring = {} as Spring<T>;
|
|
99
|
+
let started = false;
|
|
100
|
+
let applyFrom = false;
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of Object.entries(props.to)) {
|
|
103
|
+
const fromValue = props.from?.[key];
|
|
104
|
+
const assigner = (node: Instance, native: string) => {
|
|
105
|
+
if (props.immediate) {
|
|
106
|
+
tweenRef.current?.kill();
|
|
107
|
+
tweenRef.current = null;
|
|
108
|
+
node.set(native, toValueType(node, native, value));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!started) {
|
|
112
|
+
started = true;
|
|
113
|
+
applyFrom = tweenRef.current === null; // first build (mount) ⇒ apply `from`
|
|
114
|
+
tweenRef.current?.kill();
|
|
115
|
+
const t = (node as unknown as Node).createTween();
|
|
116
|
+
tweenRef.current = t;
|
|
117
|
+
if (config.transition !== undefined) t.setTrans(config.transition);
|
|
118
|
+
if (config.ease !== undefined) t.setEase(config.ease);
|
|
119
|
+
if (config.speedScale !== undefined) t.setSpeedScale(config.speedScale);
|
|
120
|
+
if (props.loops !== undefined) t.setLoops(props.loops);
|
|
121
|
+
if (props.onFinished) t.finished.connect(props.onFinished);
|
|
122
|
+
t.bindNode(node as unknown as Node);
|
|
123
|
+
t.setParallel(true);
|
|
124
|
+
props.onStart?.();
|
|
125
|
+
}
|
|
126
|
+
const tw = tweenRef.current!.tweenProperty(node, native, toValueType(node, native, value), config.duration ?? 1);
|
|
127
|
+
if (applyFrom && fromValue !== undefined) tw.from(toValueType(node, native, fromValue));
|
|
128
|
+
if (props.delay !== undefined) tw.setDelay(props.delay);
|
|
129
|
+
if (config.easing) tw.setCustomInterpolator(config.easing);
|
|
130
|
+
};
|
|
131
|
+
spring[key as keyof T] = assigner as unknown as T[keyof T];
|
|
132
|
+
}
|
|
133
|
+
return spring;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useTween<T extends Record<string, TweenValue>>(
|
|
137
|
+
create: () => TweenProps<T>,
|
|
138
|
+
deps: unknown[] = [],
|
|
139
|
+
): [Spring<T>, React.RefObject<Tween | null>] {
|
|
140
|
+
const tweenRef = useRef<Tween | null>(null);
|
|
141
|
+
// 'create' is intentionally re-evaluated via `deps`, not listed here.
|
|
142
|
+
/* oxlint-disable-next-line react-hooks/exhaustive-deps */
|
|
143
|
+
const spring = useMemo(() => makeSpring(tweenRef, create()), deps);
|
|
144
|
+
// No unmount kill: the tween is `createTween()`-bound to the node, so Godot
|
|
145
|
+
// kills it when the node exits the tree. A `useEffect` cleanup would also run
|
|
146
|
+
// on StrictMode's synthetic mount teardown and kill the just-created tween.
|
|
147
|
+
return [spring, tweenRef];
|
|
148
|
+
}
|