141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
|
import type { VoxelGroup } from "../types/runtime/voxelGroup";
|
|
import { useEffect, useImperativeHandle, useRef, type Ref, memo, useMemo } from "react";
|
|
import { Euler, Quaternion, type Group } 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";
|
|
import { greedyMesh } from "../utils/graphics/greedyMesh";
|
|
|
|
const VoxelGroupMesh = memo(function VoxelGroupMesh({ vg }: { vg: VoxelGroup }) {
|
|
const geo = useMemo(() => greedyMesh(vg.positions), [vg]);
|
|
useEffect(() => () => { geo.dispose(); }, [geo]);
|
|
|
|
return (
|
|
<mesh geometry={geo}>
|
|
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
|
|
</mesh>
|
|
);
|
|
});
|
|
|
|
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>
|
|
);
|
|
};
|