Fix position export and add target physics improvements
All checks were successful
Build / build (push) Successful in 2m0s

- Use getAbsolutePosition() in all config builders for correct world coords
- Add TargetComponent case to meshCollector for target export
- Add shared target body cache in RockFactory (asteroids can share targets)
- Add debug logging throughout export and physics pipelines
- Pass targetId to RockFactory for proper target body sharing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-12-09 12:33:31 -06:00
parent 4c9e1f65c0
commit 96bc3df51e
15 changed files with 193 additions and 166 deletions

View File

@ -14,9 +14,19 @@ function buildSingleAsteroid(mesh: AbstractMesh, index: number): AsteroidConfig
const rotation = toVector3Array(mesh.rotation);
const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0;
// Debug: compare local vs absolute position
const localPos = mesh.position;
const absPos = mesh.getAbsolutePosition();
if (Math.abs(localPos.x - absPos.x) > 1 || Math.abs(localPos.y - absPos.y) > 1 || Math.abs(localPos.z - absPos.z) > 1) {
console.warn(`[AsteroidBuilder] Position mismatch for ${mesh.name}:`,
`local=(${localPos.x.toFixed(1)}, ${localPos.y.toFixed(1)}, ${localPos.z.toFixed(1)})`,
`absolute=(${absPos.x.toFixed(1)}, ${absPos.y.toFixed(1)}, ${absPos.z.toFixed(1)})`,
`parent=${mesh.parent?.name || 'none'}`);
}
return {
id: mesh.name || `asteroid-${index}`,
position: toVector3Array(mesh.position),
position: toVector3Array(mesh.getAbsolutePosition()), // Use absolute position
rotation: hasRotation ? rotation : undefined,
scale: mesh.scaling.x,
linearVelocity: extractVector3(script.linearVelocity, [0, 0, 0]),

View File

@ -16,7 +16,7 @@ export function buildBaseConfig(mesh: AbstractMesh | null): StartBaseConfig | un
const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0;
return {
position: toVector3Array(mesh.position),
position: toVector3Array(mesh.getAbsolutePosition()),
rotation: hasRotation ? rotation : undefined,
baseGlbPath: glbPath || undefined,
landingGlbPath: (script.landingGlbPath as string) || undefined,

View File

@ -14,7 +14,7 @@ function buildSinglePlanet(mesh: AbstractMesh): PlanetConfig {
return {
name: mesh.name || "planet",
position: toVector3Array(mesh.position),
position: toVector3Array(mesh.getAbsolutePosition()),
diameter: (script.diameter as number) ?? 100,
texturePath: (script.texturePath as string) || "planet_texture.jpg",
rotation: hasRotation(mesh) ? toVector3Array(mesh.rotation) : undefined

View File

@ -13,7 +13,7 @@ export function buildShipConfig(mesh: AbstractMesh | null): ShipConfig {
const script = getScriptValues(mesh);
return {
position: toVector3Array(mesh.position),
position: toVector3Array(mesh.getAbsolutePosition()),
rotation: mesh.rotation ? toVector3Array(mesh.rotation) : undefined,
linearVelocity: extractVector3OrUndefined(script.linearVelocity),
angularVelocity: extractVector3OrUndefined(script.angularVelocity)

View File

@ -21,7 +21,7 @@ export function buildSunConfig(mesh: AbstractMesh | null): SunConfig {
const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0;
return {
position: toVector3Array(mesh.position),
position: toVector3Array(mesh.getAbsolutePosition()),
rotation: hasRotation ? rotation : undefined,
diameter: (script.diameter as number) ?? 50,
intensity: (script.intensity as number) ?? 1000000,

View File

@ -15,7 +15,7 @@ function buildSingleTarget(node: TransformNode): TargetConfig {
return {
id: node.name || node.id,
name: (script.displayName as string) || node.name || "Target",
position: toVector3Array(node.position)
position: toVector3Array(node.getAbsolutePosition())
};
}

View File

@ -14,6 +14,11 @@ import { buildTargetConfigs } from "./configBuilders/targetBuilder";
export function exportLevelConfig(scene: Scene): string {
const meshes = collectMeshesByComponent(scene);
console.log(`[Exporter] Collected: ${meshes.asteroids.length} asteroids, ${meshes.targets.length} targets, ${meshes.planets.length} planets`);
if (meshes.targets.length > 0) {
console.log(`[Exporter] Target IDs: ${meshes.targets.map(t => t.name || t.id).join(', ')}`);
}
const config: LevelConfig = {
version: "1.0",
difficulty: "rookie",

View File

@ -56,6 +56,10 @@ function categorizeByScript(
case "BaseComponent":
result.base = mesh;
break;
case "TargetComponent":
// Targets can be Mesh or TransformNode - handle both
result.targets.push(mesh as unknown as TransformNode);
break;
}
}

View File

@ -64,6 +64,7 @@
"geometryUniqueId": 1764787809875,
"geometryId": "76c7442d-fb7e-4a05-b1c5-9c27b0beb0dc",
"subMeshes": null,
"materialUniqueId": 46,
"materialId": "default material",
"metadata": {
"type": "Box",
@ -138,7 +139,108 @@
"transformNodes": [],
"cameras": [],
"lights": [],
"materials": [],
"materials": [
{
"tags": null,
"ambient": [
0,
0,
0
],
"diffuse": [
1,
1,
1
],
"specular": [
1,
1,
1
],
"emissive": [
0,
0,
0
],
"specularPower": 64,
"useAlphaFromDiffuseTexture": false,
"useEmissiveAsIllumination": false,
"linkEmissiveWithDiffuse": false,
"useSpecularOverAlpha": false,
"useReflectionOverAlpha": false,
"disableLighting": false,
"useObjectSpaceNormalMap": false,
"useParallax": false,
"useParallaxOcclusion": false,
"parallaxScaleBias": 0.05,
"roughness": 0,
"indexOfRefraction": 0.98,
"invertRefractionY": true,
"alphaCutOff": 0.4,
"useLightmapAsShadowmap": false,
"useReflectionFresnelFromSpecular": false,
"useGlossinessFromSpecularMapAlpha": false,
"maxSimultaneousLights": 32,
"invertNormalMapX": false,
"invertNormalMapY": false,
"twoSidedLighting": false,
"applyDecalMapAfterDetailMap": false,
"id": "default material",
"name": "default material",
"checkReadyOnEveryCall": false,
"checkReadyOnlyOnce": false,
"state": "",
"alpha": 1,
"backFaceCulling": true,
"cullBackFaces": true,
"_alphaMode": [
2
],
"_needDepthPrePass": false,
"disableDepthWrite": false,
"disableColorWrite": false,
"forceDepthWrite": false,
"depthFunction": 0,
"separateCullingPass": false,
"fogEnabled": true,
"pointSize": 1,
"zOffset": 0,
"zOffsetUnits": 0,
"pointsCloud": false,
"fillMode": 0,
"_isVertexOutputInvariant": false,
"stencil": {
"tags": null,
"func": 519,
"backFunc": 519,
"funcRef": 1,
"funcMask": 255,
"opStencilFail": 7680,
"opDepthFail": 7680,
"opStencilDepthPass": 7681,
"backOpStencilFail": 7680,
"backOpDepthFail": 7680,
"backOpStencilDepthPass": 7681,
"mask": 255,
"enabled": false
},
"uniqueId": 46,
"plugins": {
"DetailMapConfiguration": {
"tags": null,
"diffuseBlendLevel": 1,
"roughnessBlendLevel": 1,
"bumpLevel": 1,
"normalBlendMethod": 0,
"isEnabled": false,
"name": "DetailMap",
"priority": 140,
"resolveIncludes": false,
"registerForExtraEvents": false
}
}
}
],
"geometries": {
"boxes": [],
"spheres": [],

View File

@ -109,146 +109,7 @@
],
"parentId": 1764789858421
},
"instances": [
{
"name": "Asteroid",
"id": "Asteroid",
"isEnabled": true,
"isVisible": true,
"isPickable": true,
"checkCollisions": false,
"position": [
66.2149304569587,
40.81207511231127,
-126.79009642287176
],
"scaling": [
5,
5,
5
],
"rotationQuaternion": [
0,
0,
0,
1
],
"metadata": {
"scripts": [
{
"key": "scripts/editorScripts/AsteroidComponent.ts",
"enabled": true,
"values": {
"linearVelocity": {
"type": "vector3",
"value": [
2,
0,
0
]
},
"angularVelocity": {
"type": "vector3",
"value": [
0,
0,
0
]
},
"mass": {
"type": "number",
"value": 1
},
"targetId": {
"type": "string",
"description": "Reference to a TargetComponent node",
"value": ""
},
"targetMode": {
"type": "string",
"description": "orbit | moveToward | (empty)",
"value": ""
}
},
"_id": "74563a74-be80-46fe-8dc3-189b03247c20"
}
],
"parentId": 1764789858421
},
"animations": [],
"ranges": [],
"uniqueId": 71
},
{
"name": "Asteroid",
"id": "Asteroid",
"isEnabled": true,
"isVisible": true,
"isPickable": true,
"checkCollisions": false,
"position": [
0,
-22.646529278627046,
-74.97825372352042
],
"scaling": [
5,
5,
5
],
"rotationQuaternion": [
0,
0,
0,
1
],
"metadata": {
"scripts": [
{
"key": "scripts/editorScripts/AsteroidComponent.ts",
"enabled": true,
"values": {
"linearVelocity": {
"type": "vector3",
"value": [
0,
0,
-2
]
},
"angularVelocity": {
"type": "vector3",
"value": [
0,
0,
0
]
},
"mass": {
"type": "number",
"value": 1
},
"targetId": {
"type": "string",
"description": "Reference to a TargetComponent node",
"value": ""
},
"targetMode": {
"type": "string",
"description": "orbit | moveToward | (empty)",
"value": ""
}
},
"_id": "79ae3cbb-7653-409c-9d0a-3e0b254e6731"
}
],
"parentId": 1764789858421
},
"animations": [],
"ranges": [],
"uniqueId": 71
}
],
"instances": [],
"animations": [],
"ranges": [],
"layerMask": 268435455,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

View File

@ -5,13 +5,13 @@
import { loadScene, scriptsDictionary, scriptAssetsCache, _applyScriptsForObject, _removeRegisteredScriptInstance, _preloadScriptsAssets } from "babylonjs-editor-tools";
import * as scripts_editorScripts_AsteroidComponent from "./scripts/editorScripts/AsteroidComponent";
import * as scripts_editorScripts_ShipComponent from "./scripts/editorScripts/ShipComponent";
import * as scripts_editorScripts_BaseComponent from "./scripts/editorScripts/BaseComponent";
import * as scripts_editorScripts_ShipComponent from "./scripts/editorScripts/ShipComponent";
export const scriptsMap = {
"scripts/editorScripts/AsteroidComponent.ts": scripts_editorScripts_AsteroidComponent,
"scripts/editorScripts/ShipComponent.ts": scripts_editorScripts_ShipComponent,
"scripts/editorScripts/BaseComponent.ts": scripts_editorScripts_BaseComponent
"scripts/editorScripts/BaseComponent.ts": scripts_editorScripts_BaseComponent,
"scripts/editorScripts/ShipComponent.ts": scripts_editorScripts_ShipComponent
};
export { loadScene, scriptsDictionary, scriptAssetsCache, _applyScriptsForObject, _removeRegisteredScriptInstance, _preloadScriptsAssets };

View File

@ -42,6 +42,7 @@ interface RockConfig {
angularVelocity: Vector3;
scoreObservable: Observable<ScoreEvent>;
useOrbitConstraint: boolean;
targetId?: string;
targetPosition?: Vector3;
targetMode?: 'orbit' | 'moveToward';
}
@ -54,6 +55,9 @@ export class RockFactory {
// Store created rocks for deferred physics initialization
private static _createdRocks: Map<string, { mesh: InstancedMesh; config: RockConfig }> = new Map();
// Cache for target physics bodies (shared among asteroids with same targetId)
private static _targetBodies: Map<string, PhysicsAggregate> = new Map();
/** Public getter for explosion manager (used by WeaponSystem for shape-cast hits) */
public static get explosionManager(): ExplosionManager | null {
return this._explosionManager;
@ -124,6 +128,11 @@ export class RockFactory {
log.debug('[RockFactory] Resetting static state');
this._asteroidMesh = null;
this._createdRocks.clear();
// Dispose and clear target bodies
for (const targetBody of this._targetBodies.values()) {
targetBody.dispose();
}
this._targetBodies.clear();
if (this._explosionManager) {
this._explosionManager.dispose();
this._explosionManager = null;
@ -170,6 +179,7 @@ export class RockFactory {
scoreObservable: Observable<ScoreEvent>,
useOrbitConstraint: boolean = true,
hidden: boolean = false,
targetId?: string,
targetPosition?: Vector3,
targetMode?: 'orbit' | 'moveToward',
rotation?: Vector3
@ -197,12 +207,13 @@ export class RockFactory {
angularVelocity,
scoreObservable,
useOrbitConstraint,
targetId,
targetPosition,
targetMode
};
this._createdRocks.set(rock.id, { mesh: rock, config });
log.debug(`[RockFactory] Created rock mesh ${rock.id} (hidden: ${hidden}, target: ${targetMode || 'none'})`);
log.debug(`[RockFactory] Created rock mesh ${rock.id} (hidden: ${hidden}, target: ${targetId || 'none'}, mode: ${targetMode || 'none'})`);
return new Rock(rock);
}
@ -227,14 +238,18 @@ export class RockFactory {
// Handle target-based physics
if (config.targetPosition && config.targetMode) {
log.debug(`[RockFactory] Applying ${config.targetMode} physics to ${rock.id} toward target at ${config.targetPosition}`);
this.applyTargetPhysics(body, config);
} else if (config.useOrbitConstraint && this._orbitCenter) {
// Legacy: orbit around origin if no specific target
log.debug(`[RockFactory] Using legacy orbit constraint for ${rock.id} (no target specified)`);
const constraint = new DistanceConstraint(
Vector3.Distance(config.position, this._orbitCenter.body.transformNode.position),
DefaultScene.MainScene
);
body.addConstraint(this._orbitCenter.body, constraint);
} else {
log.debug(`[RockFactory] No orbit constraint for ${rock.id} (useOrbitConstraint=${config.useOrbitConstraint})`);
}
// Prevent sleeping
@ -262,16 +277,8 @@ export class RockFactory {
if (!config.targetPosition) return;
if (config.targetMode === 'orbit') {
// Create distance constraint to target position
// We need a static body at the target position for the constraint
const targetNode = new TransformNode(`target-${body.transformNode.id}`, DefaultScene.MainScene);
targetNode.position = config.targetPosition;
const targetBody = new PhysicsAggregate(
targetNode, PhysicsShapeType.SPHERE,
{ radius: 0.1, mass: 0 },
DefaultScene.MainScene
);
targetBody.body.setMotionType(PhysicsMotionType.STATIC);
// Get or create shared target body for this targetId
const targetBody = this.getOrCreateTargetBody(config.targetId, config.targetPosition);
const distance = Vector3.Distance(config.position, config.targetPosition);
const constraint = new DistanceConstraint(distance, DefaultScene.MainScene);
@ -291,9 +298,38 @@ export class RockFactory {
// Final velocity = direction * speed
const velocity = direction.scale(speed);
body.setLinearVelocity(velocity);
} else {
log.warn(`Invalid targetMode ${config.targetMode}`)
}
}
/**
* Get or create a shared physics body for a target position
*/
private static getOrCreateTargetBody(targetId: string | undefined, position: Vector3): PhysicsAggregate {
const cacheKey = targetId || `pos-${position.x}-${position.y}-${position.z}`;
if (this._targetBodies.has(cacheKey)) {
log.debug(`[RockFactory] Reusing existing target body for "${cacheKey}"`);
return this._targetBodies.get(cacheKey)!;
}
// Create new target body
const targetNode = new TransformNode(`target-${cacheKey}`, DefaultScene.MainScene);
targetNode.position = position;
const targetBody = new PhysicsAggregate(
targetNode, PhysicsShapeType.SPHERE,
{ radius: 0.1, mass: 0 },
DefaultScene.MainScene
);
targetBody.body.setMotionType(PhysicsMotionType.STATIC);
this._targetBodies.set(cacheKey, targetBody);
log.debug(`[RockFactory] Created new target body for "${cacheKey}" at ${position}`);
return targetBody;
}
private static setupCollisionHandler(body: PhysicsBody, scoreObservable: Observable<ScoreEvent>): void {
body.getCollisionObservable().add((eventData) => {
if (eventData.type !== 'COLLISION_STARTED') return;

View File

@ -248,10 +248,18 @@ export class LevelDeserializer {
// Resolve target position if specified
let targetPosition: Vector3 | undefined;
if (asteroidConfig.targetId && this.config.targets) {
const target = this.config.targets.find(t => t.id === asteroidConfig.targetId);
if (target) {
targetPosition = this.arrayToVector3(target.position);
if (asteroidConfig.targetId) {
if (this.config.targets && this.config.targets.length > 0) {
const target = this.config.targets.find(t => t.id === asteroidConfig.targetId);
if (target) {
targetPosition = this.arrayToVector3(target.position);
log.debug(`[LevelDeserializer] Asteroid ${asteroidConfig.id} linked to target ${target.id} at ${targetPosition}`);
} else {
const availableIds = this.config.targets.map(t => t.id).join(', ');
log.warn(`[LevelDeserializer] Asteroid ${asteroidConfig.id} has targetId "${asteroidConfig.targetId}" but no match found. Available: [${availableIds}]`);
}
} else {
log.warn(`[LevelDeserializer] Asteroid ${asteroidConfig.id} has targetId "${asteroidConfig.targetId}" but no targets array in config`);
}
}
@ -266,6 +274,7 @@ export class LevelDeserializer {
scoreObservable,
useOrbitConstraints,
hidden,
asteroidConfig.targetId,
targetPosition,
asteroidConfig.targetMode,
rotation