From ff21dcd0a7040a69c2cc350dbce292ddda391bb1 Mon Sep 17 00:00:00 2001 From: Basil Gren Date: Sun, 21 Dec 2025 19:49:12 +0100 Subject: [PATCH] Add example with buffered output an menu --- .gitignore | 4 + Engine/Render/BuffredConsoleRenderer.cs | 42 ++++++---- Program.cs | 103 ++++++++++++++++++++++-- Tools/WindowSettings.cs | 7 ++ 4 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c06ad0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +/packages/ +.idea \ No newline at end of file diff --git a/Engine/Render/BuffredConsoleRenderer.cs b/Engine/Render/BuffredConsoleRenderer.cs index 07bea4c..7e29fd1 100644 --- a/Engine/Render/BuffredConsoleRenderer.cs +++ b/Engine/Render/BuffredConsoleRenderer.cs @@ -1,5 +1,4 @@ -using Spectre.Console; -using System.Text; +using System.Text; /// /// Tightly linked with Console. Renders contents of the buffer right on the screen. @@ -9,6 +8,12 @@ public class BufferedConsoleRenderer : IConsoleRenderer private ScreenBuffer _front = new(1, 1); private ScreenBuffer _back = new(1, 1); + /// + /// Just for easier testing. Allows to disable escape-sequences while rendering to test in old console, + /// as old console doesn't support Ansi сolors. + /// + private bool _disableColors = false; + /// /// Should be set to true when full screen should be re-rendered. For example, when both buffers are cleared. /// @@ -57,7 +62,8 @@ public void Render() { continue; } - AnsiConsole.Cursor.SetPosition(0, y); + + Console.SetCursorPosition(0, y); int? lastFg = null; int? lastBg = null; @@ -71,24 +77,30 @@ public void Render() cell = _emptyCell; } - //if (cell.Value.Color != lastFg || cell.Value.BgColor != lastBg) - //{ - // sb.Append(AnsiColor.GetCode(cell.Value.Color, cell.Value.BgColor)); - // lastFg = cell.Value.Color; - // lastBg = cell.Value.BgColor; - //} + if (!_disableColors) { + if (cell.Value.Color != lastFg || cell.Value.BgColor != lastBg) { + sb.Append(AnsiColor.GetCode(cell.Value.Color, cell.Value.BgColor)); + lastFg = cell.Value.Color; + lastBg = cell.Value.BgColor; + } + } sb.Append(cell.Value.Char); } - //sb.Append(AnsiColor.Reset); - AnsiConsole.Write(sb.ToString()); - //sb.Clear(); + if (!_disableColors) { + sb.Append(AnsiColor.Reset); + } + + // Doesn't work with `AnsiConsole` from `Spectre.Console`, so leaving as designed. + Console.Write(sb.ToString()); + sb.Clear(); } - // Swap buffers - //(_front, _back) = (_back, _front); - //_front.Clear(); + // Swap buffers and clear front buffer, so we can properly check difference with previous frame + // and re-render only changed cells. + (_front, _back) = (_back, _front); + _front.Clear(); _forceRender = false; } diff --git a/Program.cs b/Program.cs index 6103d75..1db31de 100644 --- a/Program.cs +++ b/Program.cs @@ -59,23 +59,110 @@ private static async Task Main() //arena.Start(); IConsoleRenderer renderer = new BufferedConsoleRenderer(); - renderer.SetSize(56, 18); + int screenWidth = 56; + int screenHeight = 18; + int menuWidth = 15; + int gameAreaWidth = screenWidth - menuWidth; + + renderer.SetSize(screenWidth, screenHeight + 1); // extra line to prevent scrollbar + + ScreenBuffer menuBuffer = new ScreenBuffer(menuWidth, screenHeight); + DrawUtils draw = new DrawUtils(renderer.Buffer); + DrawUtils drawMenu = new DrawUtils(menuBuffer); + draw.ResetColor(); int frame = 0; + int rectWidth = 5; + int rectHeight = 3; + + int rectX = 1; + int rectY = 1; + int speedX = 1; + int speedY = 1; + int score = 0; while (true) { frame++; - - renderer.Buffer.Set(0 + frame % 15, 0, 'B', 15, 0); - renderer.Buffer.Set(1 + frame % 15, 0, 'o', 15, 0); - renderer.Buffer.Set(2 + frame % 15, 0, 'o', 15, 0); - renderer.Buffer.Set(3 + frame % 15, 0, 'b', 15, 0); - renderer.Buffer.Set(4 + frame % 15, 0, 's', 15, 0); - + + // Нашел почему было моргание через кадр - это потому что в BufferedConsoleRenderer два + // буфера - один активный, в котором идёт отрисовка в данный момент, а второй содержит прошлый кадр. + // При отрисовке сравнивается какие символы в активном отличаются от прошлого кадра и отрисовываются + // только измененные. После отрисовки буферы меняются местами (чтобы не делать лишние копирования) и + // активный буфер полностью затирается. Но `draw` был привязан только к одному буферу из двух, + // поэтому он рисовал только в четных кадрах - когда этот буфер становился активным. + // + // Обойти это можно двумя способами: + // 1. Если очень хочется рисовать напрямую в экранном буфере, то каждый кадр нужно делать + // + // draw.SetTarget(renderer.Buffer); // Здесь отрисовка переключится на текущий активный буфер. + // + // 2. Второй способ - не рисовать напрямую в экранном буфере с помощью DrawUtils, а создать отдельный + // ScreenBuffer, рисовать в нем, а в конце скопировать нарисованное в экранный буфер - как это сделано + // с меню ниже. + // Для демонстрации здесь запилил способ 1 + способ 2 показан при отрисовке меню. + draw.SetTarget(renderer.Buffer); + + // ---=== Render Menu ===--- + + drawMenu + .DrawRect(0, 0, menuWidth, screenHeight, DrawUtils.HeavyLine, true) + .DrawTextCentered(menuWidth / 2, 2, $"Frame: {frame}") + .DrawTextCentered(menuWidth / 2, 4, $"Score: {score}"); + + // Копируем то, что было нарисовано в menuBuffer в буфер экрана в нужную позицию. + // Возможно, метод можно назвать CopyFrom. + renderer.Buffer.DrawFrom(menuBuffer, gameAreaWidth, 0); + + // ---=== Render Game ===-- + + // Здесь отрисовка идёт напрямую в главный буфер экрана, хотя игровую область можно рисовать + // в буфере поменьше и потом копировать на экран. Так бывает проще управлять координатами + // отрисовки, поскольку у каждого буфера своя точка отсчета. Но пример как их комбинировать + // приведён с menu/drawMenu. + draw + // Большой прямоугольник - не только рамку рисует, но и очищает область, поскольку + // отрисовка здесь крайне простая - если объект перемещается, то его прошлую позицию + // нужно затирать вручную. Впрочем, учитывая что это всё происходит в памяти (в буферах), + // о скорости можно не переживать. Можно буфер с динамическими объектами очищать каждый кадр + // и рисовать их заново. + // Пометка - если отрисовка идёт напрямую в буфер экрана, то, в отличие от обычных ScreenBuffer, + // он очищается каждый кадр, поэтому его можно дополнительно не чистить. + // Но в целом я бы рекомендовал рисолвать всё сначала в отдельных буферах, а потом их содержимое + // копировать в нужные позиции в экранный буфер с помощью `renderer.Buffer.DrawFrom(otherBuffer)`. + .DrawRect(0, 0, gameAreaWidth, screenHeight, DrawUtils.DoubleLine, true) + .SetColor(AnsiColor.Rgb(frame % 6, (frame / 6) % 6, (frame / 36) % 6)) + .DrawRect(rectX, rectY, rectWidth, rectHeight, DrawUtils.Rounded, true) + .ResetColor(); + renderer.Render(); + + // ---=== State update ===--- + + var prevSpeedX = speedX; + var prevSpeedY = speedY; + + if (rectX >= gameAreaWidth - rectWidth - 1) { + speedX = -1; + } else if (rectX <= 1) { + speedX = 1; + } + + if (rectY >= screenHeight - rectHeight - 1) { + speedY = -1; + } else if (rectY <= 1) { + speedY = 1; + } + + if (prevSpeedX != speedX || prevSpeedY != speedY) { + score++; + } + + rectX += speedX; + rectY += speedY; + await Task.Delay(500); } } diff --git a/Tools/WindowSettings.cs b/Tools/WindowSettings.cs index 8341ddf..3dba44a 100644 --- a/Tools/WindowSettings.cs +++ b/Tools/WindowSettings.cs @@ -12,6 +12,13 @@ public static class WindowSettings public static void Initialize() { + // Если в системе не стоит Windows Terminal и если закомментировать эту строку, в старом терминале + // начинается привычная свистопляска, когда цветовые коды отображаются как текст. Возможно, + // AnsiConsole как-то определяет доступный режим работы терминала и если получается использовать + // true color режим, включает его. Можно сделать это и без Spectre но из предложенных ИИ решений - там надо + // лезть в kernel32.dll и менять режим консоли через системные функции. + // Поэтому пока вижу 2 способа - либо оставить некую инициализацию от AnsiConsole, либо устанавливать + // Windows Terminal. AnsiConsole.Cursor.Hide(); ForbidChangeSize();