The Making of Caterpillar Runner
A C++ Development Journey through custom architecture, rendering, and procedural generation.
Download Game (.exe)Creating Caterpillar Runner was not just an exercise in building an endless runner; it was a deep dive into custom system architecture, rendering pipelines, and procedural generation using C++. In this article, I'll walk you through the development process, from the first prototype to the final polished game, highlighting the major technical challenges we faced along the way.
1. The Core Concept and First Steps
The idea was to twist the classic endless runner formula. Instead of just moving a single character left or right, the player controls a caterpillar with two "heads" that can be moved independently to the sides to dodge incoming obstacles.
The project was built from scratch in C++, managed with CMake for cross-platform compatibility. The initial architecture was straightforward, dividing the codebase into core components like Game, the Input system, and entities like the Caterpillar and LevelGenerator.
2. Rendering Challenges: The "Four-Headed" Glitch
Once the base mechanics were in place, we tackled rendering. The caterpillar is structurally complex compared to a simple bounding box; it has a main body and multiple interactive head components.
The Challenge:
During the initial implementation, we encountered a bizarre visual glitch: the player appeared to have four heads instead of two, and its body sections were clipping incorrectly through the world's Z-axis (depth).
The Solution:
The issue stemmed from overlapping drawing logic between the global render loop in Game.cpp and the local drawing logic in Caterpillar.cpp. The components were essentially being drawn twice per frame with incorrect offsets.
By refactoring the render pipeline to ensure the player entity managed its own local coordinates properly without double-dispatching to the global renderer, we fixed the glitch and ensured correct depth sorting.
// Example of the corrected rendering approach in Caterpillar.cpp
void Caterpillar::draw(Renderer& renderer) {
// Save current transformation matrix
renderer.pushMatrix();
// Apply global entity transforms
renderer.translate(position.x, position.y, position.z);
// Draw body segments with correct Z-ordering
drawBodySegments(renderer);
// Draw left and right heads without duplicating calls
drawHead(renderer, leftHeadOffset);
drawHead(renderer, rightHeadOffset);
renderer.popMatrix();
}
3. Procedural Generation: Making the Game Unpredictable
An endless runner is only as good as its level generation. The LevelGenerator system was responsible for spawning terrain and obstacles as the player moved forward.
The Challenge:
In the first playable version, obstacles spawned in fixed "lanes" or static positions. We quickly realized this broke the game: a player could simply spread the caterpillar's heads to the extreme edges of the screen and safely pass by almost every obstacle without moving.
The Solution:
We had to dismantle the lane-based system and introduce continuous randomization for obstacle spawning. We updated LevelGenerator.cpp to distribute obstacles dynamically across the X-axis at random positions, forcing the player to make active, dynamic reactions rather than relying on a static "safe" posture.
// LevelGenerator.cpp snippet
void LevelGenerator::spawnObstacles(float playerSequenceZ) {
// Instead of fixed lanes, we generate a random X offset within world bounds
float randomX = Random::getFloat(-worldEdge, worldEdge);
// Ensure the new obstacle doesn't perfectly align with the previous one
if (std::abs(randomX - lastObstacleX) < minimumGap) {
randomX += (randomX > 0) ? -minimumGap : minimumGap;
}
Obstacle newObstacle(Vector3(randomX, 0.0f, playerSequenceZ + spawnDistance));
activeObstacles.push_back(newObstacle);
lastObstacleX = randomX;
}
4. Visual Polish: Adding Textures
With the gameplay loop solid, the game needed to look immersive. We upgraded the primitive shapes to textured surfaces.
We implemented a texture loading system to bind image data for the floor geometry and the various obstacle types. This step instantly shifted the project from a technical prototype to a recognizable, visually appealing game.
5. Bringing the Game to Life: Procedural Audio Engine
The final major hurdle was audio. A game feels empty without auditory feedback for player actions.
The Challenge:
Implementing audio in a custom C++ engine can be tricky, and securing the right retro sound effects takes time.
The Solution:
We took a creative, hybrid approach. First, we wrote a Python script to procedurally synthesize custom retro-style sound effects (like jumping, sliding, and collisions).
Next, we built a custom AudioEngine class in C++ to manage audio playback and integrated these sounds directly into the gameplay logic. Sounds were wired up for:
- Movement: Sliding sounds when moving.
- Jumping: Triggered on vertical input.
- Collisions: A harsh impact noise.
- Scoring & Game States: Audio cues for starting the run and crossing score thresholds.
// Integrating AudioEngine into the Core Game logic (Input / Game.cpp)
void Game::handleCollisions() {
if (physicsSystem.checkCollision(player, activeObstacles)) {
// Trigger game over state
gameState = GameState::GameOver;
// Provide immediate auditory feedback
audioEngine->playSound(SoundEffects::COLLISION);
}
}
Summary
Building Caterpillar Runner was an iterative journey. We started with building raw C++ mechanics from scratch with CMake, fixed complex spatial rendering bugs, solved critical gameplay balance issues by randomizing obstacle algorithms, and finally polished the experience with custom textures and a custom audio engine. The result is a smooth, responsive, and uniquely challenging runner!