basic physics and code optimization

This commit is contained in:
azykov@mail.ru 2026-06-03 16:56:06 +03:00
parent 7b02a75d25
commit 4e7b44dc57
No known key found for this signature in database
37 changed files with 372 additions and 238 deletions

View File

@ -0,0 +1,17 @@
import { observer } from "mobx-react-lite";
import type { ObjectInstance, Runtime } from "../types";
import { state } from "../state";
import { ObjectViewInternal } from "./ObjectViewInternal";
export type GameObjectViewProps = {
object: Runtime<ObjectInstance>;
}
export const GameObjectView = observer(function (props: GameObjectViewProps) {
const objectType = state.world.getObjectTypeById(props.object.typeId);
if (!objectType)
return null;
return <ObjectViewInternal {...props} objectType={objectType} />
});

View File

@ -0,0 +1,62 @@
import { observer } from "mobx-react-lite";
import type { ObjectInstance, R3, Runtime } from "../types";
import { useRef, type RefObject } from "react";
import type { Group } from "three";
import { TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditorState";
import { ObjectViewInternal } from "./ObjectViewInternal";
type ObjectEditorViewProps = {
object: Runtime<ObjectInstance>;
}
export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) {
const groupRef = useRef<Group>(null);
const objectType = state.world.getObjectTypeById(object.typeId);
const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === object.id;
const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined;
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
if (!objectType)
return null;
function handleClick(e: ThreeEvent<MouseEvent>) {
if (e.delta > 5) return;
e.stopPropagation();
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
}
function handleTransformEnd() {
const group = groupRef.current;
if (group)
state.worldEditor.setObjectTransform(
object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3) as R3,
group.scale.toArray(),
);
}
return (<>
{(isSelected && selectionMode !== undefined && groupRef.current) &&
<TransformControls
object={groupRef as RefObject<Group>}
mode={selectionMode}
onMouseUp={handleTransformEnd}
/>
}
<ObjectViewInternal
ref={groupRef}
object={object}
objectType={objectType}
onClick={handleClick}
/>
</>);
});

View File

@ -1,153 +0,0 @@
import { observer } from "mobx-react-lite";
import type { ObjectType, ObjectInstance } from "../types";
import { useRef, useMemo, type RefObject } from "react";
import type { Group } from "three";
import { Instance, Instances, TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditorState";
import type { R3 } from "../types/3d";
import { TrimeshCollider } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody";
import { runInAction } from "mobx";
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
type ObjectViewProps = {
object: ObjectInstance;
}
function buildTrimesh(objectType: ObjectType, voxelTypes: typeof state.world.data.voxelTypes): [Float32Array, Uint32Array] | null {
const collidable = objectType.voxels.filter(
v => voxelTypes[v.typeId]?.collidable !== false
);
if (!collidable.length) return null;
const n = collidable.length;
const verts = new Float32Array(n * 8 * 3);
const idxs = new Uint32Array(n * 36);
for (let i = 0; i < n; i++) {
const p = collidable[i].position;
const vb = i * 8;
// 8 corners of unit box at position p (no +0.5 — group transform handles offset)
verts.set([
p[0], p[1], p[2],
p[0] + 1, p[1], p[2],
p[0], p[1] + 1, p[2],
p[0] + 1, p[1] + 1, p[2],
p[0], p[1], p[2] + 1,
p[0] + 1, p[1], p[2] + 1,
p[0], p[1] + 1, p[2] + 1,
p[0] + 1, p[1] + 1, p[2] + 1,
], vb * 3);
// 12 triangles (CCW outward normals)
idxs.set([
vb, vb + 2, vb + 3, vb, vb + 3, vb + 1, // -Z
vb + 4, vb + 5, vb + 7, vb + 4, vb + 7, vb + 6, // +Z
vb, vb + 1, vb + 5, vb, vb + 5, vb + 4, // -Y
vb + 2, vb + 6, vb + 7, vb + 2, vb + 7, vb + 3, // +Y
vb, vb + 4, vb + 6, vb, vb + 6, vb + 2, // -X
vb + 1, vb + 3, vb + 7, vb + 1, vb + 7, vb + 5, // +X
], i * 36);
}
return [verts, idxs];
}
export const ObjectView = observer(function ({ object }: ObjectViewProps) {
const objectType = state.world.getObjectTypeById(object.typeId);
const groupRef = useRef<Group>(null);
const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === object.id;
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
// Must be before early return to satisfy hooks rules
const trimeshArgs = useMemo(
() => objectType
? buildTrimesh(objectType, state.world.data.voxelTypes)
: null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[objectType?.id]
);
if (!objectType)
return null;
const selectionMode = isSelected
? state.worldEditor.selectedObjectMode
: undefined;
function handleClick(e: ThreeEvent<MouseEvent>) {
if (!state.worldEditor.isEnabled)
return;
if (e.delta > 5)
return;
e.stopPropagation();
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
};
function handleTransformEnd() {
const group = groupRef.current;
if (group)
state.worldEditor.setObjectTransform(
object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3) as R3,
group.scale.toArray(),
);
};
return (<>
{
(isSelected && (selectionMode !== undefined) && groupRef.current) &&
<TransformControls
object={groupRef as RefObject<Group>}
mode={selectionMode}
onMouseUp={handleTransformEnd}
/>
}
<SyncRigidBody
colliders={false}
// gravityScale={object.gravityScale}
// type={object.physics ? 'dynamic' : 'fixed'}
gravityScale={object.physics ? 1 : 0.1}
onSync={(data) => {
// console.log(`changed object ${object.id} ${JSON.stringify(data)}`);
runInAction(() => {
const obj = state.game?.scene.objects[object.id];
if (obj) {
obj.position = data.position;
obj.rotation = data.rotation;
obj.linearVelocity = data.linearVelocity;
obj.radialVelocity = data.radialVelocity;
}
});
}}
>
<group
ref={groupRef}
name={`${object.id} (${object.typeId} instance)`}
position={object.position}
rotation={object.rotation}
scale={object.scale}
onClick={handleClick}
>
{getObjectVoxelGroups(objectType, state.world.data.voxelTypes).map((vg) =>
<Instances key={vg.id} limit={vg.positions.length}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
{vg.positions.map((pos, i) => <Instance key={i} position={pos} />)}
</Instances>
)}
{trimeshArgs && <TrimeshCollider args={trimeshArgs} />}
</group>
</SyncRigidBody>
</>);
});

View File

@ -0,0 +1,65 @@
import { observer } from "mobx-react-lite";
import type { ObjectType, RuntimeObjectInstance } from "../types";
import { forwardRef, useMemo } from "react";
import type { Group } from "three";
import { Instance, Instances } from "@react-three/drei";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { TrimeshCollider } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody";
import { runInAction } from "mobx";
import { buildObjectTrimesh } from "../utils/graphics/mesh";
type ObjectViewInternalProps = {
object: Omit<RuntimeObjectInstance, 'typeId'>;
objectType: ObjectType;
onClick?: (e: ThreeEvent<MouseEvent>) => void;
}
export const ObjectViewInternal = observer(forwardRef<Group, ObjectViewInternalProps>(
function ({ object, objectType, onClick }, ref) {
const trimeshArgs = useMemo(
() => buildObjectTrimesh(objectType, state.world.data.voxelTypes),
// eslint-disable-next-line react-hooks/exhaustive-deps
[objectType.id]
);
return (
<SyncRigidBody
colliders={false}
gravityScale={object.physics ? 1 : 0.1}
onSync={(data) => {
runInAction(() => {
const obj = state.game?.scene.objects[object.id];
if (obj) {
obj.position = data.position;
obj.rotation = data.rotation;
obj.linearVelocity = data.linearVelocity;
obj.radialVelocity = data.radialVelocity;
}
});
}}
>
<group
ref={ref}
name={`${object.id} (${objectType.id} instance)`}
position={object.position}
rotation={object.rotation}
scale={object.scale}
onClick={onClick}
>
{
object.cache.voxelGroups.map((vg) =>
<Instances key={vg.id} limit={vg.positions.length}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
{vg.positions.map((pos, i) => <Instance key={i} position={pos} />)}
</Instances>
)
}
{trimeshArgs && <TrimeshCollider args={trimeshArgs} />}
</group>
</SyncRigidBody>
);
}
));

View File

@ -1,13 +1,13 @@
import { observer } from "mobx-react-lite";
import type { Scene } from "../types";
import type { RuntimeScene } from "../types";
import { CharacterView } from "./CharacterView";
import { ObjectView } from "./ObjectView";
import { GameObjectView } from "./GameObjectView";
import { useFrame } from "@react-three/fiber";
import { useRapier } from "@react-three/rapier";
import { state } from "../state";
import { ObjectEditorView } from "./ObjectEditorView";
type SceneViewProps = {
scene: Scene,
scene: RuntimeScene,
editMode?: boolean;
}
@ -28,8 +28,13 @@ export const SceneView = observer(function (props: SceneViewProps) {
return (<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
{Object.values(props.scene.objects).map((obj) =>
<ObjectView key={obj.id} object={obj} />)}
{
Object.values(props.scene.objects).map((obj) => (
props.editMode
? <ObjectEditorView key={obj.id} object={obj} />
: <GameObjectView key={obj.id} object={obj} />
))
}
{/* {props.editMode && <CharacterView character={props.scene.character} />} */}
{<CharacterView character={props.scene.character} />}
</>);

View File

@ -2,7 +2,7 @@ import { RigidBody, type RigidBodyProps, type RapierRigidBody } from "@react-thr
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import { Euler, Quaternion } from "three";
import type { R3, V3 } from "../types/3d";
import type { R3, V3 } from "../types";
export type SyncRigidBodyData = {
position: V3;

View File

@ -1,5 +1,5 @@
import { useThree } from "@react-three/fiber";
import type { Pos3 } from "../../types/3d";
import type { Pos3 } from "../../types";
import { useLayoutEffect } from "react";
import { observer } from "mobx-react-lite";

View File

@ -1,4 +1,4 @@
import type { VoxelType } from "../types/voxel";
import type { VoxelType } from "../types";
const stone: VoxelType = {
id: 'stone',

View File

@ -1,13 +1,16 @@
import type { Game, RuntimeScene, World } from "../types";
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
import { clone } from "../utils";
import { populateRuntimeScene } from "../utils/runtime";
export class GameFactory {
public static create(world: World): Game {
const scene = populateRuntimeScene(clone(world.initialScene), world);
return {
paused: false,
time: 0,
scene: clone(world.initialScene) as RuntimeScene,
scene: GameFactory.initGameScene(scene),
}
}
@ -21,4 +24,8 @@ export class GameFactory {
public static save(game: Game): void {
localStorage.setItem("game", JSON.stringify(game));
}
private static initGameScene(scene: RuntimeScene): RuntimeGameScene {
return scene as RuntimeGameScene;
}
}

View File

@ -1,4 +1,4 @@
import type { Voxel } from "../../types/voxel";
import type { Voxel } from "../../types";
export function terrainXZ(width: number, length: number): Voxel[] {

View File

@ -1,4 +1,4 @@
import type { Voxel } from "../../types/voxel";
import type { Voxel } from "../../types";
const G = '#808080'; // gray fur
const L = '#d0c8b8'; // light beige-gray (snout / face)

View File

@ -1,10 +1,11 @@
import type { World } from "../types";
import { type RuntimeWorld, type World } from "../types";
import { depopulateRuntimeWorld, populateRuntimeWorld } from "../utils/runtime";
import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes";
export class WorldFactory {
public static create(): World {
return {
public static create(): RuntimeWorld {
return populateRuntimeWorld({
objectTypes: {},
voxelTypes: DEFAULT_VOXEL_TYPES,
initialScene: {
@ -23,17 +24,17 @@ export class WorldFactory {
position: [0, 5, 10],
look: [0, 0, 0],
},
}
});
}
public static load(): World | undefined {
public static load(): RuntimeWorld | undefined {
const json = localStorage.getItem("world");
if (json) {
return JSON.parse(json) as World;
return populateRuntimeWorld(JSON.parse(json) as World);
}
}
public static save(world: World): void {
localStorage.setItem("world", JSON.stringify(world));
public static save(world: RuntimeWorld): void {
localStorage.setItem("world", JSON.stringify(depopulateRuntimeWorld(world)));
}
}

View File

@ -1,7 +1,6 @@
import { makeAutoObservable, reaction, toJS } from "mobx";
import type { WorldState } from "./worldState";
import type { Game, RuntimeScene } from "../types";
import type { Pos3, V3, R3 } from "../types/3d";
import type { Game, Pos3, V3, R3, RuntimeGameScene } from "../types";
import type { CameraProps } from "@react-three/fiber";
import { GameFactory } from "../model/gameFactory";
@ -34,7 +33,7 @@ export class GameState {
return this.data.paused;
}
public get scene(): RuntimeScene {
public get scene(): RuntimeGameScene {
return this.data.scene;
}

View File

@ -1,10 +1,10 @@
import { makeAutoObservable, runInAction } from "mobx";
import type { WorldState } from "./worldState";
import type { ObjectInstance, Scene } from "../types";
import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene } from "../types";
import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils";
import type { Pos3, R3, V3 } from "../types/3d";
import { state } from "./rootState";
import { populateRuntimeObject } from "../utils/runtime";
export const SelectionEditModeEnum = [
'translate',
@ -51,7 +51,7 @@ export class WorldEditorState {
this.selectedObjectMode = undefined;
}
public get scene(): Scene {
public get scene(): RuntimeScene {
return this.world.data.initialScene;
}
@ -82,7 +82,7 @@ export class WorldEditorState {
typeId: string,
pos: { x: number; y: number; z: number; },
): ObjectInstance {
const obj = createObjectInstance(randomId(), typeId);
const obj = populateRuntimeObject(createObjectInstance(randomId(), typeId), this.world.data);
obj.position = [pos.x, pos.y, pos.z];
obj.rotation = [0, 0, 0];

View File

@ -1,14 +1,14 @@
import { makeAutoObservable, reaction, toJS } from "mobx";
import { WorldFactory } from "../model/worldFactory";
import type { ObjectInstance, ObjectType, World } from "../types";
import type { VoxelType } from "../types/voxel";
import { type ObjectInstance, type ObjectType, type World, type VoxelType, type RuntimeWorld } from "../types";
import { state } from "./rootState";
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
import { wolf } from "../model/objectPrefabs/wolf";
import { terrainXZ } from "../model/objectPrefabs/terrain";
import { populateRuntimeWorld } from "../utils/runtime";
export class WorldState {
public data: World = WorldFactory.create();
public data: RuntimeWorld = WorldFactory.create();
private startAutoSave() {
return reaction(() => toJS(this.data), (data) => this.saveData(data), { delay: 500 });
@ -39,7 +39,7 @@ export class WorldState {
public loadMock() {
console.log('Mocking world...');
const objects = Array(0).fill(0)
const objects = Array(3).fill(0)
.map((_, idx) => ({
id: `obj${idx}`,
typeId: 'wolf',
@ -52,7 +52,7 @@ export class WorldState {
objects.map((obj) => [obj.id, obj]),
) as Record<string, ObjectInstance>
this.data = {
this.data = populateRuntimeWorld({
objectTypes: {
wolf: {
id: 'wolf',
@ -93,7 +93,8 @@ export class WorldState {
gameRules: {
gravity: true,
},
};
});
console.log(objects);
state.worldEditor.resetSelectedObject();
}

View File

@ -1,8 +0,0 @@
import type { Pos3 } from "./3d";
import type { RuntimeObjectData } from "./object";
export type Character = {
transform: Pos3,
}
export type RuntimeCharacter = Character & RuntimeObjectData;

View File

@ -1,7 +0,0 @@
import type { RuntimeScene } from "./scene";
export type Game = {
paused: boolean;
time: number;
scene: RuntimeScene;
}

View File

@ -1,9 +1,3 @@
export * from './object';
export * from './scene';
export * from './world';
export * from './gameRules';
export * from './game';
export * from './character';
export * from './3d';
export * from './model';
export * from './runtime';

View File

@ -0,0 +1,8 @@
import type { Pos3 } from "../3d";
import type { GameObjectData } from "./object";
export type Character = {
transform: Pos3,
}
export type GameCharacter = Character & GameObjectData;

7
src/types/model/game.ts Normal file
View File

@ -0,0 +1,7 @@
import type { RuntimeGameScene } from "./scene";
export type Game = {
paused: boolean;
time: number;
scene: RuntimeGameScene;
}

8
src/types/model/index.ts Normal file
View File

@ -0,0 +1,8 @@
export * from './object';
export * from './scene';
export * from './world';
export * from './gameRules';
export * from './game';
export * from './character';
export * from './voxel';
export * from './runtime';

View File

@ -1,4 +1,5 @@
import type { R3, V3 } from "./3d";
import type { R3, V3 } from "../3d";
import type { Runtime } from "./runtime";
import type { Voxel } from "./voxel";
export type ObjectType = {
@ -17,9 +18,12 @@ export type ObjectInstance = {
scale: V3;
}
export type RuntimeObjectData = {
export type GameObjectData = {
linearVelocity: V3;
radialVelocity: R3;
}
export type RuntimeObjectInstance = ObjectInstance & RuntimeObjectData;
export type GameObjectInstance = ObjectInstance & GameObjectData;
export type RuntimeObjectInstance = Runtime<ObjectInstance>;
export type RuntimeGameObjectInstance = Runtime<GameObjectInstance>;

View File

@ -0,0 +1,10 @@
import type { VoxelGroup } from "../runtime";
import type { ObjectInstance } from "./object";
export type ObjectInstanceRuntimeData = {
cache: {
voxelGroups: VoxelGroup[];
};
}
export type Runtime<T extends ObjectInstance> = T & ObjectInstanceRuntimeData;

22
src/types/model/scene.ts Normal file
View File

@ -0,0 +1,22 @@
import type { Character, GameCharacter } from "./character";
import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object";
export type Scene = {
character: Character;
objects: Record<string, ObjectInstance>;
}
export type RuntimeScene = {
character: Character;
objects: Record<string, RuntimeObjectInstance>;
}
export type GameScene = {
character: GameCharacter;
objects: Record<string, GameObjectInstance>;
}
export type RuntimeGameScene = {
character: GameCharacter;
objects: Record<string, RuntimeGameObjectInstance>;
}

View File

@ -1,4 +1,4 @@
import type { V3 } from "./3d";
import type { V3 } from "../3d";
export type VoxelType = {
id: string;

View File

@ -1,7 +1,7 @@
import type { GameRules } from "./gameRules";
import type { ObjectType } from "./object";
import type { Scene } from "./scene";
import type { Pos3 } from "./3d";
import type { RuntimeScene, Scene } from "./scene";
import type { Pos3 } from "../3d";
import type { VoxelType } from "./voxel";
export type World = {
@ -11,3 +11,7 @@ export type World = {
initialScene: Scene;
gameRules: GameRules;
}
export type RuntimeWorld = World & {
initialScene: RuntimeScene;
}

View File

@ -0,0 +1 @@
export * from './voxelGroup';

View File

@ -0,0 +1,9 @@
import type { V3 } from "../3d";
export type VoxelGroup = {
id: string;
color: string;
opacity: number;
positions: V3[];
};

View File

@ -1,12 +0,0 @@
import type { Character, RuntimeCharacter } from "./character";
import type { ObjectInstance, RuntimeObjectInstance } from "./object";
export type Scene = {
character: Character;
objects: Record<string, ObjectInstance>;
}
export type RuntimeScene = {
character: RuntimeCharacter;
objects: Record<string, RuntimeObjectInstance>;
}

View File

@ -0,0 +1,41 @@
import type { ObjectType, VoxelType } from "../../types";
export function buildObjectTrimesh(objectType: ObjectType, voxelTypes: Record<string, VoxelType>): [Float32Array, Uint32Array] | null {
const collidable = objectType.voxels.filter((v) => voxelTypes[v.typeId]?.collidable !== false);
if (!collidable.length)
return null;
const n = collidable.length;
const verts = new Float32Array(n * 8 * 3);
const idxs = new Uint32Array(n * 36);
for (let i = 0; i < n; i++) {
const p = collidable[i].position;
const vb = i * 8;
// 8 corners of unit box at position p (no +0.5 — group transform handles offset)
verts.set([
p[0], p[1], p[2],
p[0] + 1, p[1], p[2],
p[0], p[1] + 1, p[2],
p[0] + 1, p[1] + 1, p[2],
p[0], p[1], p[2] + 1,
p[0] + 1, p[1], p[2] + 1,
p[0], p[1] + 1, p[2] + 1,
p[0] + 1, p[1] + 1, p[2] + 1,
], vb * 3);
// 12 triangles (CCW outward normals)
idxs.set([
vb, vb + 2, vb + 3, vb, vb + 3, vb + 1, // -Z
vb + 4, vb + 5, vb + 7, vb + 4, vb + 7, vb + 6, // +Z
vb, vb + 1, vb + 5, vb, vb + 5, vb + 4, // -Y
vb + 2, vb + 6, vb + 7, vb + 2, vb + 7, vb + 3, // +Y
vb, vb + 4, vb + 6, vb, vb + 6, vb + 2, // -X
vb + 1, vb + 3, vb + 7, vb + 1, vb + 7, vb + 5, // +X
], i * 36);
}
return [verts, idxs];
}

View File

@ -1,12 +1,4 @@
import type { ObjectType } from "../../types";
import type { VoxelType } from "../../types/voxel";
export type VoxelGroup = {
id: string;
color: string;
opacity: number;
positions: [number, number, number][];
};
import type { ObjectType, VoxelGroup, VoxelType } from "../../types";
export function getObjectVoxelGroups(
object: ObjectType,

View File

@ -4,7 +4,10 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance
return {
id,
typeId: typeId,
physics: false,
gravityScale: 1,
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
};
}

View File

@ -0,0 +1,3 @@
export * from './world';
export * from './scene';
export * from './object';

View File

@ -0,0 +1,19 @@
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
import type { ObjectInstance, RuntimeObjectInstance, World } from "../../types";
export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance {
const objectType = world.objectTypes[object.typeId];
return {
...object,
cache: {
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
},
};
}
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
const { cache, ...result } = object;
return result;
}

View File

@ -0,0 +1,16 @@
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)])),
}
}

View File

@ -0,0 +1,16 @@
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),
};
}