March Update - Case Study: Textures
Hello, everyone! Some of your suggestions for update topics back in February were:
- Architecture / iteration
- More tech stuff in general
I'm dealing with a problem right now that falls into the tools / tech stuff categories: Texture management. (This is exactly the kind of thing I would assume is too boring to write about, but hey you asked for it!)
Unity is a great tool. An amazing tool, really. But it has limitations. The biggest is that it assumes you'll be using 'levels' in your game. This doesn't rule out open world games, but if you were playing by their rules it would be something like Metro: Last Light where the world is carved up into discrete regions with maybe two or three entrance/exit points, plus loading screens in between them. You load a level and everything in it when you enter an area, then unload everything when you leave. Sure some minor stuff like enemies might spawn and despawn while you're there but the general rule is: load all the things / unload all the things. (Including any textures used in the level.)
FRONTIERS isn't like that. There aren't levels - there's just a world. I have exactly one 'level' that's loaded on startup. After that I load and unload individual objects - characters, rocks, terrain tiles - into whatever area the player needs to see.
This was the root of the problem I faced last year - how do I organize content so that I can load and unload it as I move through the world? How do I avoid loading too many objects at once, or too few? I came up with some decent solutions. It'll never be as seamless as a truly streaming open world like Grand Theft Auto, but it works.
Unfortunately this approach prevents also me from using a lot of Unity's built-in tools - stuff like pathfinding, occlusion culling and (apparently) texture memory management. (Note: I keep hedging when making statements about Unity's texture memory because the truth is, I have no idea what's going on under the hood, and no one else seems to either. Google this problem and all you'll find is lots of unanswered questions & educated guesses.)
So while I may have the number of objects under control, I'm now dealing with a related problem: textures. Load too many textures at once and blammo: crash to desktop. These are the main issues:
- Unity doesn't have a useful, well-documented way to manually unload a texture that has been loaded at runtime. Yes, you read that right. The only method they offer that actually works makes it impossible to load the texture again, which is useless. I could write an entire blog entry about this issue but I'll spare you the colorful language.
- The foliage and structures in FRONTIERS have a lot of variety. If I was dealing with a mono-climate I'd be fine, but we're talking a whole new set of plantlife and new structure textures for every region. Which means more textures, which means more delightful crashes.
- Unity doesn't have a useful, well-documented way to manually unload a texture that has been loaded at runtime. Oh did I already mention that? Well take a moment to let it sink in.
What to do?
Use fewer textures, duh. There are lots of ways to do this.
There are a lot of redundant materials in any game world. Got a wood chair, wood table, wood axe handle, wood cupboard? Use the same wood texture for all of them. You lose a little visual flair but I'll happily sacrifice dedicated oak, cherry and pine textures for more plant varieties.
What about color variations? For characters and for some objects I use shaders that apply colors to areas of a texture based on a red/green/blue mask:
Overlay textures are awesome. A cliff face might need a massive 2048 x 2048 texture to look good up close. A unique texture that enhances its geometric contours, mind you - you can't get away with a generic texture like you could with the non-natural wood objects. But if you can only spare 256 x 256 pixels per major rock variety - and I can barely spare that - the result is going to be a blurry mess. So you overlay another smaller, tiling texture on top. Now you've got something that looks OK up close while still preserving the broad details of the unique texture. Different overlay textures can produce different rock varieties, too:
What about snow? We used to have unique snow textures for all of the rocks on Willowpeak, but when I started adding structures to that region we ran out of memory in a hurry. So I axed all the snow textures and swapped it out for a snow shader that applies a tiling snow texture based on the surface normal. (A side benefit being that the snow can be added or removed based on season.)
Then there's terrain. When Given first started working on the terrain he told me 'I want to use four textures - dirt, grass, sand, rock.' I told him he was crazy, and I meant it. How could we possible achieve the variety I wanted with only four textures? His solution was to use a color overlay shader. The shader cleverly identifies the average color of the texture it's being applied to, then only changes that 'major' color while leaving the rest alone. I had planned to use 16 2048 x 2048 textures plus local variations, so even with the addition of a 512 x 512 color overlay texture for every terrain tile we still came out ahead. It's tough to describe so I'll just show you a before / after:
There are other methods like texture atlasing and clever UV mapping - this is especially relevant for the interface.
Still not enough
This accomplishes a lot but it's still not enough. The best way to keep texture memory in check is to unload the textures you aren't using. If you're nowhere near a palm tree - and I have ways to tell - do you really need that palm tree texture taking up 256 x 256 precious pixels? No. But as I mentioned before, Unity doesn't have a (useful) way to manually unload a texture that's been loaded at runtime. So until I hear differently - and I'm working on getting directly in touch with someone at Unity who can address this issue - I'm forced to assume that FRONTIERS must be capable of loading all of its textures at once.
I'm shaking my head as I type this because that kind of restriction is ludicrous, but the alternative is to subject players to eventual out of memory errors as I continue to load (and not effectively unload) textures as they move from one part of the world to another.
So the next week or so will be spent aggressively pruning every texture that we don't absolutely, positively need.
Oh Unity, I can't stay mad at you
This texture unloading business is a major headache and kind of inexcusable but it doesn't diminish Unity's overall usefulness. For every hour I've spent wasting time wrangling textures I've probably saved ten with their shader tools or rapid compiling. And to be fair, I am the one that broke the rules - if I had stuck with their level paradigm I wouldn't be facing this issue. (Curse you, stubborn streak!)
AND there's always the chance that someone, somewhere has figured out a useful workaround for this problem, or that the latest version of Unity will address it outright. I'm keeping my fingers crossed for that outcome.
Alright, back to work
I'll go into multiplayer in the next update. Matthew is hard at work on it right now, so by then I'll have some more in-depth information. In the meantime if you're still in the mood for some tech stuff, here are two devlogs you might have missed that touch on Creature AI and randomized character generation.