Here's one way you can do this, using dot products to check how far the vehicle is from the center of the trigger's transform along the trigger's z axis (blue arrow in the local transform gizmo).
To perform well, the trigger should be at least one vehicle length thick, or thicker if your vehicles are very fast, so they don't tunnel past it in a single frame. Align the trigger so its local z+ axis points "forward" along the track.
public class Checkpoint : MonoBehaviour {
// Identifier for "this" part of the track - maybe you have a component for that?
public int trackSegment;
// Reference to checkpoint "behind" this one for when going backwards.
public Checkpoint previous;
// Track which side a vehicle was on when it entered the trigger.
List<Vehicle> _enteredFromRear;
List<Vehicle> _enderedFromFront;
// Gets the distance in world units along the checkpoint's z+ axis (blue arrow)
// that the vehicle is in front of the checkpoint (negative for behind).
float GetForwardDistance(Vehicle vehicle) {
var toVehicle = vehicle.transform.position - transform.position;
return Vector3.Dot(toVehicle, transform.forward);
}
// When a vehicle enters the trigger, record which side it entered from.
void OnTriggerEnter(Collider other) {
// Ignore collisions not from vehicles (use physics layers to exclude these completely)
if (!other.rigidbody.TryGetComponent(out Vehicle vehicle))
return;
// Ignore repeated entries from vehicles with multiple colliders.
if (_enteredFromRear.Contains(vehicle) || _enteredFromFront.Contains(vehicle))
return;
if (GetForwardDistance(vehicle) <= 0) {
_enteredFromRear.Add(vehicle);
} else {
_enteredFromFront.Add(vehicle);
}
}
// When a vehicle exits the trigger, check whether it left from the
// opposite side it entered, and update its track segment if so.
void OnTriggerExit(Collider other) {
// Again, filtering with physics layers saves redundant calls.
if (!other.rigidbody.TryGetComponent(out Vehicle vehicle))
return;
if (GetForwardDistance(vehicle) > 0) {
// If we left from the front, and entered from the back...
if (CheckCrossing(vehicle, _enteredFromRear, _enteredFromFront)
vehicle.OnEnterTrackSegment(trackSegment);
} else {
// If we left from the back, and entered from the front...
if (CheckCrossing(vehicle, _enteredFromFront, _enteredFromRear)
vehicle.OnEnterTrackSegment(previous.trackSegment);
}
}
// Checks whether vehicle came from the "cross" side (true)
// or the "reverse" side (false), and removes the vehicle from that list.
bool CheckCrossing(Vehicle vehicle, List<Vehicle> cross, List<Vehicle> reverse) {
int index = cross.IndexOf(vehicle);
if (index < 0) {
reverse.Remove(vehicle);
return false;
}
cross.RemoveAt(index);
return true;
}
}