Unity’s random seems like a straightforward to use and easily accessible function. It is helpful when dealing with procedural generation and AI behavior because the seed can be fixed to repeat the behavior. So it seems. But this is not always the case. UnityEngine.Random can case intricate timing bugs. These bugs are hard to track down in complex systems like AI behaviors that need to behave deterministically.
Causes
But what causes this kind of bug? The use of async functions in combination with calls to the static random class. Asynchronous functions are not guaranteed to take the same amount of time each invocation. It leads to a different point in time the Random.Range call is executed. This can change the call execution order and consequently the behavior of all following classes that use the random functionality.
Symptoms
The main symptom of a timing bug related to Unity’s random is a changing list of random values for a fixed seed. Initializing the random seed makes the random numbers deterministically. If it’s not, you are dealing with a timing problem.
Asynchronous systems which can cause timing problems are:
- Sounds
- Animations
- Scene loading
- Addressables
- Tween libraries
If one of these systems triggers a function that makes a call to Random.Range the outcome is no longer dependable. I will give an example.
Example
It is an actual bug I found in our production code. The AI in our current game uses random numbers to vary its behavior. I initialized the random function with a seed and started the game. Running a game with only AI players, I expect the same AI to win each time. But this was not the case. The winning AI would change randomly while using a fixed random seed.
I investigated by comparing the log files the AI wrote and realized that the random numbers used by the AI changed every session. The numbers should be the same when using the same seed. This strange behavior was not present when I simulated the game without visual representation. It suggests that the bug is part of the visuals, not the behavior logic. The hard thing about debugging timing-related bugs is the fact that changing the timing can conceal the bug. This means that adding a debug log, changing an unrelated system, or clicking a button one frame later could camouflage the bug temporarily.
After digging through the visual representation of the AI for three days, I found a walk animation with exit time and animation events. The events triggered step sounds which would call Random.Range to modify the pitch. This call to Random.Range was the offender. It changed its execution frame because it relied on the animation timing. It caused it to be called right before or after the AI based on the frame rate.
Solution
I solved the problem by introducing a separate random object for the AI. Created a System.Random object and passed it to all AI-related systems to decouple it from the random numbers used by the visuals. Now everything works again and is more robust than before. If deterministic behavior is requiered it is not safe to use UnityEngine.Random. You don’t know if another system uses it with asynchronous function calls leading to changing random numbers when using a fixed seed.