layered three view
game object geometries layer
This commit is contained in:
@@ -2,10 +2,10 @@ import { BrowserRouter, Link, Outlet, Route, Routes, useLocation, useNavigate, u
|
|||||||
import { VoxelEditorPage } from './components/voxelEditor/VoxelEditorPage';
|
import { VoxelEditorPage } from './components/voxelEditor/VoxelEditorPage';
|
||||||
import { useEffect, useLayoutEffect, useRef } from 'react';
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import { reaction } from 'mobx';
|
import { reaction } from 'mobx';
|
||||||
import { ThreeView } from './components/ThreeView';
|
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
import { Panels } from './components/Panels';
|
import { Panels } from './components/Panels';
|
||||||
|
import { LayeredThreeView } from './components/layers/LayeredThreeView';
|
||||||
|
|
||||||
function StateToUrlSync() {
|
function StateToUrlSync() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -94,7 +94,7 @@ function EditorLayout() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ThreeView />
|
<LayeredThreeView />
|
||||||
<Panels />
|
<Panels />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Html } from "@react-three/drei";
|
|||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { RuntimeObjectInstance } from "../types";
|
|
||||||
import { debugLabelsStore } from "../state/debugLabelsState";
|
import { debugLabelsStore } from "../state/debugLabelsState";
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { formatV3 } from "../utils";
|
import { formatV3 } from "../utils";
|
||||||
|
import type { ObjectInstance } from "../types";
|
||||||
|
|
||||||
export const DebugLabel = observer(function ({ obj }: { obj: RuntimeObjectInstance; }) {
|
export const DebugLabel = observer(function ({ obj }: { obj: ObjectInstance; }) {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
const rb = obj.rb();
|
const rb = obj.rb();
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { useEffect } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { debugLabelsStore } from "../state/debugLabelsState";
|
import { debugLabelsStore } from "../state/debugLabelsState";
|
||||||
import { DebugLabel } from "./DebugLabel";
|
import { DebugLabel } from "./DebugLabel";
|
||||||
import type { RuntimeScene } from "../types";
|
|
||||||
|
|
||||||
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: RuntimeScene, editMode?: boolean }) {
|
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: Scene, editMode?: boolean }) {
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
return () => { debugLabelsStore.clear(); };
|
return () => { debugLabelsStore.clear(); };
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { RuntimeGameObjectInstance } from "../types";
|
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
import { ObjectViewInternal } from "./ObjectViewInternal";
|
import { ObjectViewInternal } from "./ObjectViewInternal";
|
||||||
import { PlayerObjectView } from "./CharacterView";
|
import { PlayerObjectView } from "./CharacterView";
|
||||||
|
|
||||||
export type GameObjectViewProps = {
|
export type GameObjectViewProps = {
|
||||||
object: RuntimeGameObjectInstance;
|
object: ObjectInstance;
|
||||||
isPlayer: boolean;
|
isPlayer: boolean;
|
||||||
isSceneEditor?: boolean;
|
isSceneEditor?: boolean;
|
||||||
}
|
}
|
||||||
@@ -16,7 +15,7 @@ export const GameObjectView = observer(function (props: GameObjectViewProps) {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (props.isPlayer)
|
if (props.isPlayer)
|
||||||
return <PlayerObjectView object={props.object as RuntimeGameObjectInstance} objectType={objectType} />;
|
return <PlayerObjectView object={props.object} objectType={objectType} />;
|
||||||
|
|
||||||
function handleClick(): void {
|
function handleClick(): void {
|
||||||
state.game?.emitEvent('objectClicked', { object: props.object });
|
state.game?.emitEvent('objectClicked', { object: props.object });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
import type { ObjectInstance, R3, Runtime } from "../types";
|
import type { ObjectInstance, R3 } from "../types";
|
||||||
import { useMemo, useRef, useEffect, useCallback, type RefObject } from "react";
|
import { useMemo, useRef, useEffect, useCallback, type RefObject } from "react";
|
||||||
import type { Group } from "three";
|
import type { Group } from "three";
|
||||||
import { Euler, Quaternion, Vector3, BoxHelper, type LineBasicMaterial } from "three";
|
import { Euler, Quaternion, Vector3, BoxHelper, type LineBasicMaterial } from "three";
|
||||||
@@ -11,7 +11,7 @@ import { nextObjectEditMode } from "../state/worldEditorState";
|
|||||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
||||||
|
|
||||||
type ObjectEditorViewProps = {
|
type ObjectEditorViewProps = {
|
||||||
object: Runtime<ObjectInstance>;
|
object: ObjectInstance;
|
||||||
isPlayer: boolean;
|
isPlayer: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
||||||
import type { VoxelGroup } from "../types/runtime/voxelGroup";
|
import type { VoxelGroup } from "../types/runtime/voxelGroup";
|
||||||
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
|
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
|
||||||
import { Euler, Quaternion, Mesh, MeshStandardMaterial, type Group } from "three";
|
import { Euler, Quaternion, Mesh, MeshStandardMaterial, type Group } from "three";
|
||||||
@@ -8,10 +8,10 @@ import { SyncRigidBody } from "./SyncRigidBody";
|
|||||||
import { reaction, runInAction } from "mobx";
|
import { reaction, runInAction } from "mobx";
|
||||||
import { v3toRapier } from "../utils";
|
import { v3toRapier } from "../utils";
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
import { buildVoxelMesh } from "../utils/graphics/voxelMesh";
|
import { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
|
||||||
|
|
||||||
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg, opacity }: { vg: VoxelGroup, opacity: number }) {
|
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg, opacity }: { vg: VoxelGroup, opacity: number }) {
|
||||||
const geo = useMemo(() => buildVoxelMesh(vg.positions), [vg]);
|
const geo = useMemo(() => buildVoxelGroupMesh(vg.positions), [vg]);
|
||||||
useEffect(() => () => { geo.dispose(); }, [geo]);
|
useEffect(() => () => { geo.dispose(); }, [geo]);
|
||||||
|
|
||||||
const op = vg.opacity * opacity;
|
const op = vg.opacity * opacity;
|
||||||
@@ -29,7 +29,7 @@ export type ObjectViewInternalHandle = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ObjectViewInternalProps = {
|
type ObjectViewInternalProps = {
|
||||||
object: Omit<RuntimeObjectInstance, 'typeId'>;
|
object: Omit<ObjectInstance, 'typeId'>;
|
||||||
objectType: ObjectType;
|
objectType: ObjectType;
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
isPlayer?: boolean;
|
isPlayer?: boolean;
|
||||||
@@ -54,19 +54,17 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
if (!rbRef.current)
|
if (!rbRef.current)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const gameObj = object as RuntimeGameObjectInstance;
|
rbRef.current.setTranslation(v3toRapier(object.position), true);
|
||||||
|
|
||||||
rbRef.current.setTranslation(v3toRapier(gameObj.position), true);
|
const euler = new Euler(...object.rotation);
|
||||||
|
|
||||||
const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]);
|
|
||||||
const q = new Quaternion().setFromEuler(euler);
|
const q = new Quaternion().setFromEuler(euler);
|
||||||
rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
|
rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
|
||||||
|
|
||||||
groupRef.current?.scale.set(...gameObj.scale);
|
groupRef.current?.scale.set(...object.scale);
|
||||||
|
|
||||||
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
|
rbRef.current.setLinvel(v3toRapier(object.linearVelocity), true);
|
||||||
|
|
||||||
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
|
rbRef.current.setAngvel(v3toRapier(object.angularVelocity), true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -141,8 +139,8 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
object.position = data.position;
|
object.position = data.position;
|
||||||
object.rotation = data.rotation;
|
object.rotation = data.rotation;
|
||||||
if (!props.editMode) {
|
if (!props.editMode) {
|
||||||
(object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity;
|
(object as ObjectInstance).linearVelocity = data.linearVelocity;
|
||||||
(object as RuntimeGameObjectInstance).angularVelocity = data.radialVelocity;
|
(object as ObjectInstance).angularVelocity = data.radialVelocity;
|
||||||
}
|
}
|
||||||
// do not change object.version to not trigger rerender
|
// do not change object.version to not trigger rerender
|
||||||
});
|
});
|
||||||
@@ -154,7 +152,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
scale={object.scale}
|
scale={object.scale}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{
|
{/* {
|
||||||
object.cache.voxelGroups.map((vg) =>
|
object.cache.voxelGroups.map((vg) =>
|
||||||
<VoxelGroupMesh
|
<VoxelGroupMesh
|
||||||
key={`${vg.id}-${object.cache.updatedAt}`}
|
key={`${vg.id}-${object.cache.updatedAt}`}
|
||||||
@@ -163,7 +161,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{object.cache.colliderMesh && <ConvexHullCollider args={[object.cache.colliderMesh[0]]} />}
|
{object.cache.colliderMesh && <ConvexHullCollider args={[object.cache.colliderMesh[0]]} />} */}
|
||||||
</group>
|
</group>
|
||||||
</SyncRigidBody>
|
</SyncRigidBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Suspense, useRef } from 'react';
|
import { Suspense, useRef } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Grid, OrbitControls } from '@react-three/drei';
|
import { OrbitControls } from '@react-three/drei';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
import { SceneView } from './SceneView';
|
import { SceneView } from './SceneView';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { RuntimeGameObjectInstance, RuntimeScene } from "../types";
|
import type { RuntimeScene } from "../types";
|
||||||
import { GameObjectView } from "./GameObjectView";
|
import { GameObjectView } from "./GameObjectView";
|
||||||
import { ObjectEditorView } from "./ObjectEditorView";
|
import { ObjectEditorView } from "./ObjectEditorView";
|
||||||
import { DebugLabelsView } from "./DebugLabelsView";
|
import { DebugLabelsView } from "./DebugLabelsView";
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
export const DebugLabelsLayer = observer(function () {
|
||||||
|
return null; //TODO
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import type { Pos3 } from "../../types";
|
||||||
|
import { Euler, Quaternion, Vector3 } from "three";
|
||||||
|
|
||||||
|
const _q = new Quaternion();
|
||||||
|
const _e = new Euler(0, 0, 0, 'YXZ');
|
||||||
|
const _offset = new Vector3();
|
||||||
|
const _charPos = new Vector3();
|
||||||
|
const _lookAt = new Vector3();
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
camera: Pos3;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CameraLayer({ camera: cameraPos }: Props) {
|
||||||
|
|
||||||
|
useFrame(({ camera }) => {
|
||||||
|
// const t = rb.translation();
|
||||||
|
// _charPos.set(t.x, t.y, t.z);
|
||||||
|
// _offset.copy(shoulderOffset).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
|
||||||
|
// camera.position.copy(_charPos).add(_offset);
|
||||||
|
// _lookAt.set(t.x, t.y + lookAtY, t.z);
|
||||||
|
camera.lookAt(_lookAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ObjectGeomertiesLayer } from "./ObjectGeomertiesLayer";
|
||||||
|
import { Canvas } from "@react-three/fiber";
|
||||||
|
import { Perf } from "r3f-perf";
|
||||||
|
import { state } from "../../state";
|
||||||
|
import { CameraLayer } from "./GameraLayer";
|
||||||
|
|
||||||
|
export const LayeredThreeView = observer(function () {
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Canvas
|
||||||
|
// camera={state.world.character.camera}
|
||||||
|
>
|
||||||
|
<ambientLight intensity={0.5} />
|
||||||
|
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||||
|
|
||||||
|
<Perf position="bottom-right" />
|
||||||
|
|
||||||
|
<CameraLayer camera={state.worldEditor.camera} />
|
||||||
|
<ObjectGeomertiesLayer />
|
||||||
|
</Canvas>
|
||||||
|
</>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useThree, useFrame } from "@react-three/fiber";
|
||||||
|
import { ObjectGeometryList } from "../../rendering/ObjectGeometryList";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { state } from "../../state";
|
||||||
|
|
||||||
|
export const ObjectGeomertiesLayer = observer(function () {
|
||||||
|
|
||||||
|
const geometries = state.view.objectGeometries;
|
||||||
|
|
||||||
|
const threeScene = useThree(s => s.scene);
|
||||||
|
const list = useRef<ObjectGeometryList | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const l = new ObjectGeometryList(threeScene, geometries);
|
||||||
|
list.current = l;
|
||||||
|
return () => {
|
||||||
|
l.dispose();
|
||||||
|
list.current = null;
|
||||||
|
};
|
||||||
|
}, [threeScene, geometries]);
|
||||||
|
|
||||||
|
useFrame(() => list.current?.tick());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link, useParams } from 'react-router-dom';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { state } from '../../state';
|
|
||||||
import type { Voxel, V3 } from '../../types';
|
import type { Voxel, V3 } from '../../types';
|
||||||
import { VoxelEditorScene } from './VoxelEditorScene';
|
import { VoxelEditorScene } from './VoxelEditorScene';
|
||||||
import { IconArrowLeft, IconArrowBackUp, IconArrowForwardUp, IconPlus, IconTrash } from '@tabler/icons-react';
|
import { IconArrowLeft, IconArrowBackUp, IconArrowForwardUp, IconPlus, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { state } from '../../state';
|
||||||
|
|
||||||
const PALETTE = [
|
const PALETTE = [
|
||||||
'#ffffff', '#cccccc', '#999999', '#555555', '#222222',
|
'#ffffff', '#cccccc', '#999999', '#555555', '#222222',
|
||||||
@@ -19,7 +19,10 @@ type HistoryEntry =
|
|||||||
| { type: 'remove'; voxel: Voxel };
|
| { type: 'remove'; voxel: Voxel };
|
||||||
|
|
||||||
export const VoxelEditorPage = observer(function () {
|
export const VoxelEditorPage = observer(function () {
|
||||||
|
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const objectType = id ? state.view.objectTypeGeometries.get(id) : undefined;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [mode, setMode] = useState<'add' | 'remove'>('add');
|
const [mode, setMode] = useState<'add' | 'remove'>('add');
|
||||||
const [color, setColor] = useState('#808080');
|
const [color, setColor] = useState('#808080');
|
||||||
@@ -28,14 +31,11 @@ export const VoxelEditorPage = observer(function () {
|
|||||||
const history = useRef<HistoryEntry[]>([]);
|
const history = useRef<HistoryEntry[]>([]);
|
||||||
const future = useRef<HistoryEntry[]>([]);
|
const future = useRef<HistoryEntry[]>([]);
|
||||||
|
|
||||||
const objectType = id ? state.world.getObjectTypeById(id) : undefined;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (id)
|
objectType?.updateRuntimeData();
|
||||||
state.worldEditor.refreshObjectTypeCaches(id);
|
|
||||||
};
|
};
|
||||||
}, [id]);
|
}, [objectType]);
|
||||||
|
|
||||||
function pushHistory(entry: HistoryEntry) {
|
function pushHistory(entry: HistoryEntry) {
|
||||||
history.current.push(entry);
|
history.current.push(entry);
|
||||||
@@ -46,31 +46,31 @@ state.worldEditor.refreshObjectTypeCaches(id);
|
|||||||
|
|
||||||
const undo = useCallback(() => {
|
const undo = useCallback(() => {
|
||||||
const entry = history.current.pop();
|
const entry = history.current.pop();
|
||||||
if (!entry || !id)
|
if (!entry)
|
||||||
return;
|
return;
|
||||||
if (entry.type === 'add') {
|
if (entry.type === 'add') {
|
||||||
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
|
objectType?.removeVoxel(entry.voxel.position);
|
||||||
} else {
|
} else {
|
||||||
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
|
objectType?.addVoxel(entry.voxel);
|
||||||
}
|
}
|
||||||
future.current.push(entry);
|
future.current.push(entry);
|
||||||
setCanUndo(history.current.length > 0);
|
setCanUndo(history.current.length > 0);
|
||||||
setCanRedo(true);
|
setCanRedo(true);
|
||||||
}, [id]);
|
}, [objectType]);
|
||||||
|
|
||||||
const redo = useCallback(() => {
|
const redo = useCallback(() => {
|
||||||
const entry = future.current.pop();
|
const entry = future.current.pop();
|
||||||
if (!entry || !id)
|
if (!entry)
|
||||||
return;
|
return;
|
||||||
if (entry.type === 'add') {
|
if (entry.type === 'add') {
|
||||||
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
|
objectType?.addVoxel(entry.voxel);
|
||||||
} else {
|
} else {
|
||||||
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
|
objectType?.removeVoxel(entry.voxel.position);
|
||||||
}
|
}
|
||||||
history.current.push(entry);
|
history.current.push(entry);
|
||||||
setCanUndo(true);
|
setCanUndo(true);
|
||||||
setCanRedo(future.current.length > 0);
|
setCanRedo(future.current.length > 0);
|
||||||
}, [id]);
|
}, [objectType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
@@ -84,24 +84,14 @@ state.worldEditor.refreshObjectTypeCaches(id);
|
|||||||
}, [undo, redo]);
|
}, [undo, redo]);
|
||||||
|
|
||||||
function handleAdd(voxel: Voxel) {
|
function handleAdd(voxel: Voxel) {
|
||||||
if (!id)
|
if (objectType?.addVoxel(voxel))
|
||||||
return;
|
pushHistory({ type: 'add', voxel });
|
||||||
state.worldEditor.addVoxelToObjectType(id, voxel);
|
|
||||||
pushHistory({ type: 'add', voxel });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemove(position: V3) {
|
function handleRemove(position: V3) {
|
||||||
if (!id || !objectType)
|
const voxel = objectType?.removeVoxel(position);
|
||||||
return;
|
if (voxel)
|
||||||
const voxel = objectType.voxels.find(v =>
|
pushHistory({ type: 'remove', voxel });
|
||||||
v.position[0] === position[0] &&
|
|
||||||
v.position[1] === position[1] &&
|
|
||||||
v.position[2] === position[2]
|
|
||||||
);
|
|
||||||
if (!voxel)
|
|
||||||
return;
|
|
||||||
state.worldEditor.removeVoxelFromObjectType(id, position);
|
|
||||||
pushHistory({ type: 'remove', voxel });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!objectType) {
|
if (!objectType) {
|
||||||
@@ -116,11 +106,11 @@ state.worldEditor.refreshObjectTypeCaches(id);
|
|||||||
<div style={{ position: 'fixed', inset: 0, background: '#1a1a2e', display: 'flex', flexDirection: 'column' }}>
|
<div style={{ position: 'fixed', inset: 0, background: '#1a1a2e', display: 'flex', flexDirection: 'column' }}>
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#11111f', flexShrink: 0 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#11111f', flexShrink: 0 }}>
|
||||||
<button onClick={() => navigate(`/editor/objectType/${id}`)} style={btnStyle}>
|
<button onClick={() => navigate(`/editor/objectType/${objectType.objectType.id}`)} style={btnStyle}>
|
||||||
<IconArrowLeft size={20} />
|
<IconArrowLeft size={20} />
|
||||||
</button>
|
</button>
|
||||||
<span style={{ color: '#ccc', fontWeight: 600 }}>{objectType.name}</span>
|
<span style={{ color: '#ccc', fontWeight: 600 }}>{objectType.objectType.name}</span>
|
||||||
<span style={{ color: '#666', fontSize: 13 }}>{objectType.voxels.length} voxels</span>
|
<span style={{ color: '#666', fontSize: 13 }}>{objectType.objectType.voxels.length} voxels</span>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<button onClick={undo} disabled={!canUndo} style={{ ...btnStyle, opacity: canUndo ? 1 : 0.35 }}>
|
<button onClick={undo} disabled={!canUndo} style={{ ...btnStyle, opacity: canUndo ? 1 : 0.35 }}>
|
||||||
<IconArrowBackUp size={20} />
|
<IconArrowBackUp size={20} />
|
||||||
@@ -134,7 +124,7 @@ state.worldEditor.refreshObjectTypeCaches(id);
|
|||||||
<div style={{ flex: 1, position: 'relative' }}>
|
<div style={{ flex: 1, position: 'relative' }}>
|
||||||
<Canvas camera={{ position: [12, 10, 12], fov: 50 }}>
|
<Canvas camera={{ position: [12, 10, 12], fov: 50 }}>
|
||||||
<VoxelEditorScene
|
<VoxelEditorScene
|
||||||
voxels={objectType.voxels.slice()}
|
voxels={objectType.objectType.voxels.slice()}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
color={color}
|
color={color}
|
||||||
typeId="stone"
|
typeId="stone"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState, useMemo, useEffect } from 'react';
|
|||||||
import type { ThreeEvent } from '@react-three/fiber';
|
import type { ThreeEvent } from '@react-three/fiber';
|
||||||
import type { Ray } from 'three';
|
import type { Ray } from 'three';
|
||||||
import type { Voxel, V3 } from '../../types';
|
import type { Voxel, V3 } from '../../types';
|
||||||
import { buildVoxelMesh } from '../../utils/graphics/voxelMesh';
|
import { buildVoxelGroupMesh } from '../../utils/graphics/voxelMesh';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
voxels: Voxel[];
|
voxels: Voxel[];
|
||||||
@@ -64,7 +64,7 @@ interface MergedVoxelMeshProps {
|
|||||||
|
|
||||||
function MergedVoxelMesh({ color, positions, onPointerMove, onClick, onPointerLeave }: MergedVoxelMeshProps) {
|
function MergedVoxelMesh({ color, positions, onPointerMove, onClick, onPointerLeave }: MergedVoxelMeshProps) {
|
||||||
const geo = useMemo(
|
const geo = useMemo(
|
||||||
() => buildVoxelMesh(positions),
|
() => buildVoxelGroupMesh(positions),
|
||||||
[positions],
|
[positions],
|
||||||
);
|
);
|
||||||
useEffect(() => () => { geo.dispose(); }, [geo]);
|
useEffect(() => () => { geo.dispose(); }, [geo]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { toJS } from "mobx";
|
import { toJS } from "mobx";
|
||||||
import type { Game, GameScene, RuntimeGameObjectInstance, RuntimeGameScene, RuntimeScene, ScriptAction, ScriptCoroutineContext, World } from "../types";
|
import type { Game, Scene, ScriptAction, ScriptCoroutineContext, World } from "../types";
|
||||||
import { clone } from "../utils";
|
import { clone } from "../utils";
|
||||||
import { populateRuntimeScene } from "../utils/runtime";
|
import { populateRuntimeScene } from "../utils/runtime";
|
||||||
import { ScriptGameEventBus } from "../scripts/eventBus";
|
import { ScriptGameEventBus } from "../scripts/eventBus";
|
||||||
@@ -24,7 +24,7 @@ export class GameFactory {
|
|||||||
const game = {
|
const game = {
|
||||||
paused: false,
|
paused: false,
|
||||||
time: 0,
|
time: 0,
|
||||||
scene: GameFactory.initGameScene(scene),
|
scene,
|
||||||
};
|
};
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
@@ -36,17 +36,13 @@ export class GameFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static save(game: Game<GameScene>): void {
|
public static save(game: Game): void {
|
||||||
localStorage.setItem("game", JSON.stringify(game));
|
localStorage.setItem("game", JSON.stringify(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static initGameScene(scene: RuntimeScene): RuntimeGameScene {
|
|
||||||
return scene as RuntimeGameScene;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static evalGameScripts(
|
public static evalGameScripts(
|
||||||
world: World,
|
world: World,
|
||||||
scene: RuntimeGameScene,
|
scene: Scene,
|
||||||
coroutines: ScriptCoroutines,
|
coroutines: ScriptCoroutines,
|
||||||
): ScriptGameEventBus {
|
): ScriptGameEventBus {
|
||||||
|
|
||||||
@@ -80,7 +76,7 @@ export class GameFactory {
|
|||||||
Object.values(scene.objects)
|
Object.values(scene.objects)
|
||||||
.filter((o) => o.typeId == ot.id)
|
.filter((o) => o.typeId == ot.id)
|
||||||
.map((liveObj) => {
|
.map((liveObj) => {
|
||||||
const snapshot = toJS(liveObj) as RuntimeGameObjectInstance;
|
const snapshot = toJS(liveObj);
|
||||||
snapshot.rb = () => liveObj.rb();
|
snapshot.rb = () => liveObj.rb();
|
||||||
return snapshot;
|
return snapshot;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class WorldFactory {
|
|||||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||||
initialScene: {
|
initialScene: {
|
||||||
objects: {},
|
objects: {},
|
||||||
|
playerObjectId: undefined,
|
||||||
},
|
},
|
||||||
gameRules: {
|
gameRules: {
|
||||||
gravity: true,
|
gravity: true,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { MobxReconcilerMap, type ReconcilerMap } from '../utils/reconciler';
|
||||||
|
import type { ObjectGeometryViewItem, ObjectTypeGeometryViewItem } from '../state/viewState';
|
||||||
|
|
||||||
|
export class ObjectGeometryList {
|
||||||
|
private items: ReconcilerMap<ObjectGeometryViewItem>;
|
||||||
|
private readonly typeRefCounts = new Map<string, number>();
|
||||||
|
|
||||||
|
private readonly matrixObj = new THREE.Object3D();
|
||||||
|
private readonly _p = new THREE.Vector3();
|
||||||
|
private readonly _e = new THREE.Euler();
|
||||||
|
private readonly _q = new THREE.Quaternion();
|
||||||
|
|
||||||
|
constructor(scene: THREE.Scene, geometiries: Iterable<ObjectGeometryViewItem>) {
|
||||||
|
this.items = new MobxReconcilerMap<ObjectGeometryViewItem>({
|
||||||
|
reaction: () => geometiries,
|
||||||
|
keyOf: (item) => item.object.id,
|
||||||
|
initializeItem: (item) => {
|
||||||
|
const typeId = item.objectTypeGeometry.objectType.id;
|
||||||
|
const count = (this.typeRefCounts.get(typeId) ?? 0) + 1;
|
||||||
|
this.typeRefCounts.set(typeId, count);
|
||||||
|
if (count === 1) {
|
||||||
|
for (const mesh of item.objectTypeGeometry.meshes ?? [])
|
||||||
|
scene.add(mesh);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disposeItem: (item) => {
|
||||||
|
const typeId = item.objectTypeGeometry.objectType.id;
|
||||||
|
const count = (this.typeRefCounts.get(typeId) ?? 1) - 1;
|
||||||
|
this.typeRefCounts.set(typeId, count);
|
||||||
|
if (count === 0) {
|
||||||
|
for (const mesh of item.objectTypeGeometry.meshes ?? [])
|
||||||
|
scene.remove(mesh);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.items?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called once per frame — reads MobX observables directly outside any reactive
|
||||||
|
// context, so no subscriptions are created and no re-renders are triggered.
|
||||||
|
tick(): void {
|
||||||
|
const touchedObjectTypes: Record<string, { typeGeometry: ObjectTypeGeometryViewItem, instanceCount: number }> = {};
|
||||||
|
|
||||||
|
this.items.values.forEach((item) => {
|
||||||
|
const obj = item.object;
|
||||||
|
|
||||||
|
this._p.set(obj.position[0], obj.position[1], obj.position[2]);
|
||||||
|
this._e.set(obj.rotation[0], obj.rotation[1], obj.rotation[2]);
|
||||||
|
this._q.setFromEuler(this._e);
|
||||||
|
|
||||||
|
const typeId = item.objectTypeGeometry.objectType.id;
|
||||||
|
if (!touchedObjectTypes[typeId])
|
||||||
|
touchedObjectTypes[typeId] = { typeGeometry: item.objectTypeGeometry, instanceCount: 0 };
|
||||||
|
const instanceIdx = touchedObjectTypes[typeId].instanceCount++;
|
||||||
|
|
||||||
|
for (const mesh of item.objectTypeGeometry.meshes ?? []) {
|
||||||
|
this.matrixObj.position.copy(this._p);
|
||||||
|
this.matrixObj.quaternion.copy(this._q);
|
||||||
|
this.matrixObj.scale.set(obj.scale[0], obj.scale[1], obj.scale[2]);
|
||||||
|
this.matrixObj.updateMatrix();
|
||||||
|
mesh.setMatrixAt(instanceIdx, this.matrixObj.matrix);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const objectType of Object.values(touchedObjectTypes)) {
|
||||||
|
for (const mesh of objectType.typeGeometry.meshes ?? []) {
|
||||||
|
mesh.count = objectType.instanceCount;
|
||||||
|
mesh.instanceMatrix.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { autorun, makeAutoObservable, toJS } from "mobx";
|
import { autorun, makeAutoObservable, toJS } from "mobx";
|
||||||
import type { WorldState } from "./worldState";
|
import type { WorldState } from "./worldState";
|
||||||
import type { Game, GameObjectInstance, GameScene, Pos3, RuntimeGameScene, ScriptContext, ScriptCoroutineContext } from "../types";
|
import type { Game, Pos3, Scene, ScriptContext, ScriptCoroutineContext } from "../types";
|
||||||
import type { CameraProps } from "@react-three/fiber";
|
import type { CameraProps } from "@react-three/fiber";
|
||||||
import type { ScriptGameEventBus } from "../scripts/eventBus";
|
import type { ScriptGameEventBus } from "../scripts/eventBus";
|
||||||
import { GameFactory } from "../model/gameFactory";
|
import { GameFactory } from "../model/gameFactory";
|
||||||
@@ -13,37 +13,16 @@ export class GameState {
|
|||||||
public debugOverlay: boolean = false;
|
public debugOverlay: boolean = false;
|
||||||
public time: number = 0;
|
public time: number = 0;
|
||||||
public isFirstTick: boolean;
|
public isFirstTick: boolean;
|
||||||
public scene: RuntimeGameScene;
|
public scene: Scene;
|
||||||
|
|
||||||
private coroutines = new ScriptCoroutines();
|
private coroutines = new ScriptCoroutines();
|
||||||
private eventBus: ScriptGameEventBus;
|
private eventBus: ScriptGameEventBus;
|
||||||
|
|
||||||
private sceneObjectsAsSaveables(): GameScene['objects'] {
|
public get asGame(): Game {
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(this.scene.objects).map(([id, obj]) => [id, {
|
|
||||||
id: obj.id,
|
|
||||||
typeId: obj.typeId,
|
|
||||||
physics: obj.physics,
|
|
||||||
gravityScale: obj.gravityScale,
|
|
||||||
position: toJS(obj.position),
|
|
||||||
rotation: toJS(obj.rotation),
|
|
||||||
scale: toJS(obj.scale),
|
|
||||||
version: obj.version,
|
|
||||||
linearVelocity: toJS(obj.linearVelocity),
|
|
||||||
angularVelocity: toJS(obj.angularVelocity),
|
|
||||||
visible: toJS(obj.visible),
|
|
||||||
} satisfies GameObjectInstance])
|
|
||||||
) as unknown as RuntimeGameScene['objects'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get asSaveableGame(): Game<GameScene> {
|
|
||||||
return {
|
return {
|
||||||
paused: this.isPaused,
|
paused: this.isPaused,
|
||||||
time: this.time,
|
time: this.time,
|
||||||
scene: {
|
scene: this.scene,
|
||||||
playerObjectId: this.scene.playerObjectId,
|
|
||||||
objects: this.sceneObjectsAsSaveables(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +105,7 @@ export class GameState {
|
|||||||
|
|
||||||
public save(): void {
|
public save(): void {
|
||||||
console.log('Saving game...');
|
console.log('Saving game...');
|
||||||
GameFactory.save(this.asSaveableGame);
|
GameFactory.save(this.asGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(): void {
|
public resume(): void {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { WorldState } from "./worldState";
|
|||||||
import { WorldEditorState } from "./worldEditorState";
|
import { WorldEditorState } from "./worldEditorState";
|
||||||
import { GameState } from "./gameState";
|
import { GameState } from "./gameState";
|
||||||
import { MenuState } from "./menuState";
|
import { MenuState } from "./menuState";
|
||||||
import type { RuntimeObjectInstance } from "../types";
|
import { ViewState } from "./viewState";
|
||||||
|
|
||||||
export type RenderInfo = {
|
export type RenderInfo = {
|
||||||
calls: number,
|
calls: number,
|
||||||
@@ -19,6 +19,7 @@ export class RootState {
|
|||||||
public game: GameState | undefined;
|
public game: GameState | undefined;
|
||||||
public renderInfo: RenderInfo | undefined;
|
public renderInfo: RenderInfo | undefined;
|
||||||
public readonly menu = new MenuState();
|
public readonly menu = new MenuState();
|
||||||
|
public readonly view = new ViewState(this.world);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.worldEditor = new WorldEditorState(this.world);
|
this.worldEditor = new WorldEditorState(this.world);
|
||||||
@@ -52,11 +53,11 @@ export class RootState {
|
|||||||
this.renderInfo = value;
|
this.renderInfo = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public markObjectAsPlayer(object: RuntimeObjectInstance) {
|
public markObjectAsPlayer(objectId: string) {
|
||||||
if (this.game)
|
if (this.game)
|
||||||
this.game.scene.playerObjectId = object.id;
|
this.game.scene.playerObjectId = objectId;
|
||||||
else
|
else
|
||||||
this.world.data.initialScene.playerObjectId = object.id;
|
this.world.data.initialScene.playerObjectId = objectId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { action, makeAutoObservable, makeObservable, observable, reaction, type IReactionDisposer } from "mobx";
|
||||||
|
import type { ObjectInstance, ObjectInstanceRigidBody, ObjectType, Scene, V3, Voxel, VoxelGroup } from "../types";
|
||||||
|
import { state } from "./rootState";
|
||||||
|
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
|
||||||
|
import { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
|
||||||
|
import { buildObjectTrimesh } from '../utils/graphics/mesh';
|
||||||
|
import { computeBoundingBox } from '../utils/runtime';
|
||||||
|
import { MobxReconcilerMap } from '../utils/reconciler';
|
||||||
|
|
||||||
|
export abstract class ViewItem {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ObjectTypeGeometryViewItem extends ViewItem {
|
||||||
|
public readonly objectType: ObjectType;
|
||||||
|
|
||||||
|
public voxelGroups: VoxelGroup[] | undefined;
|
||||||
|
public colliderMesh: [Float32Array, Uint32Array] | undefined;
|
||||||
|
public boundingBox: { min: V3; max: V3 } | undefined;
|
||||||
|
public meshes: THREE.InstancedMesh[] | undefined;
|
||||||
|
|
||||||
|
private readonly _disposeVoxelReaction: IReactionDisposer;
|
||||||
|
|
||||||
|
constructor(objectType: ObjectType) {
|
||||||
|
super();
|
||||||
|
this.objectType = objectType;
|
||||||
|
makeObservable(this, {
|
||||||
|
voxelGroups: observable.ref,
|
||||||
|
colliderMesh: observable.ref,
|
||||||
|
boundingBox: observable.ref,
|
||||||
|
meshes: observable.ref,
|
||||||
|
updateRuntimeData: action,
|
||||||
|
});
|
||||||
|
this._disposeVoxelReaction = reaction(
|
||||||
|
() => objectType.voxels.map(v => `${v.position}-${v.typeId}`).join('|'),
|
||||||
|
() => this.updateRuntimeData(),
|
||||||
|
);
|
||||||
|
this.updateRuntimeData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this._disposeVoxelReaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addVoxel(voxel: Voxel): boolean {
|
||||||
|
const occupied = this.objectType.voxels.some(v =>
|
||||||
|
v.position[0] === voxel.position[0] &&
|
||||||
|
v.position[1] === voxel.position[1] &&
|
||||||
|
v.position[2] === voxel.position[2]
|
||||||
|
);
|
||||||
|
if (!occupied) {
|
||||||
|
this.objectType.voxels.push(voxel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeVoxel(position: V3): Voxel | undefined {
|
||||||
|
const idx = this.objectType.voxels.findIndex(v =>
|
||||||
|
v.position[0] === position[0] &&
|
||||||
|
v.position[1] === position[1] &&
|
||||||
|
v.position[2] === position[2]
|
||||||
|
);
|
||||||
|
if (idx !== -1)
|
||||||
|
return this.objectType.voxels.splice(idx, 1)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateRuntimeData(): void {
|
||||||
|
this.voxelGroups = getObjectVoxelGroups(this.objectType, state.world.data.voxelTypes);
|
||||||
|
this.meshes = [];
|
||||||
|
for (const vg of this.voxelGroups) {
|
||||||
|
const geo = buildVoxelGroupMesh(vg.positions);
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: vg.color,
|
||||||
|
opacity: vg.opacity,
|
||||||
|
transparent: vg.opacity < 1,
|
||||||
|
});
|
||||||
|
const mesh = new THREE.InstancedMesh(geo, mat, 1000);
|
||||||
|
this.meshes.push(mesh);
|
||||||
|
}
|
||||||
|
this.colliderMesh = buildObjectTrimesh(this.objectType, state.world.data.voxelTypes);
|
||||||
|
this.boundingBox = computeBoundingBox(this.objectType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ObjectGeometryViewItem extends ViewItem {
|
||||||
|
public readonly object: ObjectInstance;
|
||||||
|
public readonly objectTypeGeometry: ObjectTypeGeometryViewItem;
|
||||||
|
|
||||||
|
public rb: (() => ObjectInstanceRigidBody | null) | undefined;
|
||||||
|
|
||||||
|
constructor(object: ObjectInstance, objectTypeGeometry: ObjectTypeGeometryViewItem) {
|
||||||
|
super();
|
||||||
|
this.object = object;
|
||||||
|
this.objectTypeGeometry = objectTypeGeometry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewState {
|
||||||
|
public objectTypeGeometries: MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>;
|
||||||
|
public objectGeometries: MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>;
|
||||||
|
|
||||||
|
constructor(world: { data: { objectTypes: Record<string, ObjectType>; initialScene: Scene } }) {
|
||||||
|
makeAutoObservable(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
this.objectTypeGeometries = new MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>({
|
||||||
|
reaction: () => Object.values(world.data.objectTypes),
|
||||||
|
keyOf: (ot) => ot.id,
|
||||||
|
mapItem: (ot) => new ObjectTypeGeometryViewItem(ot),
|
||||||
|
updateItem: () => true,
|
||||||
|
disposeItem: (item) => { item.dispose(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.objectGeometries = new MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>({
|
||||||
|
reaction: () => Object.values(world.data.initialScene.objects)
|
||||||
|
.filter(obj => obj.visible)
|
||||||
|
.filter(obj => !!world.data.objectTypes[obj.typeId]),
|
||||||
|
keyOf: (obj) => obj.id,
|
||||||
|
mapItem: (obj) => new ObjectGeometryViewItem(obj, this.objectTypeGeometries.get(obj.typeId)!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.objectTypeGeometries.dispose();
|
||||||
|
this.objectGeometries.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get items(): ViewItem[] {
|
||||||
|
return [
|
||||||
|
...this.objectTypeGeometries,
|
||||||
|
...this.objectGeometries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
import type { WorldState } from "./worldState";
|
import type { WorldState } from "./worldState";
|
||||||
import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene, type ObjectType, type Voxel } from "../types";
|
import { type ObjectInstance, type Pos3, type R3, type V3, type ObjectType, type Scene } from "../types";
|
||||||
import { createObjectInstance } from "../utils/object";
|
import { createObjectInstance } from "../utils/object";
|
||||||
import { randomId } from "../utils";
|
import { randomId } from "../utils";
|
||||||
import { state } from "./rootState";
|
import { state } from "./rootState";
|
||||||
import { populateRuntimeObject, computeBoundingBox } from "../utils/runtime";
|
import { populateRuntimeObject } from "../utils/runtime";
|
||||||
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
|
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
|
||||||
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
|
|
||||||
import { buildObjectTrimesh } from "../utils/graphics/mesh";
|
|
||||||
|
|
||||||
export const ObjectEditModeEnum = [
|
export const ObjectEditModeEnum = [
|
||||||
'translate',
|
'translate',
|
||||||
@@ -99,7 +97,7 @@ export class WorldEditorState {
|
|||||||
this.setSelection(undefined);
|
this.setSelection(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get scene(): RuntimeScene {
|
public get scene(): Scene {
|
||||||
return this.world.data.initialScene;
|
return this.world.data.initialScene;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,48 +166,6 @@ export class WorldEditorState {
|
|||||||
return objectType;
|
return objectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addVoxelToObjectType(typeId: string, voxel: Voxel): void {
|
|
||||||
const objectType = this.world.getObjectTypeById(typeId);
|
|
||||||
if (!objectType)
|
|
||||||
return;
|
|
||||||
const occupied = objectType.voxels.some(v =>
|
|
||||||
v.position[0] === voxel.position[0] &&
|
|
||||||
v.position[1] === voxel.position[1] &&
|
|
||||||
v.position[2] === voxel.position[2]
|
|
||||||
);
|
|
||||||
if (!occupied)
|
|
||||||
objectType.voxels.push(voxel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeVoxelFromObjectType(typeId: string, position: V3): void {
|
|
||||||
const objectType = this.world.getObjectTypeById(typeId);
|
|
||||||
if (!objectType)
|
|
||||||
return;
|
|
||||||
const idx = objectType.voxels.findIndex(v =>
|
|
||||||
v.position[0] === position[0] &&
|
|
||||||
v.position[1] === position[1] &&
|
|
||||||
v.position[2] === position[2]
|
|
||||||
);
|
|
||||||
if (idx !== -1)
|
|
||||||
objectType.voxels.splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public refreshObjectTypeCaches(typeId: string): void {
|
|
||||||
const objectType = this.world.getObjectTypeById(typeId);
|
|
||||||
if (!objectType)
|
|
||||||
return;
|
|
||||||
const world = this.world.data;
|
|
||||||
const now = Date.now();
|
|
||||||
for (const obj of Object.values(this.scene.objects)) {
|
|
||||||
if (obj.typeId !== typeId)
|
|
||||||
continue;
|
|
||||||
obj.cache.voxelGroups = getObjectVoxelGroups(objectType, world.voxelTypes);
|
|
||||||
obj.cache.colliderMesh = buildObjectTrimesh(objectType, world.voxelTypes);
|
|
||||||
obj.cache.boundingBox = computeBoundingBox(objectType);
|
|
||||||
obj.cache.updatedAt = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteObjectType(typeId: string) {
|
public deleteObjectType(typeId: string) {
|
||||||
if (this.selection?.type === 'objectType' && this.selection.id === typeId)
|
if (this.selection?.type === 'objectType' && this.selection.id === typeId)
|
||||||
this.resetSelection();
|
this.resetSelection();
|
||||||
|
|||||||
@@ -12,30 +12,11 @@ export class WorldState {
|
|||||||
|
|
||||||
private _buildSave(): World {
|
private _buildSave(): World {
|
||||||
const d = this.data;
|
const d = this.data;
|
||||||
return {
|
return toJS(d);
|
||||||
objectTypes: toJS(d.objectTypes),
|
|
||||||
voxelTypes: toJS(d.voxelTypes),
|
|
||||||
editorCamera: toJS(d.editorCamera),
|
|
||||||
gameRules: toJS(d.gameRules),
|
|
||||||
initialScene: {
|
|
||||||
playerObjectId: d.initialScene.playerObjectId,
|
|
||||||
objects: Object.fromEntries(
|
|
||||||
Object.entries(d.initialScene.objects).map(([id, obj]) => [id, {
|
|
||||||
id: obj.id,
|
|
||||||
typeId: obj.typeId,
|
|
||||||
physics: obj.physics,
|
|
||||||
gravityScale: obj.gravityScale,
|
|
||||||
position: toJS(obj.position),
|
|
||||||
rotation: toJS(obj.rotation),
|
|
||||||
scale: toJS(obj.scale),
|
|
||||||
} satisfies ObjectInstance])
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private startAutoSave() {
|
private startAutoSave() {
|
||||||
return autorun(() => WorldFactory.save(this._buildSave()), { delay: 500 });
|
return autorun(() => this.save(), { delay: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
private withoutAutoSave(fn: () => void) {
|
private withoutAutoSave(fn: () => void) {
|
||||||
@@ -105,6 +86,9 @@ export class WorldState {
|
|||||||
position: [0, -1, 0],
|
position: [0, -1, 0],
|
||||||
rotation: [0, 0, 0],
|
rotation: [0, 0, 0],
|
||||||
scale: [1, 1, 1],
|
scale: [1, 1, 1],
|
||||||
|
visible: true,
|
||||||
|
linearVelocity: [0, 0, 0],
|
||||||
|
angularVelocity: [0, 0, 0],
|
||||||
},
|
},
|
||||||
...objectMap,
|
...objectMap,
|
||||||
},
|
},
|
||||||
@@ -128,6 +112,7 @@ export class WorldState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public save(): void {
|
public save(): void {
|
||||||
|
console.log('Saving world...');
|
||||||
WorldFactory.save(this._buildSave());
|
WorldFactory.save(this._buildSave());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { GameScene, RuntimeGameScene } from "./scene";
|
import type { Scene } from "./scene";
|
||||||
|
|
||||||
export type Game<TScene extends GameScene = RuntimeGameScene> = {
|
export type Game = {
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
time: number;
|
time: number;
|
||||||
scene: TScene;
|
scene: Scene;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { R3, V3 } from "../3d";
|
import type { R3, V3 } from "../3d";
|
||||||
import type { Runtime } from "./runtime";
|
|
||||||
import type { Voxel } from "./voxel";
|
import type { Voxel } from "./voxel";
|
||||||
|
|
||||||
export type ObjectType = {
|
export type ObjectType = {
|
||||||
@@ -8,7 +7,7 @@ export type ObjectType = {
|
|||||||
voxels: Voxel[];
|
voxels: Voxel[];
|
||||||
program?: string;
|
program?: string;
|
||||||
javascript?: string;
|
javascript?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ObjectInstance = {
|
export type ObjectInstance = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,15 +18,7 @@ export type ObjectInstance = {
|
|||||||
rotation: R3;
|
rotation: R3;
|
||||||
scale: V3;
|
scale: V3;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
export type GameObjectData = {
|
|
||||||
version: number; // increment to force three and physics changes
|
|
||||||
linearVelocity: V3;
|
linearVelocity: V3;
|
||||||
angularVelocity: R3;
|
angularVelocity: V3;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GameObjectInstance = ObjectInstance & GameObjectData;
|
|
||||||
|
|
||||||
export type RuntimeObjectInstance = Runtime<ObjectInstance>;
|
|
||||||
export type RuntimeGameObjectInstance = Runtime<GameObjectInstance>;
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { V3 } from "../3d";
|
import type { V3 } from "../3d";
|
||||||
import type { VoxelGroup } from "../runtime";
|
import type { VoxelGroup } from "../runtime";
|
||||||
import type { ObjectInstance } from "./object";
|
|
||||||
|
|
||||||
export type ObjectInstanceRigidBody = {
|
export type ObjectInstanceRigidBody = {
|
||||||
handle: number; // rapier RigidBodyHandle
|
handle: number; // rapier RigidBodyHandle
|
||||||
@@ -13,13 +12,12 @@ export type ObjectInstanceRigidBody = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ObjectInstanceRuntimeData = {
|
export type ObjectInstanceRuntimeData = {
|
||||||
cache: {
|
|
||||||
voxelGroups: VoxelGroup[];
|
|
||||||
colliderMesh: [Float32Array, Uint32Array] | null;
|
|
||||||
boundingBox: { min: V3; max: V3 };
|
|
||||||
updatedAt: number;
|
|
||||||
};
|
|
||||||
rb: () => ObjectInstanceRigidBody | null;
|
rb: () => ObjectInstanceRigidBody | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Runtime<T extends ObjectInstance> = T & ObjectInstanceRuntimeData;
|
export type ObjectTypeRuntimeData = {
|
||||||
|
voxelGroups: VoxelGroup[];
|
||||||
|
colliderMesh: [Float32Array, Uint32Array] | null;
|
||||||
|
boundingBox: { min: V3; max: V3 };
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object";
|
import type { ObjectInstance } from "./object";
|
||||||
|
|
||||||
export type Scene<TObject extends ObjectInstance> = {
|
export type Scene = {
|
||||||
objects: Record<string, TObject>;
|
objects: Record<string, ObjectInstance>;
|
||||||
playerObjectId: string | undefined;
|
playerObjectId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameScene = Scene<GameObjectInstance>;
|
|
||||||
export type RuntimeScene = Scene<RuntimeObjectInstance>;
|
|
||||||
export type RuntimeGameScene = Scene<RuntimeGameObjectInstance>;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { ObjectType, VoxelType } from "../../types";
|
import type { ObjectType, VoxelType } from "../../types";
|
||||||
|
|
||||||
export function buildObjectTrimesh(objectType: ObjectType, voxelTypes: Record<string, VoxelType>): [Float32Array, Uint32Array] | null {
|
export function buildObjectTrimesh(objectType: ObjectType, voxelTypes: Record<string, VoxelType>): [Float32Array, Uint32Array] | undefined {
|
||||||
const collidable = objectType.voxels.filter((v) => voxelTypes[v.typeId]?.collidable !== false);
|
const collidable = objectType.voxels.filter((v) => voxelTypes[v.typeId]?.collidable !== false);
|
||||||
|
|
||||||
if (!collidable.length)
|
if (!collidable.length)
|
||||||
return null;
|
return;
|
||||||
|
|
||||||
const n = collidable.length;
|
const n = collidable.length;
|
||||||
const verts = new Float32Array(n * 8 * 3);
|
const verts = new Float32Array(n * 8 * 3);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function triangulatePolygon(polygon: PolygonClipping.Polygon): V2[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildVoxelMesh(voxels: V3[]): BufferGeometry {
|
export function buildVoxelGroupMesh(voxels: V3[]): BufferGeometry {
|
||||||
|
|
||||||
const vertices: V3[] = [];
|
const vertices: V3[] = [];
|
||||||
const normals: V3[] = [];
|
const normals: V3[] = [];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance
|
|||||||
typeId: typeId,
|
typeId: typeId,
|
||||||
physics: true,
|
physics: true,
|
||||||
gravityScale: 1,
|
gravityScale: 1,
|
||||||
|
visible: true,
|
||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0],
|
rotation: [0, 0, 0],
|
||||||
scale: [1, 1, 1],
|
scale: [1, 1, 1],
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { observable, runInAction, reaction, type IReactionDisposer } from "mobx";
|
||||||
|
|
||||||
|
type MapItemOption<TInput, TStored> =
|
||||||
|
TStored extends TInput ? TInput extends TStored
|
||||||
|
? { mapItem?: never }
|
||||||
|
: { mapItem(value: TInput): TStored }
|
||||||
|
: { mapItem(value: TInput): TStored };
|
||||||
|
|
||||||
|
export type ReconcilerMapOptions<TInput, TStored, TKey extends string> =
|
||||||
|
{
|
||||||
|
keyOf(item: TInput): TKey;
|
||||||
|
initializeItem?(item: TStored, source: TInput): void;
|
||||||
|
updateItem?(current: TStored, next: TStored, nextSource: TInput): void;
|
||||||
|
disposeItem?(item: TStored): void;
|
||||||
|
} & MapItemOption<TInput, TStored>;
|
||||||
|
|
||||||
|
export class ReconcilerMap<TInput, TStored = TInput, TKey extends string = string> {
|
||||||
|
private readonly options: ReconcilerMapOptions<TInput, TStored, TKey>;
|
||||||
|
private readonly items = observable.map<TKey, { input: TInput; stored: TStored }>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
options: ReconcilerMapOptions<TInput, TStored, TKey>,
|
||||||
|
) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get values(): readonly TStored[] {
|
||||||
|
return [...this.items.values()].map(e => e.stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get length(): number {
|
||||||
|
return this.items.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(key: TKey): boolean {
|
||||||
|
return this.items.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: TKey): TStored | undefined {
|
||||||
|
return this.items.get(key)?.stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
public *[Symbol.iterator](): Iterator<TStored> {
|
||||||
|
for (const { stored } of this.items.values())
|
||||||
|
yield stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reconcile(nextItems: Iterable<TInput>): void {
|
||||||
|
const nextMap = new Map<TKey, TInput>();
|
||||||
|
|
||||||
|
for (const item of nextItems) {
|
||||||
|
nextMap.set(this.options.keyOf(item), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove disappeared items
|
||||||
|
for (const [key, { stored }] of this.items) {
|
||||||
|
if (!nextMap.has(key)) {
|
||||||
|
runInAction(() => this.items.delete(key));
|
||||||
|
this.options.disposeItem?.(stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or update existing items
|
||||||
|
for (const [key, next] of nextMap) {
|
||||||
|
const existing = this.items.get(key);
|
||||||
|
|
||||||
|
if (existing === undefined) {
|
||||||
|
// New key — always call mapItem (or identity when TStored = TInput)
|
||||||
|
const nextMapped = (this.options.mapItem?.(next) ?? next) as TStored;
|
||||||
|
runInAction(() => this.items.set(key, { input: next, stored: nextMapped }));
|
||||||
|
this.options.initializeItem?.(nextMapped, next);
|
||||||
|
} else if (existing.input !== next) {
|
||||||
|
// Same key, different input reference — call mapItem and update
|
||||||
|
const nextMapped = (this.options.mapItem?.(next) ?? next) as TStored;
|
||||||
|
runInAction(() => this.items.set(key, { input: next, stored: nextMapped }));
|
||||||
|
if (this.options.updateItem?.(existing.stored, nextMapped, next))
|
||||||
|
this.options.disposeItem?.(existing.stored);
|
||||||
|
}
|
||||||
|
// Same key, same input reference — skip mapItem entirely
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
const stored = [...this];
|
||||||
|
runInAction(() => this.items.clear());
|
||||||
|
for (const item of stored)
|
||||||
|
this.options.disposeItem?.(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MobxReconcilerMapOptions<TInput, TStored, TKey extends string> =
|
||||||
|
ReconcilerMapOptions<TInput, TStored, TKey> & { reaction(): Iterable<TInput> };
|
||||||
|
|
||||||
|
export class MobxReconcilerMap<TInput, TStored = TInput, TKey extends string = string> extends ReconcilerMap<TInput, TStored, TKey> {
|
||||||
|
private readonly disposeReaction: IReactionDisposer;
|
||||||
|
|
||||||
|
constructor(options: MobxReconcilerMapOptions<TInput, TStored, TKey>) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
// reaction data fn (tracked) fires only on object list structural changes.
|
||||||
|
// effect fn (untracked) feeds current instances to the reconciler.
|
||||||
|
this.disposeReaction = reaction<TInput[], true>(
|
||||||
|
() => Array.from(options.reaction()),
|
||||||
|
() => { this.reconcile(options.reaction()); },
|
||||||
|
{ fireImmediately: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.disposeReaction();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
||||||
import type { GameObjectInstance, ObjectInstance, ObjectType, RuntimeObjectInstance, V3, World } from "../../types";
|
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, RuntimeObjectType, V3, World } from "../../types";
|
||||||
import { buildObjectTrimesh } from "../graphics/mesh";
|
import { buildObjectTrimesh } from "../graphics/mesh";
|
||||||
|
|
||||||
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
||||||
@@ -18,27 +18,34 @@ export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 }
|
|||||||
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance {
|
export function populateRuntimeObjectType(value: ObjectType, world: World): RuntimeObjectType {
|
||||||
const objectType = world.objectTypes[object.typeId];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...object,
|
...value,
|
||||||
cache: {
|
runtime: {
|
||||||
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
|
voxelGroups: getObjectVoxelGroups(value, world.voxelTypes),
|
||||||
colliderMesh: buildObjectTrimesh(objectType, world.voxelTypes),
|
colliderMesh: buildObjectTrimesh(value, world.voxelTypes),
|
||||||
boundingBox: computeBoundingBox(objectType),
|
boundingBox: computeBoundingBox(value),
|
||||||
updatedAt: 0,
|
updatedAt: 0,
|
||||||
},
|
},
|
||||||
rb: () => null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
export function populateRuntimeObject(value: ObjectInstance, _world: World): RuntimeObjectInstance {
|
||||||
const { cache: _cache, rb: _rb, ...result } = object;
|
return {
|
||||||
|
...value,
|
||||||
|
runtime: {
|
||||||
|
rb: () => null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function depopulateRuntimeObjectType(value: RuntimeObjectType, _world: World): ObjectType {
|
||||||
|
const { runtime: _, ...result } = value;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function isGameObject(obj: ObjectInstance): obj is GameObjectInstance {
|
export function depopulateRuntimeObject(value: RuntimeObjectInstance, _world: World): ObjectInstance {
|
||||||
return 'version' in obj;
|
const { runtime: _, ...result } = value;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user