Utility class that loads two dev models (box and sphere) and creates tinted ModelRenderer GameObjects under a given parent. It provides factory methods to make spheres and boxes, and helpers to resize spheres by adjusting GameObject local scale based on model bounds.
namespace Coilgarden;
/// <summary>
/// Builds tinted primitive GameObjects under one parent, and remembers the models so each
/// view does not load them again.
/// <para>
/// The whole game is made of two shapes: a box and a sphere. Everything else is scale,
/// colour and lighting. That is the constraint the art direction is built around rather than
/// worked against - soft forms come from sphere shading and a warm key light, not from
/// modelled detail.
/// </para>
/// <para>
/// Pieces use the standard <b>lit</b> material with no override. Lighting is what turns a
/// sphere into a soft rounded form, and an unlit fill would throw that away for a flatness
/// nothing here wants. It does mean a tint renders slightly darker than authored, which the
/// palette already accounts for.
/// </para>
/// </summary>
public sealed class Primitives
{
private const string BoxModelPath = "models/dev/box.vmdl";
private const string SphereModelPath = "models/dev/sphere.vmdl";
private readonly GameObject root;
private readonly Model box;
private readonly Model sphere;
public Primitives( GameObject root )
{
this.root = root;
box = Model.Load( BoxModelPath );
sphere = Model.Load( SphereModelPath );
}
/// <summary>A sphere of the given diameter, centred on <paramref name="position"/>.</summary>
public ModelRenderer Sphere( string name, Vector3 position, float diameter, Color tint ) =>
Create( name, sphere, position, new Vector3( diameter, diameter, diameter ), tint );
/// <summary>A box of the given size, centred on <paramref name="position"/>.</summary>
public ModelRenderer Box( string name, Vector3 position, Vector3 size, Color tint ) =>
Create( name, box, position, size, tint );
/// <summary>Rescales an existing sphere in place, for anything that changes size per frame.</summary>
public void Resize( ModelRenderer renderer, float diameter )
{
if ( !renderer.IsValid() ) return;
renderer.GameObject.LocalScale = ScaleFor( sphere, new Vector3( diameter, diameter, diameter ) );
}
/// <summary>
/// Rescales a sphere to different diameters per axis, turning it into an ellipsoid. This is
/// how squash and stretch is done here - there is no deformation, just a non-uniform scale
/// on a ball.
/// </summary>
public void Resize( ModelRenderer renderer, Vector3 diameters )
{
if ( !renderer.IsValid() ) return;
renderer.GameObject.LocalScale = ScaleFor( sphere, diameters );
}
private ModelRenderer Create( string name, Model model, Vector3 position, Vector3 size, Color tint )
{
var obj = new GameObject( root, true, name );
// Runtime visuals must never be written into the scene file.
obj.Flags |= GameObjectFlags.NotSaved;
obj.LocalPosition = position;
obj.LocalScale = ScaleFor( model, size );
var renderer = obj.Components.Create<ModelRenderer>();
renderer.Model = model;
renderer.Tint = tint;
return renderer;
}
/// <summary>The dev models are whatever size they are, so scale them to the size asked for.</summary>
private static Vector3 ScaleFor( Model model, Vector3 size )
{
var bounds = model.Bounds.Size;
return new Vector3(
bounds.x > 0f ? size.x / bounds.x : 1f,
bounds.y > 0f ? size.y / bounds.y : 1f,
bounds.z > 0f ? size.z / bounds.z : 1f );
}
}