basic physics and code optimization
This commit is contained in:
parent
7b02a75d25
commit
4e7b44dc57
|
|
@ -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} />
|
||||
});
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>);
|
||||
});
|
||||
|
|
@ -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>
|
||||
</>);
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
));
|
||||
|
|
@ -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} />}
|
||||
</>);
|
||||
|
|
|
|||
|
|
@ -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,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";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { VoxelType } from "../types/voxel";
|
||||
import type { VoxelType } from "../types";
|
||||
|
||||
const stone: VoxelType = {
|
||||
id: 'stone',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import type { Game, RuntimeScene, World } from "../types";
|
||||
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
||||
import { clone } from "../utils";
|
||||
import { populateRuntimeScene } from "../utils/runtime";
|
||||
|
||||
export class GameFactory {
|
||||
|
||||
public static create(world: World): Game {
|
||||
const scene = populateRuntimeScene(clone(world.initialScene), world);
|
||||
|
||||
return {
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: clone(world.initialScene) as RuntimeScene,
|
||||
scene: GameFactory.initGameScene(scene),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -21,4 +24,8 @@ export class GameFactory {
|
|||
public static save(game: Game): void {
|
||||
localStorage.setItem("game", JSON.stringify(game));
|
||||
}
|
||||
|
||||
private static initGameScene(scene: RuntimeScene): RuntimeGameScene {
|
||||
return scene as RuntimeGameScene;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Voxel } from "../../types/voxel";
|
||||
import type { Voxel } from "../../types";
|
||||
|
||||
export function terrainXZ(width: number, length: number): Voxel[] {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Voxel } from "../../types/voxel";
|
||||
import type { Voxel } from "../../types";
|
||||
|
||||
const G = '#808080'; // gray fur
|
||||
const L = '#d0c8b8'; // light beige-gray (snout / face)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { World } from "../types";
|
||||
import { type RuntimeWorld, type World } from "../types";
|
||||
import { depopulateRuntimeWorld, populateRuntimeWorld } from "../utils/runtime";
|
||||
import { DEFAULT_VOXEL_TYPES } from "./defaultVoxelTypes";
|
||||
|
||||
export class WorldFactory {
|
||||
|
||||
public static create(): World {
|
||||
return {
|
||||
public static create(): RuntimeWorld {
|
||||
return populateRuntimeWorld({
|
||||
objectTypes: {},
|
||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||
initialScene: {
|
||||
|
|
@ -23,17 +24,17 @@ export class WorldFactory {
|
|||
position: [0, 5, 10],
|
||||
look: [0, 0, 0],
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static load(): World | undefined {
|
||||
public static load(): RuntimeWorld | undefined {
|
||||
const json = localStorage.getItem("world");
|
||||
if (json) {
|
||||
return JSON.parse(json) as World;
|
||||
return populateRuntimeWorld(JSON.parse(json) as World);
|
||||
}
|
||||
}
|
||||
|
||||
public static save(world: World): void {
|
||||
localStorage.setItem("world", JSON.stringify(world));
|
||||
public static save(world: RuntimeWorld): void {
|
||||
localStorage.setItem("world", JSON.stringify(depopulateRuntimeWorld(world)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { makeAutoObservable, reaction, toJS } from "mobx";
|
||||
import type { WorldState } from "./worldState";
|
||||
import type { Game, RuntimeScene } from "../types";
|
||||
import type { Pos3, V3, R3 } from "../types/3d";
|
||||
import type { Game, Pos3, V3, R3, RuntimeGameScene } from "../types";
|
||||
import type { CameraProps } from "@react-three/fiber";
|
||||
import { GameFactory } from "../model/gameFactory";
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ export class GameState {
|
|||
return this.data.paused;
|
||||
}
|
||||
|
||||
public get scene(): RuntimeScene {
|
||||
public get scene(): RuntimeGameScene {
|
||||
return this.data.scene;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import type { WorldState } from "./worldState";
|
||||
import type { ObjectInstance, Scene } from "../types";
|
||||
import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene } from "../types";
|
||||
import { createObjectInstance } from "../utils/object";
|
||||
import { randomId } from "../utils";
|
||||
import type { Pos3, R3, V3 } from "../types/3d";
|
||||
import { state } from "./rootState";
|
||||
import { populateRuntimeObject } from "../utils/runtime";
|
||||
|
||||
export const SelectionEditModeEnum = [
|
||||
'translate',
|
||||
|
|
@ -51,7 +51,7 @@ export class WorldEditorState {
|
|||
this.selectedObjectMode = undefined;
|
||||
}
|
||||
|
||||
public get scene(): Scene {
|
||||
public get scene(): RuntimeScene {
|
||||
return this.world.data.initialScene;
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ export class WorldEditorState {
|
|||
typeId: string,
|
||||
pos: { x: number; y: number; z: number; },
|
||||
): ObjectInstance {
|
||||
const obj = createObjectInstance(randomId(), typeId);
|
||||
const obj = populateRuntimeObject(createObjectInstance(randomId(), typeId), this.world.data);
|
||||
obj.position = [pos.x, pos.y, pos.z];
|
||||
obj.rotation = [0, 0, 0];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { makeAutoObservable, reaction, toJS } from "mobx";
|
||||
import { WorldFactory } from "../model/worldFactory";
|
||||
import type { ObjectInstance, ObjectType, World } from "../types";
|
||||
import type { VoxelType } from "../types/voxel";
|
||||
import { type ObjectInstance, type ObjectType, type World, type VoxelType, type RuntimeWorld } from "../types";
|
||||
import { state } from "./rootState";
|
||||
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
|
||||
import { wolf } from "../model/objectPrefabs/wolf";
|
||||
import { terrainXZ } from "../model/objectPrefabs/terrain";
|
||||
import { populateRuntimeWorld } from "../utils/runtime";
|
||||
|
||||
export class WorldState {
|
||||
public data: World = WorldFactory.create();
|
||||
public data: RuntimeWorld = WorldFactory.create();
|
||||
|
||||
private startAutoSave() {
|
||||
return reaction(() => toJS(this.data), (data) => this.saveData(data), { delay: 500 });
|
||||
|
|
@ -39,7 +39,7 @@ export class WorldState {
|
|||
public loadMock() {
|
||||
console.log('Mocking world...');
|
||||
|
||||
const objects = Array(0).fill(0)
|
||||
const objects = Array(3).fill(0)
|
||||
.map((_, idx) => ({
|
||||
id: `obj${idx}`,
|
||||
typeId: 'wolf',
|
||||
|
|
@ -52,7 +52,7 @@ export class WorldState {
|
|||
objects.map((obj) => [obj.id, obj]),
|
||||
) as Record<string, ObjectInstance>
|
||||
|
||||
this.data = {
|
||||
this.data = populateRuntimeWorld({
|
||||
objectTypes: {
|
||||
wolf: {
|
||||
id: 'wolf',
|
||||
|
|
@ -93,7 +93,8 @@ export class WorldState {
|
|||
gameRules: {
|
||||
gravity: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
console.log(objects);
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import type { Pos3 } from "./3d";
|
||||
import type { RuntimeObjectData } from "./object";
|
||||
|
||||
export type Character = {
|
||||
transform: Pos3,
|
||||
}
|
||||
|
||||
export type RuntimeCharacter = Character & RuntimeObjectData;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import type { RuntimeScene } from "./scene";
|
||||
|
||||
export type Game = {
|
||||
paused: boolean;
|
||||
time: number;
|
||||
scene: RuntimeScene;
|
||||
}
|
||||
|
|
@ -1,9 +1,3 @@
|
|||
export * from './object';
|
||||
export * from './scene';
|
||||
export * from './world';
|
||||
export * from './gameRules';
|
||||
export * from './game';
|
||||
export * from './character';
|
||||
|
||||
|
||||
|
||||
export * from './3d';
|
||||
export * from './model';
|
||||
export * from './runtime';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import type { Pos3 } from "../3d";
|
||||
import type { GameObjectData } from "./object";
|
||||
|
||||
export type Character = {
|
||||
transform: Pos3,
|
||||
}
|
||||
|
||||
export type GameCharacter = Character & GameObjectData;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { RuntimeGameScene } from "./scene";
|
||||
|
||||
export type Game = {
|
||||
paused: boolean;
|
||||
time: number;
|
||||
scene: RuntimeGameScene;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export * from './object';
|
||||
export * from './scene';
|
||||
export * from './world';
|
||||
export * from './gameRules';
|
||||
export * from './game';
|
||||
export * from './character';
|
||||
export * from './voxel';
|
||||
export * from './runtime';
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { R3, V3 } from "./3d";
|
||||
import type { R3, V3 } from "../3d";
|
||||
import type { Runtime } from "./runtime";
|
||||
import type { Voxel } from "./voxel";
|
||||
|
||||
export type ObjectType = {
|
||||
|
|
@ -17,9 +18,12 @@ export type ObjectInstance = {
|
|||
scale: V3;
|
||||
}
|
||||
|
||||
export type RuntimeObjectData = {
|
||||
export type GameObjectData = {
|
||||
linearVelocity: V3;
|
||||
radialVelocity: R3;
|
||||
}
|
||||
|
||||
export type RuntimeObjectInstance = ObjectInstance & RuntimeObjectData;
|
||||
export type GameObjectInstance = ObjectInstance & GameObjectData;
|
||||
|
||||
export type RuntimeObjectInstance = Runtime<ObjectInstance>;
|
||||
export type RuntimeGameObjectInstance = Runtime<GameObjectInstance>;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type { VoxelGroup } from "../runtime";
|
||||
import type { ObjectInstance } from "./object";
|
||||
|
||||
export type ObjectInstanceRuntimeData = {
|
||||
cache: {
|
||||
voxelGroups: VoxelGroup[];
|
||||
};
|
||||
}
|
||||
|
||||
export type Runtime<T extends ObjectInstance> = T & ObjectInstanceRuntimeData;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Character, GameCharacter } from "./character";
|
||||
import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object";
|
||||
|
||||
export type Scene = {
|
||||
character: Character;
|
||||
objects: Record<string, ObjectInstance>;
|
||||
}
|
||||
|
||||
export type RuntimeScene = {
|
||||
character: Character;
|
||||
objects: Record<string, RuntimeObjectInstance>;
|
||||
}
|
||||
|
||||
export type GameScene = {
|
||||
character: GameCharacter;
|
||||
objects: Record<string, GameObjectInstance>;
|
||||
}
|
||||
|
||||
export type RuntimeGameScene = {
|
||||
character: GameCharacter;
|
||||
objects: Record<string, RuntimeGameObjectInstance>;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { V3 } from "./3d";
|
||||
import type { V3 } from "../3d";
|
||||
|
||||
export type VoxelType = {
|
||||
id: string;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { GameRules } from "./gameRules";
|
||||
import type { ObjectType } from "./object";
|
||||
import type { Scene } from "./scene";
|
||||
import type { Pos3 } from "./3d";
|
||||
import type { RuntimeScene, Scene } from "./scene";
|
||||
import type { Pos3 } from "../3d";
|
||||
import type { VoxelType } from "./voxel";
|
||||
|
||||
export type World = {
|
||||
|
|
@ -11,3 +11,7 @@ export type World = {
|
|||
initialScene: Scene;
|
||||
gameRules: GameRules;
|
||||
}
|
||||
|
||||
export type RuntimeWorld = World & {
|
||||
initialScene: RuntimeScene;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './voxelGroup';
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import type { V3 } from "../3d";
|
||||
|
||||
export type VoxelGroup = {
|
||||
id: string;
|
||||
color: string;
|
||||
opacity: number;
|
||||
positions: V3[];
|
||||
};
|
||||
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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,41 @@
|
|||
import type { ObjectType, VoxelType } from "../../types";
|
||||
|
||||
export function buildObjectTrimesh(objectType: ObjectType, voxelTypes: Record<string, VoxelType>): [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];
|
||||
}
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
import type { ObjectType } from "../../types";
|
||||
import type { VoxelType } from "../../types/voxel";
|
||||
|
||||
export type VoxelGroup = {
|
||||
id: string;
|
||||
color: string;
|
||||
opacity: number;
|
||||
positions: [number, number, number][];
|
||||
};
|
||||
import type { ObjectType, VoxelGroup, VoxelType } from "../../types";
|
||||
|
||||
export function getObjectVoxelGroups(
|
||||
object: ObjectType,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance
|
|||
return {
|
||||
id,
|
||||
typeId: typeId,
|
||||
physics: false,
|
||||
gravityScale: 1,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './world';
|
||||
export * from './scene';
|
||||
export * from './object';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
||||
import type { ObjectInstance, RuntimeObjectInstance, World } from "../../types";
|
||||
|
||||
export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance {
|
||||
const objectType = world.objectTypes[object.typeId];
|
||||
|
||||
return {
|
||||
...object,
|
||||
cache: {
|
||||
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
||||
const { cache, ...result } = object;
|
||||
return result;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import type { RuntimeScene, Scene, World } from "../../types";
|
||||
import { depopulateRuntimeObject, populateRuntimeObject } from "./object";
|
||||
|
||||
export function populateRuntimeScene(scene: Scene, world: World): RuntimeScene {
|
||||
return {
|
||||
...scene,
|
||||
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, populateRuntimeObject(obj, world)])),
|
||||
}
|
||||
}
|
||||
|
||||
export function depopulateRuntimeScene(scene: RuntimeScene, world: World): Scene {
|
||||
return {
|
||||
...scene,
|
||||
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, depopulateRuntimeObject(obj, world)])),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import type { RuntimeWorld, World } from "../../types";
|
||||
import { depopulateRuntimeScene, populateRuntimeScene } from "./scene";
|
||||
|
||||
export function populateRuntimeWorld(world: World): RuntimeWorld {
|
||||
return {
|
||||
...world,
|
||||
initialScene: populateRuntimeScene(world.initialScene, world),
|
||||
};
|
||||
}
|
||||
|
||||
export function depopulateRuntimeWorld(world: RuntimeWorld): World {
|
||||
return {
|
||||
...world,
|
||||
initialScene: depopulateRuntimeScene(world.initialScene, world),
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue