Loading...
Loading...
react-three-game, a JSON-first 3D game engine built on React Three Fiber, WebGPU, and Rapier Physics.
npx skill4agent add prnthh/react-three-game-skill react-three-gamePrefabEditorexportGLBexportGLBDataimport { useRef, useEffect } from 'react';
import { PrefabEditor, exportGLBData } from 'react-three-game';
import type { PrefabEditorRef } from 'react-three-game'
const jsonPrefab = {
root: {
id: "scene",
children: [
{
id: "cube",
components: {
transform: { type: "Transform", properties: { position: [0, 0, 0] } },
geometry: { type: "Geometry", properties: { geometryType: "box", args: [1, 1, 1] } },
material: { type: "Material", properties: { color: "#ff0000" } }
}
}
]
}
};
function AgentExporter() {
const editorRef = useRef<PrefabEditorRef>(null);
useEffect(() => {
const timer = setTimeout(async () => {
const sceneRoot = editorRef.current?.rootRef.current?.root;
if (!sceneRoot) return;
const glbData = await exportGLBData(sceneRoot);
// glbData is an ArrayBuffer ready for upload/storage
}, 1000); // Wait for scene to render
return () => clearTimeout(timer);
}, []);
return <PrefabEditor ref={editorRef} initialPrefab={jsonPrefab} />;
}/public/public{
"texture": "/textures/floor.png",
"model": "/models/car.glb",
"font": "/fonts/font.ttf"
}"/any/path/file.ext"/public/any/path/file.extinterface GameObject {
id: string;
disabled?: boolean;
components?: Record<string, { type: string; properties: any }>;
children?: GameObject[];
}{
"root": {
"id": "scene",
"children": [
{
"id": "my-object",
"components": {
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
"geometry": { "type": "Geometry", "properties": { "geometryType": "box" } },
"material": { "type": "Material", "properties": { "color": "#ff0000" } }
}
}
]
}
}| Component | Type | Key Properties |
|---|---|---|
| Transform | | |
| Geometry | | |
| Material | | |
| Physics | | |
| Model | | |
| SpotLight | | |
| DirectionalLight | | |
| AmbientLight | | |
| Text | | |
hb.wasm/public/fonts/"font": "/fonts/NotoSans-Regular.ttf"| geometryType | args array |
|---|---|
| |
| |
| |
| |
{
"material": {
"type": "Material",
"properties": {
"color": "white",
"texture": "/textures/floor.png",
"repeat": true,
"repeatCount": [4, 4]
}
}
}1.573.14-1.57<Physics>import { Physics } from '@react-three/rapier';
import { GameCanvas, PrefabRoot } from 'react-three-game';
<GameCanvas>
<Physics>
<PrefabRoot data={prefabData} />
<CustomComponent />
</Physics>
</GameCanvas>import { PrefabEditor } from 'react-three-game';
<PrefabEditor initialPrefab={prefabData}>
<CustomComponent />
</PrefabEditor>import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
const node = findNode(root, nodeId);
const updated = updateNode(root, nodeId, n => ({ ...n, disabled: true })); // or updateNodeById (identical)
const afterDelete = deleteNode(root, nodeId);
const cloned = cloneNode(node);
const glbData = await exportGLBData(sceneRoot);import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import { PrefabEditor, findNode } from 'react-three-game';
import type { PrefabEditorRef } from 'react-three-game';
function DynamicLight() {
const lightRef = useRef<THREE.SpotLight>(null!);
useFrame(({ clock }) => {
lightRef.current.intensity = 100 + Math.sin(clock.elapsedTime) * 50;
});
return <spotLight ref={lightRef} position={[10, 15, 10]} angle={0.5} />;
}
<PrefabEditor initialPrefab={staticScenePrefab}>
<DynamicLight />
<CustomController />
</PrefabEditor>// Static geometry with physics (floor, wall, platform, ramp)
{ "id": "floor", "components": {
"transform": { "type": "Transform", "properties": { "position": [0, -0.5, 0] } },
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 1, 40] } },
"material": { "type": "Material", "properties": { "texture": "/textures/floor.png", "repeat": true, "repeatCount": [20, 20] } },
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
}}
// Lighting
{ "id": "spot", "components": {
"transform": { "type": "Transform", "properties": { "position": [10, 15, 10] } },
"spotlight": { "type": "SpotLight", "properties": { "intensity": 200, "angle": 0.8, "castShadow": true } }
}}
// 3D Text
{ "id": "title", "components": {
"transform": { "type": "Transform", "properties": { "position": [0, 3, 0] } },
"text": { "type": "Text", "properties": { "text": "Welcome", "font": "/fonts/font.ttf", "size": 1, "depth": 0.1 } }
}}
// GLB Model
{ "id": "tree", "components": {
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0], "scale": [1.5, 1.5, 1.5] } },
"model": { "type": "Model", "properties": { "filename": "/models/tree.glb" } }
}}import { PrefabEditor } from 'react-three-game';
<PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />PrefabEditormakeDefaultimport { PerspectiveCamera } from '@react-three/drei';
import { PrefabEditor } from 'react-three-game';
<PrefabEditor initialPrefab={prefab}>
<PerspectiveCamera makeDefault position={[0, 5, 10]} fov={75} />
</PrefabEditor>PerspectiveCameraOrthographicCameraimport { useRef } from 'react';
import { PrefabEditor, updateNodeById } from 'react-three-game';
import type { PrefabEditorRef } from 'react-three-game';
function Scene() {
const editorRef = useRef<PrefabEditorRef>(null);
const moveBall = () => {
const prefab = editorRef.current!.prefab;
const newRoot = updateNodeById(prefab.root, "ball", node => ({
...node,
components: {
...node.components,
transform: {
...node.components!.transform!,
properties: { ...node.components!.transform!.properties, position: [5, 0, 0] }
}
}
}));
editorRef.current!.setPrefab({ ...prefab, root: newRoot });
};
return <PrefabEditor ref={editorRef} initialPrefab={sceneData} />;
}prefabsetPrefab()screenshot()exportGLB()rootRefimport { exportGLBData } from 'react-three-game';
const glbData = await exportGLBData(editorRef.current!.rootRef.current!.root);import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { PrefabEditor, updateNodeById } from "react-three-game";
function Animator({ editorRef }) {
useFrame(() => {
const prefab = editorRef.current!.prefab;
const newRoot = updateNodeById(prefab.root, "ball", node => ({
...node,
components: {
...node.components,
transform: {
...node.components!.transform!,
properties: { ...node.components!.transform!.properties, position: [x, y, z] }
}
}
}));
editorRef.current!.setPrefab({ ...prefab, root: newRoot });
});
return null;
}
function Scene() {
const editorRef = useRef(null);
return (
<PrefabEditor ref={editorRef} initialPrefab={data}>
<Animator editorRef={editorRef} />
</PrefabEditor>
);
}import { Component, registerComponent, FieldRenderer } from 'react-three-game';
const MyComponent: Component = {
name: 'MyComponent',
Editor: ({ component, onUpdate }) => (
<FieldRenderer fields={[{ name: 'speed', type: 'number', step: 0.1 }]} values={component.properties} onChange={onUpdate} />
),
View: ({ properties, children }) => <group>{children}</group>,
defaultProperties: { speed: 1 }
};
registerComponent(MyComponent);vector3numberstringcolorbooleanselectcustomimport { gameEvents, useGameEvent } from 'react-three-game';
// Emit events
gameEvents.emit('player:death', { playerId: 'p1', cause: 'lava' });
gameEvents.emit('score:change', { delta: 100, total: 500 });
// Subscribe (React hook - auto cleanup on unmount)
useGameEvent('player:death', (payload) => {
showGameOver(payload.cause);
}, []);
// Subscribe (manual - returns unsubscribe function)
const unsub = gameEvents.on('score:change', (payload) => {
updateUI(payload.total);
});
unsub(); // cleanup| Event | When | Payload |
|---|---|---|
| Something enters a sensor collider | |
| Something exits a sensor collider | |
| A collision starts | |
| A collision ends | |
"activeCollisionTypes": "all"GameEventMapdeclare module 'react-three-game' {
interface GameEventMap {
'player:death': { playerId: string; cause: string };
'score:change': { delta: number; total: number };
'level:complete': { levelId: number; time: number };
}
}// Gameplay controller
function GameController() {
const [score, setScore] = useState(0);
useGameEvent('score:change', ({ total }) => setScore(total), []);
useGameEvent('player:death', () => setGameOver(true), []);
return <ScoreUI score={score} />;
}
// Pickup system
useGameEvent('sensor:enter', (payload) => {
if (payload.sourceEntityId.startsWith('coin-')) {
gameEvents.emit('score:change', { delta: 10, total: score + 10 });
removeEntity(payload.sourceEntityId);
}
}, [score]);