Skip to main content

Float precision and Time in Unity

Float precision and Time in Unity

Recently, I was tasked with addressing an interesting issue that occured in a Unity game. After the game run for about 2 days, it virtually stopped streaming in map assets during world locomotion. Assets were streamed in with a huge delay (up to x-teen seconds) and it simply wouldn’t do. After finding out what caused this strange bug, I thought it would make an interesting article to share with you. Both the problem, and the process of finding its origin were quite an interesting experience.

Debugging

Since the problem only occurred after at least 2 days of ‘soaking’, I knew time is going to play a role here. I started investigating the issue by looking at the custom streaming code used in the game. It consisted of a bunch of asset loading and unloading functions, called every tick. At the start of each tick, before calling the functions, the code would cache the current Time.realtimeSinceStartup, which is a timer managed by the Unity engine that tracks for how long the application has run. There were also several constant floats defined in the same streaming manager. Those floats described the time allowed for loading and unloading data every tick.
Here is a simplified example of the code: class AssetStreamingManager { const float MAX_TIME_FOR_ASSET_LOADING = 0,005; List<Asset> AssetsToUnload; List<Asset> AssetsToLoad; private void Tick(float deltaTime) { float operationStopTime = Time.realtimeSinceStartup + MAX_TIME_FOR_ASSET_LOADING; LoadAndUnloadAssets(operationStopTime); } private void LoadAndUnloadAssets(float stopTime) { for (var asset in AssetsToLoad) { UnloadAsset(asset); AssetsToLoad.Remove(asset); if ( Time.realtimeSinceStartup >= stopTime) break; } for (var asset in AssetsToUnload) { UnloadAsset(asset); AssetsToUnload.Remove(asset); if ( Time.realtimeSinceStartup >= stopTime) break; } } private void LoadAsset(Asset asset) { // loding logic } private void UnloadAsset(Asset asset) { // unloding logic } }
So, inside the tick function, the calls to loading and unloading assets were called, and the time since startup (as it was at the start of the tick, the cached value) was passed to them. Inside, the functions would loop through the lists of assets waiting to be loaded or unloaded and perform those operations. If current Time.realtimeSinceStartup would exceed the time allotted for the type of operation increased by the time since startup as it was at the beginning of the current tick, the loop would break. The rest of the items waiting for loading or unloading would then continue to be processed in the ticks to come. The first step I took to start actually debugging the problem was to find a way to reproduce it without waiting for 2 days. Since the Time.realtimeSinceStartup returns a float representing how many seconds have passed, I simply added 200.000 everywhere this value was used. This made the game behave approximately as if it was running for 2 days. As expected, the asset streaming issue was there right after starting the game. I was ready to start looking into the source of the problem.

Finding the cause

From this point on, the debugging procedure was as straightforward as it can be. After placing breakpoints inside loading/unloading functions, I discovered that the functions returned immediately without processing any load or unload request, apparently because of the time allotted for the operations have already run out. On the loading/unloading function’s start, the value of Time.realtimeSinceStartup was equal to the time at the beginning of the tick + the time allotted for loading/unloading. This was clearly wrong.

Don’t store that in a float

It’s important to note, that the constant float values representing the time allotted for loading and unloading assets was a value of 0,00X. A very small number that actually represented how many milliseconds are allowed for these operations. I had a hunch that adding these two floats (200.000 + 0,00X) might be asking too much considering how different the numbers are. And it all comes down to float precision. Since you don’t always deal with float precision issues, I had to look up some resources to see if I’m not completely bananas. I managed to find this gem of a blog post by Bruce Dawson. I also found this great IEEE calculator online. After switching to the 32 bit float and inserting my data, it became clear that adding the small number of 0,005 to the one that’s over 200.000 results in the small number being completely lost due to insufficient float precision. We have found the cause of the issue. Now it was time to fix it.

Fixing it

First off, I have to mention that this was a legacy project that I only worked on as a part of a team that was porting it to another platform. We didn’t have a lot of time on our hands and any rewrites that would cause huge refactors were out of the question. The problems associated with working on legacy projects are quite numerous and an interesting topic to explore on its own (possible blog post). In this particular case, it was clear the original developers did not want to deal with more complicated solutions for tracking execution time or parallelize the functionality. I had to come up with a solution that fit this paradigm and was as safe as possible to introduce. The first thing I wanted to try is to store all time values in a double in a hope of increasing the precision enough. This had the downside of not really fixing the issue, but making it take longer to surface. It also didn’t work. I’ve read some articles suggesting I should add a certain number to a double to make it stick from the getgo to a precision range that would be consistent for months. This seemed quite dirty and I didn’t really want to deal with it. It was clear that in order to time the loading/unloading operations, I didn’t really need to know for how long the game has run. The only thing that mattered here was to be able to break a loop on a timer. Time.realtimeSinceStartup was probably only used because it was updated by the Unity engine, so it was convenient to access and use. Since the game was originally released on PC, they either didn’t expect anybody to have the game running for 2 days or more, or didn’t even consider the issue of float precision. I decided to move the timing to using a long (high precision integer) that would store milliseconds. This played ideally with the timer object I intended to use: System.Diagnostics.Stopwatch. Once a Stopwatch is started, checking how many milliseconds have passed is as easy as getting the value of its ElapsedMilliseconds property. With this in place, I moved the const float values that represented how long a load/unload operation can take to longs as well (could have been int, but I wanted to kill this issue with fire). With these changes in place, all that was left to do was to fire off a Stopwatch on every tick and pass it to the loading/unloading function.
Here’s how the code looked like after the fix: class AssetStreamingManager { const long MAX_MILISECONDS_FOR_ASSET_LOADING = 5; List<Asset> AssetsToUnload; List<Asset> AssetsToLoad; private void Tick(float deltaTime) { System.Diagnostics.Stopwatch loadStopwatch = System.Diagnostics.Stopwatch.StartNew(); LoadAndUnloadAssets(loadStopwatch); } private void LoadAndUnloadAssets(System.Diagnostics.Stopwatch stopwatch) { for (var asset in AssetsToLoad) { UnloadAsset(asset); AssetsToLoad.Remove(asset); if (stopwatch.ElapsedMiliseconds >= MAX_MILISECONDS_FOR_ASSET_LOADING) break; } for (var asset in AssetsToUnload) { UnloadAsset(asset); AssetsToUnload.Remove(asset); if (stopwatch.ElapsedMiliseconds >= MAX_MILISECONDS_FOR_ASSET_LOADING) break; } } private void LoadAsset(Asset asset) { // loding logic } private void UnloadAsset(Asset asset) { // unloding logic } }

Conclusions

All in all, I found this issue an interesting case to write about. It’s not often that we have to deal with float precision issues. Heck, most of us, especially in games development don’t have to think about it at all, unless you are an engine programmer and deal with projections, transforms etc. Yet, it’s important to keep in mind that the numeric types we use all have their limitations. There are types of data we should avoid storing in a float, like time or money amounts. Whenever we intend to store data that we want to be able to always depend upon in terms of precision, we should pump the breaks and think about it instead of going for the type we’re the most used to working with.

Comments

Popular posts from this blog

Array property customization in Unreal Engine 4

With the drive towards as much data-driven gameplay as possible, there comes a need for easy to edit and nice to interact with data assets that can accommodate all that data. While Unreal Engine 4’s UI framework allows us to display a wide range of data structures, its default handling of nested properties can quickly result in deeply nested structures that need to be expanded in order to edit them. This can really hurt productivity in some scenarios. Fortunately, we have the ability to fully customize how our data is laid out. While there are nice tutorials all around the web that explain how to customize our custom classes and structs, I’ve not been able to find an article that would explain how one would go about customizing how collection types display their data. In this article I will describe how to customize the display of an array property in UE4. I will follow it with the one for customizing maps display in the near future. Defining the issue I intend to explain the pr

My CD Projekt RED story, part 3

My CD Projekt RED story, part 3 We are now 2 years into my CD Projekt RED adventure and so far, it’s been mostly smooth sailing. I got promoted to a QA Analyst position, an equivalent of a specialist position for developers. I was earning around 2600zł or $650 per month, which would sometimes go as high as 3500zł with additional crunch pay. At this point I felt fairly rewarded for my efforts and could even start planning my wedding. I also received the first bonus for my participation in the creation of both Witcher 3 expansions. This amounted to roughly 13.000zł or $4250, which was an amount of money I had a hard time wrapping my head around. I still lived in a single room in an apartment I shared with other people, but at this point it was mostly my own choice. I’ve had my own wedding on the horizon so I needed every coin I could save for the occasion. Getting out of QA It was during that time that I decided I want to transition away from doing QA work. I approached the audio te