Hey friends, hope all is doing well. This is a complicated problem and I am struggling to explain it because I don’t fully understand it myself either but I’ll do my best. Alright I’ll try to summarize this as clearly as possible:
In my game, I have a system that makes boats collide with one another to deal damage to each other, and when boats enter the trigger of one another, they need to toggle their Rigidbodies and Colliders to allow collision to work through OnCollisionEnter, through code implemented in OnTriggerStay and OnTriggerExit in my game, using Vector maths to do accurate calculations:
[field: Tooltip("How far is this supposed to be from its opponent boat before switching the Rigidbody.isKinematic setup of itself, and its opponent, so that OnCollisionEnter works as expected? (Differs from boat to boat due to different boat sizes and scales)")]
[field: SerializeField] public float distanceBeforeSwitchRigidbodyKinematics = 32.0f;
// This is called consistently when one boat is in the trigger of another
// until it leaves, it's just the way Unity works (so 'Rigidbody.isKinematic'
// is not controlled externally by some Update function, it's just consistently
// called here until you leave the trigger of the other boat)
private void OnTriggerStay(Collider other)
{
// When this boat enters the trigger of something else,
// this code runs (this boat has a box collider trigger
// so that the player can mount it)
if (!other.GetComponent<BoatStateMachine>()) return;
var otherBoat = other.gameObject;
Rigidbody myRB = GetComponent<Rigidbody>();
Rigidbody otherRB = otherBoat.GetComponent<Rigidbody>();
// ----------------------- TEST ZONE - 23/6/2025 -----------------------------------------------
// ExtraBoatColliders exist because we need something that only interacts with other boats
// so that they can properly collide with this boat, and vice versa. If the player is not driving
// a boat, collisions can get pretty messed up pretty fast.
// It is placed on the
// 'ExtraBoatColliders' layer, which only interacts with boats and ignores all other layers
// in the 'Edit -> Project Settings -> Physics' system (so I don't have to go nuts with the code):
BoxCollider myExtraBoatCollider = GetComponentInChildren<ExtraBoatCollider>(true).GetComponent<BoxCollider>();
BoxCollider otherExtraBoatCollider = otherBoat.GetComponentInChildren<ExtraBoatCollider>(true).GetComponent<BoxCollider>();
// ---------------------------------------------------------------------------------------------
// 'GetInstanceID()' is Unity built-in, and the reason it
// exists here is because it ensures that only one of the boats,
// either this one or the other one, controls the isKinematic setup.
// This is crucial so that logic doesn't hit the roof
if (GetInstanceID() > otherBoat.GetInstanceID())
{
return;
}
float distance = Vector3.Distance(transform.position, otherBoat.transform.position);
// This is why we switched from 'OnTriggerEnter' to
// 'OnTriggerStay'. We need to do consistent checks when
// triggers are close, and only activate them when the boats
// are too close, otherwise you got randomly floating boats all
// over the place
if (distance > distanceBeforeSwitchRigidbodyKinematics)
{
// Debug.Log($"Distance to {otherBoat.name} from {gameObject.name} is {distance}, CollisionPrepThreshold is {collisionPrepThreshold}");
Debug.Log($"Distance to {otherBoat.name} from {gameObject.name} is {distance}, Distance Before Switching Rigidbody Kinematics is {distanceBeforeSwitchRigidbodyKinematics}");
return;
}
// If either this boat or the other boat has the player on it,
// make sure that the boat with the player on it remains non-kinematic,
// so that BoatController.cs uses the boat rigidbody to move the boat around,
// and the other boat is Kinematic, so that OnCollisionEnter works
// (which relies on one boat being Kinematic, and the other Non-Kinematic)
if (GetComponentInChildren<PlayerStateMachine>() != null)
{
otherRB.isKinematic = true;
myRB.isKinematic = false;
return;
}
if (otherBoat.GetComponentInChildren<PlayerStateMachine>() != null)
{
myRB.isKinematic = true;
otherRB.isKinematic = false;
return;
}
// Now use the same formula to determine who gets hit in OnCollisionEnter
// to determine who is kinematic, and who is not. If you are the aggressor,
// keep your kinematic turned on (so that BoatBackingUpState.cs works properly),
// and if you are the victim, keep your Rigidbody non-kinematic
Vector3 myForward = transform.forward;
Vector3 toOther = (otherBoat.transform.position - transform.position).normalized;
float angleToOther = Vector3.Dot(myForward, toOther); // Closer to 1 = 'other' is in front of you
// You are the aggressor if the other boat is in front of you (i.e: angleToOther is close to 1.0)
bool iAmAggressor = angleToOther > 0.5f;
// If you are not the aggressor, you are the victim, so turn off IsKinematic:
if (iAmAggressor)
{
myRB.isKinematic = true;
otherRB.isKinematic = false;
// ------------- TEST ZONE - 23/6/2025 ------------------
otherExtraBoatCollider.gameObject.SetActive(true);
// ------------------------------------------------------
}
else
{
myRB.isKinematic = false;
otherRB.isKinematic = true;
// ------------- TEST ZONE - 23/6/2025 ------------------
myExtraBoatCollider.gameObject.SetActive(true);
// ------------------------------------------------------
}
}
private void OnTriggerExit(Collider other)
{
if (!other.GetComponent<BoatStateMachine>())
{
return;
}
var otherBoat = other.gameObject;
Rigidbody myRB = GetComponent<Rigidbody>();
Rigidbody otherRB = otherBoat.GetComponent<Rigidbody>();
if (GetComponentInChildren<PlayerStateMachine>() != null)
{
otherRB.isKinematic = true;
myRB.isKinematic = false;
return;
}
else
{
GetComponent<Rigidbody>().isKinematic = true; // All non-player boats must be Kinematic when outside of each others' range
}
// --------------------- TEST ZONE - 23/6/2025 ------------------------
BoxCollider myExtraBoatCollider = GetComponentInChildren<ExtraBoatCollider>().GetComponent<BoxCollider>();
BoxCollider otherExtraBoatCollider = otherBoat.GetComponentInChildren<ExtraBoatCollider>().GetComponent<BoxCollider>();
myExtraBoatCollider.gameObject.SetActive(false);
otherExtraBoatCollider.gameObject.SetActive(false);
// --------------------------------------------------------------------
}
There are special triggers assigned as children to the boats for those specific operations
However, when boats enter each others’ Triggers, they mess with each others’ Mesh Colliders (see OnTriggerStay and OnTriggerExit above)
As a result, the boats have no MeshColliders, BoxColliders or any other colliders for them to use to collide with one another
To solve that, I got ProBuilder and started mocking up meshes to go with the boat (a box collider won’t cut it). The problem I have is, Unity treats ProBuilder MeshColliders with ‘Convex’ turned on (it must be turned on to stop the player and other NPCs from falling through the boat) as “The highest vertex point is where everyone must stand now”, rather than going “just make sure everyone sits like a normal person on the geometry”, as you can see marked by the Red Arrow in the image below:
(I am aware the geometry is horrible. The goal is to make it work for the time being instead of going for a beauty contest in topology, xD)
Because of the point the red arrow in the image is pointing at, when the MeshCollider gets into use in the game, it turns into something like this:
(As you can see, everyone is now standing at the highest point of the mesh created by ProBuilder. You can’t move the player or do anything in this state)
Anyone knows how to solve this problem with ProBuilder? (I pray to god I don’t have to go to Blender, their UI after 2.8 made me want to run away, lol)


