Wednesday, July 22, 2015

[IndieDev] Unity3D: Things I Wish I Knew A Year Ago

I figure it's about time I started compiling a list of the quirks, tips, tricks, oddities, etc. around programming in Unity3D. Bits and pieces of knowledge I wished I had when I started Eon Altar. Note this may get pretty technical, but forge on ahead if you want to know more!

Don't Override Update, LateUpdate, or FixedUpdate

Update and LateUpdate are something that's called every frame on each Unity GameObject, if they have the methods defined (FixedUpdate is called every physics step, which happens multiple times per frame). Unity is using Reflection to determine if your GameObject implements these kinds of methods, and keeps a list of things to call in behind the scenes as far as I can ascertain.

So what this means is that if your object has a method called Update implemented with the signature void Update(), then Unity will call Update. If it doesn't have Update, there's no overhead.

Now, when we introduced our pause system, we wanted to prevent Update/LateUpdate/FixedUpdate from running on a large subset of our components (> 90%) while the game was paused. The natural thing to do of course is implement Update on the base object, then implement a different overridden method for the actual object that the base class called, and just not call the new method if the game is paused.

You will be tempted to do this. Don't. It's a trap.

The problem with this is that you've now introduced a bunch of extra calls of Update/LateUpdate/FixedUpdate. In our case, once I had profiled it, it was a good extra 6000 - 8000 calls a frame. This caused an immense amount of overhead because we pretty well more than quintupled our calls. It tanked performance significantly.

So don't. You want to, but don't. It might be better to go the interface route, and define interfaces for the pattern you want, but you'll still need to implement the rest of the code that you wanted on each declaration of these objects. Messy, messy, messy.

OnTriggerEnter/Exit/Stay Get Called Even When Object is Disabled

For most Unity methods, like Update, FixedUpdate, and so on, many of these will not be called if your Component or GameObject are disabled. By default, calls like GetComponentinChildren won't look in disabled children (but this behaviour can be overridden by passing a bool to one of the overloads of the method). Clearly Awake and Start get called no matter what, but generally, disabled GameObjects don't do much of anything.

However, OnTriggerEnter, OnTriggerExit, and OnTriggerStay all buck that trend, which means that even disabled, your trigger Colliders are effectively active. Mind you, it's probably less of a hit on perf overall to not have to recalculate the physics spatial trees anytime you enable or disable a Collider, but I admit, this piece of behaviour threw me off completely. I'd have expected them to ignore calls like Unity does for everything else.

(Unity 4) Dynamic Trigger Colliders Should Have Rigid Bodies

Okay, I actually knew this straight out of the gate because Unity documents this pretty specifically, and I've had to implement a physics engine in my University days, but I've found almost nobody seems to get this, and your designers can do a lot of performance damage if they're not careful.

Trigger Colliders come in two flavours: static, and dynamic. Static means they shouldn't move. I don't mean like rarely move, I mean never move. Ever. Dynamic Colliders can move. If you attach a RigidBody to a GameObject that has a Trigger Collider, you've now made the trigger dynamic, meaning Unity expects it to move.

Under the covers, PhysX does a bunch of optimization for static vs. dynamic triggers. Static are faster at runtime and take less memory, because PhysX (and thus Unity) never expects them to move. Optimizations and assumptions can be made. When you do move them, however, you force a recalculation of the spatial trees PhysX has built under the covers, which is an extremely expensive operation. This can and will tank performance. Even a single static Collider moving means a massive recalculation, so you need to be vigilant.

The easiest way to keep your designers or other programmers from making this mistake? Have Awake enforce a RigidBody on Collider scripts you think will ever move. Then, even if someone forgets, as long as they're using your script, it'll ensure the mistake can never be made.

Note, as per the link above, Unity 5 doesn't suffer this issue, because they now treat static Colliders the same as dynamic. It's a bit of a let down because those of us who do know how to handle this issue don't get the performance benefit anymore, but given Unity says the static Collider issue is one of the top 3 performance issues in Unity games, I don't really blame them for taking that choice away. Still, it'd be nice to have a compile-time setting that let us pick the old way.

Unity's Network Model is Wonky

Short version, for a multiplayer game, Unity's networking model is really strange. Well, not so strange if you're just making a basic shooter or something, but for what people consider normal Server-Authoritative models, Unity's APIs are awful.
What have these APIs wrought?
Long version:

BroadcastMessage is Convenient, but Terrible

Again with the reflection. BroadcastMessage basically takes a string, and then tries to call a method of that name on every sibling Component and child Component. This is really convenient, because you can do things like tell all your children to Initialize, or things of that nature.

The issue, however, is that you don't get linker benefits (no autocomplete, won't show up as someone calling the method on the callee side). You may accidentally call something you weren't expecting--if I add a script to a child Component that is called Initialize, but wasn't expecting it be be called via BroadcastMessage, could have issues there. You also pay a performance cost, because reflection is never cheap.

Basically, BroadcastMethod is super lazy. I personally much prefer to be explicit about these things because nothing is more frustrating than hitting BroadcastMethod in the code, then not realizing what Components could possibly be called unless you're at runtime and have breakpoints on every Component you have in the game with that method name.

Coroutines Aren't Great Solutions

Unity boasts a feature called Coroutines, which are really just C# Iterators with more interesting yield statements. In C#, Iterators/yield statements are pretty powerful syntactic sugar. Unity uses the yield return to determine the next time it should call Next on the Iterator (basically, when to continue to coroutine where the yield left off). For example, you could yield return null to have it pick up the next frame, or yield return WaitForSeconds(2) to have it wait for 2 seconds before continuing.

This would be great, except coroutines have some pretty peculiar behaviours. First of all, if the MonoBehaviour that started the coroutine is disabled or destroyed, the coroutine stops, and you'll never know it did until a bug crops up. Destroyed makes sense, disabled is really annoying because you need to realize it stopped and start it manually on the next enable. It just looks like it never returned from a yield statement.

Coroutines are sort of like asynchronous operations, except they really aren't; they're completely synchronous. But you end up treating them a lot like asynchronous operations which can actually get you into a bit of trouble in terms of excessive complexity. Also, you're not saving anything on the main thread. You're divvying up your job into smaller pieces across many frames, which is great, but it's still all happening on the main thread, just like Update.

Finally, nesting coroutines is a total pain to get correct. I wouldn't recommend it if you can avoid it. If you think you have to nest coroutines, you should probably have another look at your algorithm, it may be more complex than required.

Frankly, anything that can be done in a coroutine can also be done in an Update statement (or LateUpdate if you want to wait for the end of frame). Coroutines can be great if you're careful with them, but sometimes I wonder if the pitfalls around MonoBehaviours are worth the trouble.

Prefabs Linked in Scene Are Loaded Into Memory

Prefabs in Unity are basically premade building blocks. An example might be an enemy visual (mesh, textures, skeleton) in totality, which we can then say later, "Hey, give me the visual for the turtle," and grab the prefab.

There are two ways to access these prefabs: you shove them into a folder (any folder) called Resources, and then use Resources.Load to get it into memory; or you just link the prefab in your scene.

However, when you link the prefab in your scene, as soon as that scene loads, the prefab is loaded directly into memory. It's not on demand, it's as soon as you've loaded the containing scene.

This isn't the worst thing if you're cognizant of it. Things like some textures, meshes, spell effects, etc. you may want to load with the level. But if you're naive about how you've built your levels, you may be in for a world of hurt when you start crashing the Editor because all of your textures, meshes, music, level data, and so on start breaking the 3 GB mark in memory--noting that all resources in the Editor are basically double to triple in footprint they normally would be in a standalone player; they're raw, and if you're running the game, you have the game instances and the Editor instances.

In hindsight, this one makes perfect sense. But it wasn't something we realized at first.

That's all I'll put for now, but I'll probably add more as I come across them over time. My Unity posts tend to show up in search results fairly often.

Are there things about Unity3D that threw you for a loop? I'd love to hear them! #Unity3D, #IndieDev, #EonAltar

No comments:

Post a Comment