Tuesday, July 28, 2015

Unity Event

For years Unity developers were/are using ,which is very sad, thought by so called Unity gurus, hard reference wiring. As very useful type, especially for decoupling, async states, and proper user interface handling, events were introduced in U5. Ok better ever then never. Although Unity implemented events as support to new UI, they are more useful in many situations and not just button click.

 Create event without arguments:
//event with no arguments (wouldn't pass anything to handler function)
public UnityEvent myEvent=new UnityEvent(); //you must initialize
For custom Event with one to max four arguments that you can pass to handler function when event happen, you need to subclass one of 4 abstract classes public abstract class UnityEvent<t0>.
public abstract class UnityEvent<t0, t1>.
public abstract class UnityEvent<t0, t1, t2 >.
public abstract class UnityEvent<t0, t1, t2,t3>.

 [Serializable]//<-- don't foget this or you event wouldn't be drawn in Editor
 public class SequenceEvent:UnityEvent<int> // You subclass from UnityEvent<t1> where t1 is int cos you want to send "data" is  type int
}
Define custom events:
//events 
                public SequenceEvent OnStart = new SequenceEvent ();
                public SequenceEvent OnEnd = new SequenceEvent ();
Invoke/trigger/dispatch event:
OnStart.Invoke(55555);//data=5555 that will be send to event handler function
myEvent.Invoke();// no data will be sent
create c# delegate like syntax sugar:
public event UnityAction<sequence> MyCSharpLikeEvent {
                        add {
                                
                                                onStart.AddListener (value);
                        }
                        remove {
                              
                                                onStart.RemoveListener (value);
                        }
                }

Now you can bind event handler:
MyCSharpLikeEvent += MyOnStartEventHandler;
or in standard way:
       
                                
                                OnStart.AddListener (MyOnStartEventHandler);//you put your event handler function to listen for event happening
To remove handler => not to listen any more of event happening:
MyCSharpLikeEvent -= MyOnStartEventHandler;
//OR
OnStart.RemoveListener (MyOnStartEventHandler);
Example:
using UnityEngine.Events;
using UnityEditor;
using System;
using UnityEngine;



public class Sample : MonoBehaviour {
 

 
 public int data;

        public UnityEvent myEvent=new UnityEvent();

 //custom event with int argument
 public SequenceEvent OnStart = new SequenceEvent ();
 public SequenceEvent OnEnd = new SequenceEvent ();//another event defined
 
       
 

 

 void Awake(){
  //register handler to your events (what function will be execute when event happen)
  OnStart.AddListener (MyOnStartEventHandler);
 }
 
 // Use this for initialization
 void Start () {

  
  
 }

 void MyOnStartEventHandler(int receive){//handler function should take one argument of type int cos it is handling event which dispatch argument "ready" of type int
  Debug.Log ("Event happen data=" + receive);
 }

 
 // Update is called once per frame
 void Update () {
  
  OnStart.Invoke(data);//make the event happen !!! do this somewhere else cos this will dispatch events every update
  
 }

    void OnDestroy() {
        OnStart.RemoveListener(MyOnStartEventHandler);
    }
 
 
}

How you can use events to produced decoupled code? 


 For years Unity's top evangelists thought people doing tight couple coding and referencing. That is so wrong!!! In Survival Shooter Tutorial (Nightmare project) you can see tight couple Slider reference with Player health component.


UI, Slider in this case, shouldn't know what have triggered changes(player health) or how changes are produced, and should just listen for change event and know how to display them. Player shouldn't contain reference to Slider or to know about slider that is displaying health change, but just to inform system/world/application/game manager (dispatch event that player health was changed). Fastest solution is to create custom event subclassed from UnityEvent as variable in PlayerHealth: public PlayerChangeEvent onChange=new PlayerChangeEvent(); and trigger event inside PlayerHealth process loop. onChange.Invoke(currentHealthValue). Next what I saw was that Enemy keep hardcoded reference of  Player’s object component.
There is sometimes when hard coupling is OK, that is when you do fast prototype or bottleneck that critically slow down your game. Not to teach people as common pattern. Updates and extension as game development progress would require substantial amount of work, when code is structured like that.
and control Player taking damage from Enemy.
What seem logical is that Enemy script(Weapon script) should just do the Attack (move sword or shoot bullets) and shouldn't care of producing damage directly, by call other Entity PlayerHealth component script. Here comes events too.  EnemyScript dispatch event that something is hit and the damage should be applied. It doesn't care how that damage will be applied or how will be displayed....
....and so on. (see video where Survival shooter tutorial is redone in this manner)
DO IT RIGHT!


It is interesting that all Enemies and Player are subscribed to onHit event. So when player gun dispatch event of hitting something all Enemies and player itself would take damage. Not what we desire. This can be handle with "not for me pass" logic, but seem better solution to trigger event on target.
Extend UnityEvent so we can implement custom function InvokeOnTarget (invoke on handler subscribed from component in particular target=gameobject). Now only target hit will take damage.
/// 
 /// Unity event extended no args.
 /// 
 [Serializable]
 public abstract class UnityEventEx:UnityEvent {
  public delegate void PersistantCallDelegate();
  
  
  //substitute for m_Calls which is private in UnityEventBase
  private Dictionary<int,UnityAction> m_Calls;
  
  //substitute for m_PersistentCalls which is private in UnityEventBase
  private PersistantCallDelegate[] m_PersistentCallsDelegate;
  
  
  //
  // Constructors
  //
  public UnityEventEx():base(){
   m_PersistentCallsDelegate = new PersistantCallDelegate[this.GetPersistentEventCount()];
   m_Calls=new Dictionary<int, UnityAction>();
   
  }
  
  
  public new void AddListener (UnityAction call)
  {
   
   int id = (call.Target as MonoBehaviour).gameObject.GetInstanceID ();
   m_Calls [id] = call;
   
   
   base.AddListener (call);
  }
  
  
  
  
  public void InvokeOnTarget(GameObject target){
   
   UnityAction call;
   
   int id = target.GetInstanceID ();
   
   //try for runtime added listeners
   if(m_Calls.TryGetValue(id,out call)){
    
    call.Invoke();
   }else{//try persistant(editor) added listeners
    int listenerNumber = this.GetPersistentEventCount();
    
    if(listenerNumber!=m_PersistentCallsDelegate.Length)
     m_PersistentCallsDelegate=new PersistantCallDelegate[listenerNumber];
    
    MonoBehaviour listener;
    
     for (int i=0; i<listenerNumber; i++) {
     listener=this.GetPersistentTarget(i) as MonoBehaviour;
     
     
     if(listener.gameObject==target){
      
      PersistantCallDelegate callDelegate=m_PersistentCallsDelegate[i];
      
      if(callDelegate==null){
       MethodInfo methodInfo=listener.GetType().GetMethod(this.GetPersistentMethodName(i));
       
       //Call thru reflection is slow
       //methodInfo.Invoke(listener,new object[]{damage,null});
       
       m_PersistentCallsDelegate[i]=callDelegate=System.Delegate.CreateDelegate(typeof(PersistantCallDelegate),listener,methodInfo) as PersistantCallDelegate;
       
      }
      
      callDelegate.Invoke();
      
      
     }
    }
    
    
   }
  }
  
  public new void RemoveAllListeners (){
   m_Calls.Clear ();
   
   base.RemoveAllListeners ();
   
  }
  
  public new void RemoveListener (UnityAction call)
  {
   int id = (call.Target as UnityEngine.Object).GetInstanceID ();
   
   if (m_Calls.ContainsKey (id))
    m_Calls.Remove (id);
   
   base.RemoveListener (call);
  }
 }
INFO: In EditorWindow or custom drawing scenario, you can’t use one UnityEventDrawer for many event’s instances. You need to create drawer instances for every UnityEvent instance. UnityEventBase keeps 2 lists of UnityActions(calls), one persistent list (what you see in editor when you add handler) and runtime list - formed from persistent (Runtime and Editor+Runtime) moved to runtime + that dynamically assigned thru code  with AddListener.

7 comments:

  1. look very interessing you can help me to understand

    ReplyDelete
  2. not sure how call the event my fake try


    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine.Events;
    using UnityEditor;
    using UnityEditor.Events;
    using System;
    using System.Linq;
    using System.Reflection;


    public class fake : MonoBehaviour {

    public UnityEvent myEvent= new UnityEvent(); //you must initialize

    public int ready;

    [Serializable]//<-- don't foget this or you event wouldn't be drawn in Editor
    public class SequenceEvent:UnityEvent
    {

    public fake OnStart = new fake.SequenceEvent ();
    public fake OnEnd = new fake.SequenceEvent ();


    }

    public event UnityAction SequenceNodeStart {

    add {

    onStart.AddListener (value);
    }
    remove {

    onStart.RemoveListener (value);
    }
    //sequence.onStart += onSequenceStart;

    }

    /*
    public void onStart(){

    }*/

    // Use this for initialization
    void Start () {
    // OnStart.Invoke(Sequence);


    }
    //sequence.OnEnd.RemoveListener (onSequenceStart );
    //sequence.OnEnd.AddListener (onSequenceStart );

    // Update is called once per frame
    void Update () {

    myEvent.Invoke(ready);

    }


    }

    ReplyDelete
  3. cool

    OnStart.AddListener (MyOnStartEventHandler);
    OnStart.Invoke(ready);

    resquest the int value

    ReplyDelete
  4. yes i have missing somethink's about int value

    ReplyDelete
  5. yeah, the problem with editor displaying source code stripping < and > Sorry!
    [Serializable]//<-- don't foget this or you event wouldn't be drawn in Editor
    public class SequenceEvent:UnityEvent <int>// You subclass from UnityEvent<int> where T1 is int cos you want to send "ready" which is int
    {//if you want Event with 2 arguments => you will subclass from UnityEvent<T1,T2> T1 and T2 will be your types example <string,int>




    }

    ReplyDelete
  6. using UnityEngine.Events;
    using UnityEditor;
    using System;
    using UnityEngine;



    public class fake : MonoBehaviour {



    public int ready;

    //
    public SequenceEvent OnStart = new SequenceEvent ();
    public SequenceEvent OnEnd = new SequenceEvent ();//another event defined

    [Serializable]//<-- don't foget this or you event wouldn't be drawn in Editor
    public class SequenceEvent:UnityEvent<int>// You subclass from UnityEvent <int>where t1 is int cos you want to send "ready" which is int
    {




    }




    void Awake(){
    //register handler to your events (what function will be execute when event happen)
    OnStart.AddListener (MyOnStartEventHandler);
    }

    // Use this for initialization
    void Start () {



    }

    void MyOnStartEventHandler(int data){//handler function should take one argument of type int cos it is handling event which dispatch argument "ready" of type int
    Debug.Log ("Event happen data=" + data);
    }


    // Update is called once per frame
    void Update () {

    OnStart.Invoke(ready);//make the event happen !!! do this somehere else cos this will dispatch events every update

    }


    }

    ReplyDelete