@ringozz/react-godot 1.0.0-4 → 1.0.0-6
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 +5 -17
- package/package.json +6 -2
- package/src/index.ts +1 -1
- package/src/react-fiber.ts +9 -4
- package/src/react-hooks.ts +3 -0
- package/src/react-jsx.ts +7 -3
- package/src/react-types.ts +7 -4
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Declarative, component-driven **Godot 4** scene graph management in React. Built for high-performance applications using `@ringozz/godot`.
|
|
4
4
|
|
|
5
|
-
---
|
|
6
|
-
|
|
7
5
|
## Overview
|
|
8
6
|
|
|
9
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`).
|
|
@@ -13,8 +11,6 @@ If you are familiar with React Three Fiber, the core paradigms will feel instant
|
|
|
13
11
|
- **Resource Attachment**: Use the `attach` prop to assign nested Godot resources (materials, shapes, meshes, environments) directly to parent properties.
|
|
14
12
|
- **Reactive Engine Loop**: Use hooks like `useSignal` to hook into Godot signals (`processFrame`, `windowInput`) without triggering React state re-renders.
|
|
15
13
|
|
|
16
|
-
---
|
|
17
|
-
|
|
18
14
|
## Installation & Setup
|
|
19
15
|
|
|
20
16
|
### 1. Dependencies
|
|
@@ -40,8 +36,6 @@ To enable custom JSX element resolution for Godot nodes, configure `jsxImportSou
|
|
|
40
36
|
}
|
|
41
37
|
```
|
|
42
38
|
|
|
43
|
-
---
|
|
44
|
-
|
|
45
39
|
## Application Entry & Portal Rendering
|
|
46
40
|
|
|
47
41
|
### 1. Mounting with `createRoot(parent: Node)`
|
|
@@ -80,13 +74,11 @@ function Overlay({ overlayNode }: { overlayNode: Node }) {
|
|
|
80
74
|
- **Container Constraint**: The target `container` must be a Godot `Node` (it cannot target `Resource` instances).
|
|
81
75
|
- **Cleanup**: Unmounting the portal safely disposes of and frees its child Godot nodes.
|
|
82
76
|
|
|
83
|
-
---
|
|
84
|
-
|
|
85
77
|
## Core Concepts & Patterns
|
|
86
78
|
|
|
87
79
|
### 1. Existing Objects with `object` Prop
|
|
88
80
|
|
|
89
|
-
If you already have a pre-existing Godot `Node` or `
|
|
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:
|
|
90
82
|
|
|
91
83
|
```tsx
|
|
92
84
|
<Node object={myExistingGodotNode}>
|
|
@@ -94,6 +86,10 @@ If you already have a pre-existing Godot `Node` or `Resource` instance (e.g. ins
|
|
|
94
86
|
</Node>
|
|
95
87
|
```
|
|
96
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; the web dev server's React Fast Refresh makes it especially likely). Add `key="..."` to each dynamically toggled same-type sibling to keep nodes pinned to the right elements.
|
|
92
|
+
|
|
97
93
|
### 2. Resource Nesting with `attach`
|
|
98
94
|
|
|
99
95
|
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:
|
|
@@ -154,15 +150,9 @@ useSignal(root.windowInput, (event) => {
|
|
|
154
150
|
if (event instanceof InputEventMouseMotion) {
|
|
155
151
|
// Handle mouse motion
|
|
156
152
|
}
|
|
157
|
-
// IMPORTANT: Always free transient input event objects
|
|
158
|
-
event.free();
|
|
159
153
|
});
|
|
160
154
|
```
|
|
161
155
|
|
|
162
|
-
> **Memory Tip**: Godot input events should be explicitly freed by calling `event.free()` inside your signal handler.
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
156
|
## Full Example
|
|
167
157
|
|
|
168
158
|
Below is a complete interactive 3D physics scene with custom camera controls and physics bodies:
|
|
@@ -267,8 +257,6 @@ export function App() {
|
|
|
267
257
|
}
|
|
268
258
|
```
|
|
269
259
|
|
|
270
|
-
---
|
|
271
|
-
|
|
272
260
|
## API Summary
|
|
273
261
|
|
|
274
262
|
| Export | Type | Description |
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ringozz/react-godot",
|
|
3
3
|
"author": "Vladimir Davidovich",
|
|
4
|
-
"version": "1.0.0-
|
|
4
|
+
"version": "1.0.0-6",
|
|
5
5
|
"description": "A React renderer for Godot Engine via @ringozz/godot",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"react",
|
|
8
|
+
"godot"
|
|
9
|
+
],
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./src/index.ts",
|
|
8
12
|
"types": "./src/index.ts",
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
"*.md"
|
|
17
21
|
],
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"@ringozz/godot": "^4.7.1-
|
|
23
|
+
"@ringozz/godot": "^4.7.1-9",
|
|
20
24
|
"@types/react-reconciler": "^0.33.0",
|
|
21
25
|
"react-reconciler": "^0.33.0"
|
|
22
26
|
},
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ const reconciler = Reconciler(FiberConfig);
|
|
|
13
13
|
reconciler.injectIntoDevTools(undefined as never);
|
|
14
14
|
|
|
15
15
|
export function createRoot(parent: Node) {
|
|
16
|
-
const isStrictMode =
|
|
16
|
+
const isStrictMode = process.env.NODE_ENV === 'development';
|
|
17
17
|
const concurrentUpdatesByDefaultOverride = false;
|
|
18
18
|
const identifierPrefix = '';
|
|
19
19
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
package/src/react-fiber.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { ClassDB } from '@ringozz/godot/ClassDB';
|
|
6
6
|
import type { Node } from '@ringozz/godot/Node';
|
|
7
7
|
import { Object as Instance } from '@ringozz/godot/Object';
|
|
8
|
+
import { PackedScene } from '@ringozz/godot/PackedScene';
|
|
8
9
|
import { RefCounted } from '@ringozz/godot/RefCounted';
|
|
9
10
|
import type Reconciler from 'react-reconciler';
|
|
10
11
|
import Constants from 'react-reconciler/constants.js';
|
|
@@ -32,8 +33,10 @@ const EMPTY = Object.freeze({});
|
|
|
32
33
|
const attachments = new WeakMap<Instance, string>();
|
|
33
34
|
|
|
34
35
|
export function createInstance<T extends Instance>(type: Type, props: Props, root: Container, hostContext: HostContext, internalHandle: Reconciler.OpaqueHandle): T {
|
|
35
|
-
|
|
36
|
-
if (object instanceof
|
|
36
|
+
let { attach, object, ...rest } = props;
|
|
37
|
+
if (object instanceof PackedScene)
|
|
38
|
+
object = object.instantiate();
|
|
39
|
+
else if (object instanceof RefCounted)
|
|
37
40
|
object.reference();
|
|
38
41
|
|
|
39
42
|
const instance = (object ?? ClassDB.instantiate(type)) as T;
|
|
@@ -265,10 +268,12 @@ function isEqual(a: unknown, b: unknown) {
|
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
export function commitUpdate(instance: Instance, type: Type, prevProps: Props, nextProps: Props, internalHandle: Reconciler.OpaqueHandle): void {
|
|
268
|
-
const { attach: attachOld, ref: refOld, children: childrenOld, ...restOld } = prevProps;
|
|
269
|
-
const { attach: attachNew, ref: refNew, children: childrenNew, ...restNew } = nextProps;
|
|
271
|
+
const { attach: attachOld, object: objectOld, ref: refOld, children: childrenOld, ...restOld } = prevProps;
|
|
272
|
+
const { attach: attachNew, object: objectNew, ref: refNew, children: childrenNew, ...restNew } = nextProps;
|
|
270
273
|
if (attachOld !== attachNew)
|
|
271
274
|
throw new Error(`Cannot change attachment ${attachOld} to ${attachNew}`);
|
|
275
|
+
if (objectOld !== objectNew)
|
|
276
|
+
throw new Error(`Cannot change the \`object\` of a mounted instance (${objectOld} -> ${objectNew}); remount with a new key`);
|
|
272
277
|
|
|
273
278
|
const toText = (c: unknown) => Array.isArray(c) ? c.join('') : c as string;
|
|
274
279
|
if (shouldSetTextContent(type, prevProps)) restOld['text'] = toText(childrenOld);
|
package/src/react-hooks.ts
CHANGED
|
@@ -18,5 +18,8 @@ export function useSignal<T extends (...args: any[]) => any>(signal: Signal<T>,
|
|
|
18
18
|
const fn = ((...args: any[]) => ref.current(...args)) as T;
|
|
19
19
|
signal.connect(fn);
|
|
20
20
|
return () => signal.disconnect(fn);
|
|
21
|
+
// do not specify 'signal' dependency, because it changes often.
|
|
22
|
+
// specify invariant contents instead.
|
|
23
|
+
/* oxlint-disable-next-line react-hooks/exhaustive-deps */
|
|
21
24
|
}, [signal.getObjectId(), signal.getName()]);
|
|
22
25
|
}
|
package/src/react-jsx.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { jsxDEV as reactJsxDEV } from 'react/jsx-dev-runtime';
|
|
6
6
|
import type * as ReactJSX from 'react/jsx-runtime';
|
|
7
7
|
import { Fragment, jsx as reactJsx, jsxs as reactJsxs } from 'react/jsx-runtime';
|
|
8
|
-
import type { ComponentProps,
|
|
8
|
+
import type { ComponentProps, GodotConstructor } from './react-types.ts';
|
|
9
9
|
import { GodotVar } from '@ringozz/godot/runtime';
|
|
10
10
|
|
|
11
11
|
export function jsx(type: any, props?: any, key?: any) {
|
|
@@ -22,13 +22,16 @@ export function jsxDEV(type: any, props?: any, key?: any, isStatic?: any, source
|
|
|
22
22
|
|
|
23
23
|
export { Fragment };
|
|
24
24
|
|
|
25
|
+
// JSX namespace augmentation: these types are the public JSX contract for the
|
|
26
|
+
// `@ringozz/react-godot/jsx-runtime` import source, consumed by TypeScript in
|
|
27
|
+
// consumer files. They are intentionally not referenced within this module.
|
|
28
|
+
/* oxlint-disable no-unused-vars */
|
|
25
29
|
declare module '@ringozz/react-godot/jsx-runtime' {
|
|
26
30
|
export namespace JSX {
|
|
31
|
+
interface IntrinsicAttributes extends ReactJSX.JSX.IntrinsicAttributes { }
|
|
27
32
|
interface IntrinsicElements extends ReactJSX.JSX.IntrinsicElements { }
|
|
28
33
|
interface Element extends ReactJSX.JSX.Element { }
|
|
29
34
|
|
|
30
|
-
type GodotConstructor = { new(...args: any[]): Instance } & Function;
|
|
31
|
-
|
|
32
35
|
type ElementType =
|
|
33
36
|
| ReactJSX.JSX.ElementType
|
|
34
37
|
| GodotConstructor;
|
|
@@ -39,3 +42,4 @@ declare module '@ringozz/react-godot/jsx-runtime' {
|
|
|
39
42
|
: ReactJSX.JSX.LibraryManagedAttributes<C, P>;
|
|
40
43
|
}
|
|
41
44
|
}
|
|
45
|
+
/* oxlint-enable no-unused-vars */
|
package/src/react-types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type React from 'react';
|
|
6
6
|
import type { Object } from '@ringozz/godot/Object';
|
|
7
7
|
import type { ValueTypes } from '@ringozz/godot';
|
|
8
|
+
import type { PackedScene } from '@ringozz/godot/PackedScene';
|
|
8
9
|
|
|
9
10
|
type FunctionKeys<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
|
|
10
11
|
type Properties<T> = Omit<T, FunctionKeys<T>>;
|
|
@@ -14,9 +15,11 @@ type ConstructorRepresentation<T = any> = new (...args: any[]) => T;
|
|
|
14
15
|
|
|
15
16
|
export type Instance = Object;
|
|
16
17
|
|
|
18
|
+
export type GodotConstructor<T extends Instance = Instance> = ConstructorRepresentation<T> & Function;
|
|
19
|
+
|
|
17
20
|
export type InstanceProps<T extends Instance = Instance> = {
|
|
18
21
|
/** An existing instance to render instead of creating a new one. */
|
|
19
|
-
object?: T;
|
|
22
|
+
object?: T | PackedScene;
|
|
20
23
|
/** Attaches the element to a named property of the parent instead of adding it as a child. */
|
|
21
24
|
attach?: string;
|
|
22
25
|
};
|
|
@@ -27,10 +30,10 @@ type WidenVT<T> = {
|
|
|
27
30
|
[K in keyof T]: T[K] extends ValueTypes ? T[K] | number[] : T[K];
|
|
28
31
|
};
|
|
29
32
|
|
|
30
|
-
type ElementProps<T extends
|
|
33
|
+
type ElementProps<T extends GodotConstructor, P = InstanceType<T>> = Partial<
|
|
31
34
|
Overwrite<WidenVT<P>, ReactProps<P>>
|
|
32
35
|
>;
|
|
33
36
|
|
|
34
|
-
export type ComponentProps<T extends
|
|
35
|
-
Overwrite<ElementProps<T>,
|
|
37
|
+
export type ComponentProps<T extends GodotConstructor> = Mutable<
|
|
38
|
+
Overwrite<ElementProps<T>, InstanceProps<InstanceType<T>>>
|
|
36
39
|
>;
|