Simple JS/UnityScript script for events

Hello, everyone!

I was looking around for possible JavaScript/UnityScript solutions for events that felt similar to C# events and delegates. Not finding much outside of a few mentions of frameworks (I didn’t look hard), I decided to try to make something simple myself as a personal challenge. The following class is the result of that and I was just wanting to see what people thought of it, even if other options already exist. First you’ll see the class, and then examples to how it compares to using C# events.

#pragma strict

/*
 * A simple JavaScript/UnityScript class that provides C#-style event functionality
 * using reflection.
 */
public class SimpleJsEvent extends Object {
	//A class (struct) to hold the information needed to register and fire an event
	private class SimpleJsEventInfo extends System.ValueType {
		var ObjectInstance:Object;
		var Method:String;
		var ObjectMethodInfo:System.Reflection.MethodInfo;
	}
	//The max number of arguments the registered function requires
	private var maxNumberArguments:int = -1;
	public function GetMaxNumberArguments() { return maxNumberArguments; }
	//Storage for the registered functions
	private var events:Array = new Array();
	
	/*
	 * Registers the function you wish to call when this event is fired. This will also
	 * ensure that the number of parameters for all functions registered match in length.
	 * The number of parameters is set when the first function is registered.
	 * PARAM: object (System.Object) - The object that contains the fuction you want to be called
	 * PARAM: method (String) - The name of themethod you want to be called
	 */
	public function RegisterEvent(object:Object, method:String) {
		//Get the method
		var methodInfo:System.Reflection.MethodInfo = object.GetType().GetMethod(method);
		//Get the number of parameters for the method.
		var numOfArgs = methodInfo.GetParameters().length;
		//Check to see if this is our first function registered. If so, assign the max number of params
		//to the number of parameters of the passed in method. If not and the numbers don't match,
		//throw and error.
		if(maxNumberArguments == -1) {
			maxNumberArguments = numOfArgs;
		} else {
			if(numOfArgs != maxNumberArguments) {
				throw "The number of parameters for " + object.GetType() + "." + method + " is invalid. Found " + numOfArgs + " where a number of " + maxNumberArguments + " is required.";
			}
		}
		//Create the info to be stored for later calling.
		var eventInfo:SimpleJsEventInfo;
		eventInfo.ObjectInstance = object;
		eventInfo.Method = method;
		eventInfo.ObjectMethodInfo = methodInfo;
		events.Add(eventInfo);
	}
	
	/*
	 * Unegisters the function you wish to no longer be called when this event is fired.
	 * PARAM: object (System.Object) - The object that contains the fuction you want to be unregistered
	 * PARAM: method (String) - The name of the method you want to be removed
	 */
	public function UnregisterEvent(object:Object, method:String) {
		var eventArray:SimpleJsEventInfo[] = events.ToBuiltin(SimpleJsEventInfo) as SimpleJsEventInfo[];
		for(var i=0; i < eventArray.length; ++i) {
			//If the current registered function's object and method are the same as the one's passed in,
			//remove it from the array.
			if(eventArray[i].ObjectInstance == object  eventArray[i].Method == method) {
				events.RemoveAt(i);
				break;
			}
		}
	}
	
	/*
	 * Fires the event if it requires no parameters.
	 */
	public function FireEvent() {
		this.fireEvent(null);
	}
	
	/*
	 * Takes in a JavaScript/UnityScript array and fire's the event, passing the
	 * parameters to the called function.
	 * PARAM: params (Array) - the parameters to be passed to each registered event
	 */
	public function FireEvent(params:Array) {
		this.fireEvent(params.ToBuiltin(Object) as Object[]);
	}
	
	/*
	 * Actually performs the event firing. Goes through all registered functions and
	 * invokes the method.
	 * PARAM: params (System.object[]) - The functions, as a "built in" array, to pass to Invoke
	 */
	private function fireEvent(params:Object[]) {
		var eventArray:SimpleJsEventInfo[] = events.ToBuiltin(SimpleJsEventInfo) as SimpleJsEventInfo[];
		for(var i=0; i < eventArray.length; ++i) {
			eventArray[i].ObjectMethodInfo.Invoke(eventArray[i].ObjectInstance, params);
		}
	}
}

EXAMPLES

C# using built-in events

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour {
	public delegate void ExampleDelegate();
	public event ExampleDelegate OnExample;
	public delegate void ExampleDelegateWithParams(int param1, string param2);
	public event ExampleDelegateWithParams OnExampleWithParams;

	void Update () {
		//Register the events
		if(Input.GetKeyDown(KeyCode.R)) {
			OnExample += LogText;
			OnExampleWithParams += LogTextWithParams;
			Debug.Log("---EVENTS REGISTERED");
		}
		//Fire the no param event
		if(Input.GetKeyDown(KeyCode.Space)) {
			if(OnExample != null){
				OnExample();
				Debug.Log("---OnExample FIRED");
			}
		}
		//Fire the event with params
		if(Input.GetKeyDown(KeyCode.F)) {
			if(OnExampleWithParams != null){
				OnExampleWithParams(42, "weeeeeeee!");
				Debug.Log("---OnExampleWithParams FIRED");
			}
		}
		//Unregister the events
		if(Input.GetKeyDown(KeyCode.U)) {
			OnExample -= LogText;
			OnExampleWithParams -= LogTextWithParams;
			Debug.Log("---EVENTS UNREGISTERED");
		}
	}

	private void LogText() {
		Debug.Log("Here is some text going into the logs!");
	}

	private void LogTextWithParams(int intParam, string stringParam) {
		Debug.Log("Here is some text going into the logs, but with some parameters! param1 = " + intParam + " param2 = " + stringParam);
	}
}

JS/US using SimpleJsEvent

#pragma strict

var OnExample:SimpleJsEvent = new SimpleJsEvent();
var OnExampleWithParams:SimpleJsEvent = new SimpleJsEvent();

function Update () {
	//Fire the no param event
	if(Input.GetKeyDown(KeyCode.Space)) {
		OnExample.FireEvent();
		Debug.Log("---OnExample FIRED");
	}
	//Fire the event with params
	if(Input.GetKeyDown(KeyCode.F)) {
		OnExampleWithParams.FireEvent(new Array(42, "weeeeeeee!"));
		Debug.Log("---OnExampleWithParams FIRED");

	}
	//Register the events
	if(Input.GetKeyDown(KeyCode.R)) {
		OnExample.RegisterEvent(this, "LogText");
		OnExampleWithParams.RegisterEvent(this, "LogTextWithParams");
		Debug.Log("---EVENTS REGISTERED");
	}
	//Unregister the events
	if(Input.GetKeyDown(KeyCode.U)) {
		OnExample.UnregisterEvent(this, "LogText");
		OnExampleWithParams.UnregisterEvent(this, "LogTextWithParams");
		Debug.Log("---EVENTS UNREGISTERED");
	}
}
	
function LogText() {
	Debug.Log("Here is some text going into the logs!");
}

function LogTextWithParams(intParam, stringParam) {
	Debug.Log("Here is some text going into the logs, but with some parameters! param1 = " + intParam + " param2 = " + stringParam);
}

Well done, I like it!
One remark - in C# language, events are one of few way that could lead to memory leaks, so it will be good practice to clear events:Array variable when SimpleJsEvent object will be destroyed.

us has seemed to evolved a bit since when i last used it. It seems to support delegates as proven by this code:

#pragma strict

function Start () {
	var test : System.Action.<int> = Jajaja;
	
	test(5);
	test(234);
}

function Jajaja (hi : int)
{
	Debug.Log(hi.ToString());
}

Use C# to define generic event types.

using System;

public struct SimpleEvent {
	public event Action Handle;
	public void Raise() { if (Handle != null) Handle(); }
}

public struct SimpleEvent<T1> {
	public event Action<T1> Handle;
	public void Raise(T1 t1) { if (Handle != null) Handle(t1); }
}

public struct SimpleEvent<T1, T2> {
	public event Action<T1, T2> Handle;
	public void Raise(T1 t1, T2 t2) { if (Handle != null) Handle(t1, t2); }
}

// etc

Use from UnityScript if you’re into that sort of thing.

#pragma strict

var myEvent : SimpleEvent.<int, String>;

function Start() {
	myEvent.Handle += DoStuff;
	myEvent.Raise(0, "Yay!");
}

function OnDestroy() {
	// Remove handlers registered on other, non-destroyed objects, eg:
	if (this) {
		this.myEvent.Handle -= DoStuff;
	}
}

function DoStuff(i : int, s : String) {
}

I’m actually planning to release a bunch of my utility code which includes events like these on the Unity store as free-to-use, free-to-distribute as soon as I get around to finishing the docs. If you want to download the semi-undocumented package you can find it here.

Thanks for the compliments and ideas! I had no idea there was a memory leak in C# events, and I’m glad you made me aware of that.

I also had no idea that you could do delegates like that in UnityScript, but thinking on how I’m doing my reflection it does make sense. After learning that I considered using it, but feel that the current approach kind of keeps to the idea of JS/US within Unity: flexibility. The only real drawback is if the coder isn’t keeping up with their parameters, but it will error on an invoke attempt if there are issues. I might put this on the store just for having it there (free, of course).