Search Unity

Alternative way to use GetComponent

Discussion in 'Scripting' started by Master-Frog, Jul 31, 2015.

  1. Master-Frog

    Master-Frog

    Joined:
    Jun 22, 2015
    Posts:
    2,302
    So I posted asking if I could use the ?? operator to achieve this, but @BenZed figured out a different way using an extension method and an anonymous delegate, but that required the "this" keyword... which annoyed me even in my sleep. So, I figured why not just make it a protected method. I can't stop thinking about how awesome this is going to make programming my next game.

    Basically what I wanted was the ability to create a code block that references the component internally, then forgets about the reference when it is finished without the need to declare a new variable or check for nulls. And this is it!!!

    Here's an example of using it:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TurnRed : MonoX {
    4.     void Start () {
    5.         With<Renderer> ((component) => {
    6.             component.material.color = Color.red;
    7.         });
    8.     }
    9. }
    Here's the base class:

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3.  
    4. public class MonoX : MonoBehaviour {
    5.     protected void With<T>(Action<T> action) where T : Component {
    6.         var component = GetComponent<T>();
    7.  
    8.         if (component) {
    9.             action(component);
    10.         } else {
    11.             Debug.Log("Component " + typeof(T).ToString() + " not found on " + this.name);
    12.         }
    13.     }
    14. }


    It might be a little shaky if someone didn't get/doesn't like the whole lambda thing, but the naming convention makes it so that anybody could use this abstraction easily, even copy and paste the first line and just change the names. The mind says, "Oh, if I just change the names this will work with any component." And it will. And you can do whatever you want in the space. So, it's honest. I think this is very nice.

    It fails without blowing up your code with a rogue null reference exception, but mentions that there's an issue...
     
    Last edited: Aug 2, 2015
  2. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    How would you expect this to work? I can imagine something but it's probably not what you had in mind. It requires some special planning in your design.

    If you always used GetComponent against an interface, you could use the null-coalescing operator to end up with either a real implementation of your component, or else a special case which you would have to create following the Null-Object pattern. It might look something like this:

    Code (csharp):
    1. interface IActionable
    2. {
    3.   void DoAction();
    4. }
    5. class SimpleAction : MonoBehaviour, IActionable
    6. {
    7.   void DoAction()
    8.   {
    9.     Debug.Log("Simple action complete!");
    10.   }
    11. }
    12. class NullAction : IActionable
    13. {
    14.   void DoAction()
    15.   { // don't do anything since this is "null" }
    16. }
    Then whenever you called GetComponent, you would use the null-coalescing operator to get a NullAction instead of null.

    Code (csharp):
    1. var actor = GetComponent<IActionable>() ?? new NullAction();
    2. actor.DoAction(); // don't need to worry about null exceptions. Hurray!
    Of course, you probably want to make a helper method for this so that GetComponents<T> can follow the same pattern.
     
    Last edited: Aug 1, 2015
    Master-Frog likes this.
  3. Master-Frog

    Master-Frog

    Joined:
    Jun 22, 2015
    Posts:
    2,302
    What about...

    Code (CSharp):
    1. (GetComponent<IActionable>() ?? new NullAction()).DoAction();
    Or am I just high on something.

    Where it does nothing except log "Component of 'type' not found" if it's not found...

    I think what I'm after is a combined if/assignment thing. Where, I can say in one line:

    IF THIS COMPONENT IS FOUND THEN ASSIGN IT TO THIS VARIABLE AND EXECUTE THE FOLLOWING CODE.

    Currently all I can think of is to capture the result of the GetComponent call in a variable and then do a null check on it. I absolutely detest that extra step.

    I guess you're right, I could make sure that the component itself has the method I want then there's no need for extra code blocks on the side of the object grabbing the component. I could literally just do what you described, and then each component could have a single action that they perform, and just have different components for different tasks. That would be sweet.
     
    Last edited: Aug 1, 2015
  4. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    Modern C# has a null-member check "?" Operator, where you can write:

    Code (CSharp):
    1. GetComponent<Action>()?DoAction();
    Eventually unity will update mono to its latest version and we'll get all the fun new c# sugar.

    For now, you might consider using Lambdas:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. public static class MonoBehaviour extensions {
    5.  
    6.      public static void DoIfComponent<T> (this MonoBehaviour mono, Action<T> action) where T : Component {
    7.      var component = mono.GetComponent<T>();
    8.      if (component)
    9.           action(component);
    10. }
    11.  
    12. }
    Then you can use it in a MonoBehaviour like any other extension method:

    Code (CSharp):
    1. //needs 'this' keyword, because extension method and not inherited
    2. this.DoIfComponent<MyComponent>(comp => comp.DoWhatever());
    3.  
    (Wrote this on my phone, sorry for bad format/errors)
     
    Master-Frog and eisenpony like this.
  5. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    The Null-Conditional, or Safe-Navigation operator for C# will be
    Code (csharp):
    1. ?.
    Code (CSharp):
    1. GetComponent<Action>()?.DoAction();
    Like @BenZed said, it will be a while before we get this feature in vanilla Unity.

    Actually, this operator basically operates on the same principle as the Null-Object pattern, it just creates the interface access and the special null object for free. Very nice..

    Sure, you can do this but it's common to assign the result of GetComponent<> to a variable incase we need to call more than one operation on it since there is a theoretical performance cost every time we use GetComponent<>.

    I'm not so sure this is valuable. That's basically what the NullReferenceException does already except the exception has the additional effect of preventing the script from continuing, which is probably a good thing if we aren't prepared to deal with the missing component.

    The assignment operator has a return value of the assigned value, so you can technically do this all in one line
    Code (CSharp):
    1. (var actor = GetComponent<IActionable>() ?? new NullAction()).DoAction();
    The method gets called and the variable actor ends up with the component. However, I like to draw an analogy between programming and solving algebra. After a bit of practice, you are confident enough to start skipping steps in your solutions. However, when you do this it is easy to make silly mistakes and it is much more difficult for a newbie, or even a capable colleague, to read your solutions. My advice is to do just one thing with each line of code, even though it makes for a little extra typing; clarity is king, and readability is one of the tenants of clean code. Don't worry about performance, extra lines of C# don't necessarily translate into extra lines of computer code: the optimizer is very tricksy. Focus on getting your algorithms as clear and as simple as possible.

    This, I don't quite get. There's no need for each component to have only a single method. However, it usually makes sense to split your components so they have only methods that work together to accomplish a single responsibility.
     
    Master-Frog and BenZed like this.
  6. Master-Frog

    Master-Frog

    Joined:
    Jun 22, 2015
    Posts:
    2,302
    Code (CSharp):
    1.     void Start() {
    2.         this.With<Camera>(c => c.enabled = false);
    3.     }
    23751.jpg

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3.  
    4. public static class Extensions {
    5.     public static void With<T> (this MonoBehaviour mono, Action<T> action) where T : Component {
    6.         var component = mono.GetComponent<T>();
    7.         if (component) {
    8.             action (component);
    9.         } else {
    10.             Debug.Log("Component " + typeof(T).ToString() + " not found on " + mono.name);
    11.         }
    12.     }
    13. }
    And that spits out this if the component isn't found...

    That's all I ever wanted from components but I couldn't figure out how to do it. Thank you.

    Now I can just work with components without the four step process of declaring a reference variable, assigning the return value of getcomponent to it, checking for a null reference, then writing the code I wanted to write all along.

    I generally have components that need to gather some state information upon creation, and I generally only need the reference during Awake or Start. Although this is so flexible I'm sure there will be tons of opportunities to use it down the line.

    Now I want to make a new class that derives from Mono, call it like MonoX and have With <T> as a normal method.

    Code (CSharp):
    1.     void Start() {
    2.         With<Rigidbody2D>(c => c.IsKinematic = true);
    3.     }
    And so I don't have to rename all the new scripts by hand I can change the default template for components in:

    C:\Program Files (x86)\Unity\Editor\Data\Resources\ScriptTemplates
     
    Last edited: Aug 2, 2015
    BenZed likes this.
  7. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    Extending monobehaviour for this purpose is a great idea!

    Now, I see you've logged a message if the component wasn't found. This certainly isn't wrong, but I feel like if you were using this method in Start, then you would already explicitly know if the Behaviour you're writing had the component or not, and would cache it:

    Code (CSharp):
    1. [RequireComponent(typeof(Rigidbody2D))]
    2. public class KinematicBody : MonoBehaviour {
    3.  
    4.     Rigidbody2D _body;
    5.     public Rigidbody2D body {
    6.         get {
    7.             return _body ?? (_body = GetComponent<Rigidbody2D>());
    8.         }
    9.     }
    10.  
    11.     void Start()
    12.     {
    13.         body.isKinematic = true;
    14.     }
    15.  
    16. }
    Our new With method, would be extremely handy if we could use it on any gameObject, so I'm thinking we should use a combination of Extensions Methods and Extended monobehaviour:

    Code (CSharp):
    1. //This is what I should have suggested in the first place. Rather than adding an extension method to MonoBehaviour,
    2. //we should add them to transform and gameObject. That way they'll be alongside GetComponent and AddComponent.
    3. public static class Extensions {
    4.    
    5.     public static T WithComponent<T> (this Transform transform, Action<T> action) where T : Component {
    6.         var component = transform.GetComponent<T>();
    7.         if (component)
    8.             action(component);
    9.  
    10.         return component;
    11.     }
    12.  
    13.     public static T WithComponent<T> (this GameObject gameObject, Action<T> action) where T : Component {
    14.         return gameObject.transform.WithComponent (action);
    15.     }
    16.    
    17. }
    And in our ExtendedMonobehaviour class, we can go the extra step and cache the component if we find it, so that subsequent calls to With don't always rely on GetComponent:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System.Collections;
    4. using System;
    5.  
    6. public class ExtendedMonoBehaviour : MonoBehaviour {
    7.  
    8.     Dictionary<Type, Component> cachedComponents = new Dictionary<Type, Component>();
    9.     public T WithComponent<T> (Action<T> action) where T : Component {
    10.         var type = typeof(T);
    11.         T cached = null;
    12.         bool contains = cachedComponents.ContainsKey (type);
    13.  
    14.         if (contains)
    15.             cached = cachedComponents[type] as T;
    16.  
    17.         //if the dictionary has the component and the component hasn't since been removed, we're all done!
    18.         if (contains && cached) {
    19.             action (cached);
    20.             return cached;
    21.         }
    22.  
    23.         T component = transform.WithComponent<T>(action);
    24.  
    25.         //If the component exists, we cache it into the dictionary
    26.         if (component)
    27.             cachedComponents [type] = component;
    28.         //if the component doesn't exist, but is in the dictionary, it must have been previously added and since destroyed
    29.         else if (contains)
    30.             cachedComponents.Remove (type);
    31.  
    32.         return component;
    33.     }
    34.  
    35. }
    36.  
    Now I didn't include a Debug message if the component doesn't exist, because I like the looseness of it. If I 100 % need the component to be there, I SHOULD be throwing errors. This would be useful for games in which objects might have certain components, and might not:

    Code (CSharp):
    1.     [SerializeField] int health = 100;
    2.    
    3.     void OnCollisionEnter2D (Collision2D other) {
    4.         //Imagine that the 'Sharp' component only holds a damage value.
    5.         //Some objects might be Sharp, some might not, so if you bump into
    6.         //them, you'll only be damaged if they are.
    7.         other.gameObject.WithComponent<Sharp> (s => health -= s.damage);
    8.        
    9.     }
    10.    
    11.     void Update() {
    12.         if (health <= 0) {
    13.             Debug.Log ("YER ALL SLICED UP THERE BUD :(");
    14.             Destroy (gameObject);
    15.         }
    16.     }
    Anyway, that's just my 2 cents. There is certainly nothing wrong with your implementation. (I called mine WithComponent so it matches GetComponent, AddComponent and is a little more descriptive. With is shorter and fine too)
     
    Master-Frog and landon912 like this.
  8. Master-Frog

    Master-Frog

    Joined:
    Jun 22, 2015
    Posts:
    2,302
    Interesting, so you're taking the cost out of repeated uses of With by caching the returned references... far out man.

    I don't always know which things have which components, I use the logging to help me sort errors and get my business straight. Sometimes I do drop a referenced component or mistakenly forget to add one. That's what that's about.

    As for With vs WithComponent... I'm partial to With. The reason being I name my components like nouns. To each his own!

    Very interesting code here, thanks very much.
     
    BenZed likes this.