Files
blockly3d/webclient/src/components/ObjectViewInternal.tsx
T

153 lines
5.7 KiB
TypeScript

import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
import type { VoxelGroup } from "../types/runtime/voxelGroup";
import { useEffect, useImperativeHandle, useLayoutEffect, useRef, type Ref, memo } from "react";
import { Euler, Matrix4, Quaternion, type Group, type InstancedMesh } from "three";
import type { ThreeEvent } from "@react-three/fiber";
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody";
import { reaction, runInAction } from "mobx";
import { v3toRapier } from "../utils";
import { state } from "../state";
const _m4 = new Matrix4();
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg }: { vg: VoxelGroup }) {
const ref = useRef<InstancedMesh>(null);
useLayoutEffect(() => {
const mesh = ref.current;
if (!mesh)
return;
vg.positions.forEach(([x, y, z], i) => {
mesh.setMatrixAt(i, _m4.makeTranslation(x, y, z));
});
mesh.instanceMatrix.needsUpdate = true;
mesh.computeBoundingSphere();
}, [vg]);
return (
<instancedMesh ref={ref} args={[undefined, undefined, vg.positions.length]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
</instancedMesh>
);
});
export type ObjectViewInternalHandle = {
group: Group | null;
rb: RapierRigidBody | null;
}
type ObjectViewInternalProps = {
object: Omit<RuntimeObjectInstance, 'typeId'>;
objectType: ObjectType;
isEditor?: boolean;
isPlayer?: boolean;
onClick?: () => void;
ref?: Ref<ObjectViewInternalHandle>;
}
// not observer, instead reacts only to object.version field
export const ObjectViewInternal = function ({ object, objectType, ref, ...props }: ObjectViewInternalProps) {
const rbRef = useRef<RapierRigidBody>(null);
const groupRef = useRef<Group | null>(null);
useImperativeHandle(ref, () => ({ group: groupRef.current, rb: rbRef.current }));
useEffect(
() => {
if (props.isPlayer)
return;
return reaction(
() => 'version' in object ? object.version : 0,
() => {
if (!rbRef.current)
return;
const gameObj = object as RuntimeGameObjectInstance;
rbRef.current.setTranslation(v3toRapier(gameObj.position), 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);
groupRef.current?.scale.set(...gameObj.scale);
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
},
);
},
[object, object.id, props.isPlayer]
);
const gameObj = object as RuntimeGameObjectInstance;
useEffect(
() => {
runInAction(() => { gameObj.rb = () => rbRef.current; });
return () => { runInAction(() => { gameObj.rb = () => null; }); };
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[object.id],
);
function handleClick(e: ThreeEvent<MouseEvent>) {
if (e.delta > 5)
return;
e.stopPropagation();
props.onClick?.();
}
return (
<SyncRigidBody
ref={rbRef}
colliders={false}
type={props.isPlayer ? 'kinematicPosition' : (object.physics ? 'dynamic' : 'fixed')}
gravityScale={object.gravityScale}
position={object.position}
rotation={object.rotation}
onCollisionEnter={props.isEditor ? undefined : (payload) => {
const game = state.game;
if (!game)
return;
const otherHandle = payload.other.rigidBody?.handle;
if (otherHandle === undefined)
return;
const otherObj = Object.values(game.scene.objects)
.find((o) => o.rb()?.handle === otherHandle);
game.emitEvent('objectTouchStart', { object: gameObj, other: otherObj });
if (otherObj?.id === game.scene.playerObjectId)
game.emitEvent('playerTouchStart', { object: gameObj, other: otherObj });
}}
onSync={(data) => {
runInAction(() => {
object.position = data.position;
object.rotation = data.rotation;
if (!props.isEditor) {
(object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity;
(object as RuntimeGameObjectInstance).angularVelocity = data.radialVelocity;
}
// do not change object.version to not trigger rerender
});
}}
>
<group
ref={groupRef}
name={`${object.id} (${objectType.id} instance)`}
scale={object.scale}
onClick={handleClick}
>
{
object.cache.voxelGroups.map((vg) =>
<VoxelGroupMesh key={`${vg.id}-${object.cache.updatedAt}`} vg={vg} />
)
}
{object.cache.colliderMesh && <ConvexHullCollider args={[object.cache.colliderMesh[0]]} />}
</group>
</SyncRigidBody>
);
};