March 29, 2026
/ Learning Golang the Fun Way: Building a 2D Action Game from Scratch
Why reading Go documentation isn't enough, and how writing a 2D pixel art game using raylib-go taught me more about structs, pointers, and state management than any web tutorial.
Learning a new programming language through to-do list apps and basic web servers works, but it's rarely exciting. After working on typical backend projects, I wanted a more engaging way to master Go. So, I decided to build a 2D pixel art action game from scratch using raylib-go.
Here is why ignoring the standard learning path and jumping straight into game development was the best way to deeply understand Golang, and how I built a custom engine under the hood.
The Problem With Typical Tutorials
Most Go tutorials focus heavily on microservices, JSON parsing, and HTTP handlers. While these are critical skills, they often mask the core principles of memory management and state mutations behind abstractions.
When you build a game engine from scratch — without the safety net of Unity or Godot — every pointer dereference, struct allocation, and interface implementation directly impacts the 60 FPS target. You either understand memory and state management, or your game stutters and crashes.
How It Works: Structuring a Custom Engine
The game, which I dubbed ggame, features a custom sprite animation system, local two-player support, and a layered AABB collision engine.
The Sprite and State Machine
Instead of dumping all logic into a massive game loop, I organized the project into clear domains (main.go, sprite.go, collision.go, types.go).
Go doesn't have classes or classical inheritance. Building game entities forced me to rely on composition. For instance, sprite state transitions (Idle, Run, Jump, Fall, Attack) are handled using constants and clean state machines:
// Example of how state is driven by input and physics
func (s *Sprite) UpdateState() {
if s.ActionState == Attack {
return // Lock movement during attack
}
if s.Velocity.Y != 0 {
s.ActionState = Fall
} else if s.Velocity.X != 0 {
s.ActionState = Run
} else {
s.ActionState = Idle
}
}
This strict data-oriented approach made the logic incredibly easy to reason about.
Building a Layered Collision Engine
One of the most complex challenges was creating a Godot-style collision filtering system. The CollisionManager syncs all hitbox world positions relative to the sprite bounds and then runs O(n²) pair-wise checks.
By assigning independent Layer and Mask integer lists to collision objects, I could filter checks efficiently. If a hitbox mask doesn't align with another object's layer, the check is skipped. This drastically reduces the overhead of AABB (Axis-Aligned Bounding Box) math and prevents friendly fire cleanly.
Where It Gets Complicated
Cross-Platform Window Management:
Go compiles down to a neat binary, but rendering frameworks rely on CGo bindings. When testing on Linux environments running Wayland (like Sway or Niri), I ran into severe scaling issues. The fix was forcing the XWayland backend by unsetting WAYLAND_DISPLAY, a quirky but vital reality check for desktop app distribution.
Input Management:
To handle local two-player support, each player is assigned a separate InputConfig struct. This allowed me to decouple the physical keys from the game logic completely.
The Takeaway: Gamify Your Learning
If you want to intimately learn a compiled language's features, build a game. The instantaneous audio-visual feedback when things work — and the immediate panics when they don't — forces you to internalize the syntax and patterns much faster.
You can check out the source code for ggame on my GitHub. Grab a friend, plug in some keyboards, and try the local multiplayer!