Part 8: Creating a Gaze based Input Module for Unity

This post follows: Part 7: Implementing Input Handling with Event System

We can use the Event System to manage input events to in-scene GameObjects, however the default scripts are limited to mouse pointer and touch screen action — it’s still useful for testing the events on the in-scene objects. In virtual and mixed reality setups, the mouse is not your go-to input device and the gaze is typically used by sample apps. Most first person systems simplify view tracking by using the center of the display as the gaze or line of sight, for our purposes this is equivalent to leaving the mouse pointer at the center of the screen and moving the camera around the scene.

Since we know the functionality we need is to have the mouse pointer always centered on the display, we can examine how the default input modules are setup. The “Standalone Input Module” supports mouse and touch screen input and a relatively simpler “Touch Input Module” are good references on how mouse and touch input are handled in the Event System. Fortunately the source code for the Unity UI system is available at: https://bitbucket.org/Unity-Technologies/

I opted to use the Touch Input Module source as the reference due to similar functionality we need to implement and present the concept. The key change is with the create pointer event that positions the pointer at the center of the screen instead of the mouse or touch position and set the mouse properties. (full source at the end of the post)

Screen.width and Screen.height return the pixel size of the game/display screen; we divide by 2 to get the center position. We are deriving from the PointerInputModule class instead of BaseInputModule so we can map our operations to mouse functions so we can pair game controller buttons with the gaze using the SetButtonState() methods to emulate left and right button mouse clicks. I am using custom defined buttons “LeftClick” and “RightClick” in Unity Edit/Project Settings/Input.

        protected MouseState CreateGazePointerEvent(int id)
        {
            PointerEventData leftData;
            var created = GetPointerData(kMouseLeftId, out leftData, true);
            Vector2 pos = new Vector2(Screen.width / 2f, Screen.height / 2f);

            leftData.Reset();
            leftData.delta = Vector2.zero;
            leftData.position = pos;
            leftData.scrollDelta = Vector2.zero;
            leftData.button = PointerEventData.InputButton.Left;

            eventSystem.RaycastAll(leftData, m_RaycastResultCache);
            var raycast = FindFirstRaycast(m_RaycastResultCache);

            leftData.pointerCurrentRaycast = raycast;
            m_RaycastResultCache.Clear();

            PointerEventData rightData;
            GetPointerData(kMouseRightId, out rightData, true);
            CopyFromTo(leftData, rightData);
            rightData.button = PointerEventData.InputButton.Right;

            m_MouseState.SetButtonState(
                PointerEventData.InputButton.Left,
                StateForButton("LeftClick"),
                leftData);
            m_MouseState.SetButtonState(
                PointerEventData.InputButton.Right,
                StateForButton("RightClick"),
                rightData);

            return m_MouseState;
        }

Input Module use the Process() override to manage the input. This is the method that setup the pointer event, raycasts, button states and using the event system to pass events to objects. To have more options and finer control of the Input Module such as limiting the number of events per second, default actions and input repeat — the “Standalone Input Module” can be used as the reference.

To use the module, add the script to the Event System. For well-known input systems that can be checked; such as presence of mouse or touch input, the input module can programatically determine if it should be used via IsModuleSupported(). In the example, we manually define this through the public bool forceModuleActive.

008-01-EventSystemSetup

The input module will only act on objects that are on the display center and not use mouse or touch input. If this is used on a Virtual or Mixed Reality system, it would function as expected since the Unity camera tracks head movement — this becomes a bit challenging when developing on a standard mouse and monitor based workstation and I mentioned previously we should have the ability to also move the camera independent of head tracking. Another issue that comes up is how to exactly know the display center since we don’t have a mouse cursor anymore. I’ll discuss camera controls and the gaze cursor on the next posts.

For questions, comments or contact – follow me on Twitter @rlozada

Full Source: GazeBasicInputModule.cs

using System.Text;

namespace UnityEngine.EventSystems
{
    public class GazeBasicInputModule : PointerInputModule
    {
        private readonly MouseState m_MouseState = new MouseState();

        protected GazeBasicInputModule()
        { }

        [SerializeField]
        private bool m_ForceModuleActive;

        public bool forceModuleActive
        {
            get { return m_ForceModuleActive; }
            set { m_ForceModuleActive = value; }
        }

        public override bool IsModuleSupported()
        {
            return forceModuleActive;
        }

        public override bool ShouldActivateModule()
        {
            if (!base.ShouldActivateModule())
                return false;

            if (m_ForceModuleActive)
                return true;

            return false;
        }


        public override void Process()
        {
            GazeControl();
        }


        protected static PointerEventData.FramePressState StateForButton(string buttonCode)
        {
            var pressed = Input.GetButtonDown(buttonCode);
            var released = Input.GetButtonUp(buttonCode);
            if (pressed && released)
                return PointerEventData.FramePressState.PressedAndReleased;
            if (pressed)
                return PointerEventData.FramePressState.Pressed;
            if (released)
                return PointerEventData.FramePressState.Released;
            return PointerEventData.FramePressState.NotChanged;
        }

        protected MouseState CreateGazePointerEvent(int id)
        {
            PointerEventData leftData;
            var created = GetPointerData(kMouseLeftId, out leftData, true);
            Vector2 pos = new Vector2(Screen.width / 2f, Screen.height / 2f);

            leftData.Reset();
            leftData.delta = Vector2.zero;
            leftData.position = pos;
            leftData.scrollDelta = Vector2.zero;
            leftData.button = PointerEventData.InputButton.Left;

            eventSystem.RaycastAll(leftData, m_RaycastResultCache);
            var raycast = FindFirstRaycast(m_RaycastResultCache);

            leftData.pointerCurrentRaycast = raycast;
            m_RaycastResultCache.Clear();

            PointerEventData rightData;
            GetPointerData(kMouseRightId, out rightData, true);
            CopyFromTo(leftData, rightData);
            rightData.button = PointerEventData.InputButton.Right;

            m_MouseState.SetButtonState(
                PointerEventData.InputButton.Left,
                StateForButton("LeftClick"),
                leftData);
            m_MouseState.SetButtonState(
                PointerEventData.InputButton.Right,
                StateForButton("RightClick"),
                rightData);

            return m_MouseState;
        }


        private void GazeControl()
        {
            var pointerData = CreateGazePointerEvent(0);

            var leftPressData = pointerData.GetButtonState(PointerEventData.InputButton.Left).eventData;

            ProcessPress(leftPressData.buttonData, leftPressData.PressedThisFrame(), leftPressData.ReleasedThisFrame());
            ProcessMove(leftPressData.buttonData);

            if (Input.GetButton("LeftClick"))
            {
                ProcessDrag(leftPressData.buttonData);
            }
        }


        private void ProcessPress(PointerEventData pointerEvent, bool pressed, bool released)
        {
            var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

            if (pressed)
            {
                pointerEvent.eligibleForClick = true;
                pointerEvent.delta = Vector2.zero;
                pointerEvent.dragging = false;
                pointerEvent.useDragThreshold = true;
                pointerEvent.pressPosition = pointerEvent.position;
                pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

                DeselectIfSelectionChanged(currentOverGo, pointerEvent);

                if (pointerEvent.pointerEnter != currentOverGo)
                {
                    HandlePointerExitAndEnter(pointerEvent, currentOverGo);
                    pointerEvent.pointerEnter = currentOverGo;
                }

                var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

                if (newPressed == null)
                {
                    newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
                }

                float time = Time.unscaledTime;

                if (newPressed == pointerEvent.lastPress)
                {
                    var diffTime = time - pointerEvent.clickTime;
                    if (diffTime < 0.3f)
                        ++pointerEvent.clickCount;
                    else
                        pointerEvent.clickCount = 1;

                    pointerEvent.clickTime = time;
                }
                else
                {
                    pointerEvent.clickCount = 1;
                }

                pointerEvent.pointerPress = newPressed;
                pointerEvent.rawPointerPress = currentOverGo;

                pointerEvent.clickTime = time;
                
                pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

                if (pointerEvent.pointerDrag != null)
                    ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
            }

            
            if (released)
            {
            
                ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);            
                var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
                
                if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
                {
                    ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
                }
                else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
                {
                    ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
                }

                pointerEvent.eligibleForClick = false;
                pointerEvent.pointerPress = null;
                pointerEvent.rawPointerPress = null;

                if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
                    ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

                pointerEvent.dragging = false;
                pointerEvent.pointerDrag = null;

                if (pointerEvent.pointerDrag != null)
                    ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

                pointerEvent.pointerDrag = null;
                
                ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
                pointerEvent.pointerEnter = null;
            }
        }

        public override void DeactivateModule()
        {
            base.DeactivateModule();
            ClearSelection();
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.AppendLine("Input: GazeBasicInputModule");
            var pointerData = GetLastPointerEventData(kMouseLeftId);
            if (pointerData != null)
                sb.AppendLine(pointerData.ToString());

            return sb.ToString();
        }
    }
}