Project Retrospective These reflections about a project may be outdated or limited in applicability.
Published June 2023.
I haven’t decided if I want to make this project public yet, so there isn’t a link to the game right now.
Squarez is a small game I built in about 2.5 weeks.
It’s a retro, console-style puzzler that asks the player to slide around irregularly shaped pieces to create 3 × 3 Squarez.
I wanted to get a feel for building a game with Godot 4 and C#. In order to keep the project small, I also had these things in mind:
- 2D only
- Retro, low-res style
- Remake a game to avoid having to design the gameplay from scratch (although allow room for improvements)
- Limit up-front planning; prefer organic development
I decided to remake a TI-86 game I used to play a ton of when I was in junior high: Squarez by Jimmy Mårdell.
I pulled from multiple generations of consoles and handhelds: Saturn era puzzle games with a bit of Y2K Dreamcast aesthetics, but with the low resolution of an NDS and the sleek production of a PSP title. With synthwave music, because why not.
At one point I realized I needed a mascot character, so I drew a squarish rabbit and named her Bunz. I love her.
The aesthetics never gelled into something especially cohesive and opinionated, and just settled vaguely around “retro.” It’s decent, but it could have been better.
The gameplay is similar to the TI-86 game, but I did make a few changes, taking cues from a favorite falling block puzzle game.
Piece selection uses a bag randomizer (see Bag Randomizers Create Semi-Predictable Random Sequences). The bag is initially filled with two copies of every piece, and is then refilled with one additional copy each time it’s half empty.
Movement! It’s super important! I spent the first few days of the project just on movement, which I think was the right call.
Repeating movement has two parts: an initial delay and a repeat delay. Say our initial delay is 167 ms, and our repeat delay is 50 ms. If the player holds down left, the piece moves immediately, and then again after 167 ms, and then again every 50 ms after that.
At one point each axis of movement had its own repeat timer, and this was completely wrong. If you’re holding left, and then you start holding up as well, you don’t expect the piece to start moving up and right on staggered frames; it should move diagonally on the same frame.
The one thing I didn’t add and should have is buffered input. When you tap left, the piece moves left immediately. But if you’re holding up and tap left in the middle of the repeat delay, the piece never shifts left. The left movement should be buffered and the piece move diagonally on the next movement frame.
- How should a second axis of input added early during the initial delay be handled? At which point is a second axis input buffered vs acted on immediately?
If you can’t rotate a piece, up to 8 alternate positions are tried (shifting the piece by 1 cell in each direction, including diagonals). Alternates in the direction of the player’s input are tried first. These kicks make rotation a lot more responsive around the edges of the play field and near other pieces.
There’s one special case that wasn’t handled and should have been: the straight I piece can be shoved against the edge of the play field in such a way that it requires kicking two spaces in order to rotate.
Kicks make it easier to rotate a piece into a place it can’t slide into. The game recognizes these twists and doubles any points earned from the drop (sort of like a T-Spin).
- This feels like the start of a standalone note about discrete 2D movement mechanics.
The TI-86 game has a 10 second countdown timer. If it reaches 0, you’re forced to drop the piece where it is, and a new piece spawns. At some point I decided to not add this timer, which freed new players from the time pressure. (Advanced players are still encouraged to play quickly, since the score multiplier bonus is temporary.)
But this introduced a new problem. In the TI-86 game, the player is forbidden from dropping a piece in the spawn area. The timer running out forces the drop and ends the game.
Without a timer, I’d need to allow the player to drop in the spawn area to end the game, but still prevent them from accidentally dropping and ending the game too early. I eventually settled on showing a warning on the first drop, and requiring a second drop input to force the drop in the spawn area, but it’s clunky and doesn’t feel great.
I like the combo! C# is a little verbose at times. It’d be nice if the language server had some deeper integrations (suggesting node names or resource paths in strings, for example).
Not being able to publish to the web is an unfortunate downside (Godot hasn’t yet integrated the relatively recent support for targeting WASM).
A few things I didn’t figure out and would like to:
- Unit testing
- A better memory profiler (in particular isolating just C# allocations, since those are subject to garbage collection)
I still don’t have a good feel for when to use Godot’s editor and when to build things in code. This isn’t unique to Godot; I experience the same tension with Interface Builder.
Do I connect signals in the editor or with
Connect? Do I set up node references through
GetNode or through
One thing I didn’t use and should have is
AnimationPlayer; all of the animations were built in code with
Some nodes ended up knowing a little too much about each other, or sometimes needed to follow a winding path through the scene graph in order to reach the right target.
A possible solution I’ve seen used: an autoload object that serves as an event bus for project-wide custom signals. Because it’s globally accessible, individual nodes don’t need to know how to traverse the node graph to find a signal’s emitter.
There wasn’t much organization in my project folder, and I got away with it, but it definitely won’t scale.
I should be more thoughtful about how the game is organized (scenes, resources, scripts, final assets, etc.), but also any internal artifacts (debug or prototype scenes and assets). Where do asset artifacts like GarageBand, Audacity, and Affinity project files live? Where do I put the originals of any licensed or CC0 assets? Where do sketches, notes, and other design artifacts go?
Speaking of which, asset sourcing and production was awfully ad hoc. How do I track what assets are needed? What does a better asset workflow look like? Is there a way to reduce the friction of modify → export → import → test → repeat? Are there better tools to use?
The background behind the play field is synced to the music track. This was an unexpected diversion that ended up being really fun. (It’s probably not surprising that I’m a big fan of Enhance Experience’s games—Rez, Lumines, and Tetris Effect.)
But while timing visuals based on the state of the audio thread worked well enough, triggering additional sounds synced to the main audio track did not. There were a few builds where the piece clearing sfx was a clap, which I tried to queue for the next half count. I couldn’t get it consistently precise enough.
No one reads instructions. And while you can sort of figure out the game by playing it, there should have been a tutorial overlay the first time you play, or if you start the game and don’t press anything for a bit.
Putting together the credits and licenses screen at the end was surprisingly annoying! It’d be way better to create a file at the start and add to it as assets are added.
Because this was just a learning project, I didn’t really do any marketing. But it still took a couple of hours to create new assets for the itch.io page, in the right sizes and resolutions. It also took a while to write the copy; I should have been thinking about that earlier.
A gameplay video would have been nice to include, but that’s a whole different commitment I didn’t want to make for this project. Something to keep in mind for the future.
As far as being a learning project: huge success.
I also think it’s a decent game for a 2.5 week project! Part of that is nostalgia (I really did play a ton of the TI-86 game when I was a kid). But the improvements are mostly good changes, and I’m still playing a couple of rounds a day and having fun. That’s awfully satisfying.