Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

[SOLVED]CharacterController exits a collision

Discussion in 'Scripting' started by dart, Apr 23, 2010.

  1. dart

    dart

    Joined:
    Jan 9, 2010
    Posts:
    211
    Is there anyway to know when a CharacterController leaves a collision with a specific object? I'm using OnControllerColliderHit and OnCollisionExit to know when it enters and leaves the collision but only the first is called by the engine.

    I also tried to attach a script to the other body (not the CharacterController) with a collision test, but it's not working either. Does anyone know how to solve this or can point me to a topic where this is discussed?
     
  2. ippdev

    ippdev

    Joined:
    Feb 7, 2010
    Posts:
    3,792
    Post your code..
     
  3. DominoOne

    DominoOne

    Joined:
    Apr 22, 2010
    Posts:
    433
    The function OnCollisionExit is not a specific function for the CharacterController, and it says in the manual that: "collision events are only sent if one of the colliders also has a non-kinematic rigidbody attached". Since CharacterController is not a RigidBody (and the collider it hits probably has no RigidBody attached, too), the function OnCollisionExit is not called.

    I'm not sure, how to solve this, though. Do you somehow get all the objects that your CharacterController hits? Because I think OnControllerColliderHit returns only one of them, so if you hit a wall and also have gravity applied and therefore hit the ground, too, it'll always provide you only with the ground collider. I might be wrong, though.
     
  4. dart

    dart

    Joined:
    Jan 9, 2010
    Posts:
    211
    Thanks for your answers Blusters and Domino. I have 2 areas and I need to know the moment I leave one of them, even before touching the other. The way I solved the problem was using the collider and a trigger overlapped, then I used OnTriggerExit. Thanks again.
     
  5. C-Gabriel

    C-Gabriel

    Joined:
    Jan 27, 2016
    Posts:
    114
    Just in case someone else is looking for a solution to this, I've made my own implementation. Basically CharacterControllerCollisionEvents should be added to the CharacterController and then just implement ICharacterControllerCollisionListener EXPLICITLY on any component with a collider

    Code (CSharp):
    1. // example
    2. [DisallowMultipleComponent]
    3. [RequireComponent(typeof(Collider))]
    4. public class Example : MonoBehaviour, ICharacterControllerCollisionListener
    5. {
    6.     void ICharacterControllerCollisionListener.OnCharacterControllerEnter(ControllerColliderHit hit)
    7.     {
    8.         Debug.LogError("OnCharacterCollisionEnter");
    9.         Debug.DrawLine(hit.point, hit.point + hit.normal * 5f, Color.green);
    10.     }
    11.  
    12.     void ICharacterControllerCollisionListener.OnCharacterControllerExit(ControllerColliderHit hit)
    13.     {
    14.         Debug.LogError("OnCharacterCollisionExit");
    15.         Debug.DrawLine(hit.point, hit.point + hit.normal * 5f, Color.green);
    16.     }
    17.  
    18.     void ICharacterControllerCollisionListener.OnCharacterControllerStay(ControllerColliderHit hit)
    19.     {
    20.         Debug.LogError("OnCharacterCollisionStay");
    21.         Debug.DrawLine(hit.point, hit.point + hit.normal * 5f, Color.green);
    22.     }
    23. }


    Code (CSharp):
    1. /// <summary>
    2. /// Receives OnCharacterControllerEnter, OnCharacterControllerStay, OnCharacterControllerExit from CharacterControllerCollisionEvents
    3. /// ALWAYS IMPLEMENT THIS EXPLICITLY!
    4. /// </summary>
    5. public interface ICharacterControllerCollisionListener
    6. {
    7.     void OnCharacterControllerEnter(ControllerColliderHit hit);
    8.     void OnCharacterControllerStay(ControllerColliderHit hit);
    9.     void OnCharacterControllerExit(ControllerColliderHit hit);
    10. }

    Code (CSharp):
    1. /// <summary>
    2. /// Sends OnCharacterControllerEnter, OnCharacterControllerStay, OnCharacterControllerExit
    3. /// to all ICharacterControllerCollisionListener this CharacterController interacts with.
    4. /// </summary>
    5. [DisallowMultipleComponent]
    6. [RequireComponent(typeof(CharacterController))]
    7. public class CharacterControllerCollisionEvents : MonoBehaviour
    8. {
    9.     private class WrappedListener
    10.     {
    11.         private readonly WeakReference<ICharacterControllerCollisionListener> reference = null;
    12.         private ControllerColliderHit lastHit = null;
    13.  
    14.         protected ICharacterControllerCollisionListener value
    15.         {
    16.             get
    17.             {
    18.                 if (reference != null && reference.TryGetTarget(out var target))
    19.                 {
    20.                     // Check if target is a component and hasn't been destroyed
    21.                     // Unity doesn't immediately nullify destroyed objects, but throws exceptions when trying to access them
    22.                     return target is Component component && component
    23.                         ? target
    24.                         : null;
    25.                 }
    26.  
    27.                 return null;
    28.             }
    29.         }
    30.  
    31.         public WrappedListener(ICharacterControllerCollisionListener listener)
    32.         {
    33.             reference = new WeakReference<ICharacterControllerCollisionListener>(listener);
    34.         }
    35.  
    36.         public void OnEnter(ControllerColliderHit hit)
    37.         {
    38.             lastHit = hit;
    39.             if (hit != null && hit.controller)
    40.                 value?.OnCharacterControllerEnter(lastHit);
    41.         }
    42.  
    43.         public void OnStay(ControllerColliderHit hit)
    44.         {
    45.             lastHit = hit;
    46.             if (hit != null && hit.controller)
    47.                 value?.OnCharacterControllerStay(lastHit);
    48.         }
    49.  
    50.         // can be called from OnDestroy() or OnDisable()
    51.         public void OnExit(ControllerColliderHit hit = null)
    52.         {
    53.             lastHit = hit ?? lastHit;
    54.             if (hit != null && hit.controller)
    55.                 value?.OnCharacterControllerExit(lastHit);
    56.         }
    57.     }
    58.  
    59.     private CharacterController _characterController = null;
    60.     public CharacterController characterController => gameObject.CacheComponent(ref _characterController);
    61.  
    62.     private Dictionary<int, WrappedListener> oldCachedListeners1 = new();
    63.     private Dictionary<int, WrappedListener> oldCachedListeners0 = new();
    64.     private Dictionary<int, WrappedListener> currentFrameListeners = new();
    65.  
    66.     private int lastProcessedFrame = -1;
    67.  
    68.     private void OnControllerColliderHit(ControllerColliderHit hit)
    69.     {
    70.         IList<ICharacterControllerCollisionListener> listeners = hit.collider.GetComponents<ICharacterControllerCollisionListener>();
    71.  
    72.         if (lastProcessedFrame != Time.frameCount)
    73.         {
    74.             // new data for the current frame, so reset everything
    75.             lastProcessedFrame = Time.frameCount;
    76.  
    77.             // oldCachedListeners1 is used to compare against oldCachedListeners0 and send OnCollisionExit()
    78.             // this is required to make sure all collider interactions were cached within a frame
    79.             // we don't want to send OnCollisionExit() events to colliders from oldCachedListeners0 that maybe haven't been processed yet
    80.             oldCachedListeners1?.Clear();
    81.             oldCachedListeners1 = oldCachedListeners0.Clone();
    82.  
    83.             // currentFrameColliders is now "last frame colliders", depending on when Move()/SimpleMove() is called
    84.             oldCachedListeners0?.Clear();
    85.             oldCachedListeners0 = currentFrameListeners.Clone();
    86.  
    87.             // reset for new data
    88.             currentFrameListeners.Clear();
    89.         }
    90.  
    91.         foreach (var listener in listeners)
    92.         {
    93.             int hash = listener.GetHashCode();
    94.             if (!currentFrameListeners.ContainsKey(hash))
    95.                 currentFrameListeners.Add(hash, new WrappedListener(listener));
    96.  
    97.             if (oldCachedListeners0.ContainsKey(hash))
    98.             {
    99.                 // was interacting previous frame so call "Stay"
    100.                 oldCachedListeners0[hash].OnStay(hit);
    101.             }
    102.             else
    103.             {
    104.                 // was not interacting previous frame so it's new. cache it and call "Enter"
    105.                 oldCachedListeners0.Add(hash, currentFrameListeners[hash]);
    106.                 oldCachedListeners0[hash].OnEnter(hit);
    107.             }
    108.         }
    109.  
    110.         // call "Exit" for all listeners that are in oldCachedListeners1 but not in oldCachedListeners0
    111.         // also untrack them after calling "Exit"
    112.         List<int> exitColliderHashes = (
    113.             from hash in oldCachedListeners1.Keys
    114.             where !oldCachedListeners0.ContainsKey(hash)
    115.             select hash
    116.         ).ToList();
    117.  
    118.         foreach (var hash in exitColliderHashes)
    119.         {
    120.             oldCachedListeners1[hash].OnExit(hit);
    121.             oldCachedListeners1.Remove(hash);
    122.         }
    123.         exitColliderHashes?.Clear();
    124.     }
    125.  
    126.     private void OnDisable()
    127.     {
    128.         foreach (var keyValuePair in currentFrameListeners)
    129.             keyValuePair.Value?.OnExit();
    130.  
    131.         oldCachedListeners0?.Clear();
    132.         oldCachedListeners1?.Clear();
    133.         currentFrameListeners?.Clear();
    134.     }
    135. }
     
    Last edited: Apr 5, 2023
  6. RyanGarber

    RyanGarber

    Joined:
    May 20, 2021
    Posts:
    11
    One problem with this implementation is that OnControllerColliderHit() is not called when a collision 'stays' -- only if the previous Move() 'pushed into' the collider.