player camera and controls during game

This commit is contained in:
azykov@mail.ru 2026-06-02 23:00:19 +03:00
parent c52458391d
commit 9b7cce5c79
No known key found for this signature in database
8 changed files with 130 additions and 22 deletions

View File

@ -3,11 +3,11 @@ import type { Character } from "../types";
export const CharacterView = observer(function ({ character }: { character: Character }) {
const pos = character.position;
const pos = character.transform.position;
return <mesh
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
rotation={character.look}
rotation={character.transform.look}
>
<boxGeometry args={[0.8, 0.8, 0.8]} />
<meshStandardMaterial color="yellow" />

View File

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

View File

@ -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}
@ -60,5 +66,6 @@ export const ThreeView = observer(function () {
}
</div>
</div>
</KeyboardControls>
)
});

24
src/model/gameFactory.ts Normal file
View File

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

View File

@ -9,9 +9,11 @@ export class WorldFactory {
voxelTypes: DEFAULT_VOXEL_TYPES,
initialScene: {
character: {
transform: {
position: [0, 0, 0],
look: [0, 0, 0],
},
},
objects: {},
},
gameRules: {

View File

@ -1,24 +1,32 @@
import { makeAutoObservable, toJS } from "mobx";
import { makeAutoObservable, reaction, toJS } from "mobx";
import type { WorldState } from "./worldState";
import type { Game, Scene } from "../types";
import type { Pos3 } from "../types/3d";
import type { Pos3, R3, V3 } from "../types/3d";
import type { CameraProps } from "@react-three/fiber";
import { clone } from "../utils";
import { state } from "./rootState";
import { GameFactory } from "../model/gameFactory";
export class GameState {
private readonly world: WorldState;
public readonly data: Game;
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 = {
paused: false,
time: 0,
scene: clone(toJS(this.world.data.initialScene)),
};
this.data = GameFactory.create(toJS(this.world.data));
this._stopAutoSave = this.startAutoSave();
makeAutoObservable(this);
}
@ -31,7 +39,7 @@ export class GameState {
}
public get camera(): Pos3 {
return this.scene.character;
return this.scene.character.transform;
}
public get cameraAsThree(): CameraProps {
@ -43,6 +51,25 @@ 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.data.paused = false;
}
@ -51,6 +78,13 @@ export class GameState {
this.data.paused = true;
}
public setCharacterTransform(transform: Pos3): void {
if (this.isPaused)
return;
this.scene.character.transform = transform;
}
public tick(deltaTime: number): void {
if (this.isPaused)
return;

View File

@ -5,7 +5,6 @@ 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";
export class WorldState {
public data: World = WorldFactory.create();
@ -66,8 +65,10 @@ export class WorldState {
},
initialScene: {
character: {
transform: {
position: [0, 0, 0],
look: [0, 0, 0],
}
},
objects: objectMap,
},

View File

@ -1,4 +1,5 @@
import type { Pos3 } from "./3d";
export type Character = Pos3 & {
export type Character = {
transform: Pos3,
}