最近在开发一款使用Tobii眼动仪控制的游戏,然后界面上的按钮都是使用UGUI,在前期用鼠标控制表现良好,但是最终需要用眼动仪控制按钮。而Unity默认的按钮是不支持眼动仪点击按钮的,并且Tobii眼动仪提供的API也只能拿到眼睛看到的坐标。
最后经过调查,发现Unity是通过StandaloneInputModule发送鼠标点击事件的,所以我利用已有的StandaloneInputModule源码重新写了一个GazeInputModle,最后实现了这个需求。目前眼动仪只要捕捉到眼光在按钮上停留两秒就会触发鼠标点击事件。
以下是主要的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using Tobii.Gaming;
public class GazeInputModule :
{
private GameObject currentOverGameObject;
private PointerEventData pointerEventData;
private float durationTime;
protected void HandlePointerExitAndEnter2(PointerEventData currentPointerData, GameObject newEnterTarget)
{
// if we have no target / pointerEnter has been deleted
// just send exit events to anything we are tracking
// then exit
if (newEnterTarget == null || currentPointerData.pointerEnter == null)
{
for (var i = 0; i < currentPointerData.hovered.Count; ++i)
ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Clear();
if (newEnterTarget == null)
{
currentPointerData.pointerEnter = newEnterTarget;
return;
}
}
// if we have not changed hover target
if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
return;
GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);
// and we already an entered object from last time
if (currentPointerData.pointerEnter != null)
{
// send exit handler call to all elements in the chain
// until we reach the new target, or null!
Transform t = currentPointerData.pointerEnter.transform;
while (t != null)
{
// if we reach the common root break out!
if (commonRoot != null && commonRoot.transform == t)
break;
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Remove(t.gameObject);
t = t.parent;
}
}
// now issue the enter call up to but not including the common root
currentPointerData.pointerEnter = newEnterTarget;
if (newEnterTarget != null)
{
Transform t = newEnterTarget.transform;
while (t != null && t.gameObject != commonRoot)
{
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
currentPointerData.hovered.Add(t.gameObject);
t = t.parent;
}
}
}
public void ProcessPoint()
{
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerEventData, results);
// Debug.Log("mousePosition=" + Input.mousePosition + ", count=" + results.Count);
RaycastResult raycastResult = FindFirstRaycast(results);
HandlePointerExitAndEnter2(pointerEventData, raycastResult.gameObject);
if (raycastResult.gameObject == pointerEventData.pointerEnter && raycastResult.gameObject != null)
{
durationTime += Time.deltaTime;
}
else
{
durationTime = 0;
}
if (durationTime > Configuration.GetFloat("InputMode.GazeClickDurationTime", 2)) //看一个对象两秒触发点击事件,可以配置
{
Debug.Log("Send Click event, name=" + raycastResult.gameObject.name);
ExecuteEvents.Execute(raycastResult.gameObject, pointerEventData, ExecuteEvents.pointerClickHandler);
durationTime = 0;
}
}
public void Update()
{
if (pointerEventData == null)
{
pointerEventData = new PointerEventData(gameObject.GetComponent<EventSystem>());
}
if (Configuration.GetBool("InputMode.TestMode", false) == false) //如果是测试模式则使用鼠标模拟眼动仪,便于测试
{
GazePoint gazePoint = TobiiAPI.GetGazePoint();
if (gazePoint.IsValid)
{
pointerEventData.position = gazePoint.Screen;
ProcessPoint();
}
}
else
{
pointerEventData.position = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
ProcessPoint();
}
}
public override void Process()
{
//do nothing to avoid mouse event cover ours
}
}
主要继承了StandaloneInputModule,然后在Update里检测点是否停留在按钮上。
用的时候需要把这个脚本挂载在EventSystem对象上,同时需要disable对象上的StandaloneInputModule,否则在选中按钮时每个Module都会发送PointerEnter事件。
目前只实现了PointerEnter、PointerExit、PointerClick事件,如果有需要可以增加其他事件。
代码中Configuration相关方法是自己写一个配置类,用的时候需要修改下这些代码。