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 type { ObjectInstance } from "../types";
import { useRef } from "react";
import type { Mesh } from "three";
import { Edges } from "@react-three/drei";
import { useEffect, useRef, type RefObject } from "react";
import type { Group, Mesh } from "three";
import { Edges, TransformControls } from "@react-three/drei";
import { state } from "../state";
type ObjectViewProps = {
@ -10,39 +10,61 @@ type 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);
if (!objectType)
return null;
const isSelected = state.worldEditor.isEnabled &&
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 }) => {
if (!state.worldEditor.isEnabled)
return;
e.stopPropagation();
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 (<>
<group
position={object.position}
rotation={object.rotation}
onClick={handleClick}
>
{(isSelected && groupRef.current) && <TransformControls object={groupRef as RefObject<Group>} onChange={handleObjectChange} mode="translate" />}
<group ref={groupRef} position={object.position} rotation={object.rotation} onClick={handleClick}>
{
objectType.voxels.map((v, idx) => {
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]} />
<meshStandardMaterial color={(v.color ?? vt?.color) ?? 'white'} opacity={(v.opacity ?? vt?.opacity) ?? 1} />
{isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />}
</mesh>
);
})
}
</group>
</>);
});

View File

@ -6,7 +6,10 @@ import { WorldView } from './WorldView';
export const ThreeView = observer(function () {
return (
<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 />
</Canvas>
</div>

View File

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

View File

@ -11,8 +11,6 @@ export const WorldView = observer(function () {
const world = state.world;
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
console.log(world.currentScene.objects);
useFrame((_, delta) => {
world.tick(delta);
});
@ -31,13 +29,14 @@ export const WorldView = observer(function () {
return (<>
<OrbitControls
ref={controlsRef}
enabled={!world.isPlaying}
enabled={!world.isPlaying && !state.worldEditor.isDragging}
onEnd={handleEnd}
makeDefault
enableDamping={false}
/>
<ambientLight intensity={0.5} />
<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 />
</>)
});

View File

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

View File

@ -22,47 +22,50 @@ export class WorldState {
}
public loadMock() {
const objTypeId = 'test1';
const voxelTypeId = 'red';
const objectId = 'red';
this.data = {
objectTypes: [
{
id: 'test1',
objectTypes: {
[objTypeId]: {
id: objTypeId,
name: 'Test Object',
voxels: [
{
typeId: 'red',
typeId: voxelTypeId,
position: [0, 0, 0],
color: 'red',
opacity: 1,
},
],
},
],
voxelTypes: [
{
id: 'red',
},
voxelTypes: {
[voxelTypeId]: {
id: voxelTypeId,
collidable: true,
name: 'Red',
opacity: 1,
color: 'red',
}
],
},
editorCamera: {
position: [0, 2, 10],
look: [0, 0, 0],
},
intialScene: {
initialScene: {
character: {
position: [0, 0, 0],
look: [0, 0, 0],
},
objects: [
{
id: randomId(),
typeId: 'test1',
objects: {
[objectId]: {
id: objectId,
typeId: objTypeId,
position: [0, 0, 0],
rotation: [0, 0, 0],
},
],
},
},
gameRules: {
gravity: true,
@ -88,7 +91,7 @@ export class WorldState {
public get currentScene(): Scene {
return this.isPlaying
? (this.data.state as RunningGameState).scene
: this.data.intialScene;
: this.data.initialScene;
}
public get currentCamera(): Pos3 {
@ -115,7 +118,7 @@ export class WorldState {
playing: true,
paused: false,
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 {
return this.data.objectTypes.find((obj) => obj.id === id);
return this.data.objectTypes[id];
}
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 type { WorldState } from "./world";
import type { ObjectInstance } from "../types";
import type { ObjectInstance, Scene, World } from "../types";
import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils";
import type { Pos3 } from "../types/3d";
import type { Pos3, V3 } from "../types/3d";
export class WorldEditorState {
private readonly world: WorldState;
public selectedObjectId: string | undefined;
public isDragging: boolean = false;
constructor(world: WorldState) {
this.world = world;
@ -21,7 +22,6 @@ export class WorldEditorState {
public setCamera(value: Pos3): void {
this.world.data.editorCamera = value;
console.log(JSON.stringify(this.world.data.editorCamera));
this.world.save();
}
@ -29,6 +29,53 @@ export class WorldEditorState {
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 {
return this.addObjectClone(
typeId,
@ -44,7 +91,7 @@ export class WorldEditorState {
obj.position = [pos.x, pos.y, pos.z];
obj.rotation = [0, 0, 0];
this.world.data.intialScene.objects.push(obj);
this.scene.objects[obj.id] = obj;
return obj;
}

View File

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

View File

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