GetComponent vs .NET

Your may have looked into our GetComponent tutorials and you now master the use of it. Then you may also have looked into our delegate tutorials and you are now a wizard of those. If you are in both situations, you know that one is not related to the other.

Well, it exists another programming feature that uses a bit of the second to replace the first, event. This is not Event as the Unity class that deals with input in the OnGUI method, this is event as the .NET feature.

In this tutorial, we will first look at an example using GetComponent then we will improve this example with more objects to finally achieve a way better system using event (at least better in my opinion). Note that as usual the following example could be done in many ways.

By the way this one is pretty short so enjoy.

Check my channel

Consider we have an object that needs to observe another object to trigger a method. For instance, Fan.cs is looking upon the Artist object for new release and if so, we call a method.

public class Artist: MonoBehaviour {
    public string artistName = "Bieber";
    List<GameObject>video = new List<GameObject>();
    public int VideoCount{get{return video.Count;}}
    public GameObject NewRelease() {
        return video.Last();
    }
}

Obviously there should be more methods to add video and so on but they are not relevant here. The property only returns the size of the list, the NewRelease method provides the last added material.

Then we have a Fan that is waiting for new releases, and he is so impatient that he checks in the Update because he wants to be the first.

public class Fan: MonoBehaviour {

    Artist myJustin;
    int prevCount;
    void Start() {
        myJustin = GameObject.Find("Bieber").GetComponent<Aritst>();
        prevCount = myJustin.VideoCount;
    }
    void Update() 
    {
        if(prevCount < myJustin.VideoCount)
        {
             NewVideo();
        }
    }
    void NewVideo() { 
        print("Oh my god, oh my god (slurp)...oh my GOOOOOOD!!"); 
    }
}

Well nothing too complex here.

If a new fan comes in, he just does the same thing and that works fine.

More of them coming

But then the same Fan is now after all kind of artists so we need to use a list system where all artists will inherits from a common class (inheritance).

public class Artist : MonoBehaviour
{
    public string artistName;
    protected List<GameObject> video = new List<GameObject>();
    public int VideoCount{get{return video.Count;}}
    public GameObject NewRelease() { return video.Last(); }
    private void AddingVideo(GameObject vid){
        video.Add(vid);
    }
}

Now our fan can create a list of artists and add/remove at will and check for new releases:

public class Fan : MonoBehaviour 
{
    List<Artist> artistList = new List<Artist>();
    List<int> prevCount = new List<int>();
    void ILikeYou(Artist artist)
    {
        artistList.Add(artist);
        prevCount.Add(artist.VideoCount);
    }
    void SoLastYear(Artist artist)
    {
        int index = artistList.IndexOf(artist);
        artistList.Remove(artist);
        prevCount.RemoveAt(index);
    }
    void Update() {
        for (int i = 0; i < artistList.Count; i++)
        {
            if (artistList[i].VideoCount > prevCount[i]) {
                NewVideo();
            }
        }
    }
    void NewVideo() { 
        print("Oh my god, oh my god,(slurp)...oh my GOOOOOOD!!"); 
    }
}

This works again but hold on for trouble in a moment.

Subscribe to my channel

There is a way to revert the process, instead of waiting for the Artist to inform you (via the increase of video count) he could just let anyone knows that he did directly.

We can do that with event. We declare an event, the subscribers subscribe (Mister Obvious?) and when something happens in the Artist, anyone registered will know. They do not need to constantly check, they are just pending on information.

In order to declare an event, we first need to declare a delegate type, events are somehow a subtype of delegate, not a subtype as in inheritance but in the way they work. With delegate you can add, remove, clear and call methods, with event a class can only add, remove its own members.

Example: Artist has a public delegate, Fan A and Fan B register their own methods to that delegate. Fan B decides to wipe it all out and only assigns one of its members. It is possible and Fan B is not aware that its methods have been removed from the delegate (Artist is not aware that only one fan is registered). This may be an intended action or simply an error:

public class ClassA{
    public delegate void MyDel();      // Declare delegate
    public static MyDel myDel;         // Create delegate
}

public class FanB{
    void AddToDelegate(){
       ClassA.myDel += MyMethod; // Add to multicast delegate
    }
    void MyMethod(){}
}

public class FanC{
    void AddToDelegate(){
        ClassA.myDel = MyMethod; // Wipes it all and adds only its own
    }
    void MyMethod(){}
}

Now the same with event:

public class ClassA{
    public delegate void MyDel();      // Declare delegate
    public static event MyDel myDel;   // Create an event instance
    // You can also use the generic version
    public event Action OnSomethingHappen = ()= >{};
}

public class ClassB{
    void AddToDelegate(){
        ClassA.myDel += MyMethod;  // Add to event
    }
    void MyMethod(){}
}

public class ClassC{
    void AddToDelegate(){
        ClassA.myDel = MyMethod; // Error = cannot be used with Event
    }
    void MyMethod(){}
}

The equal sign is not allowed with event, which means you can only use += and -=. As a result, a class can only subscribe itself to the event but cannot control it (as in wiping other subscription), the event can only be called from the class it was created in.

public delegate void EventHandler(IStar sender);
public interface IStar
{
    event EventHandler OnNewRelease;
    GameObject NewRelease();
}

public class Artist : MonoBehaviour, IStar{
    private string artistName;
    public event EventHandler OnNewRelease = new EventHandler(
           delegate (IStar s){});
    protected List<GameObject> video = new List<GameObject>();
    public GameObject NewRelease() { return video.Last(); }

    private void AddingVideo(GameObject vid)
    {
        video.Add(vid);
        OnNewRelease(this);
    }
}

I decided to place the delegate declaration as global and the event and one method in an interface. Then the class just implement the interface which only contains our event and the method to retrieve the item. Adding video simply adds the video to the list and call the event.

You may wonder what is this little trick:

public event EventHandler OnNewRelease = new EventHandler(
           delegate (IStar s){});

Well the left part of it is just the instantiation of the event object, the right part is meant to avoid a null reference exception. When you call the event, there may be one, many or even no subscriber at the moment of call. If there is none, bam you get the null reference and it crashes. To avoid this, you would have to check the event for nullity or place in  a try/catch section. What is done there is placing an empty method in the event which will always be called anyway hence avoiding the null reference. You may think that a check for if is faster than the call of a method, true, but if you get use to this pattern it gets safer.

This example uses a anonymous delegate keyword, if you feel real lazy you can use lambda :

public event EventHandler OnNewRelease = new EventHandler(
           (IStar s)=>{});

The delegate keyword is replaced by the =>. This is read Goes to  as in IStar s goes to…nothing in this case (there are discussions on how this should be pronounced, some say such as or maps to).

On our other side we have the fan:

public class Fan : MonoBehaviour {
   List<IStar>starList = new List<IStar>();
    void NewRelease (IStar a) 
    {
        print(a.Name+"has a new video");
        // this does not exist in Unity
        // I just assume there would be some kind of video streaming
        Video.Play(a.NewRelease());
    }
    void AddArtist(IStar a)
    {
        starList.Add(a);
        a.OnNewRelease += NewRelease; // Subscribe
    }
    void IDontLikeYou(Artist a) 
    {
        starList.Remove(a);
        a.OnNewRelease -= NewRelease; // Unsuscribe
    }
    void OnDestroy()
    {
        for(int i = 0; i < starList.Count; i++)
        {
            IStar star =  starList[i];
            if(star == null)
            {
                continue;
            }
            star.OnNewRelease -= NewRelease;
        }
        Destroy(gameObject);
    }
}

See how simple it is getting. I agree it might still be blurry but give it a try. The event is just a hook onto which the subscriber are holding. When the hook is pulled, all subscribers get a shake and know something has happened. Compare this to your Facebook wall, you are subscriber to your friend and they are of you, so when they add something to their wall, you are informed of it on yours and vice-versa. But see that you can do some actions like commenting, but you cannot remove the status, you cannot modify it and you cannot remove or add other subscriber to it. Only can you listen to it. This is what event is all about, listening.
The OnDestroy method makes sure you remove the listener from the event. It first checks the star reference to make sure the star is still alive and remove itself from the subscription.

But now consider we want to follow other types of people like politicians, the only solution we have is either to make them as artists or to create a new type but if the project is already shipped, you are going through update batches…
Nope!!! We made the event an interface and we shall use that for any type of objects. All we need to do is make the new type implement that interface and we can add to the list:

public class Politician : MonoBehaviour, IStar
{
    private string politicianName;
    public string Name { get { return politicianName; } }
    public event EventHandler OnNewRelease =new EventHandler(
           delegate (IStar s){});
    protected List<GameObject> speech= new List<GameObject>();
    private void Empty(Artist a) { }
    public GameObject NewRelease() { return speech.Last(); }

    private void AddingVideo(GameObject vid)
    {
        video.Add(vid);
        OnNewRelease(this);
    }
}

And our fan can also listen to that provider.

Reusability

To finish we will change the topic and get our attention towards reusability which will make your employer happy. Reusability is the ability for a class or a component to be reused in any case without modification. Think of all Unity custom component like Rigidbody are extremely reusable since they fit for all kind of purpose with little tweaks.

Of course the more reusable a component is, the less it fits to a specific case or the class needs to suit most cases and belong really long and repetitive with many overloads.

Now let’s consider to develop a 2D input system. In this class we want to get the control from the old console, that is a crosspad and two action buttons.

If you were to use GetComponent you would probably go for something looking like this:

using UnityEngine;
using System.Collections;

public class Input2D : MonoBehaviour 
{

    Rect padUp = new Rect();
    Rect padDown = new Rect();
    Rect padLeft = new Rect();
    Rect padRight = new Rect();

    Rect buttonA = new Rect();
    Rect buttonB = new Rect();

    SomeManager manager;
    void Start(){
         manager = GetComponent<SomeManager>();
    }
    void Update () {
        for (int i=0; i < Input.touchCount; i++) {
            Touch touch = Input.GetTouch(i);
            if ((touch.phase != TouchPhase.Canceled) && (touch.phase != TouchPhase.Ended)){
                float directionUp = 0f;
                float directionRight = 0f;
                Vector2 position = new Vector2(touch.position.x, Screen.height - touch.position.y);
                if (padDown.Contains(position)) { directionUp = -1f; }
                if (padUp.Contains(position)) { directionUp = 1f; }
                if (padLeft.Contains(position)) { directionRight = -1f; }
                if (padRight.Contains(position)) { directionRight = 1f; }
                manager.MoveMethod(directionUp, directionRight);

                if (buttonA.Contains(position))
                {
                    manager.ActionA();
                }
                if (buttonB.Contains(position))
                {
                    manager.ActionB();
                }
            }
        }
    }
}

It is fine with me but next game you use this, you have to use the same SomeManager.cs with the same method names which is also totally fine. But with event, it is even easier as you do not need to care about what is about to use the Input2D class, I mean it does not have to be called the same SomeManager.cs with the same set of methods. You could see it the other way, you update the Input2D class instead with the new naming.

Or you use event:

using UnityEngine;
using System.Collections;

public class Input2D : MonoBehaviour 
{

    Rect padUp = new Rect();
    Rect padDown = new Rect();
    Rect padLeft = new Rect();
    Rect padRight = new Rect();

    Rect buttonA = new Rect();
    Rect buttonB = new Rect();

    public delegate void EventHandlerMove(float up, float right);
    public delegate void EventHandlerAction();
    public static event EventHandlerMove OnMove = new EventHandlerMove((float up, float right) => { });
    public static event EventHandlerAction OnActionA = new EventHandlerAction(()=> { });
    public static event EventHandlerAction OnActionB = new EventHandlerAction(() => { });

    void Update () {
        for (int i=0; i < Input.touchCount; i++) {
	    Touch touch = Input.GetTouch(i);
            if ((touch.phase != TouchPhase.Canceled) && (touch.phase != TouchPhase.Ended)){
                float directionUp = 0f;
                float directionRight = 0f;
                Vector2 position = new Vector2(touch.position.x, Screen.height - touch.position.y);
                if (padDown.Contains(position)) { directionUp = -1f; }
                if (padUp.Contains(position)) { directionUp = 1f; }
                if (padLeft.Contains(position)) { directionRight = -1f; }
                if (padRight.Contains(position)) { directionRight = 1f; }
                OnMove(directionUp, directionRight);

                if (buttonA.Contains(position))
                {
                    OnActionA();
                }
                if (buttonB.Contains(position))
                {
                    OnActionB();
                }
            }
        }
    }
}

Now any class can be used regardless of their name or intention. It can be a car or a zombie killer, you just subscribe to the events and wait for them to call up:

public class Car : MonoBehaviour 
{

    void Start() 
    {
        Input2D.OnMove += UsePadInfo;
        Input2D.OnActionA += Brake;
        Input2D.OnActionB += Speed;
    }
    void UsePadInfo(float up, float left) 
    {
        Turn(left);
    }
    void Turn(float steer)
    {
        // Do some actions
    }
    void Speed(){
        // Move the car
    }
    void Brake() { 
        // Stop the car
    }
}
public class Character : MonoBehaviour
{

    void Start()
    {
        Input2D.OnMove += MoveCharacter;
        Input2D.OnActionA += Jump;
        Input2D.OnActionB += Shoot;
    }
    void MoveCharacter(float up, float left)
    {
        // Do stuff with movement
    }
    void Jump()
    {
        // Jump
    }
    void Shoot()
    {
        // Shoot
    }
}

Those classes would come from totally independent games and will use the same Input system without any modifications. The car actually discard the info concerning the up/down while the character uses everything. See how easy it is?! And your boss is happy as you saved some valuable time. With a proper documentation anyone is ready to use that Input2D with no hassle.

Conclusion

Hopefully this has been helpful. That shows how event can make life a little bit easier. Note that GetComponent could have done the job, it would just have got a little more tedious in my own opinion.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s