Game/Debris.cs
namespace Monolith;
/// <summary>
/// The cubes that just got destroyed, thrown outward in a hot colour so a blast reads as
/// something violent happening to the rock rather than voxels quietly vanishing.
///
/// Shares one procedural cube Model across every piece, so a burst costs a handful of
/// GameObjects and no mesh building.
/// </summary>
public sealed class Debris : Component
{
private static Model cubeModel;
private static int liveCount;
/// <summary>Hard ceiling: at high fire rates this would otherwise flood the scene.</summary>
private const int MaxLive = 160;
private Vector3 velocity;
private Vector3 spin;
private float lifetime;
private float startScale;
private GameTimeSince timeSinceSpawn;
private ModelRenderer renderer;
private Color hot;
/// <summary>Spawns a burst at a blast site. Count and spread scale with the blast.</summary>
public static void Burst( Scene scene, Vector3 position, float worldRadius, bool isCharge )
{
if ( scene == null ) return;
int count = Math.Clamp( (int)(worldRadius / 22f), 2, isCharge ? 14 : 6 );
for ( int i = 0; i < count; i++ )
{
if ( liveCount >= MaxLive ) return;
var obj = new GameObject( true, "Debris" );
obj.NetworkMode = NetworkMode.Never;
// Start scattered through the blast volume, not all from one point.
obj.WorldPosition = position + Vector3.Random.Normal * (worldRadius * Game.Random.Float( 0.15f, 0.75f ));
obj.WorldRotation = Rotation.Random;
float scale = Tuning.VoxelSize * Game.Random.Float( 0.35f, 0.9f );
obj.WorldScale = scale;
var piece = obj.AddComponent<Debris>();
piece.startScale = scale;
piece.velocity = Vector3.Random.Normal * (worldRadius * Game.Random.Float( 1.6f, 4.2f ));
piece.spin = Vector3.Random.Normal * Game.Random.Float( 120f, 520f );
piece.lifetime = Game.Random.Float( 0.5f, 1.1f );
// Charges throw white-hot, ordinary shots ember-orange.
piece.hot = isCharge
? Color.Lerp( new Color( 1f, 0.95f, 0.65f ), new Color( 1f, 0.55f, 0.12f ), Game.Random.Float() )
: Color.Lerp( new Color( 1f, 0.62f, 0.18f ), new Color( 0.85f, 0.22f, 0.06f ), Game.Random.Float() );
var mr = obj.AddComponent<ModelRenderer>();
mr.Model = GetCubeModel();
mr.Tint = piece.hot;
piece.renderer = mr;
liveCount++;
}
}
protected override void OnStart()
{
timeSinceSpawn = 0;
}
protected override void OnDestroy()
{
liveCount = Math.Max( 0, liveCount - 1 );
}
protected override void OnUpdate()
{
// Frozen while a blocking screen or the pause menu is up. See GameTime.
if ( GameTime.Paused )
return;
float t = timeSinceSpawn / lifetime;
if ( t >= 1f )
{
GameObject.Destroy();
return;
}
WorldPosition += velocity * Time.Delta;
WorldRotation *= Rotation.From( spin.x * Time.Delta, spin.y * Time.Delta, spin.z * Time.Delta );
// Drag, so pieces decelerate instead of sailing off forever.
velocity = velocity.LerpTo( Vector3.Zero, 2.2f * Time.Delta );
// Cool from hot to dark as it shrinks away.
WorldScale = startScale * (1f - t);
if ( renderer.IsValid() )
renderer.Tint = Color.Lerp( hot, new Color( 0.15f, 0.05f, 0.03f ), t * t );
}
/// <summary>One unit cube, built once and shared by every piece of debris.</summary>
private static Model GetCubeModel()
{
if ( cubeModel != null )
return cubeModel;
var vb = new VertexBuffer();
vb.Init( true );
// 6 faces of a unit cube centred on the origin.
Vector3[] normals =
{
Vector3.Forward, Vector3.Backward, Vector3.Left,
Vector3.Right, Vector3.Up, Vector3.Down,
};
int index = 0;
foreach ( var n in normals )
{
// Build an orthonormal basis for this face.
var tangent = MathF.Abs( n.z ) > 0.9f ? Vector3.Forward : Vector3.Up;
var u = Vector3.Cross( n, tangent ).Normal;
var v = Vector3.Cross( n, u ).Normal;
var centre = n * 0.5f;
var p0 = centre - u * 0.5f - v * 0.5f;
var p1 = centre + u * 0.5f - v * 0.5f;
var p2 = centre + u * 0.5f + v * 0.5f;
var p3 = centre - u * 0.5f + v * 0.5f;
vb.Add( new Vertex( p0, n, u, new Vector4( 0, 0, 0, 0 ) ) );
vb.Add( new Vertex( p1, n, u, new Vector4( 1, 0, 0, 0 ) ) );
vb.Add( new Vertex( p2, n, u, new Vector4( 1, 1, 0, 0 ) ) );
vb.Add( new Vertex( p3, n, u, new Vector4( 0, 1, 0, 0 ) ) );
vb.AddRawIndex( index + 0 ); vb.AddRawIndex( index + 1 ); vb.AddRawIndex( index + 2 );
vb.AddRawIndex( index + 0 ); vb.AddRawIndex( index + 2 ); vb.AddRawIndex( index + 3 );
index += 4;
}
var mesh = new Mesh( Material.Load( "materials/default.vmat" ) );
mesh.CreateBuffers( vb );
cubeModel = new ModelBuilder().AddMesh( mesh ).Create();
return cubeModel;
}
}