Refactor explosion system to use MeshExploder with animated debris
Some checks failed
Build / build (push) Failing after 19s

Completely rewrote explosion system to use BabylonJS MeshExploder instead of particle systems for more dramatic and visible explosions.

- Replace particle system pooling with MeshExploder approach
- Create 12 sphere debris pieces that explode outward
- Animate explosion force from 0 to 15 over 500ms
- Animate debris scaling from 1.0 to 0.0 (shrink to nothing)
- Use requestAnimationFrame for smooth 60fps animation
- Pass asteroid mesh to ExplosionManager instead of position/scaling

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-10-30 12:02:00 -05:00
parent bf5d33e1cb
commit 1a381ec47d
2 changed files with 174 additions and 104 deletions

View File

@ -1,37 +1,38 @@
import { import {
MeshBuilder, AbstractMesh,
ParticleHelper, Animation, Color3,
ParticleSystem, Mesh, MeshBuilder,
ParticleSystemSet, MeshExploder,
Scene, Scene,
Vector3 Vector3,
VertexData
} from "@babylonjs/core"; } from "@babylonjs/core";
import {DefaultScene} from "./defaultScene";
/** /**
* Configuration for explosion effects * Configuration for explosion effects
*/ */
export interface ExplosionConfig { export interface ExplosionConfig {
/** Size of the explosion pool */
poolSize?: number;
/** Duration of explosion in milliseconds */ /** Duration of explosion in milliseconds */
duration?: number; duration?: number;
/** Rendering group ID for particles */ /** Maximum explosion force (how far pieces spread) */
renderingGroupId?: number; explosionForce?: number;
/** Frame rate for explosion animation */
frameRate?: number;
} }
/** /**
* Manages explosion particle effects with pooling for performance * Manages mesh explosion effects using BabylonJS MeshExploder
*/ */
export class ExplosionManager { export class ExplosionManager {
private explosionPool: ParticleSystemSet[] = [];
private scene: Scene; private scene: Scene;
private config: Required<ExplosionConfig>; private config: Required<ExplosionConfig>;
// Default configuration // Default configuration
private static readonly DEFAULT_CONFIG: Required<ExplosionConfig> = { private static readonly DEFAULT_CONFIG: Required<ExplosionConfig> = {
poolSize: 10, duration: 1000,
duration: 2000, explosionForce: 5,
renderingGroupId: 1 frameRate: 60
}; };
constructor(scene: Scene, config?: ExplosionConfig) { constructor(scene: Scene, config?: ExplosionConfig) {
@ -40,108 +41,177 @@ export class ExplosionManager {
} }
/** /**
* Initialize the explosion pool by pre-creating particle systems * Initialize the explosion manager (no longer needed for MeshExploder, but kept for API compatibility)
*/ */
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
console.log(`Pre-creating ${this.config.poolSize} explosion particle systems...`); console.log("ExplosionManager initialized with MeshExploder");
}
for (let i = 0; i < this.config.poolSize; i++) { /**
const set = await ParticleHelper.CreateAsync("explosion", this.scene); * Create sphere debris pieces for explosion
set.systems.forEach((system) => { * MeshExploder requires an array of separate meshes
system.renderingGroupId = this.config.renderingGroupId; * @param mesh The mesh to explode (used for position/scale)
}); * @param pieces Number of pieces to create
this.explosionPool.push(set); * @returns Array of sphere mesh objects
*/
private splitIntoSeparateMeshes(mesh: Mesh, pieces: number = 32): Mesh[] {
console.log(`Creating ${pieces} sphere debris pieces`);
const meshPieces: Mesh[] = [];
const basePosition = mesh.position.clone();
const baseScale = mesh.scaling.clone();
// Create material for debris
const material = mesh.material?.clone('debris-material');
if (material) {
//(material as any).emissiveColor = Color3.Yellow();
} }
console.log(`Created ${this.config.poolSize} explosion particle systems in pool`); // Create sphere debris scattered around the original mesh position
const avgScale = (baseScale.x + baseScale.y + baseScale.z) / 3;
const debrisSize = avgScale * 0.3; // Size relative to asteroid
for (let i = 0; i < pieces; i++) {
// Create a small sphere for debris
const sphere = MeshBuilder.CreateIcoSphere(
`${mesh.name}_debris_${i}`,
{
radius: debrisSize,
subdivisions: 2
}, DefaultScene.MainScene
);
// Position spheres in a small cluster around the original position
const offsetRadius = avgScale * 0.5;
const angle1 = (i / pieces) * Math.PI * 2;
const angle2 = Math.random() * Math.PI;
sphere.position = new Vector3(
basePosition.x + Math.sin(angle2) * Math.cos(angle1) * offsetRadius,
basePosition.y + Math.sin(angle2) * Math.sin(angle1) * offsetRadius,
basePosition.z + Math.cos(angle2) * offsetRadius
);
sphere.material = material;
sphere.isVisible = true;
sphere.setEnabled(true);
meshPieces.push(sphere);
}
console.log(`Created ${meshPieces.length} sphere debris pieces`);
return meshPieces;
} }
/** /**
* Get an explosion from the pool * Explode a mesh by breaking it into pieces and animating them outward
* @param mesh The mesh to explode (will be cloned internally)
*/ */
private getExplosionFromPool(): ParticleSystemSet | null { public playExplosion(mesh: AbstractMesh): void {
return this.explosionPool.pop() || null; // Get the source mesh if this is an instanced mesh
} let sourceMesh: Mesh;
if ((mesh as any).sourceMesh) {
/** sourceMesh = (mesh as any).sourceMesh as Mesh;
* Return an explosion to the pool after use
*/
private returnExplosionToPool(explosion: ParticleSystemSet): void {
explosion.dispose();
ParticleHelper.CreateAsync("explosion", this.scene).then((set) => {
set.systems.forEach((system) => {
system.renderingGroupId = this.config.renderingGroupId;
});
this.explosionPool.push(set);
});
}
/**
* Play an explosion at the specified position with optional scaling
*/
public playExplosion(position: Vector3, scaling: Vector3 = Vector3.One()): void {
const explosion = this.getExplosionFromPool();
if (!explosion) {
// Pool is empty, create explosion on the fly
console.log("Explosion pool empty, creating new explosion on demand");
ParticleHelper.CreateAsync("explosion", this.scene).then((set) => {
const point = MeshBuilder.CreateSphere("explosionPoint", {
diameter: 0.1
}, this.scene);
point.position = position.clone();
point.isVisible = false;
set.start(point);
setTimeout(() => {
set.dispose();
point.dispose();
}, this.config.duration);
});
} else { } else {
// Use pooled explosion sourceMesh = mesh as Mesh;
const point = MeshBuilder.CreateSphere("explosionPoint", { }
diameter: 10
}, this.scene);
point.position = position.clone();
point.isVisible = false;
point.scaling = scaling.multiplyByFloats(0.2, 0.3, 0.2);
console.log("Using pooled explosion with", explosion.systems.length, "systems at", position); // Clone the source mesh so we don't affect the original
const meshToExplode = sourceMesh.clone("exploding-" + mesh.name, null, true, false);
if (!meshToExplode) {
console.warn("Failed to clone mesh for explosion");
return;
}
// Set emitter and start each system individually // Apply the instance's transformation to the cloned mesh
explosion.systems.forEach((system: ParticleSystem, idx: number) => { meshToExplode.position = mesh.getAbsolutePosition().clone();
system.emitter = point; meshToExplode.rotation = mesh.rotation.clone();
system.start(); meshToExplode.scaling = mesh.scaling.clone();
console.log(` System ${idx}: emitter set to`, system.emitter, "activeCount=", system.getActiveCount()); meshToExplode.setEnabled(true);
// Force world matrix computation
meshToExplode.computeWorldMatrix(true);
// Check if mesh has proper geometry
if (!meshToExplode.getTotalVertices || meshToExplode.getTotalVertices() === 0) {
console.warn("Mesh has no vertices, cannot explode");
meshToExplode.dispose();
return;
}
console.log(`Exploding mesh: ${meshToExplode.name}, vertices: ${meshToExplode.getTotalVertices()}`);
// Split the mesh into separate mesh objects (MeshExploder requirement)
const meshPieces = this.splitIntoSeparateMeshes(meshToExplode, 12);
if (meshPieces.length === 0) {
console.warn("Failed to split mesh into pieces");
meshToExplode.dispose();
return;
}
// Original mesh is no longer needed - the pieces replace it
meshToExplode.dispose();
// Create the exploder with the array of separate meshes
// The second parameter is optional - it's the center mesh to explode from
// If not provided, MeshExploder will auto-calculate the center
const exploder = new MeshExploder(meshPieces);
console.log(`Starting explosion animation for ${meshPieces.length} mesh pieces`);
// Animate the explosion by calling explode() each frame with increasing values
const startTime = Date.now();
const animationDuration = this.config.duration;
const maxForce = this.config.explosionForce;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / animationDuration, 1.0);
// Calculate current explosion value (0 to maxForce)
const currentValue = progress * maxForce;
exploder.explode(currentValue);
// Animate debris size to zero (1.0 to 0.0)
const scale = 1.0 - progress;
meshPieces.forEach(piece => {
piece.scaling.set(scale, scale, scale);
}); });
// Stop and return to pool after duration // Continue animation if not complete
setTimeout(() => { if (progress < 1.0) {
explosion.systems.forEach((system: ParticleSystem) => { requestAnimationFrame(animate);
system.stop(); } else {
}); // Animation complete - clean up
this.returnExplosionToPool(explosion); console.log(`Explosion animation complete, cleaning up`);
point.dispose(); this.cleanupExplosion(meshPieces);
}, this.config.duration); }
} };
// Start the animation
animate();
} }
/** /**
* Get the current number of available explosions in the pool * Clean up explosion meshes
*/ */
public getPoolSize(): number { private cleanupExplosion(meshPieces: Mesh[]): void {
return this.explosionPool.length; // Dispose all the mesh pieces
meshPieces.forEach(mesh => {
if (mesh && !mesh.isDisposed()) {
mesh.dispose();
}
});
console.log(`Explosion cleaned up - disposed ${meshPieces.length} pieces`);
} }
/** /**
* Dispose of all pooled explosions * Dispose of the explosion manager
*/ */
public dispose(): void { public dispose(): void {
this.explosionPool.forEach(explosion => { // Nothing to dispose with MeshExploder approach
explosion.dispose(); console.log("ExplosionManager disposed");
});
this.explosionPool = [];
} }
} }

View File

@ -45,9 +45,9 @@ export class RockFactory {
public static async init() { public static async init() {
// Initialize explosion manager // Initialize explosion manager
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, { this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
poolSize: 10, duration: 500,
duration: 2000, explosionForce: 15.0,
renderingGroupId: 1 frameRate: 60
}); });
await this._explosionManager.initialize(); await this._explosionManager.initialize();
@ -117,24 +117,24 @@ export class RockFactory {
body.setLinearDamping(0) body.setLinearDamping(0)
body.setMotionType(PhysicsMotionType.DYNAMIC); body.setMotionType(PhysicsMotionType.DYNAMIC);
body.setCollisionCallbackEnabled(true); body.setCollisionCallbackEnabled(true);
let scaling = Vector3.One();
body.getCollisionObservable().add((eventData) => { body.getCollisionObservable().add((eventData) => {
if (eventData.type == 'COLLISION_STARTED') { if (eventData.type == 'COLLISION_STARTED') {
if ( eventData.collidedAgainst.transformNode.id == 'ammo') { if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"}); score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
const position = eventData.point;
// Get the asteroid mesh before disposing
const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
// Play explosion using ExplosionManager (clones mesh internally)
RockFactory._explosionManager.playExplosion(asteroidMesh);
// Now dispose the physics objects and original mesh
eventData.collider.shape.dispose(); eventData.collider.shape.dispose();
eventData.collider.transformNode.dispose(); eventData.collider.transformNode.dispose();
eventData.collider.dispose(); eventData.collider.dispose();
scaling = eventData.collider.transformNode.scaling.clone();
console.log(scaling);
eventData.collidedAgainst.shape.dispose(); eventData.collidedAgainst.shape.dispose();
eventData.collidedAgainst.transformNode.dispose(); eventData.collidedAgainst.transformNode.dispose();
eventData.collidedAgainst.dispose(); eventData.collidedAgainst.dispose();
// Play explosion using ExplosionManager
RockFactory._explosionManager.playExplosion(position, scaling);
} }
} }
}); });