The article's main claim
Good debugging isn't fast code-swapping. It's controlled information gathering. The programmer's job isn't to change something as fast as possible – it's to understand as fast as possible what actually broke.
Our fictional case: the player sometimes can't climb a ladder. QA says it's "random." The designer says "it worked yesterday." The programmer says: okay, let's first figure out what "random" actually means here.
An NDA-free, generic gameplay engineering example. The emphasis is on the way of thinking.
01 — The bug report isn't the bug"Sometimes it doesn't work" is the starting point, not the diagnosis
A bug ticket usually describes what someone experienced. Not exactly what broke in the system. That's an important distinction. "The player can't climb the ladder" could be an input bug, interaction detection, a state transition, a movement lock, an animation event, root motion, collision, network authority, or even a camera issue that's making the game state hard to read.
- Symptom: the character doesn't start moving upward.
- Possible cause: the guard condition on the transition into the Climbing state is false.
- Possible cause: the interaction target gets lost for one frame.
- Possible cause: the animation starts, but the movement mode doesn't switch over.
- Possible cause: the interaction shows on the client, but the server has no valid target.
So the first reflex isn't to open ClimbComponent.cpp and start changing lines. First you need to state precisely: at exactly which point does the system's state diverge from the expected behavior?
First, turn "random" into a deterministic problem
A reproducible bug is incomparably cheaper than a non-reproducible one. If you can trigger it 10 out of 10 times with the same sequence of steps, you're no longer hunting ghosts.
A good reproduction matrix might look like this:
| Variable | Test | Result |
|---|---|---|
| Approach angle | head-on / 30° / 60° | frequent failure at 60° |
| Speed | standing / walking / sprinting | more frequent after sprinting |
| Frame rate | 30 / 60 / 120 FPS | doesn't occur at 120 FPS |
| State | grounded / falling | 1-2 frames after landing are critical |
| Input | tap / hold | more stable when holding |
The goal isn't to play through every combination. The goal is to find the variable that significantly raises or lowers the failure rate. That already points the investigation somewhere.
A suspicious sign
If the bug depends on FPS, ping, camera, or animation speed, it's worth paying special attention to timing, frame-specific states, async processes, and any logic that relies on a single frame's worth of a valid window.
Before you drop a breakpoint anywhere, know which chain you're following
A common mistake with gameplay bugs is starting the search right where the symptom shows up. But climbing a ladder is several systems working together.
From there, the question is much better: where does the chain break? Did the input arrive? Did we find a target? Did validation accept it? Did the state change start? Does the animation see the state we expect it to?
// Not a fix. Instrumentation.LogClimb("Input", inputPressed);
LogClimb("Target", target ? target->GetName() : "NONE");
LogClimb("State", ToString(CharacterState));
LogClimb("CanClimb", CanEnterClimb(target));
LogClimb("MovementMode", ToString(Movement->GetMode()));Don't change anything yet. First, state what you expect to see
One of the most useful steps in debugging is when the programmer states an explicit hypothesis. It might look like this:
This is a good hypothesis because it's testable.
- If it's true, the target's validity will change right before the faulty frame.
- If we artificially enlarge the interaction volume, the bug's frequency should drop.
- If we hold onto the target for a few frames with a grace period, the bug may disappear.
- If none of these happen, the hypothesis is probably wrong.
The point is defining the test in advance. We're not saying "I changed this and now it works" – we're saying "I expected to observe this, I measured it, and it either strengthened or weakened this explanation."
More senior thinking
What makes someone more experienced isn't nailing the cause of every bug on the first try. It's being able to quickly rule out bad hypotheses, without making the system less stable through unnecessary changes along the way.
The breakpoint is just one tool among many
In gameplay systems, visual state is often at least as important as the code. You can often understand an interaction bug faster with debug draw than with twenty breakpoints.
Useful tools:
- Structured logging: state transitions, target changes, input events, authority decisions.
- Debug draw: collision shapes, interaction range, nav queries, traces, velocity vectors.
- State overlay: current gameplay state, movement mode, animation state, cooldowns.
- Breakpoints/watches: great once you already know which transition or condition is suspicious.
- Frame stepping / replay: worth gold for frame-specific bugs.
- Profiler: for when the "bug" is actually a hitch, a race, or delayed processing.
if (Debug::Climb())
{
DrawCapsule(CharacterInteractionCapsule, Cyan);
DrawBox(CurrentLadder->InteractionBounds, Red);
DrawLine(CharacterPosition, TargetPoint, Yellow);
DebugText("State: " + ToString(State));
DebugText("TargetValid: " + Bool(Target != nullptr));
DebugText("CanEnterClimb: " + Bool(CanEnterClimb(Target)));
}One of the nice things about a visual debugger is that it can sometimes show you, in a single frame, what you've spent two hours trying to explain to yourself in words.
06 — The real causeThe bug is often not where it appears to be
Say the measurements end up showing: the interaction target is fine, the input arrives, the state transition even runs. And yet the climb still doesn't start.
void Character::EnterClimb()
{
State = ECharacterState::Climbing;
Animation->Play(ClimbStart);
Movement->SetMode(EMovementMode::Climbing);
}According to the log, the state is already Climbing, but the movement mode occasionally jumps back to Falling. Why? Because in that same frame, a landing callback is still processing the previous movement state:
void Character::OnLanded()
{
if (!Movement->IsGrounded())
return;
State = ECharacterState::Locomotion;
Movement->SetMode(EMovementMode::Walking);
}So the symptom is "the ladder doesn't work," but the cause is state ownership and event ordering. This is a classic gameplay bug: two systems that are each correct on their own disagree, within the same frame, about who gets the last word.
Lesson
Ordering-related bugs are often not caused by one bad condition, but by multiple callbacks or subsystems writing the same piece of state. In these cases, you don't necessarily need a patch – you need clearer state ownership.
The smallest change that removes the real cause
A good bugfix isn't one that hides the symptom in this one spot. It's one that restores the system's invariant.
Bad fix: if there's a ladder nearby, OnLanded() shouldn't reset the movement mode. This can work – right up until some other interaction or edge case ends up in the same state.
Better fix: state transitions should have explicit priority/ownership, or the locomotion callback shouldn't be allowed to overwrite an already-started, higher-priority interaction transition.
void Character::OnLanded()
{
if (StateTransition::IsCommitted())
return;
if (State == ECharacterState::Climbing)
return;
EnterLocomotion();
}The exact implementation varies by project. The principle, though, stays the same: don't build an exception around a symptom – fix the broken responsibility boundary.
A bugfix isn't done until you've deliberately tried to break it
After the fix, we rerun the original reproduction. That's only the first step. After that, we look at what other systems the state-change logic might have touched.
A minimum regression list for our example:
- ladder with a normal approach;
- ladder out of a sprint;
- ladder after a jump/landing;
- interrupting the ladder;
- other interaction objects: door, pickup, vault;
- low and high FPS;
- if relevant: client/server and latency;
- state after save/load or a checkpoint;
If the bug came from frame ordering, it's worth covering at least the state ownership invariant with an automated test. Not because every gameplay bug can be automated, but because it's worth having a machine guard the critical rule.
TEST(StateTransition, CommittedInteractionCannotBeOverwrittenByLanding)
{
Character.EnterFalling();
Character.BeginClimbTransition();
Character.OnLanded();
EXPECT_EQ(Character.State(), ECharacterState::Climbing);
}The definition of "fixed"
It's not "it works on my machine right now." It's: the original reproduction no longer happens, the related systems haven't broken, and we understand why the change was correct.
Not magic. Disciplined thinking.
One of a gameplay programmer's most important skills isn't knowledge of some API or engine feature – it's how they systematically reduce uncertainty in an uncertain situation.
- Reproduces. Turns a "random" problem into conditions.
- Maps the system. Knows which way the data and state flow.
- Measures. Doesn't just look at what they want to see.
- Forms a hypothesis. States in advance what they expect from the test.
- Rules things out. A bad hypothesis is a result too.
- Fixes the real cause. Doesn't manufacture a symptom patch.
- Tests for regression. Checks the fix's effect on the rest of the system too.
The difference between a junior and a more experienced programmer is often not that one has bugs and the other doesn't. It's that when something breaks, one of them starts trying things, and the other starts gathering information.
Gameplay code is an especially good training ground for this, because the bug is rarely just "in the code." Input, state, animation, physics, networking, camera, timing and content together make up the player experience. That's why debugging also requires thinking in systems.
It's when you understand exactly why it disappeared.