u/Edvinas108

▲ 50 r/Unity3D

Making a desert green

We went from a laggy gamejam tower defense WebGL game to an incremental where you make a desert green as a snail. The game name is Feed the Forest, you can play the demo on Steam.

Some tech juice:

We started out spawning a bunch of objects (10k at most - grid size is 100x100) and just relying on SRP to do batching. Perf quickly became trash and we had to use instanced rendering + flat arrays to speed up iterations. So now each mesh type that you spawn initially starts out as a simple renderer and still relies on SRP batching. Then as the plant grows, we have cached materials at 0.1 increments and apply those so batching works correctly (since we change material color while it grows, batching breaks at in-between steps). Once the plant fully grows, we disable the regular Unity renderer and apply our custom instanced rendering logic. Essentially we have a singleton where we keep track of different plant types, where they get grouped into continuous arrays of data that we just plop to the GPU using instanced rendering APIs.

The white lines around each plant are SDFs. We have 3 sets of them: 1. plant + SDF still growing; 2. plant fully grown and SDF is static; 3. mask for non paintable areas - also static SDF. This way we only re-calc the the second SDF as the player moves. Other SDFs are saved to Custom Render Textures where we combine them in a shader using min (all 3 are Custom Render Textures actually, but we only re-calc the second one constantly). This way we only recalculate around 50 objects per frame which allows to use SDFs even in WebGL builds. If we go past 500 objects then the game becomes laggy, hence why the caching.

Finally, since we're instantiating hundreds of objects each frame - we use object pooling for pretty much everything. When you first start the game, we pre-fill pools with up to 8k objects for each category, which results in mostly smooth experience during gameplay (WebGL is still a bit iffy here). There are still some issues with pooling audio sources, sometimes I noticed that the audio crackles when fetching stuff from pool to play audio on WebGL, the only solution I found is to greatly limit the audio pools. Wish we used FMOD as that would of made things a lot easier.

Surprisingly updating the tile logic is rather fast, so no optimizations there - we store a list of tile structs in a 2D array @ a manager script and iterate them in Update(), the rest of the game scripts look at that list and do its thing. Takes around 0.5ms per frame for ~10 objects. We did consider DOTS, but that doesn't seem to work according to the docs on WebGL and since CPU time seems low we decided to not investigate more. Tho I'd like to try it out, or maybe another ECS system as dealing with flat arrays and making custom wrappers so the API stays nice is rather PITA.

One thing that I'm still not sure how to handle is Text Mesh Pro garbage collection. In some cases, esp when you paint a lot, we have a lot of GUI updates where we do ToString and apply to TMP text. I changed most of those to use a StringBuilder (TMP has an overload for that), but it still seems to allocate a lot. I saw that TMP has a SetCharArray function, but didn't have enough time to investigate. Would appreciate any tips on how to optimize this part.

If you have any tech questions or tips, I'd be happy to hear them :D

u/Edvinas108 — 16 days ago