TextModeConsole is the writer you reach for to put characters on the grid: printing with alignment and wrapping, framed panels, region fills, and grid-to-grid blitting.
Summary
TextModeConsole is the layer between your game code and the raw cell array. It is a thin writer: no controls, no layout engine, and no retained scene of things to update. You tell it to put characters at coordinates, and it puts them there.
That thinness is deliberate. A text-mode game usually already knows exactly where everything goes, because the grid is the layout, and a retained UI framework on top would be fighting the thing that makes the grid pleasant to work with. What the console does add is the handful of operations you would otherwise write yourself and get subtly wrong: word wrapping that does not break mid-word, alignment that survives a grid resize, box drawing with corners that actually join, and region fills that clear properly.
It also handles the part of cell writing that is easy to get wrong. A cell needs its glyph’s atlas coordinates alongside its codepoint, and everything the console writes is stamped with them. Reach past the console to the grid and that becomes your responsibility; see Views and Grid for what that involves.
Construct one against a view’s grid and glyph cache and keep it around. It holds no per-frame state worth pooling, but there is no reason to build a new one for every call either.
Use the drawing API for:
- Laying out a game screen: map, panels, status bars, menus.
- Printing text that has to fit a column, wrap on word boundaries, or center itself.
- Framing regions with box-drawing characters in any of five line styles.
- Filling and clearing regions, whether to reset a panel or to paint a background.
- Copying a prepared grid onto the live one, with optional alpha for fades.
Features
- Printing with control. Plain positioned printing, alignment across the grid width, and wrapping to a column budget with a measure call so you can size a panel before you draw into it.
- Word and character wrapping.
PrintWrappedbreaks on word boundaries by default and falls back to breaking mid-word for anything that cannot fit, so a long unbroken token still renders rather than vanishing. - Five box styles. Single, Double, Rounded, Heavy and ASCII, with the corner and edge characters picked so joins look right. The ASCII style exists for fonts whose atlas lacks the box-drawing block.
- Rules and fills. Horizontal and vertical lines from any codepoint, and rectangular fills with a character, colors and attributes.
- Blitting with alpha. Copy a rectangle from one grid to another, with separate foreground and background alpha, which is what a cross-fade between two prepared screens is built on.
- Background blending. Every print takes a
BackgroundBlendMode, so you can lay text over an existing background without flattening it: multiply for shadowing, add or screen for glows, alpha for translucency. - Attributes on every call. Bold, underline, inverse, blink and the rest ride along with the text rather than needing a second pass.
Coordinates and colors
The console works in screen order: x is the column, y is the row, and the origin is the top left cell. That is the opposite order from CellGrid, which indexes (row, column) because that is how the buffer is laid out. Both are right in their own layer; the mismatch only bites when you move between them.
Every drawing call takes a foreground and a background Color32, and nothing restricts you to the ANSI palette to get one. Use ANSIColor for the standard sixteen by name, ANSIColor.FromIndex for the 256-color palette, or ANSIColor.FromRGB for truecolor, and any other Color32 your game produces works just as well. See Colors and Attributes.
Calls that write off the edge of the grid are clipped rather than throwing, so a status line longer than the grid is truncated and a box that runs past the bottom is cut off.
Recipes
Lay out a game screen
Problem. You’re building a roguelike and the screen needs three regions: the map, a message log along the bottom, and a stats panel down the right. You want the frames drawn once and the regions clear so the rest of your code can just write into them.
Solution. Frame each region with DrawBox and clear the interiors. Because the console clips, you can lay regions out from constants without guarding every edge.
/* Start from a known state. Clear uses the grid's default colors, which is why setting those
on the view matters more than it looks: everything you clear falls back to them. */
_console.Clear();
/* The map frame. DrawBox takes the outer rectangle, so the usable interior is inset by one
cell on each side. Rounded corners read as softer than Single without costing legibility. */
_console.DrawBox(0, 0, MAP_W + 2, MAP_H + 2, BoxStyle.Rounded, ANSIColor.BrightBlack, ANSIColor.Black);
/* The stats panel down the right, sharing the map's top edge. */
_console.DrawBox(MAP_W + 2, 0, STATS_W, MAP_H + 2, BoxStyle.Single, ANSIColor.BrightBlack, ANSIColor.Black);
/* The log across the bottom, in Double to mark it as a different kind of surface. */
_console.DrawBox(0, MAP_H + 2, MAP_W + 2 + STATS_W, LOG_H + 2, BoxStyle.Double, ANSIColor.BrightBlack, ANSIColor.Black);
/* A title knocked into the frame's top edge. Printing over a border is how you label a panel
without spending an interior row on it. */
_console.Print(2, 0, " STATUS ", ANSIColor.Black, ANSIColor.BrightCyan);
Wrap description text into a panel
Problem. Your item tooltip has a description of unpredictable length and a fixed-width panel to fit it in. You need to know how tall the panel has to be before you draw the frame, and then wrap the text into it.
Solution. Measure with PrintWrappedHeight, draw the frame to fit, then print. Both calls use the same wrapping rules, so the measurement and the result agree.
/* Measure first. This does no drawing; it returns the number of rows the text will occupy
at the given width, so you can size the frame around it rather than guessing. */
var textWidth = PANEL_W - 2;
var textRows = _console.PrintWrappedHeight(textWidth, item.Description);
/* Frame sized to the content, plus the border and a title row. */
_console.DrawBox(panelX, panelY, PANEL_W, textRows + 3, BoxStyle.Single,
ANSIColor.BrightBlack, ANSIColor.Black);
_console.Print(panelX + 1, panelY + 1, item.Name, ANSIColor.BrightYellow, ANSIColor.Black,
BackgroundBlendMode.Set, CellAttributes.Bold);
/* PrintWrapped returns the rows it actually used, which is worth capturing when the text
might be clipped by the bottom of the grid. */
var used = _console.PrintWrapped(panelX + 1, panelY + 2, textWidth, item.Description,
ANSIColor.White, ANSIColor.Black);
Center a title that survives a resize
Problem. Your title screen is centered, and you want it to stay centered when the player resizes the window and the grid gains or loses columns.
Solution. PrintAligned computes the position from the grid width at call time, so redrawing after a resize puts it back in the middle without you tracking the width.
/* Alignment is resolved against the grid, not against a rectangle you pass in, so this is
always the true center of the display. */
_console.PrintAligned(4, "SCYLLA TEXTMODE", TextAlignment.Center,
ANSIColor.BrightWhite, ANSIColor.Black, CellAttributes.Bold);
/* Right alignment is the useful one for a status readout that should hug the edge. */
_console.PrintAligned(_view.Rows - 1, $"FLOOR {floor}", TextAlignment.Right,
ANSIColor.BrightBlack, ANSIColor.Black);
Draw a health bar
Problem. You want a bar that fills proportionally, using block characters so it reads as a solid bar rather than as a row of hashes.
Solution. Fill the filled portion and the empty portion separately. Fill takes a codepoint, so any character in the atlas works, and the block elements are what give it a solid look.
const uint FULL_BLOCK = 0x2588; /* Full block. */
const uint LIGHT_SHADE = 0x2591; /* Light shade, for the unfilled remainder. */
var filled = Mathf.RoundToInt(BAR_W * (playerHealth / (float)playerMaxHealth));
/* The filled run. Color carries the meaning here, so the bar reads at a glance even though
both halves use block characters. */
_console.Fill(BAR_X, BAR_Y, filled, 1, FULL_BLOCK, ANSIColor.BrightRed, ANSIColor.Black);
/* The remainder, dimmed so the full width of the bar stays visible. */
_console.Fill(BAR_X + filled, BAR_Y, BAR_W - filled, 1, LIGHT_SHADE,
ANSIColor.BrightBlack, ANSIColor.Black);
Lay text over an existing background
Problem. You’ve drawn a textured backdrop out of shade characters and you want to print a caption over it without punching an opaque rectangle through the texture.
Solution. Pass a blend mode. The foreground is always written, but the background composes with what is already in the cell instead of replacing it.
/* Multiply darkens whatever is behind the text, which reads as a shadow and keeps the
backdrop's shape visible through the caption. */
_console.Print(4, 10, "PRESS ENTER", ANSIColor.BrightWhite, new Color32(60, 60, 60, 255),
BackgroundBlendMode.Multiply);
/* Alpha uses the background color's alpha channel to blend, which is the one to reach for
when you want a translucent panel rather than a tint. */
_console.Print(4, 12, "SETTINGS", ANSIColor.White, new Color32(0, 0, 0, 128),
BackgroundBlendMode.Alpha);
Cross-fade between two prepared screens
Problem. You’re switching from the map view to the inventory and you want the change to fade rather than cut, without redrawing either screen every frame of the transition.
Solution. Draw each screen into its own grid once, then blit with a ramping alpha. Blit takes separate foreground and background alpha so text and its background can fade together.
/* Both screens are drawn once, into grids of their own, when their data changes. */
var inventory = new CellGrid(_view.Rows, _view.Columns, _view.DefaultForeground, _view.DefaultBackground);
DrawInventory(new TextModeConsole(inventory, _view.GlyphCache));
/* Then each frame of the transition, blit the incoming screen over the live one at a rising
alpha. Source rectangle first, destination position after. */
_console.Blit(inventory,
srcX: 0, srcY: 0, w: _view.Columns, h: _view.Rows,
dstX: 0, dstY: 0,
fgAlpha: t, bgAlpha: t);
API Sketch
It is one class, constructed against a grid and a glyph cache.
class TextModeConsole {
TextModeConsole(CellGrid grid, GlyphAtlasCache glyphCache);
TextWrapMode WrapMode; /* Reads from the active configuration unless set. */
/* --- Printing --- */
void Print(int x, int y, string text, Color32 fg, Color32 bg,
BackgroundBlendMode blend = Set, CellAttributes attributes = None);
void PrintAligned(int y, string text, TextAlignment align, Color32 fg, Color32 bg,
CellAttributes attributes = None);
int PrintWrapped(int x, int y, int width, string text, Color32 fg, Color32 bg,
CellAttributes attributes = None); /* returns rows used */
int PrintWrappedHeight(int width, string text, int x = 0); /* measures, draws nothing */
void PutChar(int x, int y, uint codepoint, Color32 fg, Color32 bg,
CellAttributes attributes = None);
/* --- Regions --- */
void Fill(int x, int y, int w, int h, uint codepoint, Color32 fg, Color32 bg,
CellAttributes attributes = None);
void Clear();
void ClearRegion(int x, int y, int w, int h);
/* --- Lines and frames --- */
void DrawBox(int x, int y, int w, int h, BoxStyle style, Color32 fg, Color32 bg);
void DrawHLine(int x, int y, int length, uint codepoint, Color32 fg, Color32 bg);
void DrawVLine(int x, int y, int length, uint codepoint, Color32 fg, Color32 bg);
/* --- Copying --- */
void Blit(CellGrid src, int srcX, int srcY, int w, int h, int dstX, int dstY,
float fgAlpha = 1f, float bgAlpha = 1f);
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Styles and modes
| Type | Values | Notes |
|---|---|---|
BoxStyle | Single, Double, Rounded, Heavy, ASCII | Single is the workhorse. Double reads as a heavier surface, useful for marking a modal or a different class of panel. Rounded softens a frame without losing weight. Heavy is for emphasis. ASCII falls back to +, - and ` |
TextAlignment | Left, Center, Right | Used by PrintAligned, resolved against the grid width at call time rather than against a rectangle. Center rounds down on an odd remainder, so a string one cell short of the width sits flush left. |
TextWrapMode | None, Character, Word | Word is the default and breaks on spaces, falling back to a mid-word break for a token too long to fit at all. Character breaks anywhere, which suits very narrow columns where word breaks would waste most of the width. None clips instead of wrapping. |
BackgroundBlendMode | Set, Multiply, Add, Screen, Overlay, Lighten, Darken, Alpha | Set replaces the background and is the default. Multiply darkens what is there, which is the natural choice for shadowing text over a backdrop. Add and Screen brighten, useful for glows and highlights. Overlay preserves contrast. Lighten and Darken take the per-channel extreme. Alpha blends using the supplied color’s alpha channel, which is what you want for a translucent panel. |
Best Practices
- Build one console per grid and keep it. It is a thin writer over the grid, so constructing one per call is harmless but noisy. Make it where you make the view.
- Redraw whole screens. Dirty-range tracking makes a full redraw cheap, and it removes a whole class of bug where a stale cell survives because your diff missed it.
- Set the grid’s default colors to match what you draw with.
ClearandClearRegionuse the grid defaults, so a mismatch shows up as a differently colored background the first time something is cleared. - Measure before you frame.
PrintWrappedHeightexists so a panel can be sized to its content. Sizing a frame first and hoping the text fits is how tooltips end up clipped. - Prefer
PrintAlignedto computing positions. It resolves against the grid at call time, so it keeps working through resizes that hand-computed positions do not survive. - Reach for
Fillrather than loopingPutChar. Same result, one call, and it is clearer about intent when you read it back later. - Use blend modes rather than pre-flattening colors. Composing at draw time keeps the backdrop intact underneath, so you can redraw the text without redrawing what is behind it.
- Remember that drawing clips instead of throwing. That makes layout code simple, but it also means a wrong coordinate produces a silently missing panel rather than an error. When something does not appear, check the coordinates before anything else.
- Keep the ASCII box style in mind for shipped builds. A font atlas built dynamically in the editor may carry glyphs a build never requests.
- Use
Blitfor prepared screens, not for animation. Effects already implement the transitions worth having, with per-cell ordering a plain alpha ramp cannot produce.
Pitfalls
Related
- 01 Scylla TextMode
- 03 TextMode – Views and Grid
- 05 TextMode – Colors and Attributes
- 08 TextMode – Text Effects
- API reference:
Scylla.TextMode.TextModeConsole - API reference:
Scylla.TextMode.BoxStyle - API reference:
Scylla.TextMode.TextAlignment - API reference:
Scylla.TextMode.TextWrapMode