The file provides static methods to construct world-space UI elements and a component to manage their transient animation. It contains factory methods for creating printed labels, layered cardboard markers, and rising feedback effects.
namespace CitySim.Ui.DesignSystem.World;
/// <summary>Local-only, disposable world-space presentation pieces. The caller owns their lifetime.</summary>
public static class WorldPieces
{
public static readonly Color Ink = new(41f / 255, 58f / 255, 59f / 255);
public static readonly Color Paper = new(242f / 255, 232f / 255, 211f / 255);
public static readonly Color Mustard = new(213f / 255, 168f / 255, 61f / 255);
public static readonly Color Board = new(0.48f, 0.36f, 0.24f);
/// <summary>Create a camera-facing printed label. Position it in world or local space after creation.</summary>
public static GameObject Label(GameObject parent, string value, float scale = 0.04f)
{
var obj = Child(parent, "World label");
var text = obj.Components.Create<TextRenderer>();
text.Text = GameText.Plain(value);
text.Scale = scale;
text.FontSize = 48;
text.FontWeight = 700;
text.HorizontalAlignment = TextRenderer.HAlignment.Center;
text.Billboard = TextRenderer.BillboardMode.Always;
text.BlendMode = BlendMode.Normal;
text.FogStrength = 0;
text.RenderOptions.Overlay = false;
text.RenderOptions.AfterUI = false;
text.Color = Paper;
var scope = text.TextScope;
scope.Outline = new TextRendering.Outline { Enabled = true, Size = 3, Color = Ink };
text.TextScope = scope;
return obj;
}
/// <summary>A flat, layered cardboard marker. The shape provides a cue independent of its accent colour.</summary>
public static GameObject Marker(GameObject parent, float size = 48f, Color? accent = null)
{
var obj = Child(parent, "World marker");
// Each layer uses a white mesh and renderer tint: board_flat supports tint,
// while the simple material does not reliably display vertex colours.
void Layer(string name, Vector3 center, Vector3 dimensions, Color color)
{
var vertices = new VertexBuffer();
vertices.Init(true);
vertices.AddCube(center, dimensions, Rotation.Identity, Color.White);
var mesh = new Mesh(Material.Load("materials/board/board_flat.vmat"));
mesh.CreateBuffers(vertices);
var layer = Child(obj, name);
layer.LocalPosition = Vector3.Zero;
var renderer = layer.Components.Create<ModelRenderer>();
renderer.Model = new ModelBuilder().AddMesh(mesh).Create();
renderer.Tint = color;
}
Layer("Chipboard", new Vector3(0, 0, 0.8f), new Vector3(size, size, 1.6f), Board);
Layer("Ink edge", new Vector3(0, 0, 1.8f), new Vector3(size, size, 0.5f), Ink);
Layer("Printed face", new Vector3(0, 0, 2.2f), new Vector3(size * 0.86f, size * 0.86f, 0.4f), accent ?? Mustard);
Layer("Cross horizontal", new Vector3(0, 0, 2.5f), new Vector3(size * 0.48f, size * 0.1f, 0.25f), Ink);
Layer("Cross vertical", new Vector3(0, 0, 2.5f), new Vector3(size * 0.1f, size * 0.48f, 0.25f), Ink);
return obj;
}
/// <summary>Create rising, fading feedback that destroys itself after its duration.</summary>
public static GameObject Feedback(GameObject parent, string value, Vector3 worldPosition, float duration = 1.4f, float scale = 0.04f, bool pop = false)
{
var obj = Label(parent, value, scale);
obj.Name = "World feedback";
obj.WorldPosition = worldPosition;
var lifetime = obj.Components.Create<WorldFeedbackLifetime>();
lifetime.Duration = Math.Max(0.01f, duration);
lifetime.StartPosition = worldPosition;
lifetime.BaseScale = scale;
lifetime.Pop = pop;
return obj;
}
static GameObject Child(GameObject parent, string name)
{
return new GameObject(true, name) { Parent = parent, NetworkMode = NetworkMode.Never };
}
}
/// <summary>Animation and cleanup for a transient world label.</summary>
public sealed class WorldFeedbackLifetime : Component
{
public Vector3 StartPosition { get; set; }
public float Duration { get; set; } = 1.4f;
public float BaseScale { get; set; } = 0.04f;
public bool Pop { get; set; }
float age;
protected override void OnUpdate()
{
age += Time.Delta;
if (age >= Duration) { GameObject.Destroy(); return; }
var t = Math.Clamp(age / Math.Max(0.01f, Duration), 0, 1);
WorldPosition = StartPosition + Vector3.Up * (12 * (1 - MathF.Pow(1 - t, 3)));
var text = Components.Get<TextRenderer>();
if (!text.IsValid()) return;
if (Pop)
{
var p = Math.Clamp(age / 0.24f, 0, 1) - 1;
var ease = 1 + 2.70158f * p * p * p + 1.70158f * p * p;
var exit = Math.Clamp((Duration - age) / 0.22f, 0, 1);
text.Scale = BaseScale * (0.65f + 0.35f * ease) * (exit * exit * (3 - 2 * exit));
}
else text.Color = new Color(WorldPieces.Paper.r, WorldPieces.Paper.g, WorldPieces.Paper.b, Math.Clamp((Duration - age) * 3, 0, 1));
}
}