BombExplosionEffect.cs
using System;
using Sandbox;
namespace Facepunch.BombRoyale;
/// <summary>
/// Spawns explosion cloud particles along a line between two points,
/// with yellow spark/ember sprites flying off.
/// </summary>
[Title( "Bomb Explosion Effect" )]
[Category( "Bomb Royale" )]
public class BombExplosionEffect : Component
{
private static readonly Gradient FireGradient = new Gradient(
new Gradient.ColorFrame( 0.000f, Color.White ),
new Gradient.ColorFrame( 0.121f, new Color( 1.0f, 0.816f, 0.0f ) ),
new Gradient.ColorFrame( 0.230f, new Color( 1.0f, 0.616f, 0.145f ) ),
new Gradient.ColorFrame( 0.437f, new Color( 1.0f, 0.231f, 0.0f ) ),
new Gradient.ColorFrame( 1.000f, new Color( 0.165f, 0.035f, 0.0f ) )
);
public static void Create( Scene scene, Vector3 startPosition, Vector3 endPosition )
{
var go = new GameObject( false, "BombExplosion" )
{
WorldPosition = startPosition
};
var effect = go.AddComponent<BombExplosionEffect>();
effect.StartPosition = startPosition;
effect.EndPosition = endPosition;
go.AddComponent<TemporaryEffect>().DestroyAfterSeconds = 3f;
go.Enabled = true;
}
private Vector3 StartPosition { get; set; }
private Vector3 EndPosition { get; set; }
private const int CloudCount = 20;
private const int SparkCount = 50;
private const float EffectLifetime = 2f;
private const float BaseScale = 0.5f;
private struct CloudParticle
{
public SceneObject SceneObject;
public float BornTime;
public float Lifetime;
public Angles SpinRate;
public Angles CurrentAngles;
public float InitialScale;
}
private struct SparkParticle
{
public SceneObject SceneObject;
public float BornTime;
public float Lifetime;
public Vector3 Velocity;
}
private CloudParticle[] _clouds;
private SparkParticle[] _sparks;
protected override void OnEnabled()
{
SpawnClouds();
SpawnSparks();
}
private void SpawnClouds()
{
_clouds = new CloudParticle[CloudCount];
var model = Model.Load( "models/particles/explosion/explosioncloud.vmdl" );
for ( var i = 0; i < CloudCount; i++ )
{
var t = (float)i / (CloudCount - 1);
var position = Vector3.Lerp( StartPosition, EndPosition, t );
var so = new SceneObject( Scene.SceneWorld, model )
{
Transform = new Transform( position, Rotation.Random, 1f ),
RenderingEnabled = true
};
_clouds[i] = new CloudParticle
{
SceneObject = so,
BornTime = Time.Now,
Lifetime = EffectLifetime,
SpinRate = new Angles(
Game.Random.Float( -30f, 30f ),
Game.Random.Float( -30f, 30f ),
Game.Random.Float( -30f, 30f )
),
CurrentAngles = new Angles(
Game.Random.Float( -360f, 360f ),
Game.Random.Float( -360f, 360f ),
Game.Random.Float( -360f, 360f )
),
InitialScale = Game.Random.Float( 0.5f, 1.0f ) * BaseScale
};
}
}
private void SpawnSparks()
{
_sparks = new SparkParticle[SparkCount];
var sparkModel = Model.Load( "models/dev/sphere.vmdl" );
for ( var i = 0; i < SparkCount; i++ )
{
var t = Game.Random.Float( 0f, 1f );
var position = Vector3.Lerp( StartPosition, EndPosition, t );
position += Vector3.Random.Normal * 12f;
var so = new SceneObject( Scene.SceneWorld, sparkModel )
{
Transform = new Transform( position, Rotation.Identity, Game.Random.Float( 0.01f, 0.03f ) ),
RenderingEnabled = true,
ColorTint = new Color( 1f, 0.85f, 0.2f )
};
_sparks[i] = new SparkParticle
{
SceneObject = so,
BornTime = Time.Now,
Lifetime = Game.Random.Float( 1f, 2f ),
Velocity = new Vector3(
Game.Random.Float( -50f, 50f ),
Game.Random.Float( -50f, 50f ),
Game.Random.Float( 20f, 80f )
)
};
}
}
protected override void OnPreRender()
{
UpdateClouds();
UpdateSparks();
}
private void UpdateClouds()
{
if ( _clouds == null ) return;
for ( var i = 0; i < _clouds.Length; i++ )
{
ref var p = ref _clouds[i];
if ( !p.SceneObject.IsValid() ) continue;
var age = Time.Now - p.BornTime;
var normalizedAge = age / p.Lifetime;
if ( normalizedAge >= 1f )
{
p.SceneObject.Delete();
continue;
}
var sizeCurve = EvaluateSizeCurve( age );
var scale = p.InitialScale * sizeCurve;
p.CurrentAngles += p.SpinRate * Time.Delta;
var color = FireGradient.Evaluate( normalizedAge );
const float fadeStart = 0.25f;
var alpha = 1f;
if ( normalizedAge > fadeStart )
alpha = 1f - ((normalizedAge - fadeStart) / (1f - fadeStart));
p.SceneObject.Transform = new Transform( p.SceneObject.Transform.Position, p.CurrentAngles.ToRotation(), scale );
p.SceneObject.ColorTint = color.WithAlpha( alpha );
}
}
private void UpdateSparks()
{
if ( _sparks == null ) return;
var dt = Time.Delta;
const float gravity = -150f;
for ( var i = 0; i < _sparks.Length; i++ )
{
ref var s = ref _sparks[i];
if ( !s.SceneObject.IsValid() ) continue;
var age = Time.Now - s.BornTime;
var normalizedAge = age / s.Lifetime;
if ( normalizedAge >= 1f )
{
s.SceneObject.Delete();
continue;
}
s.Velocity += Vector3.Up * gravity * dt;
var pos = s.SceneObject.Transform.Position + s.Velocity * dt;
var alpha = 1f - normalizedAge;
var color = Color.Lerp( new Color( 1f, 1f, 0.8f ), new Color( 1f, 0.5f, 0f ), normalizedAge );
s.SceneObject.Transform = new Transform( pos, Rotation.Identity, s.SceneObject.Transform.Scale );
s.SceneObject.ColorTint = color.WithAlpha( alpha );
}
}
private static float EvaluateSizeCurve( float age )
{
if ( age <= 0f ) return 0f;
if ( age >= EffectLifetime ) return 0f;
if ( age < 0.05f )
return (age / 0.05f) * 0.7f;
var t = (age - 0.05f) / (EffectLifetime - 0.05f);
return MathX.Lerp( 0.7f, 0f, t * t );
}
protected override void OnDisabled() => Cleanup();
protected override void OnDestroy() => Cleanup();
private void Cleanup()
{
if ( _clouds != null )
{
for ( var i = 0; i < _clouds.Length; i++ )
{
if ( _clouds[i].SceneObject.IsValid() )
_clouds[i].SceneObject.Delete();
}
_clouds = null;
}
if ( _sparks != null )
{
for ( var i = 0; i < _sparks.Length; i++ )
{
if ( _sparks[i].SceneObject.IsValid() )
_sparks[i].SceneObject.Delete();
}
_sparks = null;
}
}
}