world structure: arrays to records

object move with mouse
This commit is contained in:
azykov@mail.ru 2026-06-02 13:14:23 +03:00
parent f3033d1d0d
commit e37cb08698
No known key found for this signature in database
9 changed files with 134 additions and 54 deletions

View File

@ -1,8 +1,8 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import type { ObjectInstance } from "../types"; import type { ObjectInstance } from "../types";
import { useRef } from "react"; import { useEffect, useRef, type RefObject } from "react";
import type { Mesh } from "three"; import type { Group, Mesh } from "three";
import { Edges } from "@react-three/drei"; import { Edges, TransformControls } from "@react-three/drei";
import { state } from "../state"; import { state } from "../state";
type ObjectViewProps = { type ObjectViewProps = {
@ -10,39 +10,61 @@ type ObjectViewProps = {
} }
export const ObjectView = observer(function ({ object }: ObjectViewProps) { export const ObjectView = observer(function ({ object }: ObjectViewProps) {
const meshRef = useRef<Mesh>(null); const groupRef = useRef<Group>(null);
const controlsRef = useRef<any>(null);
const objectType = state.world.getObjectTypeById(object.typeId); const objectType = state.world.getObjectTypeById(object.typeId);
if (!objectType) if (!objectType)
return null; return null;
const isSelected = state.worldEditor.isEnabled && const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === object.id; state.worldEditor.selectedObjectId === object.id;
useEffect(() => {
if (!isSelected)
return;
const controls = controlsRef.current;
if (!controls)
return;
const onDragChanged = (e: { value: boolean }) => state.worldEditor.setIsDragging(e.value);
controls.addEventListener('dragging-changed', onDragChanged);
return () => {
controls.removeEventListener('dragging-changed', onDragChanged);
state.worldEditor.setIsDragging(false);
};
}, [isSelected]);
const handleClick = (e: { stopPropagation: () => void }) => { const handleClick = (e: { stopPropagation: () => void }) => {
if (!state.worldEditor.isEnabled) if (!state.worldEditor.isEnabled)
return; return;
e.stopPropagation(); e.stopPropagation();
state.worldEditor.setSelectedObjectId(object.id); state.worldEditor.setSelectedObjectId(object.id);
}; };
const handleObjectChange = () => {
const group = groupRef.current;
if (!group)
return;
const { x, y, z } = group.position;
state.worldEditor.setObjectPosition(object.id, [x, y, z]);
};
return (<> return (<>
<group {(isSelected && groupRef.current) && <TransformControls object={groupRef as RefObject<Group>} onChange={handleObjectChange} mode="translate" />}
position={object.position} <group ref={groupRef} position={object.position} rotation={object.rotation} onClick={handleClick}>
rotation={object.rotation}
onClick={handleClick}
>
{ {
objectType.voxels.map((v, idx) => { objectType.voxels.map((v, idx) => {
const vt = state.world.getVoxelTypeById(v.typeId); const vt = state.world.getVoxelTypeById(v.typeId);
return <mesh key={idx} ref={meshRef} position={v.position}> return (
<mesh key={idx} position={v.position}>
<boxGeometry args={[1, 1, 1]} /> <boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={(v.color ?? vt?.color) ?? 'white'} opacity={(v.opacity ?? vt?.opacity) ?? 1} /> <meshStandardMaterial color={(v.color ?? vt?.color) ?? 'white'} opacity={(v.opacity ?? vt?.opacity) ?? 1} />
{isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />} {isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />}
</mesh> </mesh>
);
}) })
} }
</group> </group>
</>); </>);
}); });

View File

@ -6,7 +6,10 @@ import { WorldView } from './WorldView';
export const ThreeView = observer(function () { export const ThreeView = observer(function () {
return ( return (
<div style={{ width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}> <div style={{ width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
<Canvas camera={state.world.character.camera}> <Canvas
camera={state.world.character.camera}
onPointerMissed={() => state.worldEditor.setSelectedObjectId(undefined)}
>
<WorldView /> <WorldView />
</Canvas> </Canvas>
</div> </div>

View File

@ -5,11 +5,16 @@ import type { RunningGameState } from "../types";
export const Toolbar = observer(function () { export const Toolbar = observer(function () {
function handleCloneTest1ObjectClick(): void { function handleCloneTest1Object(): void {
state.worldEditor.addObjectCloneAtRandomPosition('test1'); state.worldEditor.addObjectCloneAtRandomPosition('test1');
} }
function handleLoadMockWorld(): void {
state.world.loadMock();
}
return <div className="toolbar"> return <div className="toolbar">
{state.worldEditor.isEnabled && <div>EDITOR MODE</div>}
{state.world.isPlaying {state.world.isPlaying
? <> ? <>
<button onClick={() => state.world.stop()}>Stop</button> <button onClick={() => state.world.stop()}>Stop</button>
@ -21,6 +26,7 @@ export const Toolbar = observer(function () {
</> </>
: <button onClick={() => state.world.play()}>Play</button> : <button onClick={() => state.world.play()}>Play</button>
} }
<button onClick={handleCloneTest1ObjectClick}>Clone test1</button> <button onClick={handleLoadMockWorld}>Load mock world</button>
<button onClick={handleCloneTest1Object}>Clone test1</button>
</div> </div>
}); });

View File

@ -11,8 +11,6 @@ export const WorldView = observer(function () {
const world = state.world; const world = state.world;
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null); const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
console.log(world.currentScene.objects);
useFrame((_, delta) => { useFrame((_, delta) => {
world.tick(delta); world.tick(delta);
}); });
@ -31,13 +29,14 @@ export const WorldView = observer(function () {
return (<> return (<>
<OrbitControls <OrbitControls
ref={controlsRef} ref={controlsRef}
enabled={!world.isPlaying} enabled={!world.isPlaying && !state.worldEditor.isDragging}
onEnd={handleEnd} onEnd={handleEnd}
makeDefault
enableDamping={false} enableDamping={false}
/> />
<ambientLight intensity={0.5} /> <ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} /> <directionalLight position={[5, 5, 5]} intensity={1} />
{world.currentScene.objects.map((obj) => <ObjectView key={obj.id} object={obj} />)} {Object.values(world.currentScene.objects).map((obj) => <ObjectView key={obj.id} object={obj} />)}
<CharacterView /> <CharacterView />
</>) </>)
}); });

View File

@ -5,7 +5,7 @@ export class WorldFactory {
public static create(): World { public static create(): World {
return { return {
objectTypes: [], objectTypes: [],
intialScene: { initialScene: {
character: { character: {
position: [0, 0, 0], position: [0, 0, 0],
look: [0, 0, 0], look: [0, 0, 0],

View File

@ -22,47 +22,50 @@ export class WorldState {
} }
public loadMock() { public loadMock() {
const objTypeId = 'test1';
const voxelTypeId = 'red';
const objectId = 'red';
this.data = { this.data = {
objectTypes: [ objectTypes: {
{ [objTypeId]: {
id: 'test1', id: objTypeId,
name: 'Test Object', name: 'Test Object',
voxels: [ voxels: [
{ {
typeId: 'red', typeId: voxelTypeId,
position: [0, 0, 0], position: [0, 0, 0],
color: 'red',
opacity: 1,
}, },
], ],
}, },
], },
voxelTypes: [ voxelTypes: {
{ [voxelTypeId]: {
id: 'red', id: voxelTypeId,
collidable: true, collidable: true,
name: 'Red', name: 'Red',
opacity: 1, opacity: 1,
color: 'red', color: 'red',
} }
], },
editorCamera: { editorCamera: {
position: [0, 2, 10], position: [0, 2, 10],
look: [0, 0, 0], look: [0, 0, 0],
}, },
intialScene: { initialScene: {
character: { character: {
position: [0, 0, 0], position: [0, 0, 0],
look: [0, 0, 0], look: [0, 0, 0],
}, },
objects: [ objects: {
{ [objectId]: {
id: randomId(), id: objectId,
typeId: 'test1', typeId: objTypeId,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0], rotation: [0, 0, 0],
}, },
], },
}, },
gameRules: { gameRules: {
gravity: true, gravity: true,
@ -88,7 +91,7 @@ export class WorldState {
public get currentScene(): Scene { public get currentScene(): Scene {
return this.isPlaying return this.isPlaying
? (this.data.state as RunningGameState).scene ? (this.data.state as RunningGameState).scene
: this.data.intialScene; : this.data.initialScene;
} }
public get currentCamera(): Pos3 { public get currentCamera(): Pos3 {
@ -115,7 +118,7 @@ export class WorldState {
playing: true, playing: true,
paused: false, paused: false,
time: 0, time: 0,
scene: clone(this.data.intialScene), scene: clone(this.data.initialScene),
} }
} }
@ -137,10 +140,10 @@ export class WorldState {
} }
public getObjectTypeById(id: string): ObjectType | undefined { public getObjectTypeById(id: string): ObjectType | undefined {
return this.data.objectTypes.find((obj) => obj.id === id); return this.data.objectTypes[id];
} }
public getVoxelTypeById(id: string): VoxelType | undefined { public getVoxelTypeById(id: string): VoxelType | undefined {
return this.data.voxelTypes.find((obj) => obj.id === id); return this.data.voxelTypes[id];
} }
} }

View File

@ -1,14 +1,15 @@
import { makeAutoObservable } from "mobx"; import { makeAutoObservable } from "mobx";
import type { WorldState } from "./world"; import type { WorldState } from "./world";
import type { ObjectInstance } from "../types"; import type { ObjectInstance, Scene, World } from "../types";
import { createObjectInstance } from "../utils/object"; import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils"; import { randomId } from "../utils";
import type { Pos3 } from "../types/3d"; import type { Pos3, V3 } from "../types/3d";
export class WorldEditorState { export class WorldEditorState {
private readonly world: WorldState; private readonly world: WorldState;
public selectedObjectId: string | undefined; public selectedObjectId: string | undefined;
public isDragging: boolean = false;
constructor(world: WorldState) { constructor(world: WorldState) {
this.world = world; this.world = world;
@ -21,7 +22,6 @@ export class WorldEditorState {
public setCamera(value: Pos3): void { public setCamera(value: Pos3): void {
this.world.data.editorCamera = value; this.world.data.editorCamera = value;
console.log(JSON.stringify(this.world.data.editorCamera));
this.world.save(); this.world.save();
} }
@ -29,6 +29,53 @@ export class WorldEditorState {
this.selectedObjectId = value; this.selectedObjectId = value;
} }
public setIsDragging(value: boolean): void {
this.isDragging = value;
if (!value)
this.world.save();
}
public get scene(): Scene {
return this.world.data.initialScene;
}
public mutateWorld(mutation: Partial<World>): void {
this.world.data = {
...this.world.data,
...mutation,
}
}
public mutateScene(mutation: Partial<Scene>): void {
this.mutateWorld({
initialScene: {
...this.world.data.initialScene,
...mutation,
}
});
}
public mutateObject(mutation: Partial<ObjectInstance> & Pick<ObjectInstance, 'id'>): void {
this.mutateScene({
objects: {
...this.world.data.initialScene.objects,
[mutation.id]: {
...this.world.data.initialScene.objects[mutation.id],
...mutation,
},
},
});
}
public setObjectPosition(id: string, pos: V3): void {
const obj = this.scene.objects[id];
if (!obj)
return;
obj.position = pos;
this.mutateObject(obj);
}
public addObjectCloneAtRandomPosition(typeId: string): ObjectInstance { public addObjectCloneAtRandomPosition(typeId: string): ObjectInstance {
return this.addObjectClone( return this.addObjectClone(
typeId, typeId,
@ -44,7 +91,7 @@ export class WorldEditorState {
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];
this.world.data.intialScene.objects.push(obj); this.scene.objects[obj.id] = obj;
return obj; return obj;
} }

View File

@ -3,5 +3,5 @@ import type { ObjectInstance } from "./object";
export type Scene = { export type Scene = {
character: CharacterState; character: CharacterState;
objects: ObjectInstance[]; objects: Record<string, ObjectInstance>;
} }

View File

@ -6,10 +6,10 @@ import type { Pos3 } from "./3d";
import type { VoxelType } from "./voxel"; import type { VoxelType } from "./voxel";
export type World = { export type World = {
objectTypes: ObjectType[]; objectTypes: Record<string, ObjectType>;
voxelTypes: VoxelType[]; voxelTypes: Record<string, VoxelType>;
editorCamera: Pos3; editorCamera: Pos3;
intialScene: Scene; initialScene: Scene;
gameRules: GameRules; gameRules: GameRules;
state: GameState; state: GameState;
} }