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 { 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 (
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<mesh key={idx} position={v.position}>
|
||||||
<meshStandardMaterial color={(v.color ?? vt?.color) ?? 'white'} opacity={(v.opacity ?? vt?.opacity) ?? 1} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
{isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />}
|
<meshStandardMaterial color={(v.color ?? vt?.color) ?? 'white'} opacity={(v.opacity ?? vt?.opacity) ?? 1} />
|
||||||
</mesh>
|
{isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />}
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</group>
|
</group>
|
||||||
</>);
|
</>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
</>)
|
</>)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue