Skip to content

Instantly share code, notes, and snippets.

@stramit
Last active November 21, 2018 06:36
Show Gist options
  • Save stramit/4549fa11780a2440c9c3 to your computer and use it in GitHub Desktop.
Save stramit/4549fa11780a2440c9c3 to your computer and use it in GitHub Desktop.
SpecialEventClass
/*
* When developing the UI system we came across a bunch of things we were not happy
* with with regards to how certain events and calls could be sent in a loosely coupled
* way. We had this requirement because with a UI you tend to implement widgets that receive
* certain events, but you don't really want to have lots of glue code to manage them
* and keep track of them. The eventing interfaces we developed helped with this. One of
* the interesting things, is that they are not justfor the UI system! You can use this as
* a type-safe, fast, and simple alternative to SendMessage (never use SendMessage!).
* So how does it all work?
* There are a few parts that hook together to form the eventing system.
* The definition of your Event Interface (needs to extent IEventSystemHandler):
public interface IEventSystemHandler
{}
public interface IPointerClickHandler : IEventSystemHandler
{
void OnPointerClick(PointerEventData eventData);
}
* What this does is define the function that will be called on the target. In
* this example we have the OnPointerClick function that is used by the UI system.
* This is implemented by a number of widgets we have. By implementing this interface
* you have defined a script that can receive the callback from the eventing system.
public class Button : Selectable, IPointerClickHandler
{
protected Button()
{ }
...
// implementation of the click handler
// this will be executed by the event system
public virtual void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
PressLogic();
}
}
* The next part is learning how to invoke the event on a target GameObject.
* This involves using some static functions on the ExecuteEvents class:
// starting at object root, walk the hierarchy upwards towards the scene root until
// a GameObject is found where one of the components implements T
public static GameObject GetEventHandler<T>(GameObject root)
where T : IEventSystemHandler
// Can any component on GameObject go handle event T
public static bool CanHandleEvent<T>(GameObject go)
where T : IEventSystemHandler
// Execute the given function on the target gameobject
// with given eventdata returns true if it was handled
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor)
where T : IEventSystemHandler
// walk the heirarchy (towards parent)
// when a GameObject executes the event
// return the GameObject that handled the event
public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction)
where T : IEventSystemHandler
* For the most part these functions are things for the UI system. If you look into
* the implementation of this (available on BitBucket) you can extend and add more
* utility. This is something we may or may not do in the future ourselves.
* https://bitbucket.org/Unity-Technologies/ui/src/ccb946ecc23815d1a7099aee0ed77b0cde7ff278/UnityEngine.UI/EventSystem/ExecuteEvents.cs?at=5.1
* This gives an overview of the functions you can use to invoke a method on
* a loosely coupled GameObject, but we also need to tell the system which function to invoke.
* In the EventModules of the UI system we have a number of different calls we issue,
* PointerOver, PointerClick ect. A standard call to invoke one of these looks like this:
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
* The last argument is a function pointer that we use and is defined like this:
private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
// validate event data converts the EventData from the type Base, to the expected type.
handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}
public static EventFunction<IPointerClickHandler> pointerClickHandler
{
get { return s_PointerClickHandler; }
}
* The reason we need this mapping layer is that our interface could potentially have
* MORE than one function, and we need to know how to invoke against the correct
* function. This mapping redirects the call to the correct later.
* Lets look at a custom example of how we can write a custom event handler and
* invoke it :)
*/
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// This is the interface which we will invoke against
///
/// It has two functions, one that takes an event data argument
/// and one that does not. We use the delegate binding code to
/// pick which function to invoke and with which arguments
/// </summary>
interface IRefreshHandler : IEventSystemHandler
{
void RefreshWithData(CustomEventData data);
void RefreshWithoutData();
}
/// <summary>
/// Custom event data to use when calling the Execute
/// function for the event system. In this example it's
/// pretty dumb and just has a simple public field.
/// </summary>
public class CustomEventData : BaseEventData
{
public float publicField { get; set; }
public CustomEventData() : base(null)
{}
}
/// <summary>
/// The MonoBehaviour that has the IRefreshHandler associated with it.
/// It has calls that get invoked when clicking a button on the screen.
///
/// The first uses a static delegate we have created.
/// The second uses a dynamically created delegate
/// </summary>
public class SpecialEventClass : UIBehaviour, IRefreshHandler
{
// Static cached delegate. Will be created once. Nice for the GC
// This DOES not need to exist in this scope and can be reused between
// any classes that implement the IRefreshHandler.
// It's just in this class now for convinience.
private static readonly ExecuteEvents.EventFunction<IRefreshHandler> s_RefreshWithData
= delegate(IRefreshHandler handler, BaseEventData data)
{
var casted = ExecuteEvents.ValidateEventData<CustomEventData>(data);
handler.RefreshWithData(casted);
};
/// <summary>
/// A button that when clicked sends two messages.
/// </summary>
private void OnGUI()
{
if (GUI.Button(new Rect(10, 10, 150, 100), "Execute Calls"))
{
// First call uses the cached delegate
var eventData = new CustomEventData {publicField = 5.0f};
ExecuteEvents.Execute(gameObject, eventData, s_RefreshWithData);
// anon delegate here
// nicer and simpler code, but bad for GC as each invoke it will do a 'new' internally to create the delegate
ExecuteEvents.Execute(gameObject, null, (IRefreshHandler handler, BaseEventData data) => handler.RefreshWithoutData());
}
}
// Interface implementation. Do your specific logic here
public void RefreshWithData(CustomEventData data)
{
Debug.Log(string.Format("Invoked RefreshWithData({0})", data.publicField));
}
public void RefreshWithoutData()
{
Debug.Log("Invoked RefreshWithoutData()");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment