Refactor explosion system to use MeshExploder with animated debris
Some checks failed
Build / build (push) Failing after 19s
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:
parent
bf5d33e1cb
commit
1a381ec47d
@ -1,37 +1,38 @@
|
||||
import {
|
||||
MeshBuilder,
|
||||
ParticleHelper,
|
||||
ParticleSystem,
|
||||
ParticleSystemSet,
|
||||
AbstractMesh,
|
||||
Animation, Color3,
|
||||
Mesh, MeshBuilder,
|
||||
MeshExploder,
|
||||
Scene,
|
||||
Vector3
|
||||
Vector3,
|
||||
VertexData
|
||||
} from "@babylonjs/core";
|
||||
import {DefaultScene} from "./defaultScene";
|
||||
|
||||
/**
|
||||
* Configuration for explosion effects
|
||||
*/
|
||||
export interface ExplosionConfig {
|
||||
/** Size of the explosion pool */
|
||||
poolSize?: number;
|
||||
/** Duration of explosion in milliseconds */
|
||||
duration?: number;
|
||||
/** Rendering group ID for particles */
|
||||
renderingGroupId?: number;
|
||||
/** Maximum explosion force (how far pieces spread) */
|
||||
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 {
|
||||
private explosionPool: ParticleSystemSet[] = [];
|
||||
private scene: Scene;
|
||||
private config: Required<ExplosionConfig>;
|
||||
|
||||
// Default configuration
|
||||
private static readonly DEFAULT_CONFIG: Required<ExplosionConfig> = {
|
||||
poolSize: 10,
|
||||
duration: 2000,
|
||||
renderingGroupId: 1
|
||||
duration: 1000,
|
||||
explosionForce: 5,
|
||||
frameRate: 60
|
||||
};
|
||||
|
||||
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> {
|
||||
console.log(`Pre-creating ${this.config.poolSize} explosion particle systems...`);
|
||||
|
||||
for (let i = 0; i < this.config.poolSize; i++) {
|
||||
const set = await ParticleHelper.CreateAsync("explosion", this.scene);
|
||||
set.systems.forEach((system) => {
|
||||
system.renderingGroupId = this.config.renderingGroupId;
|
||||
});
|
||||
this.explosionPool.push(set);
|
||||
}
|
||||
|
||||
console.log(`Created ${this.config.poolSize} explosion particle systems in pool`);
|
||||
console.log("ExplosionManager initialized with MeshExploder");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an explosion from the pool
|
||||
* Create sphere debris pieces for explosion
|
||||
* MeshExploder requires an array of separate meshes
|
||||
* @param mesh The mesh to explode (used for position/scale)
|
||||
* @param pieces Number of pieces to create
|
||||
* @returns Array of sphere mesh objects
|
||||
*/
|
||||
private getExplosionFromPool(): ParticleSystemSet | null {
|
||||
return this.explosionPool.pop() || null;
|
||||
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();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an explosion to the pool after use
|
||||
* Explode a mesh by breaking it into pieces and animating them outward
|
||||
* @param mesh The mesh to explode (will be cloned internally)
|
||||
*/
|
||||
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);
|
||||
});
|
||||
public playExplosion(mesh: AbstractMesh): void {
|
||||
// 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;
|
||||
} else {
|
||||
// Use pooled explosion
|
||||
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);
|
||||
|
||||
// Set emitter and start each system individually
|
||||
explosion.systems.forEach((system: ParticleSystem, idx: number) => {
|
||||
system.emitter = point;
|
||||
system.start();
|
||||
console.log(` System ${idx}: emitter set to`, system.emitter, "activeCount=", system.getActiveCount());
|
||||
});
|
||||
|
||||
// Stop and return to pool after duration
|
||||
setTimeout(() => {
|
||||
explosion.systems.forEach((system: ParticleSystem) => {
|
||||
system.stop();
|
||||
});
|
||||
this.returnExplosionToPool(explosion);
|
||||
point.dispose();
|
||||
}, this.config.duration);
|
||||
sourceMesh = mesh as Mesh;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Apply the instance's transformation to the cloned mesh
|
||||
meshToExplode.position = mesh.getAbsolutePosition().clone();
|
||||
meshToExplode.rotation = mesh.rotation.clone();
|
||||
meshToExplode.scaling = mesh.scaling.clone();
|
||||
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);
|
||||
});
|
||||
|
||||
// Continue animation if not complete
|
||||
if (progress < 1.0) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
// Animation complete - clean up
|
||||
console.log(`Explosion animation complete, cleaning up`);
|
||||
this.cleanupExplosion(meshPieces);
|
||||
}
|
||||
};
|
||||
|
||||
// Start the animation
|
||||
animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of available explosions in the pool
|
||||
* Clean up explosion meshes
|
||||
*/
|
||||
public getPoolSize(): number {
|
||||
return this.explosionPool.length;
|
||||
private cleanupExplosion(meshPieces: Mesh[]): void {
|
||||
// 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 {
|
||||
this.explosionPool.forEach(explosion => {
|
||||
explosion.dispose();
|
||||
});
|
||||
this.explosionPool = [];
|
||||
// Nothing to dispose with MeshExploder approach
|
||||
console.log("ExplosionManager disposed");
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,9 +45,9 @@ export class RockFactory {
|
||||
public static async init() {
|
||||
// Initialize explosion manager
|
||||
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
||||
poolSize: 10,
|
||||
duration: 2000,
|
||||
renderingGroupId: 1
|
||||
duration: 500,
|
||||
explosionForce: 15.0,
|
||||
frameRate: 60
|
||||
});
|
||||
await this._explosionManager.initialize();
|
||||
|
||||
@ -117,24 +117,24 @@ export class RockFactory {
|
||||
body.setLinearDamping(0)
|
||||
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||
body.setCollisionCallbackEnabled(true);
|
||||
let scaling = Vector3.One();
|
||||
body.getCollisionObservable().add((eventData) => {
|
||||
if (eventData.type == 'COLLISION_STARTED') {
|
||||
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
||||
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.transformNode.dispose();
|
||||
eventData.collider.dispose();
|
||||
scaling = eventData.collider.transformNode.scaling.clone();
|
||||
console.log(scaling);
|
||||
eventData.collidedAgainst.shape.dispose();
|
||||
eventData.collidedAgainst.transformNode.dispose();
|
||||
eventData.collidedAgainst.dispose();
|
||||
|
||||
// Play explosion using ExplosionManager
|
||||
RockFactory._explosionManager.playExplosion(position, scaling);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user