Compare commits
5 Commits
b3c7979f87
...
487a894fe5
| Author | SHA1 | Date |
|---|---|---|
|
|
487a894fe5 | |
|
|
858b47e71b | |
|
|
32386ba70f | |
|
|
9b7cce5c79 | |
|
|
c52458391d |
|
|
@ -3,14 +3,20 @@ import type { Character } from "../types";
|
|||
|
||||
export const CharacterView = observer(function ({ character }: { character: Character }) {
|
||||
|
||||
const pos = character.position;
|
||||
const pos = character.transform.position;
|
||||
|
||||
return <mesh
|
||||
return <group
|
||||
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
||||
rotation={character.look}
|
||||
rotation={character.transform.look}
|
||||
>
|
||||
{/* <mesh>
|
||||
<boxGeometry args={[0.8, 0.8, 0.8]} />
|
||||
<meshStandardMaterial color="yellow" />
|
||||
</mesh> */}
|
||||
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
||||
<coneGeometry args={[0.55, 0.8, 4]} />
|
||||
<meshStandardMaterial color="yellow" />
|
||||
</mesh>
|
||||
</group>
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,46 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { SceneView } from "./SceneView";
|
||||
import { state } from "../state";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { PointerLockControls, useKeyboardControls } from "@react-three/drei";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
function PlayerMovement() {
|
||||
const [, get] = useKeyboardControls();
|
||||
const dirty = useRef(false);
|
||||
|
||||
useFrame(({ camera }, dt) => {
|
||||
const { forward, backward, left, right } = get();
|
||||
const speed = 5 * dt;
|
||||
if (forward) { camera.translateZ(-speed); dirty.current = true; }
|
||||
if (backward) { camera.translateZ( speed); dirty.current = true; }
|
||||
if (left) { camera.translateX(-speed); dirty.current = true; }
|
||||
if (right) { camera.translateX( speed); dirty.current = true; }
|
||||
|
||||
if (!dirty.current) return;
|
||||
dirty.current = false;
|
||||
|
||||
const [rx, ry, rz] = camera.rotation.toArray();
|
||||
state.game?.setCharacterTransform({
|
||||
position: camera.position.toArray(),
|
||||
look: [rx, ry, rz],
|
||||
});
|
||||
});
|
||||
|
||||
return <PointerLockControls onChange={() => { dirty.current = true; }} />;
|
||||
}
|
||||
|
||||
export const GameView = observer(function () {
|
||||
const game = state.game;
|
||||
const { camera } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
if (!game)
|
||||
return;
|
||||
const { position, look } = game.camera;
|
||||
camera.position.set(position[0], position[1], position[2]);
|
||||
camera.rotation.set(look[0], look[1], look[2]);
|
||||
}, []);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
state.game?.tick(delta);
|
||||
|
|
@ -13,5 +49,8 @@ export const GameView = observer(function () {
|
|||
if (!game)
|
||||
return null;
|
||||
|
||||
return <SceneView scene={game.scene} />;
|
||||
return (<>
|
||||
<PlayerMovement />
|
||||
<SceneView scene={game.scene} renderCharacter={false} />
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
|||
onClick={handleClick}
|
||||
>
|
||||
{
|
||||
voxelGroups(objectType).map((vg) => (
|
||||
<Instances key={vg.id}>
|
||||
voxelGroups(objectType).map((vg) => {
|
||||
return <Instances key={vg.id} limit={vg.positions.length}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
|
||||
{
|
||||
|
|
@ -98,7 +98,7 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
|||
.map((pos, i) => <Instance key={i} position={pos} />)
|
||||
}
|
||||
</Instances>
|
||||
))
|
||||
})
|
||||
}
|
||||
</group>
|
||||
</>);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import type React from 'react';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { Grid, OrbitControls } from '@react-three/drei';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { state } from '../state';
|
||||
import { SceneView } from './SceneView';
|
||||
import type { Pos3 } from '../types/3d';
|
||||
import { type OrthographicCamera, type PerspectiveCamera } from 'three';
|
||||
import { CameraSync } from './tools/CameraSync';
|
||||
|
||||
const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
||||
const { camera: threeCamera } = useThree();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
|
||||
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
|
||||
threeCamera.updateProjectionMatrix();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export const SceneEditorView = observer(function () {
|
||||
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
|
||||
|
|
@ -68,6 +56,6 @@ export const SceneEditorView = observer(function () {
|
|||
sectionColor="white"
|
||||
infiniteGrid
|
||||
/>
|
||||
<SceneView scene={state.worldEditor.scene} />
|
||||
<SceneView scene={state.worldEditor.scene} renderCharacter={true} />
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@ import { ObjectView } from "./ObjectView";
|
|||
|
||||
type SceneViewProps = {
|
||||
scene: Scene,
|
||||
renderCharacter: boolean;
|
||||
}
|
||||
|
||||
export const SceneView = observer(function ({ scene }: SceneViewProps) {
|
||||
export const SceneView = observer(function ({ scene, renderCharacter }: SceneViewProps) {
|
||||
return (<>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||
{Object.values(scene.objects).map((obj) =>
|
||||
<ObjectView key={obj.id} object={obj} />)}
|
||||
<CharacterView character={scene.character} />
|
||||
{renderCharacter && <CharacterView character={scene.character} />}
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
import { Stats } from '@react-three/drei';
|
||||
import { KeyboardControls, Stats } from '@react-three/drei';
|
||||
import { useRef } from 'react';
|
||||
|
||||
function RenderInfoUpdater({ domRef }: { domRef: React.RefObject<HTMLDivElement | null> }) {
|
||||
|
|
@ -30,6 +30,12 @@ export const ThreeView = observer(function () {
|
|||
const isGame = state.isGamePlaying;
|
||||
const infoRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<KeyboardControls map={[
|
||||
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
|
||||
{ name: 'backward', keys: ['ArrowDown', 's', 'S'] },
|
||||
{ name: 'left', keys: ['ArrowLeft', 'a', 'A'] },
|
||||
{ name: 'right', keys: ['ArrowRight', 'd', 'D'] },
|
||||
]}>
|
||||
<div style={{ position: 'relative', width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<Canvas
|
||||
// camera={state.world.character.camera}
|
||||
|
|
@ -49,16 +55,17 @@ export const ThreeView = observer(function () {
|
|||
{
|
||||
state.game
|
||||
? <>
|
||||
<button onClick={() => state.game!.stop()}><IconStop /></button>
|
||||
<button onClick={() => state.stopGame()}><IconStop /></button>
|
||||
{
|
||||
state.game!.isPaused
|
||||
? <button onClick={() => state.game!.resume()}><IconPlay /></button>
|
||||
: <button onClick={() => state.game!.pause()}><IconPause /></button>
|
||||
}
|
||||
</>
|
||||
: <button onClick={() => state.world.play()}><IconPlay /></button>
|
||||
: <button onClick={() => state.startGame()}><IconPlay /></button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</KeyboardControls>
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { useThree } from "@react-three/fiber";
|
||||
import type { Pos3 } from "../../types/3d";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
|
||||
const { camera: threeCamera } = useThree();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
|
||||
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
|
||||
threeCamera.updateProjectionMatrix();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { Game, World } from "../types";
|
||||
import { clone } from "../utils";
|
||||
|
||||
export class GameFactory {
|
||||
|
||||
public static create(world: World): Game {
|
||||
return {
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: clone(world.initialScene),
|
||||
}
|
||||
}
|
||||
|
||||
public static load(): Game | undefined {
|
||||
const json = localStorage.getItem("game");
|
||||
if (json) {
|
||||
return JSON.parse(json) as Game;
|
||||
}
|
||||
}
|
||||
|
||||
public static save(game: Game): void {
|
||||
localStorage.setItem("game", JSON.stringify(game));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import type { Voxel } from "../../types/voxel";
|
||||
|
||||
export function terrainXZ(width: number, length: number): Voxel[] {
|
||||
|
||||
const vs = Array(width)
|
||||
.fill(0)
|
||||
.flatMap((_, x) =>
|
||||
Array(length)
|
||||
.fill(0)
|
||||
.map((_, z) => ({
|
||||
typeId: 'dirt',
|
||||
position: [x - Math.floor(width / 2), 0, z - Math.floor(length / 2)],
|
||||
color: 'green',
|
||||
} as Voxel))
|
||||
);
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
|
@ -9,13 +9,12 @@ export class WorldFactory {
|
|||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||
initialScene: {
|
||||
character: {
|
||||
transform: {
|
||||
position: [0, 0, 0],
|
||||
look: [0, 0, 0],
|
||||
},
|
||||
objects: {},
|
||||
},
|
||||
state: {
|
||||
playing: false,
|
||||
objects: {},
|
||||
},
|
||||
gameRules: {
|
||||
gravity: true,
|
||||
|
|
|
|||
|
|
@ -1,31 +1,45 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import { makeAutoObservable, reaction, toJS } from "mobx";
|
||||
import type { WorldState } from "./worldState";
|
||||
import type { RunningGameState, Scene } from "../types";
|
||||
import type { Pos3 } from "../types/3d";
|
||||
import type { Game, Scene } from "../types";
|
||||
import type { Pos3, R3, V3 } from "../types/3d";
|
||||
import type { CameraProps } from "@react-three/fiber";
|
||||
import { GameFactory } from "../model/gameFactory";
|
||||
|
||||
export class GameState {
|
||||
private readonly world: WorldState;
|
||||
|
||||
public data: Game;
|
||||
|
||||
private startAutoSave() {
|
||||
return reaction(() => toJS(this.data), (data) => this.saveData(data), { delay: 5000 });
|
||||
}
|
||||
|
||||
private withoutAutoSave(fn: () => void) {
|
||||
this._stopAutoSave();
|
||||
fn();
|
||||
this._stopAutoSave = this.startAutoSave();
|
||||
}
|
||||
|
||||
private _stopAutoSave: () => void = () => { };
|
||||
|
||||
constructor(world: WorldState) {
|
||||
this.world = world;
|
||||
this.data = GameFactory.create(toJS(this.world.data));
|
||||
|
||||
this._stopAutoSave = this.startAutoSave();
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
public get state(): RunningGameState {
|
||||
return this.world.data.state as RunningGameState;
|
||||
}
|
||||
|
||||
public get isPaused(): boolean {
|
||||
return this.state.paused;
|
||||
return this.data.paused;
|
||||
}
|
||||
|
||||
public get scene(): Scene {
|
||||
return this.state.scene;
|
||||
return this.data.scene;
|
||||
}
|
||||
|
||||
public get camera(): Pos3 {
|
||||
return this.scene.character;
|
||||
return this.scene.character.transform;
|
||||
}
|
||||
|
||||
public get cameraAsThree(): CameraProps {
|
||||
|
|
@ -37,19 +51,44 @@ export class GameState {
|
|||
}
|
||||
}
|
||||
|
||||
// public load() {
|
||||
// console.log('Loading game...');
|
||||
// this.withoutAutoSave(() => {
|
||||
// this.data = GameFactory.load() ?? GameFactory.create(this.world.data);
|
||||
// });
|
||||
// }
|
||||
|
||||
private saveData(data: Game): void {
|
||||
console.log('Saving game...');
|
||||
const stack = new Error('Saving game...').stack!.split('\n').slice(1);
|
||||
const { ...debug } = toJS(data);
|
||||
console.dir({ stack, debug });
|
||||
GameFactory.save(toJS(this.data));
|
||||
}
|
||||
|
||||
public save(): void {
|
||||
this.saveData(this.data);
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.state.paused = false;
|
||||
this.data.paused = false;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.state.paused = true;
|
||||
this.data.paused = true;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.world.data.state = { playing: false };
|
||||
public setCharacterTransform(transform: Pos3): void {
|
||||
if (this.isPaused)
|
||||
return;
|
||||
|
||||
this.scene.character.transform = transform;
|
||||
}
|
||||
|
||||
public tick(_deltaTime: number): void {
|
||||
//TODO
|
||||
public tick(deltaTime: number): void {
|
||||
if (this.isPaused)
|
||||
return;
|
||||
|
||||
this.data.time += deltaTime;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { GameState } from "./gameState";
|
|||
export class RootState {
|
||||
public readonly world = new WorldState();
|
||||
public readonly worldEditor: WorldEditorState;
|
||||
public game: GameState | undefined;
|
||||
|
||||
constructor() {
|
||||
this.worldEditor = new WorldEditorState(this.world);
|
||||
|
|
@ -19,12 +20,22 @@ export class RootState {
|
|||
}
|
||||
|
||||
public get isGamePlaying(): boolean {
|
||||
return this.world.data.state.playing;
|
||||
return this.game !== undefined;
|
||||
}
|
||||
|
||||
public get game(): GameState | undefined {
|
||||
if (this.isGamePlaying)
|
||||
return new GameState(this.world);
|
||||
public startGame(): void {
|
||||
if (this.game)
|
||||
this.stopGame();
|
||||
this.game = new GameState(this.world),
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
|
||||
public stopGame(): void {
|
||||
if (!this.game)
|
||||
return;
|
||||
|
||||
this.game.pause();
|
||||
this.game = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { VoxelType } from "../types/voxel";
|
|||
import { state } from "./rootState";
|
||||
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
|
||||
import { wolf } from "../model/objectPrefabs/wolf";
|
||||
import { clone } from "../utils";
|
||||
import { terrainXZ } from "../model/objectPrefabs/terrain";
|
||||
|
||||
export class WorldState {
|
||||
public data: World = WorldFactory.create();
|
||||
|
|
@ -58,25 +58,38 @@ export class WorldState {
|
|||
name: 'Wolf',
|
||||
voxels: wolf,
|
||||
},
|
||||
terrain: {
|
||||
id: 'terrain',
|
||||
name: 'Terrain',
|
||||
voxels: terrainXZ(50, 50),
|
||||
}
|
||||
},
|
||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||
editorCamera: {
|
||||
position: [0, 2, 10],
|
||||
look: [0, 0, 0],
|
||||
position: [-9, 11, 30],
|
||||
look: [-0.52, -0.35, -0.2],
|
||||
},
|
||||
initialScene: {
|
||||
character: {
|
||||
position: [0, 0, 0],
|
||||
transform: {
|
||||
position: [0, 5, 20],
|
||||
look: [0, 0, 0],
|
||||
}
|
||||
},
|
||||
objects: {
|
||||
terrain: {
|
||||
id: 'terrain',
|
||||
typeId: 'terrain',
|
||||
position: [0, -1, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
},
|
||||
...objectMap,
|
||||
},
|
||||
objects: objectMap,
|
||||
},
|
||||
gameRules: {
|
||||
gravity: true,
|
||||
},
|
||||
state: {
|
||||
playing: false,
|
||||
},
|
||||
};
|
||||
console.log(objects);
|
||||
state.worldEditor.resetSelectedObject();
|
||||
|
|
@ -109,18 +122,4 @@ export class WorldState {
|
|||
public getVoxelTypeById(id: string): VoxelType | undefined {
|
||||
return this.data.voxelTypes[id];
|
||||
}
|
||||
|
||||
public play(): void {
|
||||
if (state.game)
|
||||
state.game.resume();
|
||||
else {
|
||||
this.data.state = {
|
||||
playing: true,
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: clone(this.data.initialScene),
|
||||
}
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Pos3 } from "./3d";
|
||||
|
||||
export type Character = Pos3 & {
|
||||
export type Character = {
|
||||
transform: Pos3,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import type { Scene } from "./scene";
|
||||
|
||||
export type Game = {
|
||||
paused: boolean;
|
||||
time: number;
|
||||
scene: Scene;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type { Scene } from "./scene";
|
||||
|
||||
export type StoppedGameState = {
|
||||
playing: false;
|
||||
}
|
||||
|
||||
export type RunningGameState = {
|
||||
playing: true;
|
||||
paused: boolean;
|
||||
time: number;
|
||||
scene: Scene;
|
||||
}
|
||||
|
||||
export type GameState = StoppedGameState | RunningGameState;
|
||||
|
|
@ -2,7 +2,7 @@ export * from './object';
|
|||
export * from './scene';
|
||||
export * from './world';
|
||||
export * from './gameRules';
|
||||
export * from './gameState';
|
||||
export * from './game';
|
||||
export * from './character';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { GameRules } from "./gameRules";
|
||||
import type { GameState } from "./gameState";
|
||||
import type { ObjectType } from "./object";
|
||||
import type { Scene } from "./scene";
|
||||
import type { Pos3 } from "./3d";
|
||||
|
|
@ -11,5 +10,4 @@ export type World = {
|
|||
editorCamera: Pos3;
|
||||
initialScene: Scene;
|
||||
gameRules: GameRules;
|
||||
state: GameState;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue