Compare commits

..

No commits in common. "63bd5e371536c08e7a7d1ab5df534c149b90dbf1" and "b5a9772248f7673bf9c35551bd97b60d77ceb6b3" have entirely different histories.

6 changed files with 58 additions and 117 deletions

View File

@ -1,13 +1,13 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import type { ObjectInstance, R3, Runtime } from "../types"; import type { ObjectInstance, R3, Runtime } from "../types";
import { useMemo, useRef, type RefObject } from "react"; import { useRef, type RefObject } from "react";
import type { Group } from "three"; import type { Group } from "three";
import { TransformControls, useHelper } from "@react-three/drei"; import { TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three"; import { BoxHelper } from "three";
import type { ThreeEvent } from "@react-three/fiber"; import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state"; import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditorState"; import { nextSelectionEditMode } from "../state/worldEditorState";
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal"; import { ObjectViewInternal } from "./ObjectViewInternal";
type ObjectEditorViewProps = { type ObjectEditorViewProps = {
object: Runtime<ObjectInstance>; object: Runtime<ObjectInstance>;
@ -15,23 +15,17 @@ type ObjectEditorViewProps = {
type SelectionOverlayProps = { type SelectionOverlayProps = {
objectId: string; objectId: string;
ref: RefObject<ObjectViewInternalHandle | null>; groupRef: RefObject<Group | null>;
onTransformEnd: () => void; onTransformEnd: () => void;
} }
// Separate observer so only the 2 affected instances (selected/deselected) // Separate observer so only the 2 affected instances (selected/deselected)
// re-render on selection change, not all N objects in the scene. // re-render on selection change, not all N objects in the scene.
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) { const SelectionOverlay = observer(function ({ objectId, groupRef, onTransformEnd }: SelectionOverlayProps) {
const isSelected = state.worldEditor.isEnabled && const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === objectId; state.worldEditor.selectedObjectId === objectId;
const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined; const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined;
// Stable virtual ref that reads through to the group inside the handle
const groupRef = useMemo<RefObject<Group | null>>(
() => ({ get current() { return ref.current?.group ?? null; } }),
[ref],
);
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white'); useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
if (!isSelected || selectionMode === undefined || !groupRef.current) if (!isSelected || selectionMode === undefined || !groupRef.current)
@ -47,7 +41,7 @@ const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: S
}); });
export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) { export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) {
const dataRef = useRef<ObjectViewInternalHandle>(null); const groupRef = useRef<Group>(null);
// Only observes world object types — not selection state — so won't // Only observes world object types — not selection state — so won't
// re-render when a different object is selected. // re-render when a different object is selected.
@ -67,7 +61,7 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP
} }
function handleTransformEnd() { function handleTransformEnd() {
const group = dataRef.current?.group; const group = groupRef.current;
if (group) if (group)
state.worldEditor.setObjectTransform( state.worldEditor.setObjectTransform(
object.id, object.id,
@ -80,11 +74,11 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP
return (<> return (<>
<SelectionOverlay <SelectionOverlay
objectId={object.id} objectId={object.id}
ref={dataRef} groupRef={groupRef}
onTransformEnd={handleTransformEnd} onTransformEnd={handleTransformEnd}
/> />
<ObjectViewInternal <ObjectViewInternal
ref={dataRef} ref={groupRef}
object={object} object={object}
isEditor isEditor
objectType={objectType} objectType={objectType}

View File

@ -1,83 +1,43 @@
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types"; import { observer } from "mobx-react-lite";
import { useEffect, useImperativeHandle, useRef, type Ref } from "react"; import type { ObjectType, RuntimeObjectInstance } from "../types";
import { Euler, Quaternion, type Group } from "three"; import { forwardRef } from "react";
import type { Group } from "three";
import { Instance, Instances } from "@react-three/drei"; import { Instance, Instances } from "@react-three/drei";
import type { ThreeEvent } from "@react-three/fiber"; import type { ThreeEvent } from "@react-three/fiber";
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier"; import { ConvexHullCollider } from "@react-three/rapier";
import { SyncRigidBody } from "./SyncRigidBody"; import { SyncRigidBody } from "./SyncRigidBody";
import { reaction, runInAction } from "mobx";
import { v3toRapier } from "../utils";
export type ObjectViewInternalHandle = {
group: Group | null;
rb: RapierRigidBody | null;
}
type ObjectViewInternalProps = { type ObjectViewInternalProps = {
object: Omit<RuntimeObjectInstance, 'typeId'>; object: Omit<RuntimeObjectInstance, 'typeId'>;
objectType: ObjectType; objectType: ObjectType;
isEditor?: boolean; isEditor?: boolean;
onClick?: (e: ThreeEvent<MouseEvent>) => void; onClick?: (e: ThreeEvent<MouseEvent>) => void;
ref?: Ref<ObjectViewInternalHandle>;
} }
// not observer, instead reacts only to object.version field export const ObjectViewInternal = observer(forwardRef<Group | null, ObjectViewInternalProps>(
export const ObjectViewInternal = function ({ object, objectType, ref, ...props }: ObjectViewInternalProps) { function ({ object, objectType, ...props }, ref) {
const rbRef = useRef<RapierRigidBody>(null);
const groupRef = useRef<Group | null>(null);
useImperativeHandle(ref, () => ({ group: groupRef.current, rb: rbRef.current }));
useEffect(
() => reaction(
() => 'version' in object ? object.version : 0,
() => {
if (!rbRef.current)
return;
const gameObj = object as RuntimeGameObjectInstance;
// position
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);
// scale
groupRef.current?.scale.set(...gameObj.scale);
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
},
),
[object.id]
);
return ( return (
<SyncRigidBody <SyncRigidBody
ref={rbRef}
colliders={false} colliders={false}
type={object.physics ? 'dynamic' : 'fixed'} gravityScale={object.physics ? 1 : 0.1}
gravityScale={object.gravityScale} onSync={(_data) => {
position={object.position} // TODO rewrite so that is does not trigger re-render
rotation={object.rotation} // runInAction(() => {
onSync={(data) => { // object.position = data.position;
runInAction(() => { // object.rotation = data.rotation;
object.position = data.position; // if (!props.isEditor) {
object.rotation = data.rotation; // (object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity;
if (!props.isEditor) { // (object as RuntimeGameObjectInstance).radialVelocity = data.radialVelocity;
(object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity; // }
(object as RuntimeGameObjectInstance).angularVelocity = data.radialVelocity; // });
}
// do not change object.version to not trigger rerender
});
}} }}
> >
<group <group
ref={groupRef} ref={ref}
name={`${object.id} (${objectType.id} instance)`} name={`${object.id} (${objectType.id} instance)`}
position={object.position}
rotation={object.rotation}
scale={object.scale} scale={object.scale}
onClick={props.onClick} onClick={props.onClick}
> >
@ -95,3 +55,4 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
</SyncRigidBody> </SyncRigidBody>
); );
} }
));

View File

@ -19,8 +19,6 @@ export class GameState {
); );
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// @ts-ignore
private withoutAutoSave(fn: () => void) { private withoutAutoSave(fn: () => void) {
this._stopAutoSave(); this._stopAutoSave();
fn(); fn();
@ -102,7 +100,7 @@ export class GameState {
public setCharacterTransform( public setCharacterTransform(
transform: Pos3, transform: Pos3,
linearVelocity?: V3, linearVelocity?: V3,
angularVelocity?: R3, radialVelocity?: R3,
): void { ): void {
if (this.isPaused) if (this.isPaused)
return; return;
@ -110,8 +108,8 @@ export class GameState {
this.scene.character.transform = transform; this.scene.character.transform = transform;
if (linearVelocity) if (linearVelocity)
this.scene.character.linearVelocity = linearVelocity; this.scene.character.linearVelocity = linearVelocity;
if (angularVelocity) if (radialVelocity)
this.scene.character.angularVelocity = angularVelocity; this.scene.character.radialVelocity = radialVelocity;
// console.log(`changed character to ${JSON.stringify(this.scene.character)}`); // console.log(`changed character to ${JSON.stringify(this.scene.character)}`);
} }

View File

@ -43,9 +43,8 @@ export class WorldState {
.map((_, idx) => ({ .map((_, idx) => ({
id: `obj${idx}`, id: `obj${idx}`,
typeId: 'wolf', typeId: 'wolf',
physics: true,
gravityScale: 1, gravityScale: 1,
position: [idx * 10 - 10, 5, 0], position: [idx * 10 - 10, 0, 0],
rotation: [0, 0, 0], rotation: [0, 0, 0],
scale: [1, 1, 1], scale: [1, 1, 1],
} as ObjectInstance)); } as ObjectInstance));

View File

@ -19,9 +19,8 @@ export type ObjectInstance = {
} }
export type GameObjectData = { export type GameObjectData = {
version: number; // increment to force three and physics changes
linearVelocity: V3; linearVelocity: V3;
angularVelocity: R3; radialVelocity: R3;
} }
export type GameObjectInstance = ObjectInstance & GameObjectData; export type GameObjectInstance = ObjectInstance & GameObjectData;

View File

@ -1,6 +1,4 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { V3 } from '../types';
import { type Vector3Object } from '@react-three/rapier';
export function clone<T>(obj: T): T { export function clone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
@ -9,11 +7,3 @@ export function clone<T>(obj: T): T {
export function randomId(): string { export function randomId(): string {
return uuid(); return uuid();
} }
export function v3toRapier(value: V3): Vector3Object {
return {
x: value[0],
y: value[1],
z: value[2],
}
}