rapier physics
This commit is contained in:
parent
487a894fe5
commit
7b02a75d25
|
|
@ -12,6 +12,7 @@
|
|||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.6.1",
|
||||
"@react-three/rapier": "^2.2.0",
|
||||
"@types/three": "^0.184.1",
|
||||
"install": "^0.13.0",
|
||||
"mobx": "^6.15.4",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ importers:
|
|||
'@react-three/fiber':
|
||||
specifier: ^9.6.1
|
||||
version: 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-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)
|
||||
'@types/three':
|
||||
specifier: ^0.184.1
|
||||
version: 0.184.1
|
||||
|
|
@ -161,6 +164,9 @@ packages:
|
|||
'@dimforge/rapier3d-compat@0.12.0':
|
||||
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
|
||||
|
||||
'@dimforge/rapier3d-compat@0.19.2':
|
||||
resolution: {integrity: sha512-AZHL1jqUF55QJkJyU1yKeh4ImX2J93bVLIezT1+o0FZqTix6O06MOaqpKoJ4MmbDCsoZmwO+qc471/SDMDm2AA==}
|
||||
|
||||
'@emnapi/core@1.10.0':
|
||||
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
||||
|
||||
|
|
@ -386,6 +392,13 @@ packages:
|
|||
react-native:
|
||||
optional: true
|
||||
|
||||
'@react-three/rapier@2.2.0':
|
||||
resolution: {integrity: sha512-mVsqbKXlGZoN+XrqdhzFZUQmy8pibEOVzl4k7LC+LHe84bQnYBSagy1Hvbda6bL1PJDdTFyiDiBk5buKFinNIQ==}
|
||||
peerDependencies:
|
||||
'@react-three/fiber': ^9.0.4
|
||||
react: ^19
|
||||
three: '>=0.159.0'
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.2':
|
||||
resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
|
@ -1532,6 +1545,8 @@ snapshots:
|
|||
|
||||
'@dimforge/rapier3d-compat@0.12.0': {}
|
||||
|
||||
'@dimforge/rapier3d-compat@0.19.2': {}
|
||||
|
||||
'@emnapi/core@1.10.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.1
|
||||
|
|
@ -1747,6 +1762,15 @@ snapshots:
|
|||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@react-three/rapier@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)':
|
||||
dependencies:
|
||||
'@dimforge/rapier3d-compat': 0.19.2
|
||||
'@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
|
||||
suspend-react: 0.1.3(react@19.2.6)
|
||||
three: 0.184.0
|
||||
three-stdlib: 2.36.1(three@0.184.0)
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.2':
|
||||
optional: true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
allowBuilds:
|
||||
'@parcel/watcher': set this to true or false
|
||||
'@parcel/watcher': false
|
||||
|
|
|
|||
|
|
@ -1,22 +1,35 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import type { Character } from "../types";
|
||||
import { SyncRigidBody } from "./SyncRigidBody";
|
||||
import { state } from "../state";
|
||||
|
||||
export const CharacterView = observer(function ({ character }: { character: Character }) {
|
||||
|
||||
const pos = character.transform.position;
|
||||
|
||||
return <group
|
||||
return (
|
||||
<SyncRigidBody
|
||||
colliders="hull"
|
||||
restitution={2}
|
||||
onSync={(data) => {
|
||||
state.game?.setCharacterTransform(
|
||||
{
|
||||
position: data.position,
|
||||
look: character.transform.look,
|
||||
},
|
||||
data.linearVelocity,
|
||||
undefined, // do not change radial velocity
|
||||
);
|
||||
}}
|
||||
>
|
||||
<group
|
||||
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
||||
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>
|
||||
</SyncRigidBody>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,28 +3,35 @@ import { SceneView } from "./SceneView";
|
|||
import { state } from "../state";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { PointerLockControls, useKeyboardControls } from "@react-three/drei";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { Physics } from "@react-three/rapier";
|
||||
|
||||
function PlayerMovement() {
|
||||
const [, get] = useKeyboardControls();
|
||||
const dirty = useRef(false);
|
||||
|
||||
useFrame(({ camera }, dt) => {
|
||||
if (state.game?.isPaused)
|
||||
return;
|
||||
|
||||
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 (backward) { camera.translateZ(speed); dirty.current = true; }
|
||||
if (left) { camera.translateX(-speed); dirty.current = true; }
|
||||
if (right) { 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({
|
||||
state.game?.setCharacterTransform(
|
||||
{
|
||||
position: camera.position.toArray(),
|
||||
look: [rx, ry, rz],
|
||||
});
|
||||
},
|
||||
// do not change velocities
|
||||
);
|
||||
});
|
||||
|
||||
return <PointerLockControls onChange={() => { dirty.current = true; }} />;
|
||||
|
|
@ -50,7 +57,11 @@ export const GameView = observer(function () {
|
|||
return null;
|
||||
|
||||
return (<>
|
||||
<PlayerMovement />
|
||||
<SceneView scene={game.scene} renderCharacter={false} />
|
||||
<Suspense>
|
||||
{/* <PlayerMovement /> */}
|
||||
<Physics paused={game.isPaused}>
|
||||
<SceneView scene={game.scene} />
|
||||
</Physics>
|
||||
</Suspense>
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import type { ObjectInstance, ObjectType } from "../types";
|
||||
import { useRef, type RefObject } from "react";
|
||||
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";
|
||||
|
|
@ -8,35 +8,59 @@ import type { ThreeEvent } from "@react-three/fiber";
|
|||
import { state } from "../state";
|
||||
import { nextSelectionEditMode } from "../state/worldEditorState";
|
||||
import type { R3 } from "../types/3d";
|
||||
|
||||
type VoxelGroup = {
|
||||
id: string;
|
||||
color: string;
|
||||
opacity: number;
|
||||
positions: [number, number, number][];
|
||||
};
|
||||
|
||||
function voxelGroups(objectType: ObjectType): VoxelGroup[] {
|
||||
const map = new Map<string, VoxelGroup>();
|
||||
for (const idx in objectType.voxels) {
|
||||
const v = objectType.voxels[idx];
|
||||
const vt = state.world.getVoxelTypeById(v.typeId);
|
||||
const color = (v.color ?? vt?.color) ?? 'white';
|
||||
const opacity = (v.opacity ?? vt?.opacity) ?? 1;
|
||||
const key = `${color}-${opacity}`;
|
||||
if (!map.has(key))
|
||||
map.set(key, { id: key, color, opacity, positions: [] });
|
||||
const p = v.position;
|
||||
map.get(key)!.positions.push([p[0] + 0.5, p[1] + 0.5, p[2] + 0.5]);
|
||||
}
|
||||
return [...map.values()];
|
||||
}
|
||||
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 &&
|
||||
|
|
@ -44,9 +68,18 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
|||
|
||||
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
|
||||
|
||||
const objectType = state.world.getObjectTypeById(object.typeId);
|
||||
// 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;
|
||||
|
|
@ -80,6 +113,24 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
|||
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)`}
|
||||
|
|
@ -88,18 +139,15 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
|
|||
scale={object.scale}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{
|
||||
voxelGroups(objectType).map((vg) => {
|
||||
return <Instances key={vg.id} limit={vg.positions.length}>
|
||||
{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} />)
|
||||
}
|
||||
{vg.positions.map((pos, i) => <Instance key={i} position={pos} />)}
|
||||
</Instances>
|
||||
})
|
||||
}
|
||||
)}
|
||||
{trimeshArgs && <TrimeshCollider args={trimeshArgs} />}
|
||||
</group>
|
||||
</SyncRigidBody>
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef } from 'react';
|
||||
import { Suspense, useRef } from 'react';
|
||||
import type React from 'react';
|
||||
import { Grid, OrbitControls } from '@react-three/drei';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -6,6 +6,7 @@ import { state } from '../state';
|
|||
import { SceneView } from './SceneView';
|
||||
import { type OrthographicCamera, type PerspectiveCamera } from 'three';
|
||||
import { CameraSync } from './tools/CameraSync';
|
||||
import { Physics } from '@react-three/rapier';
|
||||
|
||||
|
||||
export const SceneEditorView = observer(function () {
|
||||
|
|
@ -42,6 +43,7 @@ export const SceneEditorView = observer(function () {
|
|||
};
|
||||
|
||||
return (<>
|
||||
<Suspense>
|
||||
<OrbitControls
|
||||
ref={controlsRef}
|
||||
onStart={handleStart}
|
||||
|
|
@ -56,6 +58,9 @@ export const SceneEditorView = observer(function () {
|
|||
sectionColor="white"
|
||||
infiniteGrid
|
||||
/>
|
||||
<SceneView scene={state.worldEditor.scene} renderCharacter={true} />
|
||||
<Physics colliders={false} paused={true}>
|
||||
<SceneView scene={state.worldEditor.scene} editMode />
|
||||
</Physics>
|
||||
</Suspense>
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,18 +2,35 @@ import { observer } from "mobx-react-lite";
|
|||
import type { Scene } from "../types";
|
||||
import { CharacterView } from "./CharacterView";
|
||||
import { ObjectView } from "./ObjectView";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useRapier } from "@react-three/rapier";
|
||||
import { state } from "../state";
|
||||
|
||||
type SceneViewProps = {
|
||||
scene: Scene,
|
||||
renderCharacter: boolean;
|
||||
editMode?: boolean;
|
||||
}
|
||||
|
||||
export const SceneView = observer(function ({ scene, renderCharacter }: SceneViewProps) {
|
||||
export const SceneView = observer(function (props: SceneViewProps) {
|
||||
const rapier = useRapier();
|
||||
|
||||
useFrame((_, dt) => {
|
||||
// if (props.editMode)
|
||||
// return;
|
||||
|
||||
// const game = state.game;
|
||||
// if (!game || game.isPaused)
|
||||
// return;
|
||||
|
||||
// rapier.step(dt);
|
||||
})
|
||||
|
||||
return (<>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||
{Object.values(scene.objects).map((obj) =>
|
||||
{Object.values(props.scene.objects).map((obj) =>
|
||||
<ObjectView key={obj.id} object={obj} />)}
|
||||
{renderCharacter && <CharacterView character={scene.character} />}
|
||||
{/* {props.editMode && <CharacterView character={props.scene.character} />} */}
|
||||
{<CharacterView character={props.scene.character} />}
|
||||
</>);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { RigidBody, type RigidBodyProps, type RapierRigidBody } from "@react-three/rapier";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import { Euler, Quaternion } from "three";
|
||||
import type { R3, V3 } from "../types/3d";
|
||||
|
||||
export type SyncRigidBodyData = {
|
||||
position: V3;
|
||||
rotation: R3;
|
||||
linearVelocity: V3;
|
||||
radialVelocity: R3;
|
||||
}
|
||||
|
||||
export type SyncRigidBodyOnSyncFunction = (data: SyncRigidBodyData) => void;
|
||||
|
||||
type SyncRigidBodyProps = RigidBodyProps & {
|
||||
onSync: SyncRigidBodyOnSyncFunction;
|
||||
};
|
||||
|
||||
const _q = new Quaternion();
|
||||
const _e = new Euler();
|
||||
const EPS = 1e-6;
|
||||
|
||||
// indices: 0-2 position, 3-5 rotation, 6-8 linearVelocity, 9-11 radialVelocity
|
||||
const PREV_INIT = new Float64Array(12).fill(Infinity);
|
||||
|
||||
function syncRigidBodyDataToArray(data: SyncRigidBodyData, arr: Float64Array): void {
|
||||
arr[0] = data.position[0]; arr[1] = data.position[1]; arr[2] = data.position[2];
|
||||
arr[3] = data.rotation[0]; arr[4] = data.rotation[1]; arr[5] = data.rotation[2];
|
||||
arr[6] = data.linearVelocity[0]; arr[7] = data.linearVelocity[1]; arr[8] = data.linearVelocity[2];
|
||||
arr[9] = data.radialVelocity[0]; arr[10] = data.radialVelocity[1]; arr[11] = data.radialVelocity[2];
|
||||
}
|
||||
|
||||
function compareTwoFloatArrays(a: Float64Array, b: Float64Array, epsilon: number): boolean {
|
||||
for (let i = 0; i < a.length; i++)
|
||||
if (Math.abs(a[i] - b[i]) > epsilon)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function SyncRigidBody({ onSync, children, ...props }: SyncRigidBodyProps) {
|
||||
const rbRef = useRef<RapierRigidBody>(null);
|
||||
const prevData = useRef<Float64Array>(PREV_INIT.slice());
|
||||
const currentData = useRef<Float64Array>(PREV_INIT.slice());
|
||||
|
||||
useFrame(() => {
|
||||
const body = rbRef.current;
|
||||
if (!body)
|
||||
return;
|
||||
|
||||
const { x, y, z } = body.translation();
|
||||
const rot = body.rotation();
|
||||
_q.set(rot.x, rot.y, rot.z, rot.w);
|
||||
_e.setFromQuaternion(_q);
|
||||
const lv = body.linvel();
|
||||
const av = body.angvel();
|
||||
|
||||
const data: SyncRigidBodyData = {
|
||||
position: [x, y, z],
|
||||
rotation: [_e.x, _e.y, _e.z],
|
||||
linearVelocity: [lv.x, lv.y, lv.z],
|
||||
radialVelocity: [av.x, av.y, av.z],
|
||||
};
|
||||
|
||||
syncRigidBodyDataToArray(data, currentData.current);
|
||||
|
||||
if (compareTwoFloatArrays(currentData.current, prevData.current, EPS)) {
|
||||
prevData.current.set(currentData.current);
|
||||
onSync(data);
|
||||
}
|
||||
});
|
||||
|
||||
return <RigidBody ref={rbRef} {...props}>{children}</RigidBody>;
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ const stone: VoxelType = {
|
|||
const dirt: VoxelType = {
|
||||
id: 'dirt',
|
||||
name: 'Dirt',
|
||||
opacity: 1,
|
||||
opacity: 0.8,
|
||||
collidable: true,
|
||||
color: '#302520',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Game, World } from "../types";
|
||||
import type { Game, RuntimeScene, World } from "../types";
|
||||
import { clone } from "../utils";
|
||||
|
||||
export class GameFactory {
|
||||
|
|
@ -7,7 +7,7 @@ export class GameFactory {
|
|||
return {
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: clone(world.initialScene),
|
||||
scene: clone(world.initialScene) as RuntimeScene,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { makeAutoObservable, reaction, toJS } from "mobx";
|
||||
import type { WorldState } from "./worldState";
|
||||
import type { Game, Scene } from "../types";
|
||||
import type { Pos3, R3, V3 } from "../types/3d";
|
||||
import type { Game, RuntimeScene } from "../types";
|
||||
import type { Pos3, V3, R3 } from "../types/3d";
|
||||
import type { CameraProps } from "@react-three/fiber";
|
||||
import { GameFactory } from "../model/gameFactory";
|
||||
|
||||
|
|
@ -34,11 +34,12 @@ export class GameState {
|
|||
return this.data.paused;
|
||||
}
|
||||
|
||||
public get scene(): Scene {
|
||||
public get scene(): RuntimeScene {
|
||||
return this.data.scene;
|
||||
}
|
||||
|
||||
public get camera(): Pos3 {
|
||||
return this.world.data.editorCamera;
|
||||
return this.scene.character.transform;
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +79,21 @@ export class GameState {
|
|||
this.data.paused = true;
|
||||
}
|
||||
|
||||
public setCharacterTransform(transform: Pos3): void {
|
||||
public setCharacterTransform(
|
||||
transform: Pos3,
|
||||
linearVelocity?: V3,
|
||||
radialVelocity?: R3,
|
||||
): void {
|
||||
if (this.isPaused)
|
||||
return;
|
||||
|
||||
this.scene.character.transform = transform;
|
||||
if (linearVelocity)
|
||||
this.scene.character.linearVelocity = linearVelocity;
|
||||
if (radialVelocity)
|
||||
this.scene.character.radialVelocity = radialVelocity;
|
||||
|
||||
// console.log(`changed character to ${JSON.stringify(this.scene.character)}`);
|
||||
}
|
||||
|
||||
public tick(deltaTime: number): void {
|
||||
|
|
|
|||
|
|
@ -39,14 +39,15 @@ export class WorldState {
|
|||
public loadMock() {
|
||||
console.log('Mocking world...');
|
||||
|
||||
const objects = Array(3).fill(0)
|
||||
const objects = Array(0).fill(0)
|
||||
.map((_, idx) => ({
|
||||
id: `obj${idx}`,
|
||||
typeId: 'wolf',
|
||||
gravityScale: 1,
|
||||
position: [idx * 10 - 10, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
}));
|
||||
} as ObjectInstance));
|
||||
const objectMap = Object.fromEntries(
|
||||
objects.map((obj) => [obj.id, obj]),
|
||||
) as Record<string, ObjectInstance>
|
||||
|
|
@ -74,12 +75,14 @@ export class WorldState {
|
|||
transform: {
|
||||
position: [0, 5, 20],
|
||||
look: [0, 0, 0],
|
||||
}
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
terrain: {
|
||||
id: 'terrain',
|
||||
typeId: 'terrain',
|
||||
physics: false, // pinned
|
||||
gravityScale: 0, // pinned
|
||||
position: [0, -1, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import type { Pos3 } from "./3d";
|
||||
import type { RuntimeObjectData } from "./object";
|
||||
|
||||
export type Character = {
|
||||
transform: Pos3,
|
||||
}
|
||||
|
||||
export type RuntimeCharacter = Character & RuntimeObjectData;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Scene } from "./scene";
|
||||
import type { RuntimeScene } from "./scene";
|
||||
|
||||
export type Game = {
|
||||
paused: boolean;
|
||||
time: number;
|
||||
scene: Scene;
|
||||
scene: RuntimeScene;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,16 @@ export type ObjectType = {
|
|||
export type ObjectInstance = {
|
||||
id: string;
|
||||
typeId: string;
|
||||
physics: boolean;
|
||||
gravityScale: number;
|
||||
position: V3;
|
||||
rotation: R3;
|
||||
scale: V3;
|
||||
}
|
||||
|
||||
export type RuntimeObjectData = {
|
||||
linearVelocity: V3;
|
||||
radialVelocity: R3;
|
||||
}
|
||||
|
||||
export type RuntimeObjectInstance = ObjectInstance & RuntimeObjectData;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import type { Character } from "./character";
|
||||
import type { ObjectInstance } from "./object";
|
||||
import type { Character, RuntimeCharacter } from "./character";
|
||||
import type { ObjectInstance, RuntimeObjectInstance } from "./object";
|
||||
|
||||
export type Scene = {
|
||||
character: Character;
|
||||
objects: Record<string, ObjectInstance>;
|
||||
}
|
||||
|
||||
export type RuntimeScene = {
|
||||
character: RuntimeCharacter;
|
||||
objects: Record<string, RuntimeObjectInstance>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import type { ObjectType } from "../../types";
|
||||
import type { VoxelType } from "../../types/voxel";
|
||||
|
||||
export type VoxelGroup = {
|
||||
id: string;
|
||||
color: string;
|
||||
opacity: number;
|
||||
positions: [number, number, number][];
|
||||
};
|
||||
|
||||
export function getObjectVoxelGroups(
|
||||
object: ObjectType,
|
||||
voxelTypes: Record<string, VoxelType>,
|
||||
): VoxelGroup[] {
|
||||
const map = new Map<string, VoxelGroup>();
|
||||
for (const idx in object.voxels) {
|
||||
const v = object.voxels[idx];
|
||||
const vt = voxelTypes[v.typeId];
|
||||
const color = (v.color ?? vt?.color) ?? 'white';
|
||||
const opacity = (v.opacity ?? vt?.opacity) ?? 1;
|
||||
const key = `${color}-${opacity}`;
|
||||
if (!map.has(key))
|
||||
map.set(key, { id: key, color, opacity, positions: [] });
|
||||
const p = v.position;
|
||||
map.get(key)!.positions.push([p[0] + 0.5, p[1] + 0.5, p[2] + 0.5]);
|
||||
}
|
||||
return [...map.values()];
|
||||
}
|
||||
Loading…
Reference in New Issue