From fa459ba8ec693f056f6be722ac6ffc4860fa28c4 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Fri, 5 Jun 2026 11:51:30 +0300 Subject: [PATCH] replaced characer with any object can be controlled by player --- package.json | 1 + pnpm-lock.yaml | 18 +++++++ src/components/CharacterView.tsx | 60 +++++++++-------------- src/components/GameObjectView.tsx | 10 ++-- src/components/MenuView.scss | 6 ++- src/components/MenuView.tsx | 21 +++++++- src/components/ObjectEditorView.tsx | 4 +- src/components/ObjectViewInternal.tsx | 39 ++++++++------- src/components/SceneView.tsx | 20 +------- src/components/ThreeView.tsx | 30 +++++------- src/model/gameFactory.ts | 2 +- src/model/worldFactory.ts | 6 --- src/state/gameState.ts | 20 +------- src/state/{menuState.ts => menuState.tsx} | 26 +++++++++- src/state/rootState.ts | 12 +++-- src/state/worldEditorState.ts | 2 +- src/state/worldState.ts | 7 +-- src/types/model/character.ts | 8 --- src/types/model/index.ts | 1 - src/types/model/runtime.ts | 2 +- src/types/model/scene.ts | 12 ++--- src/utils/runtime/object.ts | 3 ++ 22 files changed, 154 insertions(+), 156 deletions(-) rename src/state/{menuState.ts => menuState.tsx} (68%) delete mode 100644 src/types/model/character.ts diff --git a/package.json b/package.json index 0421c21..3b8a6c0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.6.1", "@react-three/rapier": "^2.2.0", + "@tabler/icons-react": "^3.44.0", "@types/three": "^0.184.1", "blockly": "^12.5.1", "install": "^0.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c33b61..c36244f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@react-three/rapier': specifier: ^2.2.0 version: 2.2.0(@react-three/fiber@9.6.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(three@0.184.0))(react@19.2.6)(three@0.184.0) + '@tabler/icons-react': + specifier: ^3.44.0 + version: 3.44.0(react@19.2.6) '@types/three': specifier: ^0.184.1 version: 0.184.1 @@ -537,6 +540,14 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@tabler/icons-react@3.44.0': + resolution: {integrity: sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.44.0': + resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==} + '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} @@ -2045,6 +2056,13 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@tabler/icons-react@3.44.0(react@19.2.6)': + dependencies: + '@tabler/icons': 3.44.0 + react: 19.2.6 + + '@tabler/icons@3.44.0': {} + '@tweenjs/tween.js@23.1.3': {} '@tybys/wasm-util@0.10.2': diff --git a/src/components/CharacterView.tsx b/src/components/CharacterView.tsx index 49588f0..c2215f9 100644 --- a/src/components/CharacterView.tsx +++ b/src/components/CharacterView.tsx @@ -1,13 +1,13 @@ import { observer } from "mobx-react-lite"; -import type { Character } from "../types"; -import { SyncRigidBody } from "./SyncRigidBody"; -import { state } from "../state"; +import type { ObjectType, RuntimeGameObjectInstance } from "../types"; +import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal"; import { useEffect, useRef } from "react"; import { Euler, Quaternion, Vector3 } from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { useKeyboardControls } from "@react-three/drei"; -import { useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, RoundCuboidCollider } from "@react-three/rapier"; +import { useRapier, useBeforePhysicsStep } from "@react-three/rapier"; import { joystickValues } from "../joystickInput"; +import { state } from "../state"; const SPEED = 5; const JUMP_SPEED = 8; @@ -16,7 +16,6 @@ const SENSITIVITY = 0.002; const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5); const LOOK_RATE = 2000; -// recreate private types type RapierWorldCreateCharacterControllerFunction = ReturnType['world']['createCharacterController']; type KinematicCharacterController = ReturnType; @@ -26,15 +25,13 @@ const _offset = new Vector3(); const _charPos = new Vector3(); const _lookAt = new Vector3(); -type CharacterViewProps = { - character: Character; - editMode?: boolean; +type PlayerObjectViewProps = { + object: RuntimeGameObjectInstance; + objectType: ObjectType; } -export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) { - const pos = character.transform.position; - const rbRef = useRef(null); - const colliderRef = useRef(null); +export const PlayerObjectView = observer(function ({ object, objectType }: PlayerObjectViewProps) { + const handleRef = useRef(null); const [, get] = useKeyboardControls(); const { gl } = useThree(); const { world } = useRapier(); @@ -59,7 +56,6 @@ export const CharacterView = observer(function ({ character, editMode }: Charact }, [world]); useEffect(() => { - if (editMode) return; const canvas = gl.domElement; const onClick = () => canvas.requestPointerLock(); const onMouseMove = (e: MouseEvent) => { @@ -73,14 +69,15 @@ export const CharacterView = observer(function ({ character, editMode }: Charact canvas.removeEventListener('click', onClick); document.removeEventListener('mousemove', onMouseMove); }; - }, [gl, editMode]); + }, [gl]); useBeforePhysicsStep((world) => { - if (editMode) return; - const rb = rbRef.current; - const collider = colliderRef.current; + const rb = handleRef.current?.rb; + if (!rb) return; + const collider = rb.collider(0); + if (!collider) return; const controller = controllerRef.current; - if (!rb || !collider || !controller) return; + if (!controller) return; if (state.game?.isPaused) return; const dt = world.timestep; @@ -118,8 +115,7 @@ export const CharacterView = observer(function ({ character, editMode }: Charact }); useFrame(({ camera }, delta) => { - if (editMode) return; - const rb = rbRef.current; + const rb = handleRef.current?.rb; if (!rb) return; mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta; @@ -139,23 +135,11 @@ export const CharacterView = observer(function ({ character, editMode }: Charact }); return ( - { }} - > - {/* */} - {/* */} - {/* */} - - - - - - - - + ); }); diff --git a/src/components/GameObjectView.tsx b/src/components/GameObjectView.tsx index 42df29b..3744bc0 100644 --- a/src/components/GameObjectView.tsx +++ b/src/components/GameObjectView.tsx @@ -1,17 +1,21 @@ import { observer } from "mobx-react-lite"; -import type { ObjectInstance, Runtime } from "../types"; +import type { ObjectInstance, Runtime, RuntimeGameObjectInstance } from "../types"; import { state } from "../state"; import { ObjectViewInternal } from "./ObjectViewInternal"; +import { PlayerObjectView } from "./CharacterView"; export type GameObjectViewProps = { object: Runtime; + isPlayer: boolean; } export const GameObjectView = observer(function (props: GameObjectViewProps) { - const objectType = state.world.getObjectTypeById(props.object.typeId); if (!objectType) return null; - return + if (props.isPlayer) + return ; + + return ; }); diff --git a/src/components/MenuView.scss b/src/components/MenuView.scss index f3e8815..ad3ba2d 100644 --- a/src/components/MenuView.scss +++ b/src/components/MenuView.scss @@ -10,7 +10,7 @@ display: none; } - display: block; + display: flex; padding: 8px 8px; cursor: pointer; border-radius: 4px; @@ -28,6 +28,10 @@ background: rgba(255, 255, 255, 0.15); color: #fff; } + + &>.title { + flex: 1; + } } & details { diff --git a/src/components/MenuView.tsx b/src/components/MenuView.tsx index b1acf3c..9c994c5 100644 --- a/src/components/MenuView.tsx +++ b/src/components/MenuView.tsx @@ -14,13 +14,30 @@ export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) { ref.current.open = true; }, [forceOpen]); + const classNames = []; + if (node.selected?.()) + classNames.push('selected'); + if (node.className !== undefined) + classNames.push(node.className); + return (
node.onClick?.()} > - {node.title} +
{node.title}
+ {node.actions &&
+ {node.actions.map((action) => +
{ e.stopPropagation(); e.preventDefault(); action.onClick(); }} + > + {action.content} +
+ )} +
}
{node.children?.map((child) => )}
diff --git a/src/components/ObjectEditorView.tsx b/src/components/ObjectEditorView.tsx index 9ab31ee..1f3daf7 100644 --- a/src/components/ObjectEditorView.tsx +++ b/src/components/ObjectEditorView.tsx @@ -11,6 +11,7 @@ import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewI type ObjectEditorViewProps = { object: Runtime; + isPlayer: boolean; } type SelectionOverlayProps = { @@ -50,7 +51,7 @@ const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: S ); }); -export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) { +export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectEditorViewProps) { const dataRef = useRef(null); // Only observes world object types — not selection state — so won't @@ -91,6 +92,7 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP ref={dataRef} object={object} isEditor + isPlayer={isPlayer} objectType={objectType} onClick={handleClick} /> diff --git a/src/components/ObjectViewInternal.tsx b/src/components/ObjectViewInternal.tsx index 78ee3ab..8b62d2f 100644 --- a/src/components/ObjectViewInternal.tsx +++ b/src/components/ObjectViewInternal.tsx @@ -17,6 +17,7 @@ type ObjectViewInternalProps = { object: Omit; objectType: ObjectType; isEditor?: boolean; + isPlayer?: boolean; onClick?: (e: ThreeEvent) => void; ref?: Ref; } @@ -28,30 +29,30 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props useImperativeHandle(ref, () => ({ group: groupRef.current, rb: rbRef.current })); useEffect( - () => reaction( - () => 'version' in object ? object.version : 0, - () => { - if (!rbRef.current) - return; + () => { + if (props.isPlayer) return; + return reaction( + () => 'version' in object ? object.version : 0, + () => { + if (!rbRef.current) + return; - const gameObj = object as RuntimeGameObjectInstance; + const gameObj = object as RuntimeGameObjectInstance; - // position - rbRef.current.setTranslation(v3toRapier(gameObj.position), true); + rbRef.current.setTranslation(v3toRapier(gameObj.position), true); - // rotation - const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]); - const q = new Quaternion().setFromEuler(euler); - rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true); + const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]); + const q = new Quaternion().setFromEuler(euler); + rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true); - // scale - groupRef.current?.scale.set(...gameObj.scale); + groupRef.current?.scale.set(...gameObj.scale); - rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true); + rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true); - rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true); - }, - ), + rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true); + }, + ); + }, [object.id] ); @@ -59,7 +60,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props { - // if (props.editMode) - // return; - - // const game = state.game; - // if (!game || game.isPaused) - // return; - - // rapier.step(dt); - // }) - return (<> { Object.values(props.scene.objects).map((obj) => ( props.editMode - ? - : + ? + : )) } - {/* {props.editMode && } */} - {} ); }); diff --git a/src/components/ThreeView.tsx b/src/components/ThreeView.tsx index 76aa4c2..a388325 100644 --- a/src/components/ThreeView.tsx +++ b/src/components/ThreeView.tsx @@ -2,6 +2,13 @@ import { Canvas, useFrame, useThree } from '@react-three/fiber'; import { KeyboardControls, Stats } from '@react-three/drei'; import { action } from 'mobx'; import { chartRef } from './chartRef'; +import { observer } from 'mobx-react-lite'; +import { state } from '../state'; +import { GameView } from './GameView'; +import { SceneEditorView } from './SceneEditorView'; +import { JoystickView } from './JoystickView'; +import type { RefObject } from 'react'; +import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled } from '@tabler/icons-react'; function RenderInfoUpdater() { const { gl } = useThree(); @@ -20,19 +27,8 @@ function RenderInfoUpdater() { return null; } -import { observer } from 'mobx-react-lite'; -import { state } from '../state'; -import { GameView } from './GameView'; -import { SceneEditorView } from './SceneEditorView'; -import { JoystickView } from './JoystickView'; -import type { RefObject } from 'react'; - -const IconStop = () => ; -const IconPause = () => ; -const IconPlay = () => ; - export const ThreeView = observer(function () { - const isGame = state.isGamePlaying; + const isGame = !!state.game; return ( : } {isGame && } -
+
{ state.game ? <> - + { state.game!.isPaused - ? - : + ? + : } - : + : }
diff --git a/src/model/gameFactory.ts b/src/model/gameFactory.ts index 611a39e..08d6b3b 100644 --- a/src/model/gameFactory.ts +++ b/src/model/gameFactory.ts @@ -80,11 +80,11 @@ export class GameFactory { .filter((ot) => ot.javascript) .forEach((ot) => { const otCode = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`); - const api = new ObjectApi(o, ot, world, scene); Object.values(scene.objects) .filter((o) => o.typeId == ot.id) .forEach((o) => { + const api = new ObjectApi(o, ot, world, scene); otCode(internalBus, { api, o, ot }); }) }); diff --git a/src/model/worldFactory.ts b/src/model/worldFactory.ts index a6f7413..2b0df7d 100644 --- a/src/model/worldFactory.ts +++ b/src/model/worldFactory.ts @@ -9,12 +9,6 @@ export class WorldFactory { objectTypes: {}, voxelTypes: DEFAULT_VOXEL_TYPES, initialScene: { - character: { - transform: { - position: [0, 0, 0], - look: [0, 0, 0], - }, - }, objects: {}, }, gameRules: { diff --git a/src/state/gameState.ts b/src/state/gameState.ts index 40c65b0..ee95518 100644 --- a/src/state/gameState.ts +++ b/src/state/gameState.ts @@ -1,6 +1,6 @@ import { makeAutoObservable, reaction, toJS } from "mobx"; import type { WorldState } from "./worldState"; -import type { Game, Pos3, V3, R3, RuntimeGameScene } from "../types"; +import type { Game, Pos3, RuntimeGameScene } from "../types"; import type { CameraProps } from "@react-three/fiber"; import { GameEventBus, GameFactory } from "../model/gameFactory"; @@ -83,7 +83,6 @@ export class GameState { public get camera(): Pos3 { return this.world.data.editorCamera; - return this.scene.character.transform; } public get cameraAsThree(): CameraProps { @@ -122,23 +121,6 @@ export class GameState { this.isPaused = true; } - public setCharacterTransform( - transform: Pos3, - linearVelocity?: V3, - angularVelocity?: R3, - ): void { - if (this.isPaused) - return; - - this.scene.character.transform = transform; - if (linearVelocity) - this.scene.character.linearVelocity = linearVelocity; - if (angularVelocity) - this.scene.character.angularVelocity = angularVelocity; - - // console.log(`changed character to ${JSON.stringify(this.scene.character)}`); - } - public tick(deltaTime: number): void { if (this.isPaused) return; diff --git a/src/state/menuState.ts b/src/state/menuState.tsx similarity index 68% rename from src/state/menuState.ts rename to src/state/menuState.tsx index afcda9a..706aa47 100644 --- a/src/state/menuState.ts +++ b/src/state/menuState.tsx @@ -1,9 +1,20 @@ import { makeAutoObservable } from "mobx"; import { state } from "./rootState"; +import type { ReactNode } from "react"; +import { IconRun } from '@tabler/icons-react'; + +export type MenuNodeAction = { + id: string; + content: ReactNode; + tooltip: string; + onClick: () => void; +} export type MenuNode = { id: string; title: string; + className?: string; + actions?: MenuNodeAction[]; onClick?: () => void; selected?: () => boolean; children?: MenuNode[]; @@ -26,10 +37,21 @@ export class MenuState { .map((o) => ({ id: `o-${o.id}`, title: o.id, + className: state.worldEditor.scene.playerObjectId === o.id + ? 'player-controlled-object' + : undefined, + actions: [ + { + id: 'control-by-player', + content: , + tooltip: 'Mark as player', + onClick: () => { state.markObjectAsPlayer(o); }, + } + ], onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) }, selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id, - })) - })); + } as MenuNode)) + } as MenuNode)); } private get editorMenu(): MenuNode[] { diff --git a/src/state/rootState.ts b/src/state/rootState.ts index 2738b75..caf6abe 100644 --- a/src/state/rootState.ts +++ b/src/state/rootState.ts @@ -3,6 +3,7 @@ import { WorldState } from "./worldState"; import { WorldEditorState } from "./worldEditorState"; import { GameState } from "./gameState"; import { MenuState } from "./menuState"; +import type { RuntimeObjectInstance } from "../types"; export type RenderInfo = { calls: number, @@ -31,10 +32,6 @@ export class RootState { ); } - public get isGamePlaying(): boolean { - return this.game !== undefined; - } - public startGame(): void { state.worldEditor.resetSelection(); if (this.game) @@ -53,6 +50,13 @@ export class RootState { public setRenderInfo(value: RenderInfo | undefined) { this.renderInfo = value; } + + public markObjectAsPlayer(object: RuntimeObjectInstance) { + if (this.game) + this.game.scene.playerObjectId = object.id; + else + this.world.data.initialScene.playerObjectId = object.id; + } } export const state = new RootState(); diff --git a/src/state/worldEditorState.ts b/src/state/worldEditorState.ts index 6057d5c..a67f621 100644 --- a/src/state/worldEditorState.ts +++ b/src/state/worldEditorState.ts @@ -61,7 +61,7 @@ export class WorldEditorState { } public get isEnabled(): boolean { - return !state.isGamePlaying; + return !state.game; } public setCamera(value: Pos3): void { diff --git a/src/state/worldState.ts b/src/state/worldState.ts index 9d2b59b..3a33ed3 100644 --- a/src/state/worldState.ts +++ b/src/state/worldState.ts @@ -72,12 +72,6 @@ export class WorldState { look: [-0.52, -0.35, -0.2], }, initialScene: { - character: { - transform: { - position: [0, 5, 20], - look: [0, 0, 0], - }, - }, objects: { terrain: { id: 'terrain', @@ -90,6 +84,7 @@ export class WorldState { }, ...objectMap, }, + playerObjectId: 'obj1', }, gameRules: { gravity: true, diff --git a/src/types/model/character.ts b/src/types/model/character.ts deleted file mode 100644 index 820861f..0000000 --- a/src/types/model/character.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Pos3 } from "../3d"; -import type { GameObjectData } from "./object"; - -export type Character = { - transform: Pos3, -} - -export type GameCharacter = Character & GameObjectData; diff --git a/src/types/model/index.ts b/src/types/model/index.ts index a0f4417..942d704 100644 --- a/src/types/model/index.ts +++ b/src/types/model/index.ts @@ -3,6 +3,5 @@ export * from './scene'; export * from './world'; export * from './gameRules'; export * from './game'; -export * from './character'; export * from './voxel'; export * from './runtime'; diff --git a/src/types/model/runtime.ts b/src/types/model/runtime.ts index 920ee3a..9067556 100644 --- a/src/types/model/runtime.ts +++ b/src/types/model/runtime.ts @@ -8,7 +8,7 @@ export type ObjectInstanceRuntimeData = { colliderMesh: [Float32Array, Uint32Array] | null; }; pendingActions: { - impulse: { direction: V3, amplitude: number }; + impulse?: { direction: V3, amplitude: number }; } } diff --git a/src/types/model/scene.ts b/src/types/model/scene.ts index 98ae942..24e4f8b 100644 --- a/src/types/model/scene.ts +++ b/src/types/model/scene.ts @@ -1,22 +1,18 @@ -import type { Character, GameCharacter } from "./character"; import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object"; export type Scene = { - character: Character; objects: Record; + playerObjectId: string | undefined; } -export type RuntimeScene = { - character: Character; +export type RuntimeScene = Scene & { objects: Record; } -export type GameScene = { - character: GameCharacter; +export type GameScene = Scene & { objects: Record; } -export type RuntimeGameScene = { - character: GameCharacter; +export type RuntimeGameScene = Scene & { objects: Record; } diff --git a/src/utils/runtime/object.ts b/src/utils/runtime/object.ts index 36e09e8..ae680f6 100644 --- a/src/utils/runtime/object.ts +++ b/src/utils/runtime/object.ts @@ -11,6 +11,9 @@ export function populateRuntimeObject(object: ObjectInstance, world: World): Run voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes), colliderMesh: buildObjectTrimesh(objectType, world.voxelTypes), }, + pendingActions: { + + } }; }