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 rotation = toVector3Array(mesh.rotation);
const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0; 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 { return {
id: mesh.name || `asteroid-${index}`, id: mesh.name || `asteroid-${index}`,
position: toVector3Array(mesh.position), position: toVector3Array(mesh.getAbsolutePosition()), // Use absolute position
rotation: hasRotation ? rotation : undefined, rotation: hasRotation ? rotation : undefined,
scale: mesh.scaling.x, scale: mesh.scaling.x,
linearVelocity: extractVector3(script.linearVelocity, [0, 0, 0]), 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; const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0;
return { return {
position: toVector3Array(mesh.position), position: toVector3Array(mesh.getAbsolutePosition()),
rotation: hasRotation ? rotation : undefined, rotation: hasRotation ? rotation : undefined,
baseGlbPath: glbPath || undefined, baseGlbPath: glbPath || undefined,
landingGlbPath: (script.landingGlbPath as string) || undefined, landingGlbPath: (script.landingGlbPath as string) || undefined,

View File

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

View File

@ -13,7 +13,7 @@ export function buildShipConfig(mesh: AbstractMesh | null): ShipConfig {
const script = getScriptValues(mesh); const script = getScriptValues(mesh);
return { return {
position: toVector3Array(mesh.position), position: toVector3Array(mesh.getAbsolutePosition()),
rotation: mesh.rotation ? toVector3Array(mesh.rotation) : undefined, rotation: mesh.rotation ? toVector3Array(mesh.rotation) : undefined,
linearVelocity: extractVector3OrUndefined(script.linearVelocity), linearVelocity: extractVector3OrUndefined(script.linearVelocity),
angularVelocity: extractVector3OrUndefined(script.angularVelocity) 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; const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0;
return { return {
position: toVector3Array(mesh.position), position: toVector3Array(mesh.getAbsolutePosition()),
rotation: hasRotation ? rotation : undefined, rotation: hasRotation ? rotation : undefined,
diameter: (script.diameter as number) ?? 50, diameter: (script.diameter as number) ?? 50,
intensity: (script.intensity as number) ?? 1000000, intensity: (script.intensity as number) ?? 1000000,

View File

@ -15,7 +15,7 @@ function buildSingleTarget(node: TransformNode): TargetConfig {
return { return {
id: node.name || node.id, id: node.name || node.id,
name: (script.displayName as string) || node.name || "Target", 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 { export function exportLevelConfig(scene: Scene): string {
const meshes = collectMeshesByComponent(scene); 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 = { const config: LevelConfig = {
version: "1.0", version: "1.0",
difficulty: "rookie", difficulty: "rookie",

View File

@ -56,6 +56,10 @@ function categorizeByScript(
case "BaseComponent": case "BaseComponent":
result.base = mesh; result.base = mesh;
break; 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, "geometryUniqueId": 1764787809875,
"geometryId": "76c7442d-fb7e-4a05-b1c5-9c27b0beb0dc", "geometryId": "76c7442d-fb7e-4a05-b1c5-9c27b0beb0dc",
"subMeshes": null, "subMeshes": null,
"materialUniqueId": 46,
"materialId": "default material", "materialId": "default material",
"metadata": { "metadata": {
"type": "Box", "type": "Box",
@ -138,7 +139,108 @@
"transformNodes": [], "transformNodes": [],
"cameras": [], "cameras": [],
"lights": [], "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": { "geometries": {
"boxes": [], "boxes": [],
"spheres": [], "spheres": [],

View File

@ -109,146 +109,7 @@
], ],
"parentId": 1764789858421 "parentId": 1764789858421
}, },
"instances": [ "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
}
],
"animations": [], "animations": [],
"ranges": [], "ranges": [],
"layerMask": 268435455, "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 { loadScene, scriptsDictionary, scriptAssetsCache, _applyScriptsForObject, _removeRegisteredScriptInstance, _preloadScriptsAssets } from "babylonjs-editor-tools";
import * as scripts_editorScripts_AsteroidComponent from "./scripts/editorScripts/AsteroidComponent"; 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_BaseComponent from "./scripts/editorScripts/BaseComponent";
import * as scripts_editorScripts_ShipComponent from "./scripts/editorScripts/ShipComponent";
export const scriptsMap = { export const scriptsMap = {
"scripts/editorScripts/AsteroidComponent.ts": scripts_editorScripts_AsteroidComponent, "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 }; export { loadScene, scriptsDictionary, scriptAssetsCache, _applyScriptsForObject, _removeRegisteredScriptInstance, _preloadScriptsAssets };

View File

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

View File

@ -248,10 +248,18 @@ export class LevelDeserializer {
// Resolve target position if specified // Resolve target position if specified
let targetPosition: Vector3 | undefined; let targetPosition: Vector3 | undefined;
if (asteroidConfig.targetId && this.config.targets) { if (asteroidConfig.targetId) {
const target = this.config.targets.find(t => t.id === asteroidConfig.targetId); if (this.config.targets && this.config.targets.length > 0) {
if (target) { const target = this.config.targets.find(t => t.id === asteroidConfig.targetId);
targetPosition = this.arrayToVector3(target.position); 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, scoreObservable,
useOrbitConstraints, useOrbitConstraints,
hidden, hidden,
asteroidConfig.targetId,
targetPosition, targetPosition,
asteroidConfig.targetMode, asteroidConfig.targetMode,
rotation rotation