game state separated from world state

This commit is contained in:
azykov@mail.ru 2026-06-02 21:31:01 +03:00
parent b3c7979f87
commit c52458391d
No known key found for this signature in database
11 changed files with 63 additions and 72 deletions

View File

@ -1,24 +1,12 @@
import { useLayoutEffect, useRef } from 'react'; import { useRef } from 'react';
import type React from 'react'; import type React from 'react';
import { useThree } from '@react-three/fiber';
import { Grid, OrbitControls } from '@react-three/drei'; import { Grid, OrbitControls } from '@react-three/drei';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { state } from '../state'; import { state } from '../state';
import { SceneView } from './SceneView'; import { SceneView } from './SceneView';
import type { Pos3 } from '../types/3d';
import { type OrthographicCamera, type PerspectiveCamera } from 'three'; 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 () { export const SceneEditorView = observer(function () {
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null); const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);

View File

@ -49,14 +49,14 @@ export const ThreeView = observer(function () {
{ {
state.game state.game
? <> ? <>
<button onClick={() => state.game!.stop()}><IconStop /></button> <button onClick={() => state.stopGame()}><IconStop /></button>
{ {
state.game!.isPaused state.game!.isPaused
? <button onClick={() => state.game!.resume()}><IconPlay /></button> ? <button onClick={() => state.game!.resume()}><IconPlay /></button>
: <button onClick={() => state.game!.pause()}><IconPause /></button> : <button onClick={() => state.game!.pause()}><IconPause /></button>
} }
</> </>
: <button onClick={() => state.world.play()}><IconPlay /></button> : <button onClick={() => state.startGame()}><IconPlay /></button>
} }
</div> </div>
</div> </div>

View File

@ -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;
});

View File

@ -14,9 +14,6 @@ export class WorldFactory {
}, },
objects: {}, objects: {},
}, },
state: {
playing: false,
},
gameRules: { gameRules: {
gravity: true, gravity: true,
}, },

View File

@ -1,27 +1,33 @@
import { makeAutoObservable } from "mobx"; import { makeAutoObservable, toJS } from "mobx";
import type { WorldState } from "./worldState"; import type { WorldState } from "./worldState";
import type { RunningGameState, Scene } from "../types"; import type { Game, Scene } from "../types";
import type { Pos3 } from "../types/3d"; import type { Pos3 } from "../types/3d";
import type { CameraProps } from "@react-three/fiber"; import type { CameraProps } from "@react-three/fiber";
import { clone } from "../utils";
import { state } from "./rootState";
export class GameState { export class GameState {
private readonly world: WorldState; private readonly world: WorldState;
public readonly data: Game;
constructor(world: WorldState) { constructor(world: WorldState) {
this.world = world; this.world = world;
this.data = {
paused: false,
time: 0,
scene: clone(toJS(this.world.data.initialScene)),
};
makeAutoObservable(this); makeAutoObservable(this);
} }
public get state(): RunningGameState {
return this.world.data.state as RunningGameState;
}
public get isPaused(): boolean { public get isPaused(): boolean {
return this.state.paused; return this.data.paused;
} }
public get scene(): Scene { public get scene(): Scene {
return this.state.scene; return this.data.scene;
} }
public get camera(): Pos3 { public get camera(): Pos3 {
@ -38,18 +44,17 @@ export class GameState {
} }
public resume(): void { public resume(): void {
this.state.paused = false; this.data.paused = false;
} }
public pause(): void { public pause(): void {
this.state.paused = true; this.data.paused = true;
} }
public stop(): void { public tick(deltaTime: number): void {
this.world.data.state = { playing: false }; if (this.isPaused)
} return;
public tick(_deltaTime: number): void { this.data.time += deltaTime;
//TODO
} }
} }

View File

@ -6,6 +6,7 @@ import { GameState } from "./gameState";
export class RootState { export class RootState {
public readonly world = new WorldState(); public readonly world = new WorldState();
public readonly worldEditor: WorldEditorState; public readonly worldEditor: WorldEditorState;
public game: GameState | undefined;
constructor() { constructor() {
this.worldEditor = new WorldEditorState(this.world); this.worldEditor = new WorldEditorState(this.world);
@ -19,12 +20,22 @@ export class RootState {
} }
public get isGamePlaying(): boolean { public get isGamePlaying(): boolean {
return this.world.data.state.playing; return this.game !== undefined;
} }
public get game(): GameState | undefined { public startGame(): void {
if (this.isGamePlaying) if (this.game)
return new GameState(this.world); this.stopGame();
this.game = new GameState(this.world),
state.worldEditor.resetSelectedObject();
}
public stopGame(): void {
if (!this.game)
return;
this.game.pause();
this.game = undefined;
} }
} }

View File

@ -74,9 +74,6 @@ export class WorldState {
gameRules: { gameRules: {
gravity: true, gravity: true,
}, },
state: {
playing: false,
},
}; };
console.log(objects); console.log(objects);
state.worldEditor.resetSelectedObject(); state.worldEditor.resetSelectedObject();
@ -109,18 +106,4 @@ export class WorldState {
public getVoxelTypeById(id: string): VoxelType | undefined { public getVoxelTypeById(id: string): VoxelType | undefined {
return this.data.voxelTypes[id]; 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();
}
}
} }

7
src/types/game.ts Normal file
View File

@ -0,0 +1,7 @@
import type { Scene } from "./scene";
export type Game = {
paused: boolean;
time: number;
scene: Scene;
}

View File

@ -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;

View File

@ -2,7 +2,7 @@ export * from './object';
export * from './scene'; export * from './scene';
export * from './world'; export * from './world';
export * from './gameRules'; export * from './gameRules';
export * from './gameState'; export * from './game';
export * from './character'; export * from './character';

View File

@ -1,5 +1,4 @@
import type { GameRules } from "./gameRules"; import type { GameRules } from "./gameRules";
import type { GameState } from "./gameState";
import type { ObjectType } from "./object"; import type { ObjectType } from "./object";
import type { Scene } from "./scene"; import type { Scene } from "./scene";
import type { Pos3 } from "./3d"; import type { Pos3 } from "./3d";
@ -11,5 +10,4 @@ export type World = {
editorCamera: Pos3; editorCamera: Pos3;
initialScene: Scene; initialScene: Scene;
gameRules: GameRules; gameRules: GameRules;
state: GameState;
} }