world structure: arrays to records
object move with mouse
This commit is contained in:
parent
f3033d1d0d
commit
e37cb08698
|
|
@ -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>
|
||||
</>);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export class WorldFactory {
|
|||
public static create(): World {
|
||||
return {
|
||||
objectTypes: [],
|
||||
intialScene: {
|
||||
initialScene: {
|
||||
character: {
|
||||
position: [0, 0, 0],
|
||||
look: [0, 0, 0],
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ import type { ObjectInstance } from "./object";
|
|||
|
||||
export type Scene = {
|
||||
character: CharacterState;
|
||||
objects: ObjectInstance[];
|
||||
objects: Record<string, ObjectInstance>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue