⬇️ A lot of text for the reading enthusiasts coming :P
Rendervlog 1 - The first Canvas
A little story about the past (skip if you just want the dev stuff)
~3 years ago school tasked us with creatively coding a console minigame in .NET using Unicode characters. I got inspired by my favorite game of all time, Geometry Dash. I got a basic renderer, 3 levels, saves, menus & controls working, but the app was full of bottlenecks - rough object placement, inaccurate collisions, one shared thread for controls/physics/rendering, and color switching via Console API instead of ANSI. Result: ~3-10 FPS and janky logic.
Years later, with way more knowledge on rendering, multithreading, performance & physics, I felt thrilled to redo it properly.
So here we are: building a console rendering engine in C# .NET.
- Why this framework? It’s the one I’m most fluent (and have the most fun) in.
- Why not use an existing renderer? I want to actually learn rendering principles - way simpler to apply to console apps than diving into something like Vulkan - and couldn’t find a C# lib fitting my exact needs anyway.
- An entire engine for one game??? No, I’ll reuse this for future console projects. Maybe even run Doom on it once it’s done 👀
Now onto the actual development :D
Setup a solution with Lib (library code) and Demo (testing features). It all started with an empty canvas - “In The Beginning There Was Darkness”:
⬇️Screenshot 1 below⬇️
The ‘Screen’ was a 2D array of custom objects, so changing a pixel at [X,Y] was easy. Instead of Console.ForegroundColor/BackgroundColor (the biggest bottleneck last time), I used ANSI escape sequences - strings telling the terminal what to do to the next characters, here changing the background color of a whitespace ‘pixel’.
Pros & Cons of ANSI:
+ Faster - build one long string of pixels with their ANSI color codes and let the terminal resolve it, instead of calling the console API per-pixel.
+ Full 16.8 million RGB colors vs ConsoleColor’s 16.
- Not universally supported, but most modern terminals handle it fine.
Here’s changing a single pixel on a small canvas:
⬇️Screenshot 2 below⬇️
Why the long pixel? Console cells are tall, not square. Fixing that (print twice, or double precision) is for the next vlog. For now we keep pixels stretched - which also lets us layer text on top via the ForegroundColor layer, on top of pixels drawn to BackgroundColor.
Added more utilities next: painting the whole screen, painting a rectangular selection, and letting text overwrite its own BackgroundColor:
⬇️Screenshot 3 below⬇️
Bug(s) during development 🪲
Prefilled the grid with black Pixels via Array.Fill. Since Pixel was a class, every cell got the same instance. So the first blue DrawPixel call after Fill/DrawRect turned the entire grid blue - all pixels shared one reference. Fixed by making Pixel a record struct, but not before accidentally bluescreening the whole canvas with a single pixel call:
⬇️Screenshot 4 below⬇️
Final Optimizations
Switched back to a 1D array for the pixel grid - closer to how real buffers work (one continuous memory block), and bulk ops like full-screen painting get simpler. For [X,Y] access, added a private ref Pixel this[uint x, uint y] custom indexer. And since this grid is the frame’s render buffer, we don’t render on every change - changes go into the frameBuffer and wait for render().