react-three-game 0.0.86 → 0.0.88

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/dist/index.d.ts CHANGED
@@ -11,13 +11,15 @@ export type { EditorContextType } from './tools/prefabeditor/PrefabEditor';
11
11
  export { createPrefabStore, prefabStoreToPrefab, usePrefabStoreApi } from './tools/prefabeditor/prefabStore';
12
12
  export type { PrefabStoreApi, PrefabStoreState } from './tools/prefabeditor/prefabStore';
13
13
  export { denormalizePrefab } from './tools/prefabeditor/prefab';
14
+ export { gameEvents, getEntityIdFromRigidBody, useClickEvent, useGameEvent, usePhysicsEvent } from './tools/prefabeditor/GameEvents';
15
+ export type { ClickEventPayload, GameEventHandler, GameEventMap, PhysicsEventPayload } from './tools/prefabeditor/GameEvents';
14
16
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
15
17
  export { FieldRenderer, FieldGroup, ListEditor, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
16
18
  export { loadJson, saveJson, exportGLB, exportGLBData, regenerateIds, computeParentWorldMatrix, } from './tools/prefabeditor/utils';
17
19
  export type { ExportGLBOptions } from './tools/prefabeditor/utils';
18
20
  export { createModelNode, createImageNode, } from './tools/prefabeditor/prefab';
19
21
  export type { PrefabEditorProps, PrefabEditorRef, } from './tools/prefabeditor/PrefabEditor';
20
- export type { SpawnOptions, } from './tools/prefabeditor/scene';
22
+ export type { Entity, EntityComponent, EntityData, EntityUpdate, PropertyPath, Scene, SceneUpdates, SpawnOptions, } from './tools/prefabeditor/scene';
21
23
  export type { PrefabRootProps } from './tools/prefabeditor/PrefabRoot';
22
24
  export type { AssetRuntime, EntityRuntime, LiveObjectRef, LiveRigidBodyRef } from './tools/prefabeditor/assetRuntime';
23
25
  export { useAssetRuntime, useEntityRuntime, useEntityObjectRef, useEntityRigidBodyRef } from './tools/prefabeditor/assetRuntime';
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { useEditorContext } from './tools/prefabeditor/PrefabEditor';
11
11
  // Prefab Editor - Store & Scene API
12
12
  export { createPrefabStore, prefabStoreToPrefab, usePrefabStoreApi } from './tools/prefabeditor/prefabStore';
13
13
  export { denormalizePrefab } from './tools/prefabeditor/prefab';
14
+ export { gameEvents, getEntityIdFromRigidBody, useClickEvent, useGameEvent, usePhysicsEvent } from './tools/prefabeditor/GameEvents';
14
15
  // Prefab Editor - Component Registry
15
16
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
16
17
  // Prefab Editor - Input Components
@@ -0,0 +1,47 @@
1
+ export type GameEventHandler<TPayload = unknown> = (payload: TPayload) => void;
2
+ export type PhysicsEventPayload = {
3
+ sourceEntityId?: string;
4
+ sourceNodeId?: string;
5
+ sourceObject?: unknown;
6
+ sourceRigidBody?: unknown;
7
+ targetEntityId?: string | null;
8
+ targetNodeId?: string | null;
9
+ targetObject?: unknown;
10
+ targetRigidBody?: unknown;
11
+ rapierEvent?: unknown;
12
+ };
13
+ export type ClickEventPayload = {
14
+ sourceEntityId?: string;
15
+ sourceNodeId?: string;
16
+ instanceEntityId?: string;
17
+ nodeId?: string;
18
+ node?: unknown;
19
+ object?: unknown;
20
+ point?: [number, number, number];
21
+ button?: number;
22
+ altKey?: boolean;
23
+ ctrlKey?: boolean;
24
+ metaKey?: boolean;
25
+ shiftKey?: boolean;
26
+ r3fEvent?: unknown;
27
+ };
28
+ export interface GameEventMap {
29
+ 'sensor:enter': PhysicsEventPayload;
30
+ 'sensor:exit': PhysicsEventPayload;
31
+ 'collision:enter': PhysicsEventPayload;
32
+ 'collision:exit': PhysicsEventPayload;
33
+ click: ClickEventPayload;
34
+ [eventType: string]: unknown;
35
+ }
36
+ export declare const gameEvents: {
37
+ emit<TType extends string>(type: TType, payload: TType extends keyof GameEventMap ? GameEventMap[TType] : unknown): void;
38
+ on<TType extends string>(type: TType, handler: GameEventHandler<TType extends keyof GameEventMap ? GameEventMap[TType] : unknown>): () => void;
39
+ clear(): void;
40
+ hasListeners(type: string): boolean;
41
+ };
42
+ export declare function useGameEvent<TType extends string>(type: TType, handler: GameEventHandler<TType extends keyof GameEventMap ? GameEventMap[TType] : unknown>, deps?: React.DependencyList): void;
43
+ export declare function usePhysicsEvent<TType extends string>(type: TType, handler: GameEventHandler<TType extends keyof GameEventMap ? GameEventMap[TType] : unknown>, deps?: React.DependencyList): void;
44
+ export declare function useClickEvent<TType extends string>(type: TType, handler: GameEventHandler<TType extends keyof GameEventMap ? GameEventMap[TType] : unknown>, deps?: React.DependencyList): void;
45
+ export declare function getEntityIdFromRigidBody(rigidBody: {
46
+ userData?: unknown;
47
+ } | null | undefined): string | null;
@@ -0,0 +1,66 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ const subscribers = new Map();
3
+ export const gameEvents = {
4
+ emit(type, payload) {
5
+ const trimmedType = type.trim();
6
+ if (!trimmedType)
7
+ return;
8
+ const handlers = subscribers.get(trimmedType);
9
+ if (!handlers)
10
+ return;
11
+ handlers.forEach(handler => {
12
+ try {
13
+ handler(payload);
14
+ }
15
+ catch (error) {
16
+ console.error(`Error in gameEvents handler for ${trimmedType}:`, error);
17
+ }
18
+ });
19
+ },
20
+ on(type, handler) {
21
+ const trimmedType = type.trim();
22
+ if (!trimmedType) {
23
+ return () => { };
24
+ }
25
+ let handlers = subscribers.get(trimmedType);
26
+ if (!handlers) {
27
+ handlers = new Set();
28
+ subscribers.set(trimmedType, handlers);
29
+ }
30
+ handlers.add(handler);
31
+ return () => {
32
+ const currentHandlers = subscribers.get(trimmedType);
33
+ if (!currentHandlers)
34
+ return;
35
+ currentHandlers.delete(handler);
36
+ if (currentHandlers.size === 0) {
37
+ subscribers.delete(trimmedType);
38
+ }
39
+ };
40
+ },
41
+ clear() {
42
+ subscribers.clear();
43
+ },
44
+ hasListeners(type) {
45
+ var _a, _b;
46
+ return ((_b = (_a = subscribers.get(type.trim())) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0) > 0;
47
+ },
48
+ };
49
+ export function useGameEvent(type, handler, deps = []) {
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ const stableHandler = useCallback(handler, deps);
52
+ useEffect(() => {
53
+ return gameEvents.on(type, stableHandler);
54
+ }, [type, stableHandler]);
55
+ }
56
+ export function usePhysicsEvent(type, handler, deps = []) {
57
+ useGameEvent(type, handler, deps);
58
+ }
59
+ export function useClickEvent(type, handler, deps = []) {
60
+ useGameEvent(type, handler, deps);
61
+ }
62
+ export function getEntityIdFromRigidBody(rigidBody) {
63
+ var _a;
64
+ const entityId = (_a = rigidBody === null || rigidBody === void 0 ? void 0 : rigidBody.userData) === null || _a === void 0 ? void 0 : _a.entityId;
65
+ return typeof entityId === 'string' ? entityId : null;
66
+ }
@@ -24,6 +24,7 @@ import { composeTransform, decompose } from "./utils";
24
24
  import { isPhysicsProps } from "./components/PhysicsComponent";
25
25
  import { createPrefabStore, PrefabStoreProvider, useOptionalPrefabStoreApi, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
26
26
  import { AssetRuntimeContext, EntityRuntimeScope } from "./assetRuntime";
27
+ import { gameEvents } from "./GameEvents";
27
28
  import { sound as soundManager } from "../../helpers/SoundManager";
28
29
  builtinComponents.forEach(registerComponent);
29
30
  const IDENTITY = new Matrix4();
@@ -44,12 +45,6 @@ function isNodeReady(node, loadedModels) {
44
45
  return true;
45
46
  return Boolean(loadedModels[model.properties.filename]);
46
47
  }
47
- function emitNativeEvent(type, detail) {
48
- const trimmedType = type === null || type === void 0 ? void 0 : type.trim();
49
- if (!trimmedType || typeof window === 'undefined')
50
- return;
51
- window.dispatchEvent(new CustomEvent(trimmedType, { detail }));
52
- }
53
48
  function getNodeClickEventName(node) {
54
49
  var _a;
55
50
  const clickComponents = [
@@ -214,12 +209,16 @@ function StoreRootNode(props) {
214
209
  }
215
210
  function emitNodePointerEvent(eventName, event, nodeId, node, fallbackObject) {
216
211
  var _a;
217
- if (!eventName)
212
+ const trimmedEventName = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
213
+ if (!trimmedEventName)
218
214
  return;
219
- emitNativeEvent(eventName, {
215
+ gameEvents.emit(trimmedEventName, {
216
+ sourceEntityId: nodeId,
217
+ sourceNodeId: nodeId,
220
218
  nodeId,
221
219
  node,
222
220
  object: (_a = event.object) !== null && _a !== void 0 ? _a : fallbackObject,
221
+ point: [event.point.x, event.point.y, event.point.z],
223
222
  button: event.button,
224
223
  altKey: event.nativeEvent.altKey,
225
224
  ctrlKey: event.nativeEvent.ctrlKey,
@@ -13,6 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { CapsuleCollider, RigidBody, useRapier } from "@react-three/rapier";
14
14
  import { useCallback, useEffect, useRef } from 'react';
15
15
  import { useAssetRuntime, useEntityRuntime } from "../assetRuntime";
16
+ import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
16
17
  import { usePrefabNode } from "../prefabStore";
17
18
  import { BooleanField, FieldGroup, NumberField, SelectField, StringField, Vector3Field } from "./Input";
18
19
  import { getNodeUserData } from "../types";
@@ -80,16 +81,6 @@ function PhysicsComponentEditor({ component, onUpdate }) {
80
81
  { value: 'all', label: 'All (includes kinematic & fixed)' },
81
82
  ] })] }));
82
83
  }
83
- function emitNativeEvent(type, detail) {
84
- const trimmedType = type === null || type === void 0 ? void 0 : type.trim();
85
- if (!trimmedType || typeof window === 'undefined')
86
- return;
87
- window.dispatchEvent(new CustomEvent(trimmedType, { detail }));
88
- }
89
- function getEntityIdFromRigidBody(rigidBody) {
90
- const userData = rigidBody === null || rigidBody === void 0 ? void 0 : rigidBody.userData;
91
- return typeof (userData === null || userData === void 0 ? void 0 : userData.entityId) === 'string' ? userData.entityId : null;
92
- }
93
84
  function PhysicsComponentView({ properties, children, position, rotation, scale }) {
94
85
  var _a, _b, _c;
95
86
  const { registerRigidBodyRef } = useAssetRuntime();
@@ -157,11 +148,17 @@ function PhysicsComponentView({ properties, children, position, rotation, scale
157
148
  var _a, _b, _c;
158
149
  if (!nodeId)
159
150
  return;
160
- emitNativeEvent(eventType, {
151
+ const trimmedEventType = eventType === null || eventType === void 0 ? void 0 : eventType.trim();
152
+ if (!trimmedEventType)
153
+ return;
154
+ const targetEntityId = getEntityIdFromRigidBody(payload.other.rigidBody);
155
+ gameEvents.emit(trimmedEventType, {
156
+ sourceEntityId: nodeId,
161
157
  sourceNodeId: nodeId,
162
158
  sourceObject: getObject(),
163
159
  sourceRigidBody: rigidBodyRef.current,
164
- targetNodeId: getEntityIdFromRigidBody(payload.other.rigidBody),
160
+ targetEntityId,
161
+ targetNodeId: targetEntityId,
165
162
  targetObject: (_b = (_a = payload.other.rigidBodyObject) !== null && _a !== void 0 ? _a : payload.other.colliderObject) !== null && _b !== void 0 ? _b : null,
166
163
  targetRigidBody: (_c = payload.other.rigidBody) !== null && _c !== void 0 ? _c : null,
167
164
  rapierEvent: payload,
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
- import { useThree } from '@react-three/fiber';
4
3
  import { SoundPicker } from '../../assetviewer/page';
5
- import { sound as soundManager } from '../../../helpers/SoundManager';
6
4
  import { useAssetRuntime, useEntityRuntime } from '../assetRuntime';
5
+ import { gameEvents } from '../GameEvents';
7
6
  import { BooleanField, FieldGroup, FieldRenderer, ListEditor, NumberField, SelectField, StringField } from './Input';
8
7
  import { colors } from '../styles';
9
8
  import { AudioListener } from 'three';
@@ -62,6 +61,32 @@ function pickClip(paths, mode, sequenceIndexRef) {
62
61
  }
63
62
  return paths[Math.floor(Math.random() * paths.length)];
64
63
  }
64
+ function payloadMatchesNode(nodeId, payload) {
65
+ if (!nodeId || !payload || typeof payload !== 'object') {
66
+ return true;
67
+ }
68
+ const eventPayload = payload;
69
+ const relatedNodeIds = [
70
+ eventPayload.nodeId,
71
+ eventPayload.sourceEntityId,
72
+ eventPayload.sourceNodeId,
73
+ eventPayload.targetEntityId,
74
+ eventPayload.targetNodeId,
75
+ eventPayload.instanceEntityId,
76
+ ].filter((value) => typeof value === 'string');
77
+ return relatedNodeIds.length > 0 ? relatedNodeIds.includes(nodeId) : true;
78
+ }
79
+ function playBufferedAudio(audio, buffer, properties) {
80
+ void audio.listener.context.resume();
81
+ if (audio.isPlaying) {
82
+ audio.stop();
83
+ }
84
+ audio.setBuffer(buffer);
85
+ audio.setLoop(Boolean(properties.loop));
86
+ audio.setPlaybackRate(getPitchValue(properties));
87
+ audio.setVolume(getVolumeValue(properties));
88
+ audio.play();
89
+ }
65
90
  function SoundComponentEditor({ component, onUpdate, basePath = '' }) {
66
91
  const clips = Array.isArray(component.properties.clips)
67
92
  ? component.properties.clips.map((clip) => typeof clip === 'string' ? clip : '')
@@ -83,7 +108,7 @@ function SoundComponentEditor({ component, onUpdate, basePath = '' }) {
83
108
  const removeClip = (index) => {
84
109
  setClips(clips.filter((_, clipIndex) => clipIndex !== index));
85
110
  };
86
- return (_jsxs(FieldGroup, { children: [_jsx(StringField, { name: "eventName", label: "Listen Event", values: component.properties, onChange: onUpdate, placeholder: "player:footstep" }), _jsx(FieldRenderer, { fields: [
111
+ return (_jsxs(FieldGroup, { children: [_jsx(StringField, { name: "eventName", label: "Listen Event", values: component.properties, onChange: onUpdate, placeholder: "player:footstep" }), _jsx(BooleanField, { name: "autoplay", label: "Autoplay", values: component.properties, onChange: onUpdate, fallback: false }), _jsx(BooleanField, { name: "loop", label: "Loop", values: component.properties, onChange: onUpdate, fallback: false }), _jsx(FieldRenderer, { fields: [
87
112
  {
88
113
  name: 'clipMode',
89
114
  label: 'Clip Mode',
@@ -117,8 +142,7 @@ function SoundComponentEditor({ component, onUpdate, basePath = '' }) {
117
142
  function SoundComponentView({ properties, children }) {
118
143
  const { getSound } = useAssetRuntime();
119
144
  const { editMode, nodeId } = useEntityRuntime();
120
- const { camera } = useThree();
121
- const { eventName, positional = false, refDistance = 1, maxDistance = 24, rolloffFactor = 1, distanceModel = 'inverse' } = properties;
145
+ const { eventName, autoplay = false, positional = false, refDistance = 1, maxDistance = 24, rolloffFactor = 1, distanceModel = 'inverse' } = properties;
122
146
  const sequenceIndexRef = useRef(0);
123
147
  const listenerRef = useRef(null);
124
148
  const positionalAudioRef = useRef(null);
@@ -126,87 +150,56 @@ function SoundComponentView({ properties, children }) {
126
150
  if (!listenerRef.current) {
127
151
  listenerRef.current = getSharedAudioListener();
128
152
  }
129
- useEffect(() => {
130
- var _a;
131
- if (!positional) {
132
- return;
133
- }
134
- const listener = listenerRef.current;
135
- if (!listener) {
136
- return;
137
- }
138
- if (listener.parent !== camera) {
139
- (_a = listener.parent) === null || _a === void 0 ? void 0 : _a.remove(listener);
140
- camera.add(listener);
141
- }
142
- return () => {
143
- if (listener.parent === camera) {
144
- camera.remove(listener);
145
- }
146
- };
147
- }, [camera, positional]);
148
153
  useEffect(() => {
149
154
  const audio = positionalAudioRef.current;
150
155
  if (!audio) {
151
156
  return;
152
157
  }
153
- audio.setRefDistance(refDistance);
154
- audio.setMaxDistance(maxDistance);
155
- audio.setRolloffFactor(rolloffFactor);
156
- audio.setDistanceModel(distanceModel);
157
- }, [distanceModel, maxDistance, refDistance, rolloffFactor]);
158
+ audio.setRefDistance(positional ? refDistance : Math.max(refDistance, 1));
159
+ audio.setMaxDistance(positional ? maxDistance : 1000000);
160
+ audio.setRolloffFactor(positional ? rolloffFactor : 0);
161
+ audio.setDistanceModel(positional ? distanceModel : 'inverse');
162
+ }, [distanceModel, maxDistance, positional, refDistance, rolloffFactor]);
158
163
  useEffect(() => {
159
- if (editMode || paths.length === 0 || !eventName || typeof window === 'undefined') {
164
+ if (editMode || paths.length === 0 || !eventName) {
160
165
  return;
161
166
  }
162
- const handleEvent = (event) => {
163
- const customEvent = event;
164
- const detail = customEvent.detail;
165
- if (nodeId && detail && typeof detail === 'object') {
166
- const relatedNodeIds = [detail.nodeId, detail.sourceNodeId, detail.targetNodeId].filter((value) => typeof value === 'string');
167
- if (relatedNodeIds.length > 0 && !relatedNodeIds.includes(nodeId)) {
168
- return;
169
- }
167
+ return gameEvents.on(eventName, (payload) => {
168
+ if (!payloadMatchesNode(nodeId, payload)) {
169
+ return;
170
170
  }
171
171
  const clip = pickClip(paths, mode, sequenceIndexRef);
172
172
  if (!clip)
173
173
  return;
174
- const pitch = getPitchValue(properties);
175
- const volume = getVolumeValue(properties);
176
- if (!positional) {
177
- const loadedBuffer = getSound(clip);
178
- if (loadedBuffer && !soundManager.hasBuffer(clip)) {
179
- soundManager.setBuffer(clip, loadedBuffer);
180
- }
181
- if (soundManager.hasBuffer(clip)) {
182
- soundManager.playSync(clip, { pitch, volume });
183
- return;
184
- }
185
- void soundManager.play(clip, { pitch, volume });
186
- return;
187
- }
188
174
  const audio = positionalAudioRef.current;
189
- const listener = listenerRef.current;
190
175
  const buffer = getSound(clip);
191
- if (!audio || !listener || !buffer) {
176
+ if (!audio || !buffer) {
192
177
  return;
193
178
  }
194
- void listener.context.resume();
195
- if (audio.isPlaying) {
179
+ playBufferedAudio(audio, buffer, properties);
180
+ });
181
+ }, [editMode, eventName, getSound, mode, nodeId, paths, properties]);
182
+ useEffect(() => {
183
+ if (editMode || !autoplay || paths.length === 0) {
184
+ return;
185
+ }
186
+ const clip = pickClip(paths, mode, sequenceIndexRef);
187
+ if (!clip) {
188
+ return;
189
+ }
190
+ const audio = positionalAudioRef.current;
191
+ const buffer = getSound(clip);
192
+ if (!audio || !buffer) {
193
+ return;
194
+ }
195
+ playBufferedAudio(audio, buffer, properties);
196
+ return () => {
197
+ if (audio === null || audio === void 0 ? void 0 : audio.isPlaying) {
196
198
  audio.stop();
197
199
  }
198
- audio.setBuffer(buffer);
199
- audio.setLoop(false);
200
- audio.setPlaybackRate(pitch);
201
- audio.setVolume(volume);
202
- audio.play();
203
- };
204
- window.addEventListener(eventName, handleEvent);
205
- return () => {
206
- window.removeEventListener(eventName, handleEvent);
207
200
  };
208
- }, [editMode, eventName, getSound, mode, nodeId, paths, positional, properties]);
209
- return (_jsxs(_Fragment, { children: [positional && listenerRef.current ? _jsx("positionalAudio", { ref: positionalAudioRef, args: [listenerRef.current] }) : null, children] }));
201
+ }, [autoplay, editMode, getSound, mode, paths, properties]);
202
+ return (_jsxs(_Fragment, { children: [listenerRef.current ? _jsx("positionalAudio", { ref: positionalAudioRef, args: [listenerRef.current] }) : null, children] }));
210
203
  }
211
204
  const SoundComponent = {
212
205
  name: 'Sound',
@@ -214,6 +207,8 @@ const SoundComponent = {
214
207
  View: SoundComponentView,
215
208
  defaultProperties: {
216
209
  eventName: '',
210
+ autoplay: false,
211
+ loop: false,
217
212
  clips: [],
218
213
  clipMode: 'single',
219
214
  positional: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.86",
3
+ "version": "0.0.88",
4
4
  "description": "high performance 3D game engine built in React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",