Recently I added a new spy mission to Dogfight Elite. The idea was simple: allow players to unmount their plane and find a spy that is hiding inside an abandoned subway tunnel. The problem is that the tunnel is quite large with tons of assets and custom lighting. At the same time, outside the tunnel, you have a large terrain and multiplayer combat going on. This must run at 60 fps on Oculus VR.
How did I solve this?
There were several steps:
- The usual draw calls, shader, and mesh optimizations. Most items inside the tunnel share textures and materials. They are highly optimized for polygon counts. The items also utilize the same shaders with the exception of the lights. Everything that should be static, is marked as static. Batching was easy to achieve.
But this was not enough. I had to write a custom dynamic loading/unloading system for the assets in memory. The RAM was not enough to load all the assets and Unity culling is not smart enough to avoid rendering distant objects.
2. Based on distance and orientation, load and unload items. Also, enable/disable assets that will require no rendering. I had to create an in-memory grid coupled with some triggers. This way I could achieve things like: when you enter this area, you can stop rendering the water, terrain, and trees. You will not see them when deep inside the subway, but you need to see them when you look out from the large subway entrance.
You can see here a video while developing the dynamic system. When the pilot arrives and enters, it begins to dynamically load the assets while walking inside the tunnel. The assets also unload from memory when you leave those areas. In the next iteration, I also stopped rendering water, trees, and so on, as you go deeper into the tunnel. But I could not unload them from memory for the reasons that I will explain below.
So far it all seemed good. Framerate was good. RAM was good. I could run this even a Kindle Fire 7 at a decent speed. But then I arrived at the hardest problem to solve: hiccups.
In Unity, when you load an asset into memory for the first time there is always a hiccup. It does not matter if you load them asynchronously. Unity has a design flaw that when you add a new GameObject, it instantiates/runs initialization code in the main thread. There is no way around this. The Monobehaviour class is just badly designed for this scenario. You can hide it by preloading or “warming up” different assets and then unloading them. But the problem with this is that you will need a large amount of RAM and make people wait a long time on their loading scenes. In my case, I also have a terrain with trees. When I unloaded the terrain from memory and I loaded it back upon exiting the tunnel, the hiccup was huge. So how can we solve this?
Well, it is certainly a known problem. After years of complaints, Unity decided to address it by creating a new system called DOTS/ECS, or Data-Oriented Technology Stack and Entity Component System. This was what seemed the right approach. Unity developed several demos of the technology, including this one loading/unloading thousands of assets in real-time with no hiccups.
But then, it lost traction and Unity stopped talking about it or releasing updates on its progress. Until recently. You can follow the thread in the forum here:
https://forum.unity.com/threads/dots-development-status-and-next-milestones-march-2022.1253355/
With this announcement, Unity acknowledged this is a standing problem in their design and they will include DOTS as part of Unity’s core. This should be the way to go for large world asset loading now. Development is still in the early stages but you can already use it in your projects. You can follow the roadmap and download the latest packages in: