Posted On: 2020-12-07
When working with a built-in physics system, one needs to be consistent about configuring each individual object. In Unity, this is especially true, as there are a number of per-project configurations that must be set on a per-object basis. Unfortunately, this has often caused me to get things wrong on my first attempt to configure an object, whether due to inattention or misunderstanding. To protect myself from making further mistakes, I've come up with a set of rules for configuring objects - which I will share here, in the hope they are useful for you as well.
The headers listed below are rules that I personally follow to avoid the listed problem. I am not advocating that this is the best possible solution - merely one that works for me.
Unity has a built-in physics system that appears, at first glance, to work with both 2D and 3D objects. In actuality, however, Unity's single physics system is a wrapper around two different physics engines - one for 2D and one for 3D*. This means that physics interactions in 2D are handled separately from those in 3D, and therefore 2D objects and 3D objects won't interact with each other.
For my own work, I'm locked into using the 2D due to building on top of third-party code that uses 2D. This is fine, as my intent is to make a game that exclusively uses 2D for its simulation. If I had any ambitions of mixing 3D in, however, I would likely have to reconfigure every object in the game - switching every 2D component and message handler into its 3D equivalent*.
The way objects move when the player interacts with them is an essential part of a game's feel. Whether it's snappy, smooth, or something else - it is essential that the game's code precisely controls the movement of objects to create the desired feel. Such precise control, however, is antithetical to how physics engines approach collision. Objects controlled via physics are (generally) only controlled indirectly - such as through applying forces to the object. Furthermore, when objects make contact with one another, the physics engine takes over, applying forces and generally resolving the objects such that they are overlapping for the minimum amount of time possible.
Unity's physics engine allows designers to set objects as Kinematic, which (generally) indicates that the object will only be controlled via code, and not via the physics simulation. At first, this may seem like a simple solution to the game-feel problem: any objects that need a distinctive feel could be set to "Kinematic", while other objects can be left as "Dynamic". Unfortunately, this has been far from simple in my experience: mixing kinematic and dynamic objects together often results in buggy or unexpected behavior. Specifically, I encounter issues where kinematic objects behave correctly when interacting with dynamic objects, but fail to behave correctly when interacting with other kinematic objects. Often, this is due to a design constraint of the physics engine: kinematic objects don't collide with each other - and therefore collision messages cannot be used to detect when two kinematic objects contact.
To avoid such issues, I have forsworn using collisions and collision messages. Instead, I rely on a combination of triggers and physics queries* to handle any contact between objects. This reliance on triggers has reduced the number of dynamic physics objects as well, leading to more control over the feel of objects' movements. This has, of course, also led to reinventing the wheel - as objects whose triggers overlap one another need need some way to separate themselves (aka collide.)**
One of the nuisances of using trigger messages is that the messages do not indicate which object the relevant trigger belongs to. Instead, all triggers under a single rigidbody are indistinguishable from one another. This has been a source of bugs for me, as a moving parent object (say, a character walking) often has non-physics triggers attached to it (say, an area in which the player character can initiate a dialogue). If the parent object has a RigidBody, then the non-physics triggers will send messages to it, and those messages will be indistinguishable from the messages from the character's actual (trigger-based) collision box.
To work around* this, I am now attaching RigidBodies to each individual trigger. Thus, an object may have one trigger and rigidbody representing its collider, and another trigger and rigidbody representing an unrelated area (such as the aforementioned range for talking.) From there, it's merely a matter of attaching the components with the message handlers to the correct objects (such as the collision handler on the collision box, and the "talk" button prompt on the talk range box.)
As mentioned previously, I can (and sometimes do) use physics functions directly to detect and resolve interactions between objects. Unfortunately, I am not particularly adept at it: I often make simple mistakes (such as using the wrong trig formula, or forgetting that positive Y is up in Unity*), and even mundane physics interactions often take me several tries to get right. I understand that practice would improve my skill and consistency, but I don't need to create new physics interactions frequently enough to justify deliberate practice*. Thus, I instead try to solve such problems by relying on built-in functionality wherever possible - often by using triggers to represent areas of interest or potential contacts.
Sticking to these rules I've made for myself helps me mitigate many of the problems I've faced in the past. I no longer waste time wondering whether I should use 2D or 3D, or attaching logic in a collision message handler - only to wonder why the (kinematic) player character isn't triggering it. While I'm still developing the habit to add a kinematic RigidBody to every trigger, most of the recent (physics) issues that I've faced have been neatly resolved once I remembered to do so.
Regardless of whether or not my particular set of rules are relevant to you, I hope that the explanation I've provided will help you with your own strategy for consistently configuring your project's collisions.