As I mentioned in the last dev update, I’ve been working on re-implementing the collision contact system, which is used for triggering impact sounds and applying damage to constructions. I’ve now finally completed this work, not without some challenges along the way though, and it ended up taking far longer than I was hoping. Not to mention my video card died and my Internet connection went down last week, fun times!
Apologies for the wall of text, but here’s my attempt to explain what I’ve been up to.
Impact overload
The old collision code dated from the early prototype days, and simply played an impact sound (and applied damage to the construction) directly in the OnCollisionEnter() event handler. This event gets triggered once for every collider pair that makes contact, which can end up being a lot particularly if there are many moving parts in a construction, and meant that way too many impact sounds were being triggered concurrently.
Also, I’ve been working on adding sliding sounds for when parts slide past one another. This requires continuously tracking collision contacts, for which I use the OnCollisionStay() event. Again, this event gets triggered once for every contacting collider pair, except that unlike OnCollisionEnter(), it gets called every fixed update for the duration that the colliders are contacting one another, so the performance cost of any code in the event handler becomes a real concern.
Unity performance woes
On the subject of performance, the overhead for Unity to collect the collision contact data is one thing, but what I find even more frustrating is the way it must be accessed. For every single collision contact, a call from the Unity engine (unmanaged C++ code) into the C# script is made via an OnCollision…() event, with an attendant GC alloc for the collision data being passed into the event handler. This means in my “worst case” tests where I had thousands of collision contacts per update, I was seeing a performance cost in the tens of milliseconds, and thousands of GC allocs totaling a few MB. This cost is just for reporting the collision contacts, and does not include the physics sim update or anything else.
I wish it were possible to access all of the per update collision contact data in one call, preferably into a pre-allocated buffer, but for now we’re stuck with the OnCollision…() events. Hopefully at some point Unity will improve this situation!
I tried to find a way of eliminating OnCollisionStay() while still keeping the sliding sounds working. It seemed like it should have been possible because you can still keep track of what colliders are currently contacting by using OnCollisionEnter() / OnCollisionExit(), and then get the velocities from their rigidbodies. Unfortunately what you don’t have is the new contact position and normal each update, which are required to calculate the relative velocity at the point of contact, necessary for the sliding sounds to work properly. I tried fudging my way around this by estimating these values, but couldn’t come up with a solution that worked reliably.
In the end I resigned myself to keep using OnCollisionStay(), and turned my attention to optimising the code inside the OnCollision…() event handlers as much as possible, and consolidating the collision event data into something more manageable.
Discard and merge
The first step was to discard any collision contacts whose separation is larger than a small threshold value, happily this eliminated most of the spurious impact sounds that were being triggered when parts were merely sliding past one another.
The second part was to merge collision contacts such that for each update, only one contact is considered per Rigidbody pair / PhysicMaterial combination. This means that, for example, a construction with a large number of parts all made of the same material and all rigidly attached together will only generate one impact or sliding sound. The most important thing was to perform this merging as efficiently as possible because the OnCollision…() events can be called so frequently; it was crucial to avoid any computation, conversion, GetComponent…() calls, etc. inside the event handlers.
To keep track of the currently active contacts, the system now uses a dictionary whose keys are a struct containing the two Rigidbodies and the PhysicMaterial (these are all available directly from the data passed into the event handlers). The dictionary’s values are a struct containing the contact position and normal, the merging happens by only keeping this data for the contact with the smallest separation, the rest are discarded. Then every update this dictionary of active contacts (of which there aren’t that many due to the merging) is looped over, calculating the required relative velocities, and updating the sliding sounds accordingly.
To mitigate the OnCollisionStay() performance overhead further, I also added an option in the game-play settings to disable it, for players with low end machines and / or particularly complex constructions. This effectively disables the sliding sounds, but the impact sounds still work, so it’s not the end of the world.
Audio materials
Once ready to trigger an impact or sliding sound, I wanted to add some variety and sophistication to the sounds, while also making configuration easier. So now, rather than each part explicitly referencing which AudioClips to use, the system automatically maps from the PhysicMaterial to an “audio material”. Each audio material specifies the AudioClips to be played on impact and during a slide. The pitch of these sounds are scaled based on the mass of the part that is colliding, and there can be different AudioClips chosen based on the pitch scaling factor.
I also added support in the audio materials for a “rolling sound” (played based on the angular velocity of a part when it’s contacting something). This allowed me to make the wheels (which have had sliding and rolling sounds for some time now) use the same unified system. I do love me some unification!
AudioSource pools
Despite the aforementioned reduction in number of collision sounds being triggered, there’s still no real limit on how many could be triggered concurrently. Also, each part behaviour might have a sound playing (e.g. motor whine, gear whirr, propeller wash, etc.) which is only limited by the number of active part behaviours.
To bring this situation under control and place a hard cap on the number of AudioSources, I implemented a pooling system. This pre-creates a fixed number of AudioSources and keeps track of which ones are currently in use. The collision contact system and the part behaviours can request to play an AudioClip via the pool, and if a free AudioSource isn’t available the request is ignored. Once an AudioClip has stopped playing, the corresponding AudioSource in the pool is automatically freed up to be available for a future request.
Damage propagation
In the game, damage (based on the collision impulse) is only dealt with in the OnCollisionEnter() event handler, not OnCollisionStay(). However I still wanted to optimise this as much as possible, so rather than applying damage directly in the handler, it is now accumulated over an update. The total damage is then applied once per update (this is where the damage is divided up and propagated out to part attachments).
I still have some work to do on the damage system but this at least moves the code out of the event handler, and means that if I need to increase the complexity of the damage propagation code, it shouldn’t affect performance too much. This is a topic I’ll be revisiting in a future update.