>> [MUSIC] My name is Charles. In this video,
I'm going to show you how to use conditional breakpoints
to focus on the data and values that actually matter.
That way, you can debug smarter and fix bugs faster.
More often than not, your games most crucial features are
the ones that present the biggest debugging challenges.
Take this project, for example.
Spawning is one of its key mechanics
and I've tried to improve that by writing
code that changes the SpawnRate in
response to the player's skill level.
The idea is that enemy should spawn faster
when the player is doing well
and slower when the player is lagging behind.
To achieve this, my logic modifies an animation curve
that represents enemy SpawnRate over time.
Here, the x-axis represents time
and the y-axis represents SpawnRate.
This graph represents a linear progression in
which enemies will spawn faster as time progresses.
However, that's just a static, hard-coded example.
In reality, our graph will fluctuate as the player plays
and there's no telling what it will actually look like at runtime.
If it's working as expected,
the players should be met with a challenging
yet manageable flow of enemies.
However, somehow I've introduced a bug that causes
the SpawnRate animation curve to fall below zero,
which results in a ton of enemies being spawned.
That's no fun for the player and especially no fun for me,
the developer who now has to figure out
what in the world has gone wrong.
Looking at the code that's responsible
for updating the animation curve,
we can see that it uses an algorithm that applies
some modifiers to a configurable base SpawnRate.
The key modifier here is the difficulty modifier,
which is used to increase the time in-between each spawn.
This is really good information to work with,
but I'm not really sure where to begin my debugging process.
The problem is that algorithms are not my strong point
and I have no idea what could be causing
the SpawnRate to spin so wildly out of control.
I'll need to do some debugging.
Now, the standard approach is to use debug.log statements
to get more information about what's happening.
However, in this case,
we've got a lot of noises,
there's just too much happening in the scene.
We could add some logic
to make our logging a little more intelligent,
but then we just have a bunch of debug code peppered
throughout our logic
and no one wants to maintain that.
Instead, we're going to use a feature of
the Visual Studio debugger that'll help us
quickly get more information without all of this extra code.
Let's start by adding a normal breakpoint on
the line right after SpawnRate gets assigned.
Then let's attach Visual Studio to Unity.
Perfect.
Now, switch back to Unity and hit "Play".
This will cause the debugger to immediately cause
the execution of our code on our breakpoint just as expected.
Now that's helpful because as we've covered
in our other videos about debugging,
we'll be able to inspect any
and all variables that are currently in SQL.
But I'm not really interested in
inspecting those variables just yet.
At this point in our games lifecycle,
SpawnRate is being set to a reasonable value,
it's working exactly as I expect it to.
While I could poke around,
I don't know if I'd be able to discern anything of importance.
What I really care about is inspecting the code
when the bug occurs and only when the bug occurs.
That's where conditional breakpoints come in.
Conditional breakpoints are a feature of
the Visual Studio debugger.
They allow you to pause the execution of your code
when certain conditions have been met.
This can give us a great deal of
additional control over our debugging process.
Let's see it in action.
Back in Visual Studio.
Let's modify our existing breakpoints so it pauses execution
when SpawnRate dips below a certain threshold.
For example, a threshold that we deemed to be unacceptable
or too dangerous for the player.
To do that, all we have to do is right-click on the breakpoint
and then select "Conditions" from the context menu.
This will present a breakpoint settings block that appears
just below the line on which the breakpoint is set.
From here, we have a couple of options to choose from.
Let's expand the first drop down.
Our options are conditional expression, hit count, and filter.
Conditional expressions represent
any logical condition that you can think of,
so long as the values you reference are in scope.
When using conditional expressions,
the breakpoint will be triggered when your expression
has been satisfied or when a value has been changed.
For example, I can reference SpawnRate so that
execution pauses whenever its value changes.
Hit count triggers your break point whenever
the line has been executed a certain amount of times.
This is great for loops that you suspect have gotten out of control
and are iterating more times than they should.
Finally, the filter option triggers your break point
when specific low-level conditions have been met.
There are a number of predefined filters available,
such as machine name, process ID,
and thread name that you can use to restrict
your breakpoints to select devices, processes, or threads.
In our case, we don't need anything complicated.
We'll just use a conditional expression.
Since something is causing the spawner
to spawn enemies way too quickly,
I suspect that SpawnRate is being
set to a ridiculously small number.
Let's confirm that suspicion by adding a condition that triggers
the breakpoint when the value of SpawnRate drops below 0.5,
then we can examine the other values
and try to determine what's going on.
This condition is possible because SpawnRate
is in the same scope as the breakpoint.
You may recall from our debugging basics
video that you should take care to add
breakpoints to the lines of code for all of
the variables you want to inspect are accessible.
In our case, SpawnRate is a variable that's local
to the function where our breakpoint is currently placed.
So we're good to go.
Let's play the scene until our breakpoint is triggered.
[MUSIC].
Great. Our conditional breakpoint worked.
The execution of our code is pause at the point
where SpawnRate has reached an unexpected value.
Now we can figure out what's going on.
Again, I'm terrible with algorithms.
Hopefully, the solution will be simple.
Let's analyze each variable
that's used in our dynamic SpawnRate algorithm.
Base SpawnRate is a field that can be set in the inspector.
It represents the base SpawnRate of the spawner.
It should be set to a moderate value,
which it looks like it is.
Next, spawn enemies is an instance of
a scriptable object that holds a list of game objects.
Here we're adding the count to the base SpawnRate
in order to increase the time between each spawn.
That way, the game becomes easier
when there are more enemies in the scene.
Moving on, difficulty modifier is another field
that can be set in the inspector.
Its job is to tune down the impact
that enemy count has on the base SpawnRate.
The lower the value, the harder the difficulty.
In this case, it looks like it's way too low.
In fact, a negative value should not be allowed here at all.
This is a common problem that happens
when you expose properties in the editor,
especially when those properties have
an effect on your games key algorithms.
Users will always find a way to provide
values that fundamentally break your logic.
That being said, being able to
parameterize your component is extremely important
because you want to be able to hand off tasks like
tuning difficulty to game designers wherever possible.
Instead of restricting this property to fix the bug,
we're going to make a couple of quick changes
to guard against bad user input.
The first is the range attribute.
Range is a Unity specific attribute that limits
the value of a property in the editor to a predefined range.
Let's limit difficulty modifier to a value between 0.1 and 1.
Perfect. Now, our game designer
won't be able to input a game breaking value.
That's a great start, but we still need one more check.
A dynamic SpawnRate algorithm partially depends on elapsed time.
That means that given enough time,
SpawnRate will eventually dip down into an unacceptable range.
Luckily for us, the solution is as simple as
clamping SpawnRate between two acceptable values.
This will allow us to provide minimum and maximum values.
That's it, our game is back to normal.
More importantly, we've limited the ability for our game designer
or anyone else who modifies
the spawner component to break our logic.
It was all thanks to conditional breakpoints
in the Visual Studio debugger.
Without them, we either have to sift
through tons of log statements
or manually pause and continue
the execution of our code until we reach the broken state.
Conditional breakpoints gave us the ability to place
a breakpoint that pause only when we needed it to,
which helps us close our feedback loop
and dramatically speed up our debugging workflow.
Best of all, it works the same way
across all Visual Studio products.
Whether you're using Visual Studio for Mac or Visual Studio code,
be sure to take advantage of these powerful time-saving features.
[MUSIC]