MIT License NuGet

Home / Features / 2D, text and UI

2D, text and UI

2D is not a separate framework bolted on the side. Sprites, shapes and text are controls in the same panel tree as 3D models, drawn in the Overlay phase of the same frame, and positioned in design resolution so a layout written once holds on a phone and on a 4K monitor.

The control set

Five types cover everything the engine draws in two dimensions. They all derive from Control, which means they share the placement fields, the Enable flag, the async Load contract and the OnClick handler with every 3D control.

ControlWhat it is for
Sprite2DScreen-space images, including live effect outputs addressed by texture name.
Sprite3DWorld-anchored billboards — markers, emotes, floating icons.
ShapeDots, rectangles, circles, rounded rectangles and frames for panel chrome.
TextsMSDF text: resolution independent, scaled by Scale, optionally auto-translated.
TextureThe engine-side texture handle, for custom uploads and shared references.

Sprites. SpriteBase carries what both sprite types need: a Color tint, Ext for the file extension, OriginWidth and OriginHeight for the source pixel size, and SourceX/Y/Width/Height for drawing one cell out of a sheet. Clock advances a sprite-sheet frame, FlipX and FlipY mirror it, and TextureOverride takes a one-shot upload — a file path or already-decoded RGBA8 pixels — which the next update consumes and clears.

Showing what compute wrote. Storage textures produced by compute effects are registered under a compute:// name, and a Sprite2D can be pointed straight at one. That is the whole debug-view mechanism: no readback, no copy, no special control type.

// A normal image.
var badge = new Sprite2D { Name = "Assets/badge", Ext = ".png", PosX = 24, PosY = 24 };

// The ambient-occlusion buffer, on screen, as a sprite.
var aoView = new Sprite2D
{
    Name = Season.Rendering.Effects.GtaoEffect.TextureName,
    PosX = 20, PosY = 580, Width = 240
};

Volumes are the exception. Three-dimensional outputs use the compute3d:// prefix and live in a separate backend dictionary, so a sprite cannot display one directly — a slice kernel has to bridge it down to 2D first, which is exactly what Sdf3DViewEffect is for.

Billboards. Sprite3D lives in world space and sorts with the transparent pass. Mode chooses how it faces the camera: Spherical turns on both axes and always faces you, Cylindrical turns only around world Y so a tree or a nameplate stays upright, and None hands orientation entirely to the quaternion Rotation.

Shapes. ShapeType is Dot, Square, Circle, RoundRect, RectFrame, Gradual and GradualCircle. Circles and rounded rectangles are analytically antialiased rather than being scaled bitmaps, and Border sets the frame thickness. It is a deliberately small set: enough to build panel chrome without shipping a vector graphics engine.

Text is MSDF, only

There is one glyph pipeline, on every backend. Glyphs are rasterised into multi-channel signed distance fields at a fixed PixelRange of 4 and packed into the engine's own atlas, so text stays sharp at any scale and no per-size atlas is ever rebuilt. Scale is a free parameter, not a quality setting.

title = new Texts
{
    Content = "Settings",
    Color = Colors.DarkRed,
    Scale = Vector2.One * 1.2f
};
AddControl(title);
MemberNotes
ContentThe string. Assigning it triggers layout; ContentOrigin keeps the untranslated original.
TranslateOn by default. Runs the string through Localization before layout.
ScaleVector2, so text can be stretched on one axis. Costs nothing in sharpness.
LineHeightLine advance in design units. 40 by default.
WidthRequest, HeightRequestWrapping box. Leave them null and the text is a single line.
WordsSpace, EmptySpaceExtra tracking, and the width of a space in Latin runs.
TextsTypeImmediately or FadeIn, for text that appears rather than pops.
Color, AlphaPer-control colour and opacity.
Append(string)Appends a fragment and lays out only the tail. See below.

Inline colour. A small span markup is understood inside Content, which is enough to highlight one word without splitting a paragraph into three controls.

log.Content = "Build <span style='color:Red;font-weight:bold;'>failed</span> in 2.4s";

Streaming text. Append exists because token streams exist. It lays out only the new tail against a checkpoint of the layout state at the start of the current line, so an incremental append matches a full relayout pixel for pixel, including whole-word lookahead. That is what makes a language model's output land in a Texts control at token rate without rebuilding the paragraph sixty times a second.

Span markup disables incremental append

A colour scope can straddle a chunk boundary, and span indices are absolute within the whole string, so the tail cannot be parsed on its own. Once span markup appears in Content, appends fall back to full relayout until the next whole-content assignment. Streaming text and inline colour are each supported; combined, you pay for the colour.

Line breaking is script-aware: CJK breaks between characters, including the punctuation that lives outside the main unified block and must not start a line, while Latin breaks on whole words. Fonts are loaded with Font.CreateAsync(fileName, size) and several can be resident at once, with glyph metrics cached per font size and code point.

Panels

Panel is the composition unit for both 2D and 3D. It holds a list of child Panels and a list of Controls, and it is where a screen, a HUD or a whole scene is assembled. AddControl and AddPanel define load order; Layer and Order define drawing order. OnClose fires when the panel goes away, and SetMode lets a panel reconfigure itself without being torn down.

Ready-made panelWhat you get
BoardPanelA framed board with a settable FrameColor. The background for most overlays.
FrameButtonText button with separate normal and hover colours for ground and text.
ImageViewThumbnail with an optional clear affordance; FullView in the same file is the expanded form.
InputText field with description, alignment, abbreviation and an optional clear button.
MovePanelScrolling and sliding container with configurable padding, size and motion type.
Picker, SimplePickerSingle or multi-select lists over a List<EData>, with hover colours and OnSelect.
ObjectPickerNot a widget — the 3D pick and edit panel. See Picking and editing.

Every control accepts an OnClick and an OnTouch handler, and MouseOver is maintained for you. There is no separate UI event system, no bubbling model and no command pattern to learn: a button is a panel with a delegate on it.

Layers, order and the two domains

Draw order is explicit. Controls and panels that implement IRenderOrder sort by Layer, then Order, then insertion index — a stable, total order with no z-fighting between overlay elements and no surprises when a list is rebuilt.

RenderDomain decides when in the frame a control is drawn rather than where it sits in the tree.

RenderDomainMeaning
InheritFollow the parent. The default, and almost always what you want.
SceneDraw with the 3D scene, before post-processing. Affected by tonemapping, TAA and the rest of the chain.
OverlayDraw in the Overlay pass, after everything. Untouched by post-processing, which is why UI stays crisp.

Setting a 2D control to Scene is occasionally exactly right — a diegetic screen inside the world should be bloomed and graded with everything else. Setting a HUD to Scene is how you accidentally make your text ghost under TAA.

One layout, every screen

You lay out against DesignResolution, 1280×720 by default. What happens next depends on the platform, and the difference is worth knowing because it is the one place the 2D model is not identical everywhere.

PropertyDesktop and webPhones and tablets
BasicResolutionEquals DeviceResolution. You are laying out in real pixels.Equals DesignResolution. The design box is preserved.
Scale1. The window is the canvas.The smaller of the two axis ratios, so the whole design box stays visible.
ExtendResolutionEquals DeviceResolution.The design box grown along the longer axis, covering the extra space a tall phone has.
DeviceResolutionAlways the true swapchain size in pixels.

The practical rule: anchor anything that must stay reachable to BasicResolution, and stretch backgrounds and edge decoration to ExtendResolution. That gives you letterbox-free layouts on a 20:9 phone without a second design.

Resizing recomputes Scale and ExtendResolution only; BasicResolution is deliberately left alone so a window drag does not reflow a mobile layout. Orientation changes come through a separate event.

Known gaps

  • Absent No layout system. No flexbox, no constraints, no anchors, no docking. Positions are numbers you compute, which is fine for a HUD and tedious for a settings screen with forty rows.
  • Partial Text entry delegates to the platform. Input raises the native keyboard through DeviceServices.Dialog.ShowKeyboard. There is no in-engine caret, selection or IME composition, so text editing looks like the operating system rather than like your game.
  • Absent No accessibility surface. No screen reader tree, no focus ring, no keyboard navigation between controls.
  • Absent No markup or styling language. Span colour is the whole of it. No rich text document model, no stylesheets, no data binding.