• Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
1
Question by Cirrocumulus · Jan 18, 2018 at 03:37 PM · scripting beginner

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.

Comment
Add comment · Show 9
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Harinezumi · Jan 19, 2018 at 10:29 AM 0
Share

I see that I vastly overreacted my response, but there was no code example when I started to write my answer (thanks for accepting it anyway).
In this situation ins$$anonymous$$d of checking for the "Asteroid" tag in WeaponBullet, I would get the AsteroidController and check if its null (you will need a GetComponent no matter what, but only once), and if not, then call on the AsteroidController a HandleHit() function. There you already know your own type, you know what is your phase, and you can even access the private parameters of the spawned asteroids directly!
Another great trick to avoid GetComponent after Instantiate is to have a reference to the prefab not as a GameObject, but as the type you want to use. So in your case you can have AsteroidController pfAsteroid, and then you just write
AsteroidController astControler = Instantiate(pfAsteroid, /* the rest is the same */);
You instantiate your prefab and get reference to its controller component at the same time.
Finally, I would encapsulate the adding of force to the spawned asteroids into a function, and then even if you call it from a different class you don't need GetComponent<Rigidbody>() anymore, because AsteroidController gets its own Rigidbody in Awake.

avatar image Cirrocumulus Harinezumi · Jan 19, 2018 at 10:53 AM 0
Share

Thank you for these comments. I have already reworked the classes to reflect some of the ideas in this thread, and it goes in the direction you mentioned. The AsteroidController class handles its own forces on instantiation, and also spawns new asteroids when hit, setting their phase appropriately. I am trying to get the Game $$anonymous$$anager to be as less God-like as possible (although I still need asteroids, enemies and the player to hold a reference to the manager, to notify it e.g. for scoring). In my current version it only instantiates the player and the starting asteroids, the rest is handled by the classes themselves.

I didn't know you could assign the returned object from Instantiate to a script class. Thanks a lot for that.

Concerning the tag comparison on the bullet script, I am indeed using both a tag comparison and GetComponent, like this:

     private void OnTriggerEnter2D(Collider2D collision)
     {
         if (collision.gameObject.tag == "Asteroid")
         {
             collision.gameObject.GetComponent<AsteroidController>().Break();
             Destroy(gameObject);
         }
     }

Are you suggesting I could avoid the tag comparison entirely, and ins$$anonymous$$d just use GetComponent of the AsteroidController type, and if it's null it means that the collision object was an Asteroid?

BTW, everything works really well now, with a menu screen and score count. I'm starting to incorporate successive levels, powerups, UFOs, sound... I'm still amazed that you can now create a working Asteroids clone in 24 hours!

P.S. Where would I post a corrected version of the scripts, if necessary? In a reply?

avatar image Harinezumi Cirrocumulus · Jan 19, 2018 at 11:07 AM 0
Share

What you describe sounds like a good approach.
Yes, I am suggesting to avoid checking the tag, because in my opinion if a game object has a specific script on it, then it's that kind of game object (specifically, if it has AsteroidController on it, then it is an asteroid). But I'm saying it if is NOT null, then it is an asteroid ( AsteroidController asteroid = collision.GetComponent(); if (asteroid != null) { ... } ).
About needing a reference onto the Game$$anonymous$$anager, if it is such a central object AND there is only one of it all the time, then it should be a singleton. One possible implementation of the singleton in Unity looks like this:

 public class Game$$anonymous$$anager : $$anonymous$$onoBehaviour {
     private static Game$$anonymous$$anager instance = null;
     public static Game$$anonymous$$anager Instance { get { return instance; } } // make sure no one calls this before Game$$anonymous$$anager.Awake() is called!
 
     private void Awake () {
         if (instance == null) { instance = this; }
         else {
             if (instance != this) { Destroy(gameObject); }
         }
 
         // do the rest of the things you need to do in Awake()
     }
 
     private void OnDestroy () {
         if (instance == this) { instance = null; }
     }
 
     // other functions
 
 }

About posting corrected version, you could edit your question, or post a reply and mark that as answer.

Show more comments
avatar image fafase · Jan 19, 2018 at 10:50 AM 0
Share

There is a difference between GetComponent every frame and GetComponent every time a specific action is called.

In your case, GetComponent is fine on creation of an item. Pooling will save some of it but it won't see a major different.

Though GetComponent is "slow" I would think it is quite fast compared to string search of GameObject for instance.

3 Replies

· Add your reply
  • Sort: 
avatar image
1
Best Answer

Answer by Harinezumi · Jan 18, 2018 at 04:03 PM

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 :D

Comment
Add comment · Show 6 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Cirrocumulus · Jan 18, 2018 at 04:08 PM 1
Share

Thank you very much for these ideas, which are unfortunately way over my head... I have some understanding of the basic building blocks of Unity & C#, but I'm not familiar at all with the concepts you mentioned (abstract classes and double-dispatch, which I'll look up soon I hope).

I understand perfectly, though, your remark about the "God object" which is something I'm trying to avoid. I understand that the whole idea of OOP is to let each object do its business and I find that a very helpful idea. I just can't find an elegant to make all those objects talk to each other efficiently (without using Send$$anonymous$$essage which seems like a bad idea).

avatar image Harinezumi Cirrocumulus · Jan 18, 2018 at 04:24 PM 0
Share

Sorry, I realise now that I went too far with high level solutions.
What I was trying to explain is the way to avoid everything going through a central object is to use interfaces that have functions for reacting to each other. In Unity, unfortunately, we have to use abstract base classes, because otherwise you cannot access the interface with GetComponent.
An abstract base class is just a class that has the abstract modifier in front of its name, and has unimplemented abstract methods. $$anonymous$$aking it abstract means that it cannot be instantiated, only classes that implement it can be instantiated. The classes that implement it have to define the abstract methods (or declare themselves abstract again), using the override modifier before the return type.

Either way, you have a good intuition to not use GetComponent too much, and to avoid god objects, so you should be fine.

avatar image Cirrocumulus Harinezumi · Jan 18, 2018 at 04:28 PM 0
Share

O$$anonymous$$ thanks a lot for your help!

BTW I also read here the following: "GetComponent is really not particularly slow. You should profile/benchmark first to be sure it's actually a problem rather than relying on misinformation." $$anonymous$$aybe GetComponents are simply unavoidable in Update(), at my program$$anonymous$$g level anyway.

Show more comments
avatar image
0

Answer by Hellium · Jan 18, 2018 at 04:30 PM

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!" );
 } 
Comment
Add comment · Show 1 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Cirrocumulus · Jan 18, 2018 at 04:35 PM 0
Share

Thanks a lot for the suggestions. I've been trying to get the Asteroid class to spawn copies of itself but couldn't get it to work, because I didn't want a spawning method both in the manager and in the asteroid. I'll try to incorporate some of the ideas I understand. BTW, the topmost suggestions about attaching objects in the Editor are things I'd rather avoid if I can, I prefer to do it via code and as long as it's in Awake or Start I don't $$anonymous$$d. It's wasting time in Update that bothers me. Thanks again.

avatar image
0

Answer by meat5000 · Jan 18, 2018 at 04:44 PM

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.

Comment
Add comment · Show 1 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Cirrocumulus · Jan 18, 2018 at 04:46 PM 0
Share

O$$anonymous$$, thanks a lot. I'm indeed trying to cache as much as possible.

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Welcome to Unity Answers

If you’re new to Unity Answers, please check our User Guide to help you navigate through our website and refer to our FAQ for more information.

Before posting, make sure to check out our Knowledge Base for commonly asked Unity questions.

Check our Moderator Guidelines if you’re a new moderator and want to work together in an effort to improve Unity Answers and support our users.

Follow this Question

Answers Answers and Comments

80 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

How to grab, store and reference an objects position on trigger? 1 Answer

Making a button appear on a specific state? 1 Answer

Can't seem to get dash cooldown working... 1 Answer

How to stop all StopAllCoroutines(); from another script 1 Answer

Is there a way to change the desktop icon from the game itself? 1 Answer


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges