layered three view

game object geometries layer
This commit is contained in:
azykov@mail.ru
2026-06-19 20:54:27 +03:00
parent 076cded364
commit 579fe36e6c
31 changed files with 521 additions and 219 deletions
+5 -26
View File
@@ -1,6 +1,6 @@
import { autorun, makeAutoObservable, toJS } from "mobx";
import type { WorldState } from "./worldState";
import type { Game, GameObjectInstance, GameScene, Pos3, RuntimeGameScene, ScriptContext, ScriptCoroutineContext } from "../types";
import type { Game, Pos3, Scene, ScriptContext, ScriptCoroutineContext } from "../types";
import type { CameraProps } from "@react-three/fiber";
import type { ScriptGameEventBus } from "../scripts/eventBus";
import { GameFactory } from "../model/gameFactory";
@@ -13,37 +13,16 @@ export class GameState {
public debugOverlay: boolean = false;
public time: number = 0;
public isFirstTick: boolean;
public scene: RuntimeGameScene;
public scene: Scene;
private coroutines = new ScriptCoroutines();
private eventBus: ScriptGameEventBus;
private sceneObjectsAsSaveables(): GameScene['objects'] {
return Object.fromEntries(
Object.entries(this.scene.objects).map(([id, obj]) => [id, {
id: obj.id,
typeId: obj.typeId,
physics: obj.physics,
gravityScale: obj.gravityScale,
position: toJS(obj.position),
rotation: toJS(obj.rotation),
scale: toJS(obj.scale),
version: obj.version,
linearVelocity: toJS(obj.linearVelocity),
angularVelocity: toJS(obj.angularVelocity),
visible: toJS(obj.visible),
} satisfies GameObjectInstance])
) as unknown as RuntimeGameScene['objects'];
}
public get asSaveableGame(): Game<GameScene> {
public get asGame(): Game {
return {
paused: this.isPaused,
time: this.time,
scene: {
playerObjectId: this.scene.playerObjectId,
objects: this.sceneObjectsAsSaveables(),
},
scene: this.scene,
};
}
@@ -126,7 +105,7 @@ export class GameState {
public save(): void {
console.log('Saving game...');
GameFactory.save(this.asSaveableGame);
GameFactory.save(this.asGame);
}
public resume(): void {
+5 -4
View File
@@ -3,7 +3,7 @@ import { WorldState } from "./worldState";
import { WorldEditorState } from "./worldEditorState";
import { GameState } from "./gameState";
import { MenuState } from "./menuState";
import type { RuntimeObjectInstance } from "../types";
import { ViewState } from "./viewState";
export type RenderInfo = {
calls: number,
@@ -19,6 +19,7 @@ export class RootState {
public game: GameState | undefined;
public renderInfo: RenderInfo | undefined;
public readonly menu = new MenuState();
public readonly view = new ViewState(this.world);
constructor() {
this.worldEditor = new WorldEditorState(this.world);
@@ -52,11 +53,11 @@ export class RootState {
this.renderInfo = value;
}
public markObjectAsPlayer(object: RuntimeObjectInstance) {
public markObjectAsPlayer(objectId: string) {
if (this.game)
this.game.scene.playerObjectId = object.id;
this.game.scene.playerObjectId = objectId;
else
this.world.data.initialScene.playerObjectId = object.id;
this.world.data.initialScene.playerObjectId = objectId;
}
}
+135
View File
@@ -0,0 +1,135 @@
import * as THREE from 'three';
import { action, makeAutoObservable, makeObservable, observable, reaction, type IReactionDisposer } from "mobx";
import type { ObjectInstance, ObjectInstanceRigidBody, ObjectType, Scene, V3, Voxel, VoxelGroup } from "../types";
import { state } from "./rootState";
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
import { buildVoxelGroupMesh } from "../utils/graphics/voxelMesh";
import { buildObjectTrimesh } from '../utils/graphics/mesh';
import { computeBoundingBox } from '../utils/runtime';
import { MobxReconcilerMap } from '../utils/reconciler';
export abstract class ViewItem {
}
export class ObjectTypeGeometryViewItem extends ViewItem {
public readonly objectType: ObjectType;
public voxelGroups: VoxelGroup[] | undefined;
public colliderMesh: [Float32Array, Uint32Array] | undefined;
public boundingBox: { min: V3; max: V3 } | undefined;
public meshes: THREE.InstancedMesh[] | undefined;
private readonly _disposeVoxelReaction: IReactionDisposer;
constructor(objectType: ObjectType) {
super();
this.objectType = objectType;
makeObservable(this, {
voxelGroups: observable.ref,
colliderMesh: observable.ref,
boundingBox: observable.ref,
meshes: observable.ref,
updateRuntimeData: action,
});
this._disposeVoxelReaction = reaction(
() => objectType.voxels.map(v => `${v.position}-${v.typeId}`).join('|'),
() => this.updateRuntimeData(),
);
this.updateRuntimeData();
}
public dispose(): void {
this._disposeVoxelReaction();
}
public addVoxel(voxel: Voxel): boolean {
const occupied = this.objectType.voxels.some(v =>
v.position[0] === voxel.position[0] &&
v.position[1] === voxel.position[1] &&
v.position[2] === voxel.position[2]
);
if (!occupied) {
this.objectType.voxels.push(voxel);
return true;
}
return false;
}
public removeVoxel(position: V3): Voxel | undefined {
const idx = this.objectType.voxels.findIndex(v =>
v.position[0] === position[0] &&
v.position[1] === position[1] &&
v.position[2] === position[2]
);
if (idx !== -1)
return this.objectType.voxels.splice(idx, 1)[0];
}
public updateRuntimeData(): void {
this.voxelGroups = getObjectVoxelGroups(this.objectType, state.world.data.voxelTypes);
this.meshes = [];
for (const vg of this.voxelGroups) {
const geo = buildVoxelGroupMesh(vg.positions);
const mat = new THREE.MeshStandardMaterial({
color: vg.color,
opacity: vg.opacity,
transparent: vg.opacity < 1,
});
const mesh = new THREE.InstancedMesh(geo, mat, 1000);
this.meshes.push(mesh);
}
this.colliderMesh = buildObjectTrimesh(this.objectType, state.world.data.voxelTypes);
this.boundingBox = computeBoundingBox(this.objectType);
}
}
export class ObjectGeometryViewItem extends ViewItem {
public readonly object: ObjectInstance;
public readonly objectTypeGeometry: ObjectTypeGeometryViewItem;
public rb: (() => ObjectInstanceRigidBody | null) | undefined;
constructor(object: ObjectInstance, objectTypeGeometry: ObjectTypeGeometryViewItem) {
super();
this.object = object;
this.objectTypeGeometry = objectTypeGeometry;
}
}
export class ViewState {
public objectTypeGeometries: MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>;
public objectGeometries: MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>;
constructor(world: { data: { objectTypes: Record<string, ObjectType>; initialScene: Scene } }) {
makeAutoObservable(
this,
);
this.objectTypeGeometries = new MobxReconcilerMap<ObjectType, ObjectTypeGeometryViewItem>({
reaction: () => Object.values(world.data.objectTypes),
keyOf: (ot) => ot.id,
mapItem: (ot) => new ObjectTypeGeometryViewItem(ot),
updateItem: () => true,
disposeItem: (item) => { item.dispose(); },
});
this.objectGeometries = new MobxReconcilerMap<ObjectInstance, ObjectGeometryViewItem>({
reaction: () => Object.values(world.data.initialScene.objects)
.filter(obj => obj.visible)
.filter(obj => !!world.data.objectTypes[obj.typeId]),
keyOf: (obj) => obj.id,
mapItem: (obj) => new ObjectGeometryViewItem(obj, this.objectTypeGeometries.get(obj.typeId)!),
});
}
public dispose(): void {
this.objectTypeGeometries.dispose();
this.objectGeometries.dispose();
}
public get items(): ViewItem[] {
return [
...this.objectTypeGeometries,
...this.objectGeometries,
];
}
}
+3 -47
View File
@@ -1,13 +1,11 @@
import { makeAutoObservable, runInAction } from "mobx";
import type { WorldState } from "./worldState";
import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene, type ObjectType, type Voxel } from "../types";
import { type ObjectInstance, type Pos3, type R3, type V3, type ObjectType, type Scene } from "../types";
import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils";
import { state } from "./rootState";
import { populateRuntimeObject, computeBoundingBox } from "../utils/runtime";
import { populateRuntimeObject } from "../utils/runtime";
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
import { buildObjectTrimesh } from "../utils/graphics/mesh";
export const ObjectEditModeEnum = [
'translate',
@@ -99,7 +97,7 @@ export class WorldEditorState {
this.setSelection(undefined);
}
public get scene(): RuntimeScene {
public get scene(): Scene {
return this.world.data.initialScene;
}
@@ -168,48 +166,6 @@ export class WorldEditorState {
return objectType;
}
public addVoxelToObjectType(typeId: string, voxel: Voxel): void {
const objectType = this.world.getObjectTypeById(typeId);
if (!objectType)
return;
const occupied = objectType.voxels.some(v =>
v.position[0] === voxel.position[0] &&
v.position[1] === voxel.position[1] &&
v.position[2] === voxel.position[2]
);
if (!occupied)
objectType.voxels.push(voxel);
}
public removeVoxelFromObjectType(typeId: string, position: V3): void {
const objectType = this.world.getObjectTypeById(typeId);
if (!objectType)
return;
const idx = objectType.voxels.findIndex(v =>
v.position[0] === position[0] &&
v.position[1] === position[1] &&
v.position[2] === position[2]
);
if (idx !== -1)
objectType.voxels.splice(idx, 1);
}
public refreshObjectTypeCaches(typeId: string): void {
const objectType = this.world.getObjectTypeById(typeId);
if (!objectType)
return;
const world = this.world.data;
const now = Date.now();
for (const obj of Object.values(this.scene.objects)) {
if (obj.typeId !== typeId)
continue;
obj.cache.voxelGroups = getObjectVoxelGroups(objectType, world.voxelTypes);
obj.cache.colliderMesh = buildObjectTrimesh(objectType, world.voxelTypes);
obj.cache.boundingBox = computeBoundingBox(objectType);
obj.cache.updatedAt = now;
}
}
public deleteObjectType(typeId: string) {
if (this.selection?.type === 'objectType' && this.selection.id === typeId)
this.resetSelection();
+6 -21
View File
@@ -12,30 +12,11 @@ export class WorldState {
private _buildSave(): World {
const d = this.data;
return {
objectTypes: toJS(d.objectTypes),
voxelTypes: toJS(d.voxelTypes),
editorCamera: toJS(d.editorCamera),
gameRules: toJS(d.gameRules),
initialScene: {
playerObjectId: d.initialScene.playerObjectId,
objects: Object.fromEntries(
Object.entries(d.initialScene.objects).map(([id, obj]) => [id, {
id: obj.id,
typeId: obj.typeId,
physics: obj.physics,
gravityScale: obj.gravityScale,
position: toJS(obj.position),
rotation: toJS(obj.rotation),
scale: toJS(obj.scale),
} satisfies ObjectInstance])
),
},
};
return toJS(d);
}
private startAutoSave() {
return autorun(() => WorldFactory.save(this._buildSave()), { delay: 500 });
return autorun(() => this.save(), { delay: 500 });
}
private withoutAutoSave(fn: () => void) {
@@ -105,6 +86,9 @@ export class WorldState {
position: [0, -1, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
visible: true,
linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0],
},
...objectMap,
},
@@ -128,6 +112,7 @@ export class WorldState {
}
public save(): void {
console.log('Saving world...');
WorldFactory.save(this._buildSave());
}