Table of Contents

Entity Text (Screen-Space)

A gallery of everything EntityTextComponent can do, one feature per pole: anchoring, shadows, backgrounds, scaling, rotation, opacity, distance fading, several texts on one entity, and HUD text pinned to window corners that survives resizing. Screen-space text keeps its pixel size at any distance and is never hidden by geometry.

The Program.cs file shows how to:

  • Registering the text renderer once: AddEntityTextRenderer
  • Centring a label over an object with TextAnchor, not TextAlignment
  • Shadow and background for readability over a 3D scene
  • Animating Scale instead of FontSize
  • Distance fading with FadeStartDistance and MaxDistance
  • Several EntityTextComponents on one entity
  • HUD text that survives window resizing: TextPositionMode.Anchored

Entity Text (Screen-Space)

View on GitHub.

using Stride.CommunityToolkit.Bepu;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Rendering.Text;
using Stride.CommunityToolkit.Skyboxes;
using Stride.CommunityToolkit.Windows;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Graphics;

// A gallery of everything EntityTextComponent can do. Each pole on the ground carries a label whose
// text describes the settings that produced it, so the scene is its own documentation.
//
// EntityTextComponent is SCREEN-SPACE text: the entity's position is projected to a point on the
// screen and flat pixels are drawn there. The text is always the same size however far away the
// entity is, and it is never hidden by geometry. For text that lives inside the scene - shrinking
// with distance, blocked by walls - see E03_3D_WorldText.

// Before the window exists: a sharp window on a scaled display, and every label then sizes itself -
// this is screen-space text, so a 150% display draws it 1.5x, rasterised at that size rather than stretched
WindowsDpiManager.EnablePerMonitorV2();

using var game = new Game();

game.Run(start: Start);

void Start(Scene scene)
{
    game.Window.AllowUserResizing = true;
    //DisplayScale.GetOrCreate(game).Override = 1f;
    game.SetupBase3DScene();
    game.AddSkybox();

    // One call, however much text the scene has. Without the renderer, components collect quietly
    // and nothing appears - there is no error for a missing renderer.
    game.AddEntityTextRenderer();

    // --- Row 1: placement ----------------------------------------------------------------------

    AddStation(scene, new Vector3(-4.5f, 0, -2), "Plain label\n(all defaults)", station => { });

    AddStation(scene, new Vector3(-1.5f, 0, -2), "Anchor = BottomCenter\nOffset = (0, -12)", station =>
    {
        // The label floats centred above its pole. This pairing is what "put a name over a thing"
        // means, and it is Anchor doing the centring - not Alignment, which only arranges the lines
        // of a multi-line block relative to each other.
        station.Anchor = TextAnchor.BottomCenter;
        station.Offset = new Vector2(0, -12);
    });

    AddStation(scene, new Vector3(1.5f, 0, -2), "FontSize = 26", station =>
    {
        station.FontSize = 26;
    });

    AddStation(scene, new Vector3(4.5f, 0, -2), "Scale = 1.6\n(cheaper to animate\nthan FontSize)", station =>
    {
        // Scale multiplies the drawn size without re-rasterising glyphs, which is why a score punch
        // animates Scale and leaves FontSize alone
        station.Scale = 1.6f;
    });

    // --- Row 2: readability --------------------------------------------------------------------

    AddStation(scene, new Vector3(-4.5f, 0, 1), "EnableShadow = true", station =>
    {
        station.EnableShadow = true;
    });

    AddStation(scene, new Vector3(-1.5f, 0, 1), "EnableBackground = true", station =>
    {
        station.EnableBackground = true;
    });

    AddStation(scene, new Vector3(1.5f, 0, 1), "TextColor = Orange\nOpacity = 0.55", station =>
    {
        station.TextColor = Color.Orange;
        station.Opacity = 0.55f;
    });

    AddStation(scene, new Vector3(4.5f, 0, 1), "Rotation = 0.35 rad", station =>
    {
        station.Rotation = 0.35f;
    });

    // --- Row 3: distance -----------------------------------------------------------------------

    AddStation(scene, new Vector3(-4.5f, 0, 4), "FadeStartDistance = 8\nMaxDistance = 14\n(walk backwards!)", station =>
    {
        // World-positioned text can fade with camera distance; past MaxDistance it stops drawing
        station.EnableShadow = true;
        station.FadeStartDistance = 8;
        station.MaxDistance = 14;
    });

    // Two components on one entity: a name and a subtitle. LayerDepth decides who wins overlaps.
    var titled = AddStation(scene, new Vector3(-1.5f, 0, 4), "Two components,\none entity", station =>
    {
        station.Anchor = TextAnchor.BottomCenter;
        station.Offset = new Vector2(0, -26);
        station.EnableShadow = true;
    });

    titled.Add(new EntityTextComponent
    {
        Text = "(the subtitle)",
        FontSize = 12,
        TextColor = Color.LightGray,
        Anchor = TextAnchor.BottomCenter,
        Offset = new Vector2(0, -12),
        EnableShadow = true,
    });

    // --- HUD: not attached to anything in the scene --------------------------------------------

    // Anchored mode snaps to a window corner and survives resizing; the entity's own position is
    // ignored entirely, so a HUD entity can sit anywhere
    var hud = new Entity("Hud");

    hud.Add(new EntityTextComponent
    {
        Text = "PositionMode = Anchored, TopLeft",
        PositionMode = TextPositionMode.Anchored,
        ScreenAnchor = DisplayPosition.TopLeft,
        Offset = new Vector2(16, 16),
        EnableShadow = true,
    });

    hud.Add(new EntityTextComponent
    {
        Text = "Anchored, BottomRight\n(resize the window - I stay put)",
        PositionMode = TextPositionMode.Anchored,
        ScreenAnchor = DisplayPosition.BottomRight,
        Offset = new Vector2(16, 40),
        EnableShadow = true,
    });

    hud.Add(new EntityTextComponent
    {
        Text = "PositionMode = Screen at (16, 300)",
        PositionMode = TextPositionMode.Screen,
        ScreenPosition = new Vector2(16, 300),
        TextColor = Color.LightGreen,
        EnableShadow = true,
    });

    hud.Scene = scene;
}

// Creates one gallery station: a pole with a label above it, then hands the label over for the
// station's own settings. Returns the pole so a station can add more components to it.
Entity AddStation(Scene scene, Vector3 position, string text, Action<EntityTextComponent> configure)
{
    var pole = game.Create3DPrimitive(PrimitiveModelType.Cylinder, new Primitive3DEntityOptions
    {
        EntityName = "Pole",
        Size = new Vector3(0.15f, 1f, 0.15f),
    });

    pole.Transform.Position = position + new Vector3(0, 0.5f, 0);

    var label = new EntityTextComponent
    {
        Text = text,
        FontSize = 14,
        Alignment = TextAlignment.Center,
    };

    configure(label);
    pole.Add(label);

    pole.Scene = scene;

    return pole;
}