-1

I'm creating a touch manager, which fires Began and Ended events and other monobehaviours can subscribe to them. For some reason I am unable to find any information on creating a list of events actions.

This is an illustration of what I currently have:

public class TouchHandler : MonoBehaviour {

    private static readonly int MAX_TOUCH_COUNT = 2;

    public event Action<int> Began;
    public event Action<int> Ended;

    private void Update() {
        if (Input.touchCount > 0) {
            Touch[] touches = Input.touches;

            int touchCount = Mathf.Min(touches.Length, MAX_TOUCH_COUNT);

            for (int i = 0; i < touchCount; i++) {
                Touch t = touches[i];

                if (t.phase == TouchPhase.Began)
                    Began?.Invoke(i);
                else if (t.phase == TouchPhase.Ended)
                    Ended?.Invoke(i);
            }
        }
    }
}

public class Listener : MonoBehaviour {

    [SerializeField] private TouchHandler th;

    private int touchIndexToListen = 0;

    private void OnEnable() {
        th.Began += TouchBegan;
        th.Ended += TouchEnded;
    }

    private void OnDisable() {
        th.Began -= TouchBegan;
        th.Ended -= TouchEnded;
    }

    private void TouchBegan(int index) {
        if (index != touchIndexToListen)
            return;

        Debug.Log("TouchBegan");
    }

    private void TouchEnded(int index) {
        if (index != touchIndexToListen)
            return;

        Debug.Log("TouchEnded");
    }
}

This is how I'd want it to work:

public class TouchHandler : MonoBehaviour {

    private static readonly int MAX_TOUCH_COUNT = 2;

    public event Action[] Began = new Action[MAX_TOUCH_COUNT];
    public event Action[] Ended = new Action[MAX_TOUCH_COUNT];

    private void Update() {
        if (Input.touchCount > 0) {
            Touch[] touches = Input.touches;

            int touchCount = Mathf.Min(touches.Length, MAX_TOUCH_COUNT);

            for (int i = 0; i < touchCount; i++) {
                Touch t = touches[i];

                if (t.phase == TouchPhase.Began)
                    Began[i]?.Invoke();
                else if (t.phase == TouchPhase.Ended)
                    Ended[i]?.Invoke();
            }
        }
    }
}

public class Listener : MonoBehaviour {

    [SerializeField] private TouchHandler th;

    private int touchIndexToListen = 0;

    private void OnEnable() {
        th.Began[touchIndexToListen] += TouchBegan;
        th.Ended[touchIndexToListen] += TouchEnded;
    }

    private void OnDisable() {
        th.Began[touchIndexToListen] -= TouchBegan;
        th.Ended[touchIndexToListen] -= TouchEnded;
    }

    private void TouchBegan(int index) {
        Debug.Log("TouchBegan");
    }

    private void TouchEnded(int index) {
        Debug.Log("TouchEnded");
    }
}

So in short: I want to subscribe only to specific touch indexes, which also helps me get rid of the if (index != touchIndexToListen) check in other monobehaviours.

The reason I'm doing this touch manager is because I want a clean way to handle mouse input using the same class and I need to be able to fire the ended event when app is minimized in Android. I've left those parts out of this illustration, since they are irrelevant to this question.

Crossoni
  • 193
  • 1
  • 10
  • 1
    What would that be good for? What exactly are you really trying to achieve? To an `event` anyway multiple listeners can be attached to somewhat do you need the array for? Simply pass in the index as argument and check on the receiver side .. – derHugo Apr 29 '21 at 06:43
  • @derHugo Well the main reason why I want this is to get rid of the `if (index != touchIndexToListen)` check. I would also guess that it does have some performance impact, if for example my MAX_TOUCH_COUNT is at 7 and some monobehaviour's function gets called 6 times for no reason when it only wants to subscribe to a single finger index. – Crossoni Apr 29 '21 at 07:00

2 Answers2

1

You can use EventHandlerList for subscribing multiple kind of event in same object.

Be aware that keys are references, so passing int there and retrieving it by value will not work. You must save object references somewhere to be able to fire specific events.

Simple purpose demo app here:

class Program
{
    static void Main(string[] args)
    {
        EventHandlerList _eventList = new EventHandlerList();
        
        var mouseHandler = new MouseHandlerClass();
        var appStateHandler = new AppStateHandlerClass();

        //attach mouse handlers
        _eventList.AddHandler(mouseHandler, (Action<int>)mouseHandler.DoSomething);
        _eventList.AddHandler(mouseHandler, (Action<int>)mouseHandler.DoAnotherThing);

        //attach appState handlers
        _eventList.AddHandler(appStateHandler, (Action<int>)appStateHandler.DoSomething);

        //fire mouse event
        (_eventList[mouseHandler] as Action<int>).Invoke(99);

        //fire appState event
        (_eventList[appStateHandler] as Action<int>).Invoke(52);
    }
}

public class MouseHandlerClass
{
    public void DoSomething(int input)
    {
        Console.WriteLine($"MouseHandler processed: {input}");
    }

    public void DoAnotherThing(int input)
    {
        Console.WriteLine($"MouseHandler processed another thing: {input}");
    }
}

public class AppStateHandlerClass
{
    public void DoSomething(int input)
    {
        Console.WriteLine($"AppStateHandlerClass processed: {input}");
    }
}

Edit


The handler method signature can be simplified to Action if you don't need to pass any parameters to attached handlers
Tomas Chabada
  • 2,869
  • 1
  • 17
  • 18
  • As I understood OP though he doesn't want to get the `int` as parameter (then you could just use his first solution with a simple `event Action`) .. OP rather wants to only receive the event callback if it is for the correct index in the first place ;) – derHugo Apr 29 '21 at 07:34
  • @derHugo Ok, then the method signature can be simplified to `Action` only – Tomas Chabada Apr 29 '21 at 07:40
1

First of all one important note: The i used for touches[i] is NOT guaranteed to refer to the same Touch across multiple frames!

For that you rather want to use the Touch.fingerId

The unique index for the touch.

All current touches are reported in the Input.touches array or by using the Input.GetTouch function with the equivalent array index. However, the array index is not guaranteed to be the same from one frame to the next. The fingerId value, however, consistently refers to the same touch across frames. This ID value is very useful when analysing gestures and is more reliable than identifying fingers by their proximity to previous position, etc.

Touch.fingerId is not the same as "first" touch, "second" touch and so on. It is merely a unique id per gesture. You cannot make any assumptions about fingerId and the number of fingers actually on screen, since virtual touches will be introduced to handle the fact that the touch structure is constant for an entire frame (while in reality the number of touches obviously might not be true, e.g. if multiple tappings occur within a single frame).

The array makes no sense actually. event can only be used for a single delegate (Action is just a shorthand for a delegate void Action();) it can't be used for an Action[].

You actually can store a single Action for each index and then use the += and -= operators on it.

You could go a complete different way and use a pattern like

public class TouchHandler : MonoBehaviour
{
    private const int MAX_TOUCH_COUNT = 2;

    // Store callback Actions by phase and index
    // NOTE: I would still pass in the actual Touch in order to be able to use the information in it
    private readonly Dictionary<TouchPhase, Dictionary<int, Action<Touch>>> _touchActions = new Dictionary<TouchPhase, Dictionary<int, Action<Touch>>>
    {
        {TouchPhase.Began, new Dictionary<int, Action<Touch>>()},
        {TouchPhase.Ended, new Dictionary<int, Action<Touch>>()}
    };

    // I would keep one where the listeners can still check "manually"
    public event Action<Touch> generalTouchEvent;

    public void Subscribe(TouchPhase phase, int touchIndex, Action<Touch> action)
    {
        if (touchIndex < 0 || touchIndex > MAX_TOUCH_COUNT)
        {
            Debug.LogError($"Touch index {touchIndex} is not supported!", this);

            return;
        }

        // First get the according inner dictionary by TouchPhase
        if (!_touchActions.TryGetValue(phase, out var touchPhaseActions))
        {
            Debug.LogError($"Touch phase {phase} is not supported!", this);

            return;
        }

        // Next check if there already is an action for given index
        if (touchPhaseActions.TryGetValue(touchIndex, out var touchAction))
        {
            // If it already exists we append the new action to the existing one

            // Unsubscribing is fine even if it wasn't added before
            // This just makes sure it is only added once
            touchAction -= action;
            touchAction += action;
        }
        else
        {
            // otherwise this new action is the only one for now
            touchAction = action;
        }

        // (over)write it back into the dictionary
        touchPhaseActions[touchIndex] = touchAction;
    }

    public void UnSubscripe(TouchPhase phase, int touchIndex, Action<Touch> action)
    {
        if (touchIndex < 0 || touchIndex > MAX_TOUCH_COUNT)
        {
            Debug.LogError($"Touch index {touchIndex} is not supported!", this);

            return;
        }

        // First get the according inner dictionary by TouchPhase
        if (!_touchActions.TryGetValue(phase, out var touchPhaseActions))
        {
            Debug.LogError($"Touch phase {phase} is not supported!", this);

            return;
        }

        if (touchPhaseActions.TryGetValue(touchIndex, out var touchAction))
        {
            touchAction -= action;

            touchPhaseActions[touchIndex] = touchAction;
        }
        else
        {
            Debug.LogWarning($"Nothing was listening to {phase} on index {touchIndex} anyway");
        }
    }

    private void Update()
    {
        if (Input.touchCount > 0)
        {
            // Get all touches, order them ascending by their fingerId then take up to MAX_TOUCH_COUNT of them
            foreach (var t in Input.touches.OrderBy(t => t.fingerId).Take(MAX_TOUCH_COUNT))
            {
                InvokeTouchEvent(t);
            }
        }
    }

    private void InvokeTouchEvent(Touch touch)
    {
        generalTouchEvent?.Invoke(touch);

        if (!_touchActions.TryGetValue(touch.phase, out var touchPhaseActions))
        {
            return;
        }

        if (!touchPhaseActions.TryGetValue(touch.fingerId, out var touchAction))
        {
            return;
        }

        touchAction?.Invoke(touch);
    }
}

and then on the listener do e.g.

public class TouchListener : MonoBehaviour
{
    [SerializeField] private TouchHandler _touchHandler;
    [SerializeField] public int touchIndexToListenTo;

    private void Start()
    {
        _touchHandler.Subscribe(TouchPhase.Began, touchIndexToListenTo, OnTouchBegan);
        _touchHandler.Subscribe(TouchPhase.Ended, touchIndexToListenTo, OnTouchEnded);

        _touchHandler.generalTouchEvent += OnGeneralTouchEvent;
    }

    private void OnGeneralTouchEvent(Touch touch)
    {
        Debug.Log($"Received a general callback for {touch.phase} with index {touch.fingerId}!", this);
    }

    private void OnTouchEnded(Touch touch)
    {
        Debug.Assert(touch.fingerId == touchIndexToListenTo, $"Why do I get an event for {touch.fingerId} if I'm only listening to {touchIndexToListenTo}?!", this);
        Debug.Assert(touch.phase == TouchPhase.Ended, $"Why do I get an event for {touch.phase} if I'm only listening to {TouchPhase.Ended}?!", this);

        Debug.Log($"Received a {touch.phase} event for index {touch.fingerId}", this);
    }

    private void OnTouchBegan(Touch touch)
    {
        Debug.Assert(touch.fingerId == touchIndexToListenTo, $"Why do I get an event for {touch.fingerId} if I'm only listening to {touchIndexToListenTo}?!", this);
        Debug.Assert(touch.phase == TouchPhase.Began, $"Why do I get an event for {touch.phase} if I'm only listening to {TouchPhase.Began}?!", this);

        Debug.Log($"Received a {touch.phase} event for index {touch.fingerId}", this);
    }
}
derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Thanks Hugo for being patient with me. That fingerId part was very informative. I really need to improve my google skills, since it did not even cross my mind to google for "array of events in C#", I only googled Unity related stuff, since I thought that events and actions are only in Unity. – Crossoni Apr 29 '21 at 08:40