Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea8f251599 | ||
|
|
7ce9e6dca6 | ||
|
|
b873286dfd | ||
|
|
bf7f803a99 | ||
|
|
21c48e44ee | ||
|
|
0b7cdd9f05 | ||
|
|
a60e01e4d1 | ||
|
|
98e387c490 |
@@ -1,3 +1,8 @@
|
|||||||
|
.editor-layout {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.counter {
|
.counter {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ function HomePage() {
|
|||||||
|
|
||||||
function EditorLayout() {
|
function EditorLayout() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="editor-layout">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<LayeredThreeView />
|
<LayeredThreeView />
|
||||||
<Panels />
|
<Panels />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { ObjectType, RuntimeGameObjectInstance } from "../types";
|
import type { ObjectType, ObjectInstance } from "../types";
|
||||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
||||||
import { Euler, Quaternion, Vector3 } from "three";
|
import { Euler, Quaternion, Vector3 } from "three";
|
||||||
@@ -25,7 +25,7 @@ const _charPos = new Vector3();
|
|||||||
const _lookAt = new Vector3();
|
const _lookAt = new Vector3();
|
||||||
|
|
||||||
type PlayerObjectViewProps = {
|
type PlayerObjectViewProps = {
|
||||||
object: RuntimeGameObjectInstance;
|
object: ObjectInstance;
|
||||||
objectType: ObjectType;
|
objectType: ObjectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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 { Scene } from "../types";
|
||||||
|
|
||||||
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: Scene, editMode?: boolean }) {
|
export const DebugLabelsView = observer(function ({ scene, editMode }: { scene: Scene, editMode?: boolean }) {
|
||||||
useEffect(
|
useEffect(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
import { ObjectViewInternal } from "./ObjectViewInternal";
|
import { ObjectViewInternal } from "./ObjectViewInternal";
|
||||||
import { PlayerObjectView } from "./CharacterView";
|
import { PlayerObjectView } from "./CharacterView";
|
||||||
|
import type { ObjectInstance } from "../types";
|
||||||
|
|
||||||
export type GameObjectViewProps = {
|
export type GameObjectViewProps = {
|
||||||
object: ObjectInstance;
|
object: ObjectInstance;
|
||||||
|
|||||||
@@ -94,9 +94,6 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
|
|||||||
if (!objectType)
|
if (!objectType)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Track cache so observer re-renders when refreshObjectTypeCaches is called
|
|
||||||
void object.cache.updatedAt;
|
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (state.worldEditor.isEnabled) {
|
if (state.worldEditor.isEnabled) {
|
||||||
// Reading selection state inside an event handler: not tracked by observer.
|
// Reading selection state inside an event handler: not tracked by observer.
|
||||||
|
|||||||
@@ -1,27 +1,12 @@
|
|||||||
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
import type { ObjectInstance, ObjectType } from "../types";
|
||||||
import type { VoxelGroup } from "../types/runtime/voxelGroup";
|
import { useEffect, useImperativeHandle, useRef, type Ref } 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";
|
||||||
import type { ThreeEvent } from "@react-three/fiber";
|
import type { ThreeEvent } from "@react-three/fiber";
|
||||||
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier";
|
import { RapierRigidBody } from "@react-three/rapier";
|
||||||
import { SyncRigidBody } from "./SyncRigidBody";
|
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 { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
|
|
||||||
|
|
||||||
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg, opacity }: { vg: VoxelGroup, opacity: number }) {
|
|
||||||
const geo = useMemo(() => buildVoxelGroupMesh(vg.positions), [vg]);
|
|
||||||
useEffect(() => () => { geo.dispose(); }, [geo]);
|
|
||||||
|
|
||||||
const op = vg.opacity * opacity;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh geometry={geo} userData={{ baseOpacity: vg.opacity }}>
|
|
||||||
<meshStandardMaterial color={vg.color} opacity={op} transparent={op < 1} />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ObjectViewInternalHandle = {
|
export type ObjectViewInternalHandle = {
|
||||||
group: Group | null;
|
group: Group | null;
|
||||||
@@ -71,8 +56,6 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
[object, object.id, props.isPlayer]
|
[object, object.id, props.isPlayer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const gameObj = object as RuntimeGameObjectInstance;
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
return reaction(
|
return reaction(
|
||||||
@@ -98,8 +81,8 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
runInAction(() => { gameObj.rb = () => rbRef.current; });
|
runInAction(() => { object.rb = () => rbRef.current; });
|
||||||
return () => { runInAction(() => { gameObj.rb = () => null; }); };
|
return () => { runInAction(() => { object.rb = () => null; }); };
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[object.id],
|
[object.id],
|
||||||
@@ -130,9 +113,11 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||||||
return;
|
return;
|
||||||
const otherObj = Object.values(game.scene.objects)
|
const otherObj = Object.values(game.scene.objects)
|
||||||
.find((o) => o.rb()?.handle === otherHandle);
|
.find((o) => o.rb()?.handle === otherHandle);
|
||||||
game.emitEvent('objectTouchStart', { object: gameObj, other: otherObj });
|
|
||||||
|
const context = { object: { ...object, typeId: objectType.id }, other: otherObj };
|
||||||
|
game.emitEvent('objectTouchStart', context);
|
||||||
if (otherObj?.id === game.scene.playerObjectId)
|
if (otherObj?.id === game.scene.playerObjectId)
|
||||||
game.emitEvent('playerTouchStart', { object: gameObj, other: otherObj });
|
game.emitEvent('playerTouchStart', context);
|
||||||
}}
|
}}
|
||||||
onSync={(data) => {
|
onSync={(data) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.overlay-panels {
|
.overlay-panels {
|
||||||
z-index: 15000;
|
z-index: 9000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -4,49 +4,32 @@ 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';
|
||||||
import { MOUSE, type OrthographicCamera, type PerspectiveCamera } from 'three';
|
import { MOUSE } from 'three';
|
||||||
import { CameraSync } from './tools/CameraSync';
|
import { useSyncCamera } from '../utils/hooks/useSyncCamera';
|
||||||
import { Physics } from '@react-three/rapier';
|
import { Physics } from '@react-three/rapier';
|
||||||
|
|
||||||
|
|
||||||
export const SceneEditorView = observer(function () {
|
export const SceneEditorView = observer(function () {
|
||||||
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
||||||
|
|
||||||
const cameraAtStart = useRef<string>('');
|
useSyncCamera(() => state.view.camera.camera);
|
||||||
|
|
||||||
function cameraSnapshot(camera: OrthographicCamera | PerspectiveCamera): string {
|
|
||||||
return JSON.stringify({
|
|
||||||
position: camera.position,
|
|
||||||
rotation: camera.rotation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStart = () => {
|
|
||||||
const controls = controlsRef.current;
|
|
||||||
if (!controls)
|
|
||||||
return;
|
|
||||||
cameraAtStart.current = cameraSnapshot(controls.object);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnd = () => {
|
const handleEnd = () => {
|
||||||
const controls = controlsRef.current;
|
const controls = controlsRef.current;
|
||||||
if (!controls)
|
if (!controls)
|
||||||
return;
|
return;
|
||||||
const snapshot = cameraSnapshot(controls.object);
|
|
||||||
if (snapshot === cameraAtStart.current)
|
|
||||||
return;
|
|
||||||
const [x, y, z] = controls.object.rotation.toArray();
|
const [x, y, z] = controls.object.rotation.toArray();
|
||||||
state.worldEditor.setCamera({
|
state.view.camera.camera = {
|
||||||
position: controls.object.position.toArray(),
|
position: controls.object.position.toArray(),
|
||||||
look: [x, y, z],
|
look: [x, y, z],
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
ref={controlsRef}
|
ref={controlsRef}
|
||||||
onStart={handleStart}
|
|
||||||
onEnd={handleEnd}
|
onEnd={handleEnd}
|
||||||
makeDefault
|
makeDefault
|
||||||
mouseButtons={{
|
mouseButtons={{
|
||||||
@@ -57,7 +40,6 @@ export const SceneEditorView = observer(function () {
|
|||||||
rotateSpeed={0.2}
|
rotateSpeed={0.2}
|
||||||
enableDamping={false}
|
enableDamping={false}
|
||||||
/>
|
/>
|
||||||
<CameraSync camera={state.worldEditor.camera} />
|
|
||||||
{/* <Grid
|
{/* <Grid
|
||||||
sectionThickness={0.5}
|
sectionThickness={0.5}
|
||||||
cellThickness={0}
|
cellThickness={0}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
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";
|
||||||
|
import type { Scene } from "../types";
|
||||||
|
|
||||||
type SceneViewProps = {
|
type SceneViewProps = {
|
||||||
scene: RuntimeScene,
|
scene: Scene,
|
||||||
isEditor?: boolean;
|
isEditor?: boolean;
|
||||||
debugOverlay?: boolean;
|
debugOverlay?: boolean;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export const SceneView = observer(function (props: SceneViewProps) {
|
|||||||
Object.values(props.scene.objects)
|
Object.values(props.scene.objects)
|
||||||
.map((obj) => (
|
.map((obj) => (
|
||||||
!props.isEditor
|
!props.isEditor
|
||||||
? <GameObjectView key={obj.id} object={obj as RuntimeGameObjectInstance} isPlayer={props.scene.playerObjectId === obj.id} isSceneEditor={props.isEditor} />
|
? <GameObjectView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} isSceneEditor={props.isEditor} />
|
||||||
: <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
|
: <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { SceneEditorView } from './SceneEditorView';
|
|||||||
import { JoystickView } from './JoystickView';
|
import { JoystickView } from './JoystickView';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { runInAction } from 'mobx';
|
import { runInAction } from 'mobx';
|
||||||
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
export const ThreeView = observer(function () {
|
export const ThreeView = observer(function () {
|
||||||
const isGame = !!state.game;
|
const isGame = !!state.game;
|
||||||
@@ -39,23 +38,7 @@ export const ThreeView = observer(function () {
|
|||||||
{isGame ? <GameView /> : <SceneEditorView />}
|
{isGame ? <GameView /> : <SceneEditorView />}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
{isGame && <JoystickView />}
|
{isGame && <JoystickView />}
|
||||||
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, fontSize: 14 }}>
|
|
||||||
{
|
|
||||||
state.game
|
|
||||||
? <>
|
|
||||||
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
|
|
||||||
{
|
|
||||||
state.game!.isPaused
|
|
||||||
? <>
|
|
||||||
<button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
|
|
||||||
<button onClick={() => state.game!.step()}><IconPlayerSkipForwardFilled size="1em" /></button>
|
|
||||||
</>
|
|
||||||
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
: <button onClick={() => state.startGame()}><IconPlayerPlayFilled size="1em" /></button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</KeyboardControls>
|
</KeyboardControls>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { measuredRenderTime } from "../../utils/layerTimings";
|
||||||
|
|
||||||
export const DebugLabelsLayer = observer(function () {
|
export const DebugLabelsLayer = observer(measuredRenderTime("DebugLabels", function () {
|
||||||
return null; //TODO
|
return null; //TODO
|
||||||
});
|
}));
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ObjectGeomertiesLayer } from "./ObjectGeomertiesLayer";
|
import { ObjectGeometriesLayer } from "./ObjectGeometriesLayer";
|
||||||
import { Canvas } from "@react-three/fiber";
|
import { Canvas } from "@react-three/fiber";
|
||||||
import { Perf } from "r3f-perf";
|
import { SceneEditorCameraLayer } from "./SceneEditor/SceneEditorGameraLayer";
|
||||||
import { state } from "../../state";
|
import { SceneEditorObjectSelectionLayer } from "./SceneEditor/SceneEditorObjectSelectionLayer";
|
||||||
import { CameraLayer } from "./GameraLayer";
|
import { LightsLayer } from "./LighsLayer";
|
||||||
|
import { StatsLayer } from './StatsLayer';
|
||||||
|
import { SceneEditorObjectGizmosLayer } from "./SceneEditor/SceneEditorObjectGizmosLayer";
|
||||||
|
import { PlayButtons } from "./PlayButtons";
|
||||||
|
import { SceneEditorPlayerGizmosLayer } from "./SceneEditor/SceneEditorPlayerGizmosLayer";
|
||||||
|
|
||||||
export const LayeredThreeView = observer(function () {
|
export const LayeredThreeView = observer(function () {
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Canvas
|
<Canvas>
|
||||||
// camera={state.world.character.camera}
|
<LightsLayer />
|
||||||
>
|
<SceneEditorCameraLayer />
|
||||||
<ambientLight intensity={0.5} />
|
<ObjectGeometriesLayer>
|
||||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
<SceneEditorObjectSelectionLayer />
|
||||||
|
<SceneEditorObjectGizmosLayer />
|
||||||
<Perf position="bottom-right" />
|
</ObjectGeometriesLayer>
|
||||||
|
<SceneEditorPlayerGizmosLayer />
|
||||||
<CameraLayer camera={state.worldEditor.camera} />
|
<StatsLayer />
|
||||||
<ObjectGeomertiesLayer />
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
<PlayButtons />
|
||||||
</>;
|
</>;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { measuredRenderTime } from "../../utils/layerTimings";
|
||||||
|
|
||||||
|
export const LightsLayer = observer(measuredRenderTime("Lights", function () {
|
||||||
|
return <>
|
||||||
|
<ambientLight intensity={0.5} />
|
||||||
|
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||||
|
</>;
|
||||||
|
}));
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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;
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { createContext, useContext, useEffect, useRef } from "react";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import { ObjectGeometryList } from "../../rendering/ObjectGeometryList";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { state } from "../../state";
|
||||||
|
import { useLayerFrame } from "../../utils/hooks/useLayerFrame";
|
||||||
|
import { measuredRenderTime } from "../../utils/layerTimings";
|
||||||
|
|
||||||
|
export const ObjectGeometryListContext = createContext<React.RefObject<ObjectGeometryList | null>>(
|
||||||
|
{ current: null },
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useObjectGeometryList() {
|
||||||
|
return useContext(ObjectGeometryListContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ObjectGeometriesLayer = observer(measuredRenderTime("ObjectGeometries", function ({ children }: { children?: React.ReactNode }) {
|
||||||
|
const typeGeometries = state.view.objectTypeGeometries;
|
||||||
|
|
||||||
|
const threeScene = useThree(s => s.scene);
|
||||||
|
const list = useRef<ObjectGeometryList | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const l = new ObjectGeometryList(threeScene, typeGeometries);
|
||||||
|
list.current = l;
|
||||||
|
return () => {
|
||||||
|
l.dispose();
|
||||||
|
list.current = null;
|
||||||
|
};
|
||||||
|
}, [threeScene, typeGeometries]);
|
||||||
|
|
||||||
|
useLayerFrame("ObjectGeometries", () => list.current?.tick());
|
||||||
|
|
||||||
|
return <ObjectGeometryListContext.Provider value={list}>
|
||||||
|
{children}
|
||||||
|
</ObjectGeometryListContext.Provider>;
|
||||||
|
}));
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { state } from "../../state";
|
||||||
|
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export const PlayButtons = observer(function () {
|
||||||
|
return <div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, fontSize: 14 }}>
|
||||||
|
{
|
||||||
|
state.game
|
||||||
|
? <>
|
||||||
|
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
|
||||||
|
{
|
||||||
|
state.game!.isPaused
|
||||||
|
? <>
|
||||||
|
<button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
|
||||||
|
<button onClick={() => state.game!.step()}><IconPlayerSkipForwardFilled size="1em" /></button>
|
||||||
|
</>
|
||||||
|
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
: <button onClick={() => state.startGame()}><IconPlayerPlayFilled size="1em" /></button>
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { state } from "../../../state";
|
||||||
|
import type { ThreeEvent } from "@react-three/fiber";
|
||||||
|
import type { ObjectGeometryViewItem } from "../../../state/viewLayers";
|
||||||
|
|
||||||
|
export function ObjectClickTarget({ item }: { item: ObjectGeometryViewItem }) {
|
||||||
|
const obj = item.object;
|
||||||
|
const bb = item.objectTypeGeometry.boundingBox;
|
||||||
|
|
||||||
|
const size = useMemo(() => {
|
||||||
|
if (!bb)
|
||||||
|
return [1, 1, 1] as const;
|
||||||
|
|
||||||
|
return [bb.max[0] - bb.min[0], bb.max[1] - bb.min[1], bb.max[2] - bb.min[2]] as const;
|
||||||
|
}, [bb]);
|
||||||
|
|
||||||
|
const center = useMemo(() => {
|
||||||
|
if (!bb)
|
||||||
|
return [0.5, 0.5, 0.5] as const;
|
||||||
|
|
||||||
|
return [
|
||||||
|
(bb.min[0] + bb.max[0]) / 2,
|
||||||
|
(bb.min[1] + bb.max[1]) / 2,
|
||||||
|
(bb.min[2] + bb.max[2]) / 2,
|
||||||
|
] as const;
|
||||||
|
}, [bb]);
|
||||||
|
|
||||||
|
const handleClick = (e: ThreeEvent<MouseEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
state.worldEditor.setSelectedObject({ id: obj.id, editMode: "translate" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return <mesh
|
||||||
|
position={obj.position}
|
||||||
|
rotation={obj.rotation}
|
||||||
|
scale={obj.scale}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<mesh position={center}>
|
||||||
|
<boxGeometry args={size} />
|
||||||
|
<meshBasicMaterial visible={false} />
|
||||||
|
</mesh>
|
||||||
|
</mesh>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { OrbitControls } from "@react-three/drei";
|
||||||
|
import { state } from "../../../state";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useSyncCamera } from "../../../utils/hooks/useSyncCamera";
|
||||||
|
import { MOUSE } from "three";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import type { V3 } from "../../../types";
|
||||||
|
import { measuredRenderTime } from "../../../utils/layerTimings";
|
||||||
|
|
||||||
|
export const SceneEditorCameraLayer = observer(measuredRenderTime("Camera", function () {
|
||||||
|
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
||||||
|
|
||||||
|
useSyncCamera(() => state.view.camera?.camera);
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
const controls = controlsRef.current;
|
||||||
|
const camera = state.view.camera;
|
||||||
|
if (!controls || !camera)
|
||||||
|
return;
|
||||||
|
|
||||||
|
camera.camera = {
|
||||||
|
position: controls.object.position.toArray(),
|
||||||
|
look: controls.object.rotation.toArray().slice(0, 3) as V3,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!state.view.camera)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return <OrbitControls
|
||||||
|
ref={controlsRef}
|
||||||
|
onEnd={handleEnd}
|
||||||
|
makeDefault
|
||||||
|
mouseButtons={{
|
||||||
|
LEFT: MOUSE.ROTATE,
|
||||||
|
MIDDLE: MOUSE.PAN,
|
||||||
|
RIGHT: MOUSE.PAN,
|
||||||
|
}}
|
||||||
|
rotateSpeed={0.2}
|
||||||
|
enableDamping={false}
|
||||||
|
/>;
|
||||||
|
}));
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { TransformControls } from "@react-three/drei";
|
||||||
|
import { state } from "../../../state";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { measuredRenderTime } from "../../../utils/layerTimings";
|
||||||
|
import type { V3, R3 } from "../../../types";
|
||||||
|
|
||||||
|
export const SceneEditorObjectGizmosLayer = observer(measuredRenderTime(
|
||||||
|
"ObjectGizmos",
|
||||||
|
function () {
|
||||||
|
const selection = state.view.selectedObject;
|
||||||
|
|
||||||
|
const onObjectChange = useCallback((e?: THREE.Event) => {
|
||||||
|
if (!selection || !e)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const controlled = (e.target as { object?: THREE.Object3D }).object;
|
||||||
|
if (!controlled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
selection.geometry.setObjectTransform(
|
||||||
|
controlled.position.toArray() as V3,
|
||||||
|
controlled.rotation.toArray().slice(0, 3) as R3,
|
||||||
|
controlled.scale.toArray() as V3,
|
||||||
|
);
|
||||||
|
}, [selection]);
|
||||||
|
|
||||||
|
if (!selection)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const obj = selection.geometry.object;
|
||||||
|
|
||||||
|
return <TransformControls
|
||||||
|
mode={selection.mode}
|
||||||
|
position={obj.position}
|
||||||
|
rotation={obj.rotation}
|
||||||
|
scale={obj.scale}
|
||||||
|
onObjectChange={onObjectChange}
|
||||||
|
/>;
|
||||||
|
},
|
||||||
|
));
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useCallback, useRef } from "react";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { state } from "../../../state";
|
||||||
|
import { nextObjectEditMode } from "../../../state/worldEditorState";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { SelectionBoundingBox } from "./SelectionBoundingBox";
|
||||||
|
import { measuredRenderTime } from "../../../utils/layerTimings";
|
||||||
|
import { useObjectGeometryList } from "../ObjectGeometriesLayer";
|
||||||
|
|
||||||
|
const DRAG_THRESHOLD = 4;
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
const pointer = new THREE.Vector2();
|
||||||
|
|
||||||
|
export const SceneEditorObjectSelectionLayer = observer(measuredRenderTime(
|
||||||
|
"ObjectSelection",
|
||||||
|
function () {
|
||||||
|
const geometryListRef = useObjectGeometryList();
|
||||||
|
|
||||||
|
const selection = state.view.selectedObject;
|
||||||
|
const { camera, gl, size } = useThree();
|
||||||
|
const pointerDownPos = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback((e: PointerEvent) => {
|
||||||
|
if (e.button !== 0)
|
||||||
|
return;
|
||||||
|
pointerDownPos.current = { x: e.offsetX, y: e.offsetY };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePointerUp = useCallback((e: PointerEvent) => {
|
||||||
|
if (e.button !== 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const down = pointerDownPos.current;
|
||||||
|
pointerDownPos.current = null;
|
||||||
|
if (!down)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const dx = e.offsetX - down.x;
|
||||||
|
const dy = e.offsetY - down.y;
|
||||||
|
if (dx * dx + dy * dy > DRAG_THRESHOLD * DRAG_THRESHOLD)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pointer.x = (e.offsetX / size.width) * 2 - 1;
|
||||||
|
pointer.y = -(e.offsetY / size.height) * 2 + 1;
|
||||||
|
|
||||||
|
raycaster.setFromCamera(pointer, camera);
|
||||||
|
|
||||||
|
const instancedMeshes = state.view.objectTypeGeometries.values.filter((ot) => ot.meshes?.length).flatMap((ot) => ot.meshes!);
|
||||||
|
|
||||||
|
const intersections = raycaster.intersectObjects(instancedMeshes, false);
|
||||||
|
|
||||||
|
if (intersections.length === 0) {
|
||||||
|
state.worldEditor.resetSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hit = intersections[0];
|
||||||
|
|
||||||
|
const instanceId = hit.instanceId;
|
||||||
|
if (instanceId === undefined) {
|
||||||
|
state.worldEditor.resetSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometryList = geometryListRef.current;
|
||||||
|
if (!geometryList)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const mesh = hit.object as THREE.InstancedMesh;
|
||||||
|
const instanceGeometry = geometryList.getObjectGeometryByMesh(mesh, instanceId);
|
||||||
|
if (instanceGeometry) {
|
||||||
|
const sel = state.worldEditor.selection;
|
||||||
|
const editMode = sel?.type === 'object' && sel.id === instanceGeometry.object.id
|
||||||
|
? nextObjectEditMode(sel.editMode)
|
||||||
|
: 'translate';
|
||||||
|
state.worldEditor.setSelectedObject({ id: instanceGeometry.object.id, editMode });
|
||||||
|
} else {
|
||||||
|
state.worldEditor.resetSelection();
|
||||||
|
}
|
||||||
|
}, [camera, size, geometryListRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const domElement = gl.domElement;
|
||||||
|
domElement.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
domElement.addEventListener("pointerup", handlePointerUp);
|
||||||
|
return () => {
|
||||||
|
domElement.removeEventListener("pointerdown", handlePointerDown);
|
||||||
|
domElement.removeEventListener("pointerup", handlePointerUp);
|
||||||
|
};
|
||||||
|
}, [gl, handlePointerDown, handlePointerUp]);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{selection &&
|
||||||
|
<SelectionBoundingBox item={selection.geometry} />
|
||||||
|
}
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
));
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { useThree, useFrame } from "@react-three/fiber";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { state } from "../../../state";
|
||||||
|
import { computeBoundingBox } from "../../../utils/runtime";
|
||||||
|
|
||||||
|
const _offset = new THREE.Vector3();
|
||||||
|
const _lookAt = new THREE.Vector3();
|
||||||
|
const _euler = new THREE.Euler(0, 0, 0, 'YXZ');
|
||||||
|
|
||||||
|
export const SceneEditorPlayerGizmosLayer = observer(function () {
|
||||||
|
const scene = useThree(s => s.scene);
|
||||||
|
const helperRef = useRef<THREE.CameraHelper | null>(null);
|
||||||
|
|
||||||
|
const playerCam = useMemo(() => new THREE.PerspectiveCamera(75, 16 / 9, 0.1, 1000), []);
|
||||||
|
|
||||||
|
const playerObjectId = state.world.data.initialScene.playerObjectId;
|
||||||
|
const playerObject = playerObjectId
|
||||||
|
? state.world.data.initialScene.objects[playerObjectId]
|
||||||
|
: undefined;
|
||||||
|
const playerType = playerObject
|
||||||
|
? state.world.data.objectTypes[playerObject.typeId]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!playerObject || !playerType)
|
||||||
|
return;
|
||||||
|
const helper = new THREE.CameraHelper(playerCam);
|
||||||
|
scene.add(helper);
|
||||||
|
helperRef.current = helper;
|
||||||
|
return () => {
|
||||||
|
scene.remove(helper);
|
||||||
|
helper.dispose();
|
||||||
|
helperRef.current = null;
|
||||||
|
};
|
||||||
|
}, [playerObject, playerType, playerCam, scene]);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!playerObject || !playerType || !helperRef.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const bb = computeBoundingBox(playerType);
|
||||||
|
const W = bb.max[0] - bb.min[0];
|
||||||
|
const H = bb.max[1] - bb.min[1];
|
||||||
|
const D = bb.max[2] - bb.min[2];
|
||||||
|
const radius = Math.sqrt(W * W + H * H + D * D);
|
||||||
|
const centerY = (bb.min[1] + bb.max[1]) / 2;
|
||||||
|
|
||||||
|
const yaw = playerObject.rotation[1];
|
||||||
|
|
||||||
|
_offset.set(W * 0.1, centerY + H * 0.3, bb.max[2] + radius);
|
||||||
|
_offset.applyEuler(_euler.set(0, yaw, 0, 'YXZ'));
|
||||||
|
|
||||||
|
playerCam.position.set(
|
||||||
|
playerObject.position[0] + _offset.x,
|
||||||
|
playerObject.position[1] + _offset.y,
|
||||||
|
playerObject.position[2] + _offset.z,
|
||||||
|
);
|
||||||
|
_lookAt.set(
|
||||||
|
playerObject.position[0],
|
||||||
|
playerObject.position[1] + centerY,
|
||||||
|
playerObject.position[2],
|
||||||
|
);
|
||||||
|
playerCam.lookAt(_lookAt);
|
||||||
|
playerCam.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
helperRef.current.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import type { ObjectGeometryViewItem } from "../../../state/viewLayers";
|
||||||
|
|
||||||
|
export const SelectionBoundingBox = observer(function({ item }: { item: ObjectGeometryViewItem }) {
|
||||||
|
const obj = item.object;
|
||||||
|
const bb = item.objectTypeGeometry.boundingBox;
|
||||||
|
|
||||||
|
const geometry = useMemo(() => {
|
||||||
|
if (!bb)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
const box = new THREE.BoxGeometry(
|
||||||
|
bb.max[0] - bb.min[0],
|
||||||
|
bb.max[1] - bb.min[1],
|
||||||
|
bb.max[2] - bb.min[2],
|
||||||
|
);
|
||||||
|
return new THREE.EdgesGeometry(box);
|
||||||
|
}, [bb]);
|
||||||
|
|
||||||
|
const center = useMemo(() => {
|
||||||
|
if (!bb)
|
||||||
|
return [0.5, 0.5, 0.5] as const;
|
||||||
|
|
||||||
|
return [
|
||||||
|
(bb.min[0] + bb.max[0]) / 2,
|
||||||
|
(bb.min[1] + bb.max[1]) / 2,
|
||||||
|
(bb.min[2] + bb.max[2]) / 2,
|
||||||
|
] as const;
|
||||||
|
}, [bb]);
|
||||||
|
|
||||||
|
if (!geometry)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return <mesh
|
||||||
|
position={obj.position}
|
||||||
|
rotation={obj.rotation}
|
||||||
|
scale={obj.scale}
|
||||||
|
>
|
||||||
|
<lineSegments position={center} geometry={geometry}>
|
||||||
|
<lineBasicMaterial color="#ffff00" />
|
||||||
|
</lineSegments>
|
||||||
|
</mesh>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import { layerTimings } from "../../utils/layerTimings";
|
||||||
|
|
||||||
|
export const StatsLayer = function () {
|
||||||
|
const divRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const fpsFrames = useRef(0);
|
||||||
|
const fpsLastTime = useRef(0);
|
||||||
|
const renderAcc = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fpsLastTime.current = performance.now();
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
Object.assign(div.style, {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "8px",
|
||||||
|
right: "8px",
|
||||||
|
color: "#0f0",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
zIndex: "9999",
|
||||||
|
pointerEvents: "none",
|
||||||
|
whiteSpace: "pre",
|
||||||
|
});
|
||||||
|
document.body.appendChild(div);
|
||||||
|
divRef.current = div;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(div);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame(({ gl, scene, camera }) => {
|
||||||
|
const renderStart = performance.now();
|
||||||
|
gl.render(scene, camera);
|
||||||
|
renderAcc.current += performance.now() - renderStart;
|
||||||
|
|
||||||
|
fpsFrames.current++;
|
||||||
|
const now = performance.now();
|
||||||
|
const elapsed = now - fpsLastTime.current;
|
||||||
|
if (elapsed >= 1000) {
|
||||||
|
const frames = fpsFrames.current;
|
||||||
|
const fps = Math.round(frames * 1000 / elapsed);
|
||||||
|
const renderAvg = (renderAcc.current / frames).toFixed(3);
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
`${fps} FPS gl.render ${renderAvg} ms`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(layerTimings)) {
|
||||||
|
const frameTime = entry.frame.frames > 0 ? (entry.frame.acc / entry.frame.frames) : 0;
|
||||||
|
const renderTime = entry.render.frames > 0 ? (entry.render.acc / entry.render.frames) : 0;
|
||||||
|
lines.push(
|
||||||
|
`${name} ${(renderTime + frameTime).toFixed(3)} ms` +
|
||||||
|
(frameTime > 0
|
||||||
|
? ` (${frameTime.toFixed(3)} frame)`
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
entry.frame.acc = 0;
|
||||||
|
entry.frame.frames = 0;
|
||||||
|
entry.render.acc = 0;
|
||||||
|
entry.render.frames = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (divRef.current) {
|
||||||
|
divRef.current.textContent = lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsFrames.current = 0;
|
||||||
|
renderAcc.current = 0;
|
||||||
|
fpsLastTime.current = now;
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { useThree } from "@react-three/fiber";
|
|
||||||
import type { Pos3 } from "../../types";
|
|
||||||
import { useLayoutEffect } from "react";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
export const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
|
||||||
const { camera: threeCamera } = useThree();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
|
|
||||||
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
|
|
||||||
threeCamera.updateProjectionMatrix();
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { toJS } from "mobx";
|
import { toJS } from "mobx";
|
||||||
import type { Game, Scene, 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 { ScriptGameEventBus } from "../scripts/eventBus";
|
import { ScriptGameEventBus } from "../scripts/eventBus";
|
||||||
import * as ScriptActions from "../scripts/actions";
|
import * as ScriptActions from "../scripts/actions";
|
||||||
import { positionOfObject } from "../scripts/positionOfObject";
|
import { positionOfObject } from "../scripts/positionOfObject";
|
||||||
@@ -19,7 +18,7 @@ import type { ScriptCoroutines } from "../scripts/coroutines";
|
|||||||
export class GameFactory {
|
export class GameFactory {
|
||||||
|
|
||||||
public static create(world: World): Game {
|
public static create(world: World): Game {
|
||||||
const scene = populateRuntimeScene(clone(world.initialScene), world);
|
const scene = clone(world.initialScene);
|
||||||
|
|
||||||
const game = {
|
const game = {
|
||||||
paused: false,
|
paused: false,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { type RuntimeWorld, type World } from "../types";
|
import { type World } from "../types";
|
||||||
import { depopulateRuntimeWorld, populateRuntimeWorld } from "../utils/runtime";
|
|
||||||
import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes";
|
import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes";
|
||||||
|
|
||||||
export class WorldFactory {
|
export class WorldFactory {
|
||||||
|
|
||||||
public static create(): RuntimeWorld {
|
public static create(): World {
|
||||||
return populateRuntimeWorld({
|
return {
|
||||||
objectTypes: {},
|
objectTypes: {},
|
||||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||||
initialScene: {
|
initialScene: {
|
||||||
@@ -19,17 +18,17 @@ export class WorldFactory {
|
|||||||
position: [0, 5, 10],
|
position: [0, 5, 10],
|
||||||
look: [0, 0, 0],
|
look: [0, 0, 0],
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static load(): RuntimeWorld | undefined {
|
public static load(): World | undefined {
|
||||||
const json = localStorage.getItem("world");
|
const json = localStorage.getItem("world");
|
||||||
if (json) {
|
if (json) {
|
||||||
return populateRuntimeWorld(JSON.parse(json) as World);
|
return JSON.parse(json) as World;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static save(world: RuntimeWorld): void {
|
public static save(world: World): void {
|
||||||
localStorage.setItem("world", JSON.stringify(depopulateRuntimeWorld(world)));
|
localStorage.setItem("world", JSON.stringify(world));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,28 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { MobxReconcilerMap, type ReconcilerMap } from '../utils/reconciler';
|
import { MobxReconcilerMap, type ReconcilerMap } from '../utils/reconciler';
|
||||||
import type { ObjectGeometryViewItem, ObjectTypeGeometryViewItem } from '../state/viewState';
|
import type { ObjectGeometryViewItem, ObjectTypeGeometryViewItem } from '../state/viewLayers';
|
||||||
|
|
||||||
export class ObjectGeometryList {
|
export class ObjectGeometryList {
|
||||||
private items: ReconcilerMap<ObjectGeometryViewItem>;
|
private items: ReconcilerMap<ObjectTypeGeometryViewItem>;
|
||||||
private readonly typeRefCounts = new Map<string, number>();
|
|
||||||
|
private readonly instancesByMesh = new Map<THREE.InstancedMesh, ObjectGeometryViewItem[]>();
|
||||||
|
|
||||||
private readonly matrixObj = new THREE.Object3D();
|
private readonly matrixObj = new THREE.Object3D();
|
||||||
private readonly _p = new THREE.Vector3();
|
private readonly _p = new THREE.Vector3();
|
||||||
private readonly _e = new THREE.Euler();
|
private readonly _e = new THREE.Euler();
|
||||||
private readonly _q = new THREE.Quaternion();
|
private readonly _q = new THREE.Quaternion();
|
||||||
|
|
||||||
constructor(scene: THREE.Scene, geometiries: Iterable<ObjectGeometryViewItem>) {
|
constructor(scene: THREE.Scene, typeGeometries: Iterable<ObjectTypeGeometryViewItem>) {
|
||||||
this.items = new MobxReconcilerMap<ObjectGeometryViewItem>({
|
this.items = new MobxReconcilerMap<ObjectTypeGeometryViewItem>({
|
||||||
reaction: () => geometiries,
|
reaction: () => typeGeometries,
|
||||||
keyOf: (item) => item.object.id,
|
keyOf: (item) => item.objectType.id,
|
||||||
initializeItem: (item) => {
|
initializeItem: (item) => {
|
||||||
const typeId = item.objectTypeGeometry.objectType.id;
|
for (const mesh of item.meshes ?? [])
|
||||||
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);
|
scene.add(mesh);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
disposeItem: (item) => {
|
disposeItem: (item) => {
|
||||||
const typeId = item.objectTypeGeometry.objectType.id;
|
for (const mesh of item.meshes ?? [])
|
||||||
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);
|
scene.remove(mesh);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,37 +31,37 @@ export class ObjectGeometryList {
|
|||||||
this.items?.dispose();
|
this.items?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called once per frame — reads MobX observables directly outside any reactive
|
getObjectGeometryByMesh(mesh: THREE.InstancedMesh, instanceId: number): ObjectGeometryViewItem | undefined {
|
||||||
// context, so no subscriptions are created and no re-renders are triggered.
|
return this.instancesByMesh.get(mesh)?.[instanceId];
|
||||||
tick(): void {
|
}
|
||||||
const touchedObjectTypes: Record<string, { typeGeometry: ObjectTypeGeometryViewItem, instanceCount: number }> = {};
|
|
||||||
|
|
||||||
this.items.values.forEach((item) => {
|
tick(): void {
|
||||||
|
this.instancesByMesh.clear();
|
||||||
|
|
||||||
|
this.items.values.forEach((typeGeometry) => {
|
||||||
|
const instances = typeGeometry.instances;
|
||||||
|
const meshes = typeGeometry.meshes ?? [];
|
||||||
|
|
||||||
|
instances.forEach((item, instanceIdx) => {
|
||||||
const obj = item.object;
|
const obj = item.object;
|
||||||
|
|
||||||
this._p.set(obj.position[0], obj.position[1], obj.position[2]);
|
this._p.set(...obj.position);
|
||||||
this._e.set(obj.rotation[0], obj.rotation[1], obj.rotation[2]);
|
this._e.set(...obj.rotation);
|
||||||
this._q.setFromEuler(this._e);
|
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.position.copy(this._p);
|
||||||
this.matrixObj.quaternion.copy(this._q);
|
this.matrixObj.quaternion.copy(this._q);
|
||||||
this.matrixObj.scale.set(obj.scale[0], obj.scale[1], obj.scale[2]);
|
this.matrixObj.scale.set(...obj.scale);
|
||||||
this.matrixObj.updateMatrix();
|
this.matrixObj.updateMatrix();
|
||||||
|
|
||||||
|
for (const mesh of meshes)
|
||||||
mesh.setMatrixAt(instanceIdx, this.matrixObj.matrix);
|
mesh.setMatrixAt(instanceIdx, this.matrixObj.matrix);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const objectType of Object.values(touchedObjectTypes)) {
|
for (const mesh of meshes) {
|
||||||
for (const mesh of objectType.typeGeometry.meshes ?? []) {
|
this.instancesByMesh.set(mesh, typeGeometry.instances);
|
||||||
mesh.count = objectType.instanceCount;
|
mesh.count = instances.length;
|
||||||
mesh.instanceMatrix.needsUpdate = true;
|
mesh.instanceMatrix.needsUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { RuntimeGameObjectInstance, V3 } from "../types";
|
import type { ObjectInstance, V3 } from "../types";
|
||||||
|
|
||||||
export function positionOfObject(obj: RuntimeGameObjectInstance): V3 {
|
export function positionOfObject(obj: ObjectInstance): V3 {
|
||||||
const t = obj.rb()?.translation();
|
const t = obj.rb()?.translation();
|
||||||
const result: V3 = t
|
const result: V3 = t
|
||||||
? [t.x, t.y, t.z]
|
? [t.x, t.y, t.z]
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ export type RenderInfo = {
|
|||||||
|
|
||||||
export class RootState {
|
export class RootState {
|
||||||
public readonly world = new WorldState();
|
public readonly world = new WorldState();
|
||||||
public readonly worldEditor: WorldEditorState;
|
public readonly worldEditor = new WorldEditorState(this.world);
|
||||||
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);
|
public readonly view = new ViewState(this.world, this.worldEditor);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.worldEditor = new WorldEditorState(this.world);
|
|
||||||
|
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
this,
|
this,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export abstract class ViewItem {
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Pos3 } from "../../types";
|
||||||
|
import { state } from "../rootState";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
|
||||||
|
export abstract class CameraViewItem extends ViewItem {
|
||||||
|
public abstract get camera(): Pos3;
|
||||||
|
public abstract set camera(value: Pos3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ControlledCameraViewItem extends CameraViewItem {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get camera(): Pos3 {
|
||||||
|
return state.worldEditor.camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set camera(value: Pos3) {
|
||||||
|
state.worldEditor.setCamera(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class ThirdPersonCameraViewItem extends CameraViewItem {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './base';
|
||||||
|
export * from './objectTypeGeometry';
|
||||||
|
export * from './objectGeometry';
|
||||||
|
export * from './camera';
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { ObjectInstance, ObjectInstanceRigidBody, V3 } from "../../types";
|
||||||
|
import { state } from "../rootState";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
import type { ObjectTypeGeometryViewItem } from './objectTypeGeometry';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setObjectTransform(position: V3, rotation: V3, scale: V3) {
|
||||||
|
state.worldEditor.setObjectTransform(this.object.id, position, rotation, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { action, makeObservable, observable } from "mobx";
|
||||||
|
import { nextObjectEditMode, type ObjectEditMode } from "../worldEditorState";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
import type { ObjectGeometryViewItem } from "./objectGeometry";
|
||||||
|
|
||||||
|
export class ObjectSelectionViewItem extends ViewItem {
|
||||||
|
public readonly geometry: ObjectGeometryViewItem;
|
||||||
|
public mode: ObjectEditMode;
|
||||||
|
|
||||||
|
constructor(geometry: ObjectGeometryViewItem, mode: ObjectEditMode) {
|
||||||
|
super();
|
||||||
|
this.geometry = geometry;
|
||||||
|
this.mode = mode;
|
||||||
|
|
||||||
|
makeObservable(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
geometry: observable.ref,
|
||||||
|
mode: true,
|
||||||
|
nextMode: action,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextMode(): void {
|
||||||
|
this.mode = nextObjectEditMode(this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import type { ObjectType, V3, Voxel, VoxelGroup } from "../../types";
|
||||||
|
import { ViewItem } from "./base";
|
||||||
|
import { action, computed, makeObservable, observable, reaction, type IReactionDisposer } from 'mobx';
|
||||||
|
import { getObjectVoxelGroups } from '../../utils/graphics/voxelGroup';
|
||||||
|
import { buildVoxelGroupMesh } from '../../utils/graphics/voxelMesh';
|
||||||
|
import { state } from '../rootState';
|
||||||
|
import { buildObjectTrimesh } from '../../utils/graphics/mesh';
|
||||||
|
import { computeBoundingBox } from '../../utils/runtime';
|
||||||
|
import type { ObjectGeometryViewItem } from './objectGeometry';
|
||||||
|
import type { ViewState } from '../viewState';
|
||||||
|
|
||||||
|
export class ObjectTypeGeometryViewItem extends ViewItem {
|
||||||
|
public readonly owner: ViewState;
|
||||||
|
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(owner: ViewState, objectType: ObjectType) {
|
||||||
|
super();
|
||||||
|
this.owner = owner;
|
||||||
|
this.objectType = objectType;
|
||||||
|
makeObservable(this, {
|
||||||
|
voxelGroups: observable.ref,
|
||||||
|
colliderMesh: observable.ref,
|
||||||
|
boundingBox: observable.ref,
|
||||||
|
meshes: observable.ref,
|
||||||
|
updateRuntimeData: action,
|
||||||
|
instances: computed,
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get instances(): ObjectGeometryViewItem[] {
|
||||||
|
return this.owner.objectGeometries.values.filter((o) => o.objectTypeGeometry === this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ViewItem } from "./base";
|
||||||
|
|
||||||
|
export class PlayerViewItem extends ViewItem {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,113 +1,31 @@
|
|||||||
import * as THREE from 'three';
|
import { makeAutoObservable } from "mobx";
|
||||||
import { action, makeAutoObservable, makeObservable, observable, reaction, type IReactionDisposer } from "mobx";
|
import type { ObjectInstance, ObjectType } from "../types";
|
||||||
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';
|
import { MobxReconcilerMap } from '../utils/reconciler';
|
||||||
|
import { ObjectGeometryViewItem, ObjectTypeGeometryViewItem, ControlledCameraViewItem, ViewItem } from './viewLayers';
|
||||||
export abstract class ViewItem {
|
import type { WorldState } from "./worldState";
|
||||||
}
|
import { ObjectSelectionViewItem } from "./viewLayers/objectSelection";
|
||||||
|
import type { WorldEditorState } from "./worldEditorState";
|
||||||
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 {
|
export class ViewState {
|
||||||
public objectTypeGeometries: MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>;
|
public objectTypeGeometries: MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>;
|
||||||
public objectGeometries: MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>;
|
public objectGeometries: MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>;
|
||||||
|
public camera?: ControlledCameraViewItem;
|
||||||
|
|
||||||
|
private readonly editor: WorldEditorState;
|
||||||
|
|
||||||
|
constructor(world: WorldState, editor: WorldEditorState) {
|
||||||
|
this.editor = editor;
|
||||||
|
|
||||||
constructor(world: { data: { objectTypes: Record<string, ObjectType>; initialScene: Scene } }) {
|
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.camera = new ControlledCameraViewItem();
|
||||||
|
|
||||||
this.objectTypeGeometries = new MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>({
|
this.objectTypeGeometries = new MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>({
|
||||||
reaction: () => Object.values(world.data.objectTypes),
|
reaction: () => Object.values(world.data.objectTypes),
|
||||||
keyOf: (ot) => ot.id,
|
keyOf: (ot) => ot.id,
|
||||||
mapItem: (ot) => new ObjectTypeGeometryViewItem(ot),
|
mapItem: (ot) => new ObjectTypeGeometryViewItem(this, ot),
|
||||||
updateItem: () => true,
|
updateItem: () => true,
|
||||||
disposeItem: (item) => { item.dispose(); },
|
disposeItem: (item) => { item.dispose(); },
|
||||||
});
|
});
|
||||||
@@ -126,10 +44,24 @@ export class ViewState {
|
|||||||
this.objectGeometries.dispose();
|
this.objectGeometries.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get selectedObject(): ObjectSelectionViewItem | undefined {
|
||||||
|
const sel = this.editor.selection;
|
||||||
|
if (sel?.type !== 'object')
|
||||||
|
return;
|
||||||
|
|
||||||
|
const geom = this.objectGeometries.get(sel.id);
|
||||||
|
if (!geom)
|
||||||
|
return;
|
||||||
|
|
||||||
|
return new ObjectSelectionViewItem(geom, sel.editMode);
|
||||||
|
}
|
||||||
|
|
||||||
public get items(): ViewItem[] {
|
public get items(): ViewItem[] {
|
||||||
return [
|
return [
|
||||||
...this.objectTypeGeometries,
|
...this.objectTypeGeometries,
|
||||||
...this.objectGeometries,
|
...this.objectGeometries,
|
||||||
|
...[this.camera ? [this.camera] : []],
|
||||||
|
...[this.selectedObject ? [this.selectedObject] : []],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { type ObjectInstance, type Pos3, type R3, type V3, type ObjectType, type
|
|||||||
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 } from "../utils/runtime";
|
|
||||||
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
|
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
|
||||||
|
|
||||||
export const ObjectEditModeEnum = [
|
export const ObjectEditModeEnum = [
|
||||||
@@ -66,6 +65,8 @@ export class WorldEditorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setCamera(value: Pos3): void {
|
public setCamera(value: Pos3): void {
|
||||||
|
if (JSON.stringify(value) === JSON.stringify(this.world.data.editorCamera))
|
||||||
|
return;
|
||||||
this.world.data.editorCamera = value;
|
this.world.data.editorCamera = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ export class WorldEditorState {
|
|||||||
typeId: string,
|
typeId: string,
|
||||||
pos: { x: number; y: number; z: number; },
|
pos: { x: number; y: number; z: number; },
|
||||||
): ObjectInstance {
|
): ObjectInstance {
|
||||||
const obj = populateRuntimeObject(createObjectInstance(randomId(), typeId), this.world.data);
|
const obj = createObjectInstance(randomId(), typeId);
|
||||||
obj.position = [pos.x, pos.y, pos.z];
|
obj.position = [pos.x, pos.y, pos.z];
|
||||||
obj.rotation = [0, 0, 0];
|
obj.rotation = [0, 0, 0];
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { autorun, makeAutoObservable, toJS } from "mobx";
|
import { autorun, makeAutoObservable, toJS } from "mobx";
|
||||||
import { WorldFactory } from "../model/worldFactory";
|
import { WorldFactory } from "../model/worldFactory";
|
||||||
import { type ObjectInstance, type ObjectType, type World, type VoxelType, type RuntimeWorld } from "../types";
|
import { type ObjectInstance, type ObjectType, type World, type VoxelType } from "../types";
|
||||||
import { state } from "./rootState";
|
import { state } from "./rootState";
|
||||||
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
|
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
|
||||||
import { wolf } from "../model/objectPrefabs/wolf";
|
import { wolf } from "../model/objectPrefabs/wolf";
|
||||||
import { terrainXZ } from "../model/objectPrefabs/terrain";
|
import { terrainXZ } from "../model/objectPrefabs/terrain";
|
||||||
import { populateRuntimeWorld } from "../utils/runtime";
|
|
||||||
|
|
||||||
export class WorldState {
|
export class WorldState {
|
||||||
public data: RuntimeWorld = WorldFactory.create();
|
public data: World = WorldFactory.create();
|
||||||
|
|
||||||
private _buildSave(): World {
|
private _buildSave(): World {
|
||||||
const d = this.data;
|
const d = this.data;
|
||||||
@@ -58,7 +57,7 @@ export class WorldState {
|
|||||||
objects.map((obj) => [obj.id, obj]),
|
objects.map((obj) => [obj.id, obj]),
|
||||||
) as Record<string, ObjectInstance>;
|
) as Record<string, ObjectInstance>;
|
||||||
|
|
||||||
this.data = populateRuntimeWorld({
|
this.data = {
|
||||||
objectTypes: {
|
objectTypes: {
|
||||||
wolf: {
|
wolf: {
|
||||||
id: 'wolf',
|
id: 'wolf',
|
||||||
@@ -97,7 +96,7 @@ export class WorldState {
|
|||||||
gameRules: {
|
gameRules: {
|
||||||
gravity: true,
|
gravity: true,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
console.log(objects);
|
console.log(objects);
|
||||||
state.worldEditor.resetSelection();
|
state.worldEditor.resetSelection();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GameRules } from "./gameRules";
|
import type { GameRules } from "./gameRules";
|
||||||
import type { ObjectType } from "./object";
|
import type { ObjectType } from "./object";
|
||||||
import type { RuntimeScene, Scene } from "./scene";
|
import type { Scene } from "./scene";
|
||||||
import type { Pos3 } from "../3d";
|
import type { Pos3 } from "../3d";
|
||||||
import type { VoxelType } from "./voxel";
|
import type { VoxelType } from "./voxel";
|
||||||
|
|
||||||
@@ -11,7 +11,3 @@ export type World = {
|
|||||||
initialScene: Scene;
|
initialScene: Scene;
|
||||||
gameRules: GameRules;
|
gameRules: GameRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeWorld = World & {
|
|
||||||
initialScene: RuntimeScene;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model";
|
import type { ObjectInstance, ObjectType, Scene, World } from "../model";
|
||||||
import type { ScriptAction } from "./actions";
|
import type { ScriptAction } from "./actions";
|
||||||
|
|
||||||
export type ScriptCoroutine = Generator<ScriptAction, void, void>;
|
export type ScriptCoroutine = Generator<ScriptAction, void, void>;
|
||||||
@@ -9,15 +9,15 @@ export type NamedScriptCoroutine = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptCoroutineContext = {
|
export type ScriptCoroutineContext = {
|
||||||
thisObject: RuntimeGameObjectInstance,
|
thisObject: ObjectInstance,
|
||||||
thisObjectType: ObjectType;
|
thisObjectType: ObjectType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScriptContext = ScriptCoroutineContext & {
|
export type ScriptContext = ScriptCoroutineContext & {
|
||||||
world: World;
|
world: World;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
object?: RuntimeGameObjectInstance;
|
object?: ObjectInstance;
|
||||||
other?: RuntimeGameObjectInstance;
|
other?: ObjectInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptGameEventHandler = (context: ScriptContext) => ScriptCoroutine;
|
export type ScriptGameEventHandler = (context: ScriptContext) => ScriptCoroutine;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useFrame, type RootState } from "@react-three/fiber";
|
||||||
|
import { recordLayerFrameTime } from "../layerTimings";
|
||||||
|
|
||||||
|
export function useLayerFrame(
|
||||||
|
name: string,
|
||||||
|
callback: (state: RootState, delta: number) => void,
|
||||||
|
priority?: number,
|
||||||
|
) {
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
const start = performance.now();
|
||||||
|
callback(state, delta);
|
||||||
|
recordLayerFrameTime(name, performance.now() - start);
|
||||||
|
}, priority);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import type { Pos3 } from "../../types";
|
||||||
|
import { useLayoutEffect } from "react";
|
||||||
|
import { autorun } from "mobx";
|
||||||
|
|
||||||
|
export function useSyncCamera(getCamera: () => Pos3 | undefined) {
|
||||||
|
const { camera: threeCamera } = useThree();
|
||||||
|
|
||||||
|
useLayoutEffect(() => autorun(() => {
|
||||||
|
const camera = getCamera();
|
||||||
|
if (camera) {
|
||||||
|
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
|
||||||
|
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
|
||||||
|
threeCamera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}), [getCamera, threeCamera]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export type LayerTiming = {
|
||||||
|
frame: { acc: number; frames: number };
|
||||||
|
render: { acc: number; frames: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const layerTimings: Record<string, LayerTiming> = {};
|
||||||
|
|
||||||
|
function getEntry(name: string): LayerTiming {
|
||||||
|
return layerTimings[name] ??= {
|
||||||
|
frame: { acc: 0, frames: 0 },
|
||||||
|
render: { acc: 0, frames: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordLayerFrameTime(name: string, ms: number) {
|
||||||
|
const t = getEntry(name).frame;
|
||||||
|
t.acc += ms;
|
||||||
|
t.frames++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startLayerRender(name: string): () => void {
|
||||||
|
const start = performance.now();
|
||||||
|
return () => {
|
||||||
|
const t = getEntry(name).render;
|
||||||
|
t.acc += performance.now() - start;
|
||||||
|
t.frames++;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function measuredRenderTime<TArgs extends any[], TResult extends ReactNode>(
|
||||||
|
name: string,
|
||||||
|
callback: (...args: TArgs) => TResult
|
||||||
|
): ((...args: TArgs) => TResult) {
|
||||||
|
return (...args) => {
|
||||||
|
const endRender = startLayerRender(name);
|
||||||
|
const result = callback(...args);
|
||||||
|
endRender();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,5 +10,8 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance
|
|||||||
position: [0, 0, 0],
|
position: [0, 0, 0],
|
||||||
rotation: [0, 0, 0],
|
rotation: [0, 0, 0],
|
||||||
scale: [1, 1, 1],
|
scale: [1, 1, 1],
|
||||||
|
|
||||||
|
linearVelocity: [0, 0, 0],
|
||||||
|
angularVelocity: [0, 0, 0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export * from './world';
|
|
||||||
export * from './scene';
|
|
||||||
export * from './object';
|
export * from './object';
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
import type { ObjectType, V3 } from "../../types";
|
||||||
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, RuntimeObjectType, V3, World } from "../../types";
|
|
||||||
import { buildObjectTrimesh } from "../graphics/mesh";
|
|
||||||
|
|
||||||
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
||||||
if (!objectType.voxels.length)
|
if (!objectType.voxels.length)
|
||||||
@@ -17,35 +15,3 @@ 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 populateRuntimeObjectType(value: ObjectType, world: World): RuntimeObjectType {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
runtime: {
|
|
||||||
voxelGroups: getObjectVoxelGroups(value, world.voxelTypes),
|
|
||||||
colliderMesh: buildObjectTrimesh(value, world.voxelTypes),
|
|
||||||
boundingBox: computeBoundingBox(value),
|
|
||||||
updatedAt: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function populateRuntimeObject(value: ObjectInstance, _world: World): RuntimeObjectInstance {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
runtime: {
|
|
||||||
rb: () => null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function depopulateRuntimeObjectType(value: RuntimeObjectType, _world: World): ObjectType {
|
|
||||||
const { runtime: _, ...result } = value;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function depopulateRuntimeObject(value: RuntimeObjectInstance, _world: World): ObjectInstance {
|
|
||||||
const { runtime: _, ...result } = value;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { RuntimeScene, Scene, World } from "../../types";
|
|
||||||
import { depopulateRuntimeObject, populateRuntimeObject } from "./object";
|
|
||||||
|
|
||||||
export function populateRuntimeScene(scene: Scene, world: World): RuntimeScene {
|
|
||||||
return {
|
|
||||||
...scene,
|
|
||||||
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, populateRuntimeObject(obj, world)])),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function depopulateRuntimeScene(scene: RuntimeScene, world: World): Scene {
|
|
||||||
return {
|
|
||||||
...scene,
|
|
||||||
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, depopulateRuntimeObject(obj, world)])),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { RuntimeWorld, World } from "../../types";
|
|
||||||
import { depopulateRuntimeScene, populateRuntimeScene } from "./scene";
|
|
||||||
|
|
||||||
export function populateRuntimeWorld(world: World): RuntimeWorld {
|
|
||||||
return {
|
|
||||||
...world,
|
|
||||||
initialScene: populateRuntimeScene(world.initialScene, world),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function depopulateRuntimeWorld(world: RuntimeWorld): World {
|
|
||||||
return {
|
|
||||||
...world,
|
|
||||||
initialScene: depopulateRuntimeScene(world.initialScene, world),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user