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 {
|
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);
|
|
||||||
set.systems.forEach((system) => {
|
|
||||||
system.renderingGroupId = this.config.renderingGroupId;
|
|
||||||
});
|
|
||||||
this.explosionPool.push(set);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Created ${this.config.poolSize} explosion particle systems in pool`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
private splitIntoSeparateMeshes(mesh: Mesh, pieces: number = 32): Mesh[] {
|
||||||
return this.explosionPool.pop() || null;
|
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 {
|
public playExplosion(mesh: AbstractMesh): void {
|
||||||
explosion.dispose();
|
// Get the source mesh if this is an instanced mesh
|
||||||
ParticleHelper.CreateAsync("explosion", this.scene).then((set) => {
|
let sourceMesh: Mesh;
|
||||||
set.systems.forEach((system) => {
|
if ((mesh as any).sourceMesh) {
|
||||||
system.renderingGroupId = this.config.renderingGroupId;
|
sourceMesh = (mesh as any).sourceMesh as Mesh;
|
||||||
});
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
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 = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user