A classic Snake, rebuilt pixel by pixel to learn what a game loop actually is.
A classic Snake, written from scratch in C# on top of Aiv.Draw, a bare-bones educational graphics library that hands you a raw bitmap and nothing else.
Built in November 2022, during my first year at AIV (Accademia Italiana Videogiochi), as a way to actually use what I'd just learned instead of just reading about it: variables, loops, arrays, basic OOP, and how a game loop ties input, update, and draw together every frame.
The only twist on the classic formula: every 10 points, the screen gets a short shower of falling pixels as a little celebration/warning.
What I took away from it:
- My first real "input → update → draw" loop, built without any engine underneath it
- Working directly with a raw pixel buffer forced me to understand things like alpha blending and grid alignment instead of trusting a framework to handle them
- A first, honest look at how quickly "quick and simple" code turns into something that's hard to extend — which shaped how I approach architecture today
- Status: complete, not maintained — it is what it was: a two-month-in learning exercise.
Tech stack: C#, .NET Framework 4.8, Aiv.Draw 1.0.1 (NuGet)
Requirements:
- Windows (Aiv.Draw targets
net48) - Visual Studio (or any IDE that can restore NuGet packages and build .NET Framework console apps)
Setup:
- Clone the repo:
git clone https://github.com/Manu2402/Snake.git - Open
Snake/Snake.slnin Visual Studio - Let NuGet restore
Aiv.Draw(declared inpackages.config) - Build and run — a console window (instructions + final score) and a game window (855×855) will open side by side
- Controls:
WASDfor movement (Up, Left, Down, Right). No diagonal movement, no pause — this is Snake in its purest, least forgiving form.
Configuration: none exposed — window size, grid size, tick rate, and rain trigger (every 10 points) are all hardcoded constants in the source.
The snake growing on a 30px grid, with the pixel-rain effect triggering after crossing a multiple of 10 points.
Playable build: none — source only, build it yourself.
Everything runs off a set of static "manager" classes that double as global state — Game, Gfx, Timer, Score, RandomGenerator, ColorsFactory. Game owns the loop: input → timer tick → update → collision checks → draw, all inside a single while (Gfx.Window.IsOpened) in Game.Play(). There's no scene graph, no component system, no abstraction layer between "game" and "rendering" — Gfx writes directly into Window.Bitmap, the raw RGB byte array Aiv.Draw exposes, and every drawable object (Pixel, SnakeRect, sprites) just calls into Gfx to stamp itself onto that buffer each frame.
Movement is grid-based rather than physics-based: everything snaps to a fixed 30px cell. The snake's head reads directional input (SnakeRect.CheckInput), and each following body piece simply inherits the previous piece's last direction (PrevCharPressed) on the next tick — a manual follow-the-leader chain instead of a position-history buffer, which is the classic naive way to implement a snake body.
- Fixed-size snake body:
SnakeBody.Snakeis a pre-allocatedSnakeRect[784](a 29×29 grid's worth of cells) rather than a growable list — the array is sized for the theoretical maximum length up front and filled in as the snake eats. - Rejection-sampling apple spawn:
Apple.Update()picks random coordinates in ado...whileloop until they land on a valid grid intersection (multiples of 15 but not 30, to center the sprite inside a cell) and inside the play field. - Manual sprite alpha blending:
Gfx.DrawSpritereads each sprite pixel's RGBA, computes the alpha percentage by hand, and blends it channel-by-channel against whatever is already in the window's bitmap — no built-in compositing, since Aiv.Draw doesn't provide any. - Grid-equality collision detection: collisions (snake ↔ apple, snake ↔ itself) are just float equality checks on grid-aligned coordinates. It works only because every position is snapped to the 30px grid — there's no tolerance or bounding-box math anywhere.
- Custom decoupled timer:
Timerruns onDeltaTimebut only flips analarmflag every 0.5s, so the snake's movement tick is decoupled from the render frame rate. - Particle-style rain effect:
Rainrecycles a pool of 420 pixels with anIsAliveflag — pixels fall with a randomized velocity and get reset to the top once they leave the screen, a small, self-contained particle system built without any pooling framework.
Whether to give the snake body a proper dynamic structure (a List<SnakeRect>, or a linked list of positions) or a fixed, pre-sized array. I went with the fixed SnakeRect[784], sized for the maximum possible length on the grid, and grew a counter (counterPieces) into it instead of resizing anything at runtime. It's wasteful and inflexible — but at that point in the course I hadn't covered dynamic collections yet, and the goal was to practice arrays and manual indexing, not to write the "correct" solution. It's a decision I'd reverse immediately today, but it's an honest snapshot of where I was at the time.
No automated tests — this predates that habit for me. Debugging was done live with Visual Studio's debugger, stepping through the grid math and collision checks pixel by pixel.