Avoiding GetComponent calls

Hello,

I’m trying to figure out how to properly use inter-object communication in Unity. I’ve read (many times) that I should avoid using too many GetComponent calls at runtime. So I’m dealing with it basically by getting the components during Awake() and assigning what I need to variables.

However, it seems I’m doing something wrong because there are many occasions where I don’t see any other solution besides using GetComponent, even in my very simple 2D shooter.

Consider this: a Spaceship Controller script is instantiating a bullet every time the player fires (yes, I know about object pooling, I’ll do that later…), the bullet is using OnTriggerEnter2D to get whatever enemy it hits. This has to be reported to the GameManager script so the bullet script passes the object reference onwards to the GameManager. Now the GameManager has to do lots of stuff with that object, figure out what type it is, run a couple of methods attached to it, read some C# properties off it, so it has to have access to its scripts. But then I find myself using GetComponent all the time.

It’s the same if I have the “enemy” handle some of that instead of having GameManager take care of it, it just moves the problem elsewhere.

Is there a cleaner way to handling this?

e.g. my PlayerController script has this (simplified):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {

    public int RotationScaler = 5;
    public int ThrustScaler = 2;

    private Rigidbody2D rb;
    private SpriteSwitcher spriteSwitcher;

    public GameObject prefabWeaponBullet;
    private Transform anchorMainGun;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        spriteSwitcher = GetComponentInChildren<SpriteSwitcher>();
        anchorMainGun = transform.Find("AnchorMainGun");
    }
	
	void FixedUpdate ()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(prefabWeaponBullet, anchorMainGun.position, transform.rotation);
        }
	}
}

The bullet script has this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WeaponBullet : MonoBehaviour {

    public float speed = 1.0f;
    private Rigidbody2D rb;
    private GameManager gameManager;

    void Awake()
    {
        // Get the Rigidbody2D when instantiated
        rb = (Rigidbody2D)GetComponent(typeof(Rigidbody2D));
        gameManager = FindObjectOfType<GameManager>();
    }

	void Start ()
    {
        // Add forward impulse
        rb.AddRelativeForce(Vector2.up * speed * 10, ForceMode2D.Impulse);
	}

    void FixedUpdate()
    {
        // Destroy if left the screen
        Vector3 positionVP = Camera.main.WorldToViewportPoint(transform.position);
        if (positionVP.x > 1.0 || positionVP.x < 0 || positionVP.y > 1.0 || positionVP.y < 0)
        {
            Destroy(gameObject);
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Asteroid")
        {
            gameManager.HitAsteroid(collision.gameObject);
            Destroy(gameObject);
        }
    }
}

And the mess in the GameManager looks likes this for now:

public void HitAsteroid(GameObject go)
    {
        // Get phase of destroyed asteroid
        int phase = go.GetComponent<AsteroidController>().Phase;
        if (phase < 3)
        {
            SpawnAsteroid(go.transform.position, phase + 1);
            SpawnAsteroid(go.transform.position, phase + 1);
        }
        Destroy(go);
    }

    private void SpawnAsteroid(Vector2 pos, int phase)
    {
        float x = Random.Range(pos.x - 0.5f, pos.x + 0.5f);
        float y = Random.Range(pos.y - 0.5f, pos.y + 0.5f);
        float rot = Random.Range(0f, 1f);
        GameObject ast = Instantiate(pfAsteroid, new Vector2(x, y), new Quaternion(0, 0, rot, 0));
        AsteroidController astController = ast.GetComponent(typeof(AsteroidController)) as AsteroidController;
        astController.Phase = phase;
        Rigidbody2D rb = ast.GetComponent(typeof(Rigidbody2D)) as Rigidbody2D;

        float dirX = Random.Range(-1f, 1f);
        float dirY = Random.Range(-1f, 1f);
        rb.AddRelativeForce(new Vector2(dirX, dirY) * 20 * phase, ForceMode2D.Force);
        rb.AddTorque(Random.Range(-10, 10), ForceMode2D.Force);
    }

Thanks a lot.

This is the typical problem that object oriented design, more specifically separation of responsibility is trying to solve. Your GameManager class is doing too many things, it’s what is called a god object.

Instead, the solution is to create an abstract base class (ABC) that has some kind of abstract handling method that is overriden by each type that can collide, and call that from OnTriggerEnter2D. You won’t be able to eliminate all the GetComponent() calls, but at least you can greatly reduce their numbers.

public abstract class CollisionHandler : MonoBehaviour {
    public abstract void HandleCollision(GameObject other);
}

// your updated OnTriggerEnter2D:
private void OnTriggerEnter2D(Collider other) {
     CollisionHandler handler = other.GetComponent<CollisionHandler>();
     if (handler != null) { handler.HandleCollision(gameObject); }
}

Of course, now you have the problem that you don’t know the type of the caller, GameObject other, so you would need GetComponent() in your HandleCollision implementations… fear not, double-dispatch (or rather, a simulation of it using the visitor pattern) comes to the rescue:

EDIT: rewrote example that is easier to understand and actually works

public abstract class TriggerHandler : MonoBehaviour {

    private void OnTriggerEnter2D (Collider other) {
        TriggerHandler handler = other.GetComponent<TriggerHandler>();
        if (handler != null) { handler.DispatchEnter(this); } // calls DispatchEnter(TriggerHandler), with the base type
    }

    public abstract void DispatchEnter (TriggerHandler self);

    // declare an abstract method for each type of specific type that needs to handle triggering
    public abstract void Triggered (TriggerableA other);
    public abstract void Triggered (TriggerableB other);
    public abstract void Triggered (TriggerableC other);
}

public class TriggerableC : TriggerHandler {
   
    public override void DispatchEnter (TriggerHandler handler) {
        handler.Triggered(this); // calls Triggered( with the specific TriggerHandler type, because 'this' provides the actual type
    }

    public override void Triggered (TriggerableA a) { // the override keyword is important!
        // now we know the actual type of both variables, so we can call a specific function in 'a'
        a.HandleTriggerableC(this); 
    }

    public override void Triggered (TriggerableB b) {
        // you need to implement all versions (because of 'abstract'), but you don't need to do anything if the triggering doesn't make sense
    }

    public override void Triggered(TriggerableC c) {
        c.HandleTriggerableC(this); // can call private function, even though not 'this' and 'c' are not the same object, because they are the same class
    }

    public void HandleTriggerableB(TriggerableB a) { 
        // implement a public function if you need to handle triggering from a different class
        ... 
    } 

    // don't need to implement a version for TriggerableA if you don't handle that

    private void HandleTriggerableC(TriggerableC c) {
        // implement a private function if you need to handle triggering from a own class
        ... 
    }

}

EDITED So I corrected the original example (which was horribly wrong). The example makes even more sense if you replace the names with Player and Bullet (or better, Projectile), and PoweUp. Anyway, this is double-dispatch in a nutshell using the visitor pattern, it’s pretty fun :smiley:

I’m seeing a problem in the Intro that doesnt exist in your script. You completely avoid RunTime GetComponent calls by CACHING the reference to the script/component----- You have done this… All access to said object is then as fast as accessing variables in a local script.

An example of Runtime GetComponent calls which would consistute a problem would be, for example, making a repeat call to a particular variable in a script, in a loop or in Update. This would be intensive. I repeat…you are already not doing this…no problem here.

I see nothing really wrong with your scripts.

You can avoid calling GetComponent by using the inspector to add the references you need.

Here is some code with personnal modifications. Feel free to comment this answer if you want additional details:

// PlayerController.cs

 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class PlayerController : MonoBehaviour {
 
     public int RotationScaler = 5;
     public int ThrustScaler = 2;
 
     // Drag & Drop the rigidbody component in the inspector
     [SerializeField]
     private Rigidbody2D rb;
     
     // Drag & Drop the spriteSwitcher gameobject in the inspector
     [SerializeField]
     private SpriteSwitcher spriteSwitcher;
     
     // Drag & Drop the anchorMainGun gameobject in the inspector
     [SerializeField]
     private Transform anchorMainGun;
 
     public GameObject prefabWeaponBullet;
     
     void FixedUpdate ()
     {
         if (Input.GetKeyDown(KeyCode.Space))
         {
             Instantiate(prefabWeaponBullet, anchorMainGun.position, transform.rotation);
         }
     }
 }

 // WeaponBullet.cs
 
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class WeaponBullet : MonoBehaviour {
 
     public float speed = 1.0f;
     
     // Select your bullet prefab, and
     // drag & Drop its rigidbody component into the rb field
     [SerializeField]
     private Rigidbody2D rb;
 
     void Start ()
     {
         // Add forward impulse
         rb.AddRelativeForce(Vector2.up * speed * 10, ForceMode2D.Impulse);
     }
 
     void FixedUpdate()
     {
         // Destroy if left the screen
         Vector3 positionVP = Camera.main.WorldToViewportPoint(transform.position);
         if (positionVP.x > 1.0 || positionVP.x < 0 || positionVP.y > 1.0 || positionVP.y < 0)
         {
             Destroy(gameObject);
         }
     }
 
     private void OnTriggerEnter2D(Collider2D collision)
     {
         // Detect if you hit an asteroid by checking whether the appropriate script is attached
         AsteroidController asteroidController = collision.GetComponent<AsteroidController>();
         if (asteroidController != null)
         {
             // Indicate to the hit asteroid it has been hit and should break
             asteroidController.Break( this );
             Destroy(gameObject);
         }
     }
 }

 //AsteroidController.cs
 
using UnityEngine;

public class AsteroidController : MonoBehaviour
{
    public int Phase ;

    // Select your bullet prefab, and
    // drag & Drop its rigidbody component into the rb field
    [SerializeField]
    private Rigidbody2D rb;

    // Function to call when the asteroid is broken
    private UnityEngine.Events.UnityAction<AsteroidController> onBreak = null

    // Called by the bullet hitting the asteroid
    public void Break( WeaponBullet bullet )
    {
        if ( Phase < 3 )
        {
            Spawn( this, transform.position, Phase + 1, onBreak );
            Spawn( this, transform.position, Phase + 1, onBreak );
        }

        if ( onBreak != null )
            onBreak.Invoke( this );

        Destroy( gameObject );
    }

    // Spawning "children" asteroids should be handled by the Asteroid class i think
    public static AsteroidController Spawn( AsteroidController prefab, Vector2 pos, int phase, UnityEngine.Events.UnityAction<AsteroidController> onBreakCallback )
    {
        float x = Random.Range(pos.x - 0.5f, pos.x + 0.5f);
        float y = Random.Range(pos.y - 0.5f, pos.y + 0.5f);
        float rot = Random.Range(0f, 1f);
        AsteroidController asteroid = Instantiate(prefab, new Vector2(x, y), new Quaternion(0, 0, rot, 0));
        asteroid.Phase = phase;

        float dirX = Random.Range(-1f, 1f);
        float dirY = Random.Range(-1f, 1f);
        asteroid.rb.AddRelativeForce( new Vector2( dirX, dirY ) * 20 * phase, ForceMode2D.Force );
        asteroid.rb.AddTorque( Random.Range( -10, 10 ), ForceMode2D.Force );
        
        asteroid.onBreak = onBreakCallback ;
        
        return asteroid;
    }
}

 //GameManager.cs
 
 [SerializeField]
 private AsteroidController prefab;
 
void Start()
{
    AsteroidController asteroid1 = AsteroidController.Spawn( prefab, Vector3.zero, 5, onAsteroidBroken );
}

private void OnAsteroidBroken( AsteroidController brokenAsteroid )
{
    Debug.Log( "Hey, take some points!" );
}