basic physics and code optimization

This commit is contained in:
azykov@mail.ru
2026-06-03 16:56:06 +03:00
parent 7b02a75d25
commit 4e7b44dc57
37 changed files with 372 additions and 238 deletions
+17
View File
@@ -0,0 +1,17 @@
import { observer } from "mobx-react-lite";
import type { ObjectInstance, Runtime } from "../types";
import { state } from "../state";
import { ObjectViewInternal } from "./ObjectViewInternal";
export type GameObjectViewProps = {
object: Runtime<ObjectInstance>;
}
export const GameObjectView = observer(function (props: GameObjectViewProps) {
const objectType = state.world.getObjectTypeById(props.object.typeId);
if (!objectType)
return null;
return <ObjectViewInternal {...props} objectType={objectType} />
});
+62
View File
@@ -0,0 +1,62 @@
import { observer } from "mobx-react-lite";
import type { ObjectInstance, R3, Runtime } from "../types";
import { useRef, type RefObject } from "react";
import type { Group } from "three";
import { TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditorState";
import { ObjectViewInternal } from "./ObjectViewInternal";
type ObjectEditorViewProps = {
object: Runtime<ObjectInstance>;
}
export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) {
const groupRef = useRef<Group>(null);
const objectType = state.world.getObjectTypeById(object.typeId);
const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === object.id;
const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined;
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
if (!objectType)
return null;
function handleClick(e: ThreeEvent<MouseEvent>) {
if (e.delta > 5) return;
e.stopPropagation();
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
}
function handleTransformEnd() {
const group = groupRef.current;
if (group)
state.worldEditor.setObjectTransform(
object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3) as R3,
group.scale.toArray(),
);
}
return (<>
{(isSelected && selectionMode !== undefined && groupRef.current) &&
<TransformControls
object={groupRef as RefObject<Group>}
mode={selectionMode}
onMouseUp={handleTransformEnd}
/>
}
<ObjectViewInternal
ref={groupRef}
object={object}
objectType={objectType}
onClick={handleClick}
/>
</>);
});
-153
View File
@@ -1,153 +0,0 @@
import { observer } from "mobx-react-lite";
import type { ObjectType, ObjectInstance } from "../types";
import { useRef, useMemo, type RefObject } from "react";
import type { Group } from "three";
import { Instance, Instances, TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditorState";
import type { R3 } from "../types/3d";
import { TrimeshCollider } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody";
import { runInAction } from "mobx";
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
type ObjectViewProps = {
object: ObjectInstance;
}
function buildTrimesh(objectType: ObjectType, voxelTypes: typeof state.world.data.voxelTypes): [Float32Array, Uint32Array] | null {
const collidable = objectType.voxels.filter(
v => voxelTypes[v.typeId]?.collidable !== false
);
if (!collidable.length) return null;
const n = collidable.length;
const verts = new Float32Array(n * 8 * 3);
const idxs = new Uint32Array(n * 36);
for (let i = 0; i < n; i++) {
const p = collidable[i].position;
const vb = i * 8;
// 8 corners of unit box at position p (no +0.5 — group transform handles offset)
verts.set([
p[0], p[1], p[2],
p[0] + 1, p[1], p[2],
p[0], p[1] + 1, p[2],
p[0] + 1, p[1] + 1, p[2],
p[0], p[1], p[2] + 1,
p[0] + 1, p[1], p[2] + 1,
p[0], p[1] + 1, p[2] + 1,
p[0] + 1, p[1] + 1, p[2] + 1,
], vb * 3);
// 12 triangles (CCW outward normals)
idxs.set([
vb, vb + 2, vb + 3, vb, vb + 3, vb + 1, // -Z
vb + 4, vb + 5, vb + 7, vb + 4, vb + 7, vb + 6, // +Z
vb, vb + 1, vb + 5, vb, vb + 5, vb + 4, // -Y
vb + 2, vb + 6, vb + 7, vb + 2, vb + 7, vb + 3, // +Y
vb, vb + 4, vb + 6, vb, vb + 6, vb + 2, // -X
vb + 1, vb + 3, vb + 7, vb + 1, vb + 7, vb + 5, // +X
], i * 36);
}
return [verts, idxs];
}
export const ObjectView = observer(function ({ object }: ObjectViewProps) {
const objectType = state.world.getObjectTypeById(object.typeId);
const groupRef = useRef<Group>(null);
const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === object.id;
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
// Must be before early return to satisfy hooks rules
const trimeshArgs = useMemo(
() => objectType
? buildTrimesh(objectType, state.world.data.voxelTypes)
: null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[objectType?.id]
);
if (!objectType)
return null;
const selectionMode = isSelected
? state.worldEditor.selectedObjectMode
: undefined;
function handleClick(e: ThreeEvent<MouseEvent>) {
if (!state.worldEditor.isEnabled)
return;
if (e.delta > 5)
return;
e.stopPropagation();
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
};
function handleTransformEnd() {
const group = groupRef.current;
if (group)
state.worldEditor.setObjectTransform(
object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3) as R3,
group.scale.toArray(),
);
};
return (<>
{
(isSelected && (selectionMode !== undefined) && groupRef.current) &&
<TransformControls
object={groupRef as RefObject<Group>}
mode={selectionMode}
onMouseUp={handleTransformEnd}
/>
}
<SyncRigidBody
colliders={false}
// gravityScale={object.gravityScale}
// type={object.physics ? 'dynamic' : 'fixed'}
gravityScale={object.physics ? 1 : 0.1}
onSync={(data) => {
// console.log(`changed object ${object.id} ${JSON.stringify(data)}`);
runInAction(() => {
const obj = state.game?.scene.objects[object.id];
if (obj) {
obj.position = data.position;
obj.rotation = data.rotation;
obj.linearVelocity = data.linearVelocity;
obj.radialVelocity = data.radialVelocity;
}
});
}}
>
<group
ref={groupRef}
name={`${object.id} (${object.typeId} instance)`}
position={object.position}
rotation={object.rotation}
scale={object.scale}
onClick={handleClick}
>
{getObjectVoxelGroups(objectType, state.world.data.voxelTypes).map((vg) =>
<Instances key={vg.id} limit={vg.positions.length}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
{vg.positions.map((pos, i) => <Instance key={i} position={pos} />)}
</Instances>
)}
{trimeshArgs && <TrimeshCollider args={trimeshArgs} />}
</group>
</SyncRigidBody>
</>);
});
+65
View File
@@ -0,0 +1,65 @@
import { observer } from "mobx-react-lite";
import type { ObjectType, RuntimeObjectInstance } from "../types";
import { forwardRef, useMemo } from "react";
import type { Group } from "three";
import { Instance, Instances } from "@react-three/drei";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { TrimeshCollider } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody";
import { runInAction } from "mobx";
import { buildObjectTrimesh } from "../utils/graphics/mesh";
type ObjectViewInternalProps = {
object: Omit<RuntimeObjectInstance, 'typeId'>;
objectType: ObjectType;
onClick?: (e: ThreeEvent<MouseEvent>) => void;
}
export const ObjectViewInternal = observer(forwardRef<Group, ObjectViewInternalProps>(
function ({ object, objectType, onClick }, ref) {
const trimeshArgs = useMemo(
() => buildObjectTrimesh(objectType, state.world.data.voxelTypes),
// eslint-disable-next-line react-hooks/exhaustive-deps
[objectType.id]
);
return (
<SyncRigidBody
colliders={false}
gravityScale={object.physics ? 1 : 0.1}
onSync={(data) => {
runInAction(() => {
const obj = state.game?.scene.objects[object.id];
if (obj) {
obj.position = data.position;
obj.rotation = data.rotation;
obj.linearVelocity = data.linearVelocity;
obj.radialVelocity = data.radialVelocity;
}
});
}}
>
<group
ref={ref}
name={`${object.id} (${objectType.id} instance)`}
position={object.position}
rotation={object.rotation}
scale={object.scale}
onClick={onClick}
>
{
object.cache.voxelGroups.map((vg) =>
<Instances key={vg.id} limit={vg.positions.length}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
{vg.positions.map((pos, i) => <Instance key={i} position={pos} />)}
</Instances>
)
}
{trimeshArgs && <TrimeshCollider args={trimeshArgs} />}
</group>
</SyncRigidBody>
);
}
));
+11 -6
View File
@@ -1,13 +1,13 @@
import { observer } from "mobx-react-lite";
import type { Scene } from "../types";
import type { RuntimeScene } from "../types";
import { CharacterView } from "./CharacterView";
import { ObjectView } from "./ObjectView";
import { GameObjectView } from "./GameObjectView";
import { useFrame } from "@react-three/fiber";
import { useRapier } from "@react-three/rapier";
import { state } from "../state";
import { ObjectEditorView } from "./ObjectEditorView";
type SceneViewProps = {
scene: Scene,
scene: RuntimeScene,
editMode?: boolean;
}
@@ -28,8 +28,13 @@ export const SceneView = observer(function (props: SceneViewProps) {
return (<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
{Object.values(props.scene.objects).map((obj) =>
<ObjectView key={obj.id} object={obj} />)}
{
Object.values(props.scene.objects).map((obj) => (
props.editMode
? <ObjectEditorView key={obj.id} object={obj} />
: <GameObjectView key={obj.id} object={obj} />
))
}
{/* {props.editMode && <CharacterView character={props.scene.character} />} */}
{<CharacterView character={props.scene.character} />}
</>);
+1 -1
View File
@@ -2,7 +2,7 @@ import { RigidBody, type RigidBodyProps, type RapierRigidBody } from "@react-thr
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import { Euler, Quaternion } from "three";
import type { R3, V3 } from "../types/3d";
import type { R3, V3 } from "../types";
export type SyncRigidBodyData = {
position: V3;
+1 -1
View File
@@ -1,5 +1,5 @@
import { useThree } from "@react-three/fiber";
import type { Pos3 } from "../../types/3d";
import type { Pos3 } from "../../types";
import { useLayoutEffect } from "react";
import { observer } from "mobx-react-lite";