Search Unity

Detecting which collider was hit?

Discussion in 'Scripting' started by Marscaleb, Sep 1, 2019.

  1. Marscaleb

    Marscaleb

    Joined:
    Jan 7, 2014
    Posts:
    1,037
    Let's say I have a game object with multiple colliders on it. Is there a way I can easily/efficiently check if one specific collider is hitting something?

    I suppose it would be possible to take the collision message and work through it to retrieve exactly where the collision occurred, and work some math through that to determine logically which collider must cave hit something. But that's a rather intensive process to go through.

    In fact, what I really want is to check if a specific collider is touching anything each tick. I'd like to just plug a variable with a reference to this collider, and every tick just check if that collider is touching another collider or not.

    Is this possible?
     
  2. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    One way I can see this done is by separating your specific collider onto a child GameObject, and giving this child object a small script that invokes a UnityEvent inside of OnCollisionStay. The UnityEvent can then call whatever method you want to use whenever its invoked on your parent or other GameObjects.

    For example, with a hierarchy structure like this:
    • GameObject (parent object with multiple colliders)
      • MainCollider (child object with one collider and a collision detection script)
    And the child object could have a script like this attached onto it:
    Code (CSharp):
    1. public class CollisionStayEvent : MonoBehaviour {
    2.    public UnityEvent collisionEvent;
    3.  
    4.    void OnCollisionStay(Collision other) => collisionEvent.Invoke();
    5. }
    If you want to pass the Collision parameter from OnCollisionStay however, you'll have to programmatically assign an event listener method, as UnityEvents with parameters do not show up in the inspector.
    In this case, the child script could look like this:
    Code (CSharp):
    1. public class CollisionStayCallback : MonoBehaviour {
    2.    public UnityEvent<Collision> collisionEvent;
    3.  
    4.    void OnCollisionStay(Collision other) => collisionEvent.Invoke(other);
    5. }
    And you would need to attach a script on your parent object that listens for this child object's event:
    Code (CSharp):
    1. public class ParentExample : MonoBehaviour {
    2.    public CollisionStayCallback childCallback;
    3.  
    4.    void Awake() => childCallback.collisionEvent.AddListener(Tick);
    5.  
    6.    void Tick() {
    7.       //Do things here while the main collider is touching somehting.
    8.    }
    9. }
     
  3. Marscaleb

    Marscaleb

    Joined:
    Jan 7, 2014
    Posts:
    1,037
    Hmm, that would work, and would be more efficient than the system I'm trying to replace. But it still seems inefficient. I'd need an extra game object for the system to track and another script to run more code every tick. There's an extra middle man in there using up resources that I feel like I should be able to bypass and just directly check. Is it not possible to run these functions for a single collider directly, instead of working with the entire compound collision hull of an object?

    Does it make a difference if I say this is actually for 2D colliders?
     
    VortexInCortex likes this.
  4. Emolk

    Emolk

    Joined:
    Feb 11, 2014
    Posts:
    241
    Couldn't you make a child object like Vry said, then check if that object is touching other objects through your main script?

    I.e

    Code (CSharp):
    1. gameObject.transform.GetChild(0).GetComponent<Collider2D>().IsTouching()
    Then parse the nearest gameObject/Collider2D into IsTouching() I

    Code (CSharp):
    1. gameObject.transform.GetChild(0).GetComponent<Collider2D>().IsTouching(nearestCollider)
    To find the closest object:

    Code (CSharp):
    1. public GameObject FindClosestEnemy()
    2.     {
    3.         GameObject[] gos;
    4.         gos = GameObject.FindGameObjectsWithTag("Enemy");
    5.         GameObject closest = null;
    6.         float distance = Mathf.Infinity;
    7.         Vector3 position = transform.position;
    8.         foreach (GameObject go in gos)
    9.         {
    10.             Vector3 diff = go.transform.position - position;
    11.             float curDistance = diff.sqrMagnitude;
    12.             if (curDistance < distance)
    13.             {
    14.                 closest = go;
    15.                 distance = curDistance;
    16.             }
    17.         }
    18.         return closest;
    19.     }
     
  5. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    There's no need for an extra GameObject.
    Simply add a reference to a collider to your collider script, and during collision check if it is this collider is the one you're looking for.
    You can drag and drop a specific collider by click and drag on the header for the collider in the inspector into your collider slot to pick the correct one.
     
  6. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    How/where would you compare colliders when detecting collisions?
    All of the "OnCollision..." and "OnTrigger..." callback functions only pass in the collider from the other GameObject that was hit, and not their self GameObject:
    Code (CSharp):
    1. void OnCollisionEnter(Collision other) {
    2.    //No self-collider reference to compare.
    3. }
    Perhaps this might be a feature request for Unity; to add an overload for these functions containing a reference to both the other GameObject's collider that was hit as well as the self GameObject's collider that was hit. Then something like this would be possible:
    Code (CSharp):
    1. public Collider mainCollider;
    2.  
    3. void OnCollisionEnter(Collision self, Collision other) {
    4.    if(self.collider == mainCollider) {
    5.       //etc...
    6.    }
    7.    else {
    8.       //etc...
    9.    }
    10. }
     
  7. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    Hmm. It's a bit reversed, indeed.
    You can get both(self & other) colliders looking through the ContactPoint in the Collision object.
    But that doesn't work for triggers.
     
    Bunny83 likes this.
  8. Marscaleb

    Marscaleb

    Joined:
    Jan 7, 2014
    Posts:
    1,037
    Thank you all for the help! You guys got me on the right track!
    Now that I've got the chance to actually sit in front of my computer and test things out, I've got it how I want it!

    Code (CSharp):
    1. public class Actor : MonoBehaviour
    2. {
    3.     public Collider2D GroundCol;
    4.     public LayerMask WhatIsGround;
    5.     // ...
    6.  
    7.     private void FixedUpdate()
    8.     {
    9.         bGrounded = GroundCol.IsTouchingLayers(WhatIsGround);
    10.        // ...
    11.     }
    I have a single edge collider just placed as a flat line on the bottom of my character, and when I drop its reference into the "GroundCol" variable, it actually does respond to ONLY THAT ONE collider. And that IsTouchingLayers gives me one simple bool to check if it is in contact with anything.

    This is so much more efficient than the system I have been using for years, ever since I first started using Unity. I used to have several children attached to my characters so I could use their transforms as positions to check a circle overlap, and I needed three different transforms just for checking if the player was standing on the ground, plus transforms for checking walls and ceilings...
    I am so glad this system works. I can't wait to update my old code.
     
    VortexInCortex, Dextozz and Vryken like this.
  9. VortexInCortex

    VortexInCortex

    Joined:
    Apr 13, 2019
    Posts:
    12
    Hello, do you think you have an idea how you would do this in 3d? There is no such IsTouchingLayer function in 3d and therefore I cannot figure out the logic to do this in 3d. I also would like to avoid children. Might just do a ray cast at this point.
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,938
    Usually for 3d it's easy to use just any of the Overlay physics functions to see if a particular shape is overlapping with the ground.
     
  11. khoa199960

    khoa199960

    Joined:
    May 5, 2020
    Posts:
    2
    I have one solution, that maybe solve this:
    Code (CSharp):
    1. [SerializeField] private Collider2D m_triggerA;
    2. [SerializeField] private Collider2D m_triggerB;
    3.  
    4. private bool m_isTriggedA = false;
    5. private bool m_isTriggedB = false;
    6.  
    7. protected void OnTriggerEnter2D(Collider2D collision)
    8. {
    9.     if (!m_isTriggedA && m_triggerA.IsTouching(collision))
    10.     {
    11.         m_isTriggedA = true;
    12.         Debug.Log("[Debug] A Enter!");
    13.     }
    14.  
    15.     if (!m_isTriggedB && m_triggerB.IsTouching(collision))
    16.     {
    17.         m_isTriggedB = true;
    18.         Debug.Log("[Debug] B Enter!");
    19.     }
    20. }
    21.  
    22. protected void OnTriggerExit2D(Collider2D collision)
    23. {
    24.     if (m_isTriggedA && !m_triggerA.IsTouching(collision))
    25.     {
    26.         m_isTriggedA = false;
    27.         Debug.Log("[Debug] A Exit!");
    28.     }
    29.  
    30.     if (m_isTriggedB && !m_triggerB.IsTouching(collision))
    31.     {
    32.         m_isTriggedB = false;
    33.         Debug.Log("[Debug] B Exit!");
    34.     }
    35. }
    This is an idea code from Cocos that I learned, and idea from you guy.
     
    Last edited: Mar 22, 2024