Voxel/VoidGrid.cs
namespace Monolith;
/// <summary>
/// A wireframe floor under the shape.
///
/// The void gives the eye nothing to hold on to: with a handful of cubes left you cannot tell
/// where you are, which way you are facing, or how far you have drifted. Devil Daggers solves
/// exactly this with a visible floor plane, and the same trick works here. It is geometry
/// rather than a texture so it needs no art.
/// </summary>
public sealed class VoidGrid : Component
{
[Property] public Color LineColor { get; set; } = new( 0.55f, 0.18f, 0.08f );
/// <summary>Grid lines per axis. Kept modest: this is a reference, not scenery.</summary>
[Property] public int Divisions { get; set; } = 32;
private GameObject gridObject;
private float builtForExtent = -1f;
/// <summary>Half-width of the arena, matching the walls the player is contained by.</summary>
public float HalfExtent => MonolithManager.Instance.IsValid()
? MonolithManager.Instance.ArenaHalfExtent
: Tuning.ArenaMinHalfExtent;
/// <summary>Sizes and positions the grid to sit beneath the given bounds.</summary>
public void FitTo( BBox bounds )
{
// Scales with the shape rather than sitting at a constant. It used to be deliberately
// fixed, on the reasoning that a floor which changes size feels unstable. That was fine
// while every stage fitted inside it, and wrong for the shared Monolith, whose barriers
// orbited outside the walls entirely. The floor has to agree with what contains you.
float extent = HalfExtent * 2f;
if ( gridObject.IsValid() && MathF.Abs( extent - builtForExtent ) < 1f )
{
PlaceUnder( bounds );
return;
}
builtForExtent = extent;
gridObject?.Destroy();
gridObject = new GameObject( true, "Void Grid" );
gridObject.NetworkMode = NetworkMode.Never;
gridObject.Parent = GameObject;
var renderer = gridObject.AddComponent<ModelRenderer>();
renderer.Model = BuildGrid( extent, Divisions );
renderer.Tint = LineColor;
PlaceUnder( bounds );
}
private void PlaceUnder( BBox bounds )
{
if ( !gridObject.IsValid() ) return;
// Exactly at the shape's base: this is the arena floor you actually stand on, so it
// must line up with PlayerMovement.FloorZ rather than float somewhere below.
gridObject.WorldPosition = new Vector3(
bounds.Center.x,
bounds.Center.y,
bounds.Mins.z );
}
protected override void OnDestroy()
{
gridObject?.Destroy();
gridObject = null;
}
/// <summary>
/// Builds the grid as thin flat quads rather than lines, because a quad is guaranteed to
/// render with any standard material while line primitives are not.
/// </summary>
private static Model BuildGrid( float extent, int divisions )
{
var vb = new VertexBuffer();
vb.Init( true );
float half = extent * 0.5f;
float step = extent / divisions;
float width = MathF.Max( 1.5f, extent * 0.0009f );
int index = 0;
var up = Vector3.Up;
for ( int i = 0; i <= divisions; i++ )
{
float offset = -half + i * step;
// Line running along Y.
AddQuad( vb, ref index,
new Vector3( offset - width, -half, 0 ),
new Vector3( offset + width, -half, 0 ),
new Vector3( offset + width, half, 0 ),
new Vector3( offset - width, half, 0 ), up );
// Line running along X.
AddQuad( vb, ref index,
new Vector3( -half, offset - width, 0 ),
new Vector3( half, offset - width, 0 ),
new Vector3( half, offset + width, 0 ),
new Vector3( -half, offset + width, 0 ), up );
}
// Containing walls: the same grid lines turned up at each edge so the arena reads as a
// room rather than an infinite plane you might wander off.
float wall = Tuning.ArenaWallHeight;
float vStep = wall / 6f;
for ( int i = 0; i <= divisions; i++ )
{
float offset = -half + i * step;
AddQuad( vb, ref index,
new Vector3( offset - width, -half, 0 ), new Vector3( offset + width, -half, 0 ),
new Vector3( offset + width, -half, wall ), new Vector3( offset - width, -half, wall ),
Vector3.Forward );
AddQuad( vb, ref index,
new Vector3( offset - width, half, 0 ), new Vector3( offset + width, half, 0 ),
new Vector3( offset + width, half, wall ), new Vector3( offset - width, half, wall ),
Vector3.Backward );
AddQuad( vb, ref index,
new Vector3( -half, offset - width, 0 ), new Vector3( -half, offset + width, 0 ),
new Vector3( -half, offset + width, wall ), new Vector3( -half, offset - width, wall ),
Vector3.Right );
AddQuad( vb, ref index,
new Vector3( half, offset - width, 0 ), new Vector3( half, offset + width, 0 ),
new Vector3( half, offset + width, wall ), new Vector3( half, offset - width, wall ),
Vector3.Left );
}
// Horizontal bands up the walls, so height is readable while airborne.
for ( int i = 1; i <= 6; i++ )
{
float z = i * vStep;
AddQuad( vb, ref index,
new Vector3( -half, -half, z - width ), new Vector3( half, -half, z - width ),
new Vector3( half, -half, z + width ), new Vector3( -half, -half, z + width ),
Vector3.Forward );
AddQuad( vb, ref index,
new Vector3( -half, half, z - width ), new Vector3( half, half, z - width ),
new Vector3( half, half, z + width ), new Vector3( -half, half, z + width ),
Vector3.Backward );
AddQuad( vb, ref index,
new Vector3( -half, -half, z - width ), new Vector3( -half, half, z - width ),
new Vector3( -half, half, z + width ), new Vector3( -half, -half, z + width ),
Vector3.Right );
AddQuad( vb, ref index,
new Vector3( half, -half, z - width ), new Vector3( half, half, z - width ),
new Vector3( half, half, z + width ), new Vector3( half, -half, z + width ),
Vector3.Left );
}
var mesh = new Mesh( Material.Load( "materials/default.vmat" ) );
mesh.CreateBuffers( vb );
return new ModelBuilder().AddMesh( mesh ).Create();
}
private static void AddQuad( VertexBuffer vb, ref int index,
Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal )
{
var tangent = (p1 - p0).Normal;
vb.Add( new Vertex( p0, normal, tangent, new Vector4( 0, 0, 0, 0 ) ) );
vb.Add( new Vertex( p1, normal, tangent, new Vector4( 1, 0, 0, 0 ) ) );
vb.Add( new Vertex( p2, normal, tangent, new Vector4( 1, 1, 0, 0 ) ) );
vb.Add( new Vertex( p3, normal, tangent, 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;
}
}