因为GVRDemo的光标系统就是用的EventSystem,所以在讲GVRDemo的光标的时候,必须要先对Unity3D的EventSystem系统有所了解。
可以参考http://blog.csdn.net/yupu56/article/details/53010868
了解了怎么使用EventSystem后,接下来简单分析下GVRDemo的光标系统怎么实现的。
关键是从哪里开始入手分析,看一个demo或一大块代码的时候,总是有这个疑问。我一般就从现象入手分析。对于GVRDemo光标系统主要体现在两块。
1.当光标对准立方体的时候,会变成蓝色。
2.当光标对准菜单的时候,菜单会变绿色。
我主要分析立方体的那个,菜单的原理是一样的。
设计到的文件有:
在Main Camera孩子中有GvrReticlePointer:负责画出光标,也就是正常是个小圆点,如果落到一个object上面这个圆的半径变大。
GvrReticlePointerImpl:在GvrReticlePointer文件中被用到了。
在Main Camera里面有GvrPointerPhysicsRaycaster:主要实现了Raycast方法,实现了射线投射,会配合EventSystem使用。它继承方法GvrBasePointerRaycaster类,然后GvrBasePointerRaycaster继承于BaseRaycaster,BaseRaycaster是EventSystem中实现射线投射的基类。
在GvrBasePointerRaycaster中会根据不同的模式产生不同的射线
/// Calculates the ray to use for raycasting based on
/// the selected raycast mode.
protected Ray GetRay() {
if (!IsPointerAvailable()) {
Debug.LogError("Calling GetRay when the pointer isn't available.");
lastRay = new Ray();
return lastRay;
}
Transform pointerTransform = GvrPointerManager.Pointer.PointerTransform;
switch (raycastMode) {
case RaycastMode.Camera:
Vector3 rayPointerStart = pointerTransform.position;
Vector3 rayPointerEnd = rayPointerStart + (pointerTransform.forward * MaxPointerDistance);
Vector3 cameraLocation = Camera.main.transform.position;
Vector3 finalRayDirection = rayPointerEnd - cameraLocation;
finalRayDirection.Normalize();
Vector3 finalRayStart = cameraLocation + (finalRayDirection * Camera.main.nearClipPlane);
lastRay = new Ray(finalRayStart, finalRayDirection);
break;
case RaycastMode.Direct:
lastRay = new Ray(pointerTransform.position, pointerTransform.forward);
break;
default:
lastRay = new Ray();
break;
}
return lastRay;
}
其中的GvrPointerManager.Pointer是在哪赋值的呢?
答案就在GvrReticlePointer类中:
public void SetAsMainPointer() {
GvrPointerManager.Pointer = reticlePointerImpl;
}
reticlePointerImpl的类型是如下类,关于它的介绍如下:
/// Draws a circular reticle in front of any object that the user points at.
/// The circle dilates if the object is clickable.
public class GvrReticlePointerImpl : GvrBasePointer {
大概意思就是它就是画一个光标,当物体被点击的时候,光标变大。
google为什么不用默认的射线投射,很明显因为默认的射线投射是需要有光标点的,用光标点来实现物体的选择,unity默认是没有这个的。
因为其他博客中对于射线的一个说明
系统实现的射线投射类组件有PhysicsRaycaster, Physics2DRaycaster, GraphicRaycaster。这个模块也是可以自己继承BaseRaycaster实现个性化定制。
总的来说,EventSystem负责管理,BaseInputModule负责输入,BaseRaycaster负责确定目标对象,目标对象负责接收事件并处理,然后一个完整的事件系统就有了。
在GVREventSystem模块下面有GvrPointerInputModule和GvrPointerManager:
其中GvrPointerManager主要是注册光标的,它里面有方法
/// GvrBasePointer calls this when it is created.
/// If a pointer hasn't already been assigned, it
/// will assign the newly created one by default.
///
/// This simplifies the common case of having only one
/// GvrBasePointer so is can be automatically hooked up
/// to the manager. If multiple GvrGazePointers are in
/// the scene, the app has to take responsibility for
/// setting which one is active.
public static void OnPointerCreated(GvrBasePointer createdPointer) {
if (instance != null && GvrPointerManager.Pointer == null) {
GvrPointerManager.Pointer = createdPointer;
}
}
会被GvrBasePointer里的方法调用
public virtual void OnStart() {
GvrPointerManager.OnPointerCreated(this);
}
OnStart方法会被GvrReticlePointer里的方法调用如下:
void Start() {
reticlePointerImpl.OnStart();
reticlePointerImpl.MaterialComp = gameObject.GetComponent<Renderer>().material;
UpdateReticleProperties();
CreateReticleVertices();
}
GvrPointerInputModule:
里面重要的方法是
public override void Process() {
UpdateImplProperties();
Impl.Process();
}
这个方法什么时候被执行?
当我们在场景中创建任一UI对象后,Hierarchy面板中都可以看到系统自动创建了对象EventSystem,可以看到该对象下有三个组件:EventSystem、StandaloneInputModule、TouchInputModule,后面两个组件都继承自BaseInputModule。
EventSystem组件主要负责处理输入、射线投射以及发送事件。一个场景中只能有一个EventSystem组件,并且需要BaseInputModule类型组件的协助才能工作。EventSystem在一开始的时候会把自己所属对象下的BaseInputModule类型组件加到一个内部列表,并且在每个Update周期通过接口UpdateModules接口调用这些基本输入模块的UpdateModule接口,然后BaseInputModule会在UpdateModule接口中将自己的状态修改成’Updated’,之后BaseInputModule的Process接口才会被调用。
BaseInputModule是一个基类模块,负责发送输入事件(点击、拖拽、选中等)到具体对象。EventSystem下的所有输入模块都必须继承自BaseInputModule组件。StandaloneInputModule和TouchInputModule组件是系统提供的标准输入模块和触摸输入模块,我们可以通过继承BaseInputModule实现自己的输入模块。
然后调用GvrPointerInputModuleImpl中的方法:
public void Process() {
if (Pointer == null) {
return;
}
// Save the previous Game Object
GameObject previousObject = GetCurrentGameObject();
CastRay();
UpdateCurrentObject(previousObject);
UpdatePointer(previousObject);
...
}
里面剩下了三个重要的方法:
CastRay
private void CastRay() {
if (Pointer == null || Pointer.PointerTransform == null) {
return;
}
Vector2 currentPose = GvrMathHelpers.NormalizedCartesianToSpherical(Pointer.PointerTransform.forward);
if (CurrentEventData == null) {
CurrentEventData = new PointerEventData(ModuleController.eventSystem);
lastPose = currentPose;
}
// Store the previous raycast result.
RaycastResult previousRaycastResult = CurrentEventData.pointerCurrentRaycast;
// The initial cast must use the enter radius.
if (Pointer != null) {
Pointer.ShouldUseExitRadiusForRaycast = false;
}
// Cast a ray into the scene
CurrentEventData.Reset();
// Set the position to the center of the camera.
// This is only necessary if using the built-in Unity raycasters.
RaycastResult raycastResult;
CurrentEventData.position = GvrMathHelpers.GetViewportCenter();
bool isPointerActiveAndAvailable = IsPointerActiveAndAvailable();
if (isPointerActiveAndAvailable) {
ModuleController.eventSystem.RaycastAll(CurrentEventData, ModuleController.RaycastResultCache);
raycastResult = ModuleController.FindFirstRaycast(ModuleController.RaycastResultCache);
} else {
raycastResult = new RaycastResult();
raycastResult.Clear();
}
// If we were already pointing at an object we must check that object against the exit radius
// to make sure we are no longer pointing at it to prevent flicker.
if (previousRaycastResult.gameObject != null
&& raycastResult.gameObject != previousRaycastResult.gameObject
&& isPointerActiveAndAvailable) {
if (Pointer != null) {
Pointer.ShouldUseExitRadiusForRaycast = true;
}
ModuleController.RaycastResultCache.Clear();
ModuleController.eventSystem.RaycastAll(CurrentEventData, ModuleController.RaycastResultCache);
RaycastResult firstResult = ModuleController.FindFirstRaycast(ModuleController.RaycastResultCache);
if (firstResult.gameObject == previousRaycastResult.gameObject) {
raycastResult = firstResult;
}
}
if (raycastResult.gameObject != null && raycastResult.worldPosition == Vector3.zero) {
raycastResult.worldPosition =
GvrMathHelpers.GetIntersectionPosition(CurrentEventData.enterEventCamera, raycastResult);
}
CurrentEventData.pointerCurrentRaycast = raycastResult;
// Find the real screen position associated with the raycast
// Based on the results of the hit and the state of the pointerData.
if (raycastResult.gameObject != null) {
CurrentEventData.position = raycastResult.screenPosition;
} else {
Transform pointerTransform = Pointer.PointerTransform;
float maxPointerDistance = Pointer.MaxPointerDistance;
Vector3 pointerPos = pointerTransform.position + (pointerTransform.forward * maxPointerDistance);
if (CurrentEventData.pressEventCamera != null) {
CurrentEventData.position = CurrentEventData.pressEventCamera.WorldToScreenPoint(pointerPos);
} else if (Camera.main != null) {
CurrentEventData.position = Camera.main.WorldToScreenPoint(pointerPos);
}
}
ModuleController.RaycastResultCache.Clear();
CurrentEventData.delta = currentPose - lastPose;
lastPose = currentPose;
// Check to make sure the Raycaster being used is a GvrRaycaster.
if (raycastResult.module != null
&& !(raycastResult.module is GvrPointerGraphicRaycaster)
&& !(raycastResult.module is GvrPointerPhysicsRaycaster)) {
Debug.LogWarning("Using Raycaster (Raycaster: " + raycastResult.module.GetType() +
", Object: " + raycastResult.module.name + "). It is recommended to use " +
"GvrPointerPhysicsRaycaster or GvrPointerGrahpicRaycaster with GvrPointerInputModule.");
}
}
其中ModuleController.eventSystem.RaycastAll(CurrentEventData, ModuleController.RaycastResultCache);
官方解释:Raycast into the scene using all configured BaseRaycasters.
意思应该是它会调用已经注册的所有BaseRaycaster的Raycast方法。
GVRDemo中,有两个一个GvrPointerPhysicsRaycaster(是给3d物体用的),另外一个是Floor Canves下面的GvrPointerGraphicRaycaster(是给UI系统用的)
UpdateCurrentObject
private void UpdateCurrentObject(GameObject previousObject) {
if (Pointer == null || CurrentEventData == null) {
return;
}
// Send enter events and update the highlight.
GameObject currentObject = GetCurrentGameObject(); // Get the pointer target
HandlePointerExitAndEnter(CurrentEventData, currentObject);
// Update the current selection, or clear if it is no longer the current object.
var selected = EventExecutor.GetEventHandler<ISelectHandler>(currentObject);
if (selected == ModuleController.eventSystem.currentSelectedGameObject) {
EventExecutor.Execute(ModuleController.eventSystem.currentSelectedGameObject, ModuleController.GetBaseEventData(),
ExecuteEvents.updateSelectedHandler);
} else {
ModuleController.eventSystem.SetSelectedGameObject(null, CurrentEventData);
}
// Execute hover event.
if (currentObject != null && currentObject == previousObject) {
EventExecutor.ExecuteHierarchy(currentObject, CurrentEventData, GvrExecuteEventsExtension.pointerHoverHandler);
}
}
只保留了关键代码,这段代码会触发击中object的:
onPointerEnter
onPointExit
onPointerHover
onSelect
UpdatePointer:
private void UpdatePointer(GameObject previousObject) {
if (Pointer == null || CurrentEventData == null) {
return;
}
GameObject currentObject = GetCurrentGameObject(); // Get the pointer target
bool isInteractive = CurrentEventData.pointerPress != null ||
EventExecutor.GetEventHandler<IPointerClickHandler>(currentObject) != null ||
EventExecutor.GetEventHandler<IDragHandler>(currentObject) != null;
if (isPointerHovering && currentObject != null && currentObject == previousObject) {
Pointer.OnPointerHover(CurrentEventData.pointerCurrentRaycast, GetLastRay(), isInteractive);
} else {
// If the object's don't match or the hovering object has been destroyed
// then the pointer has exited.
if (previousObject != null || (currentObject == null && isPointerHovering)) {
Pointer.OnPointerExit(previousObject);
isPointerHovering = false;
}
if (currentObject != null) {
Pointer.OnPointerEnter(CurrentEventData.pointerCurrentRaycast, GetLastRay(), isInteractive);
isPointerHovering = true;
}
}
}
它会触发GvrReticlePointerImpl的
/// Called when the user is pointing at valid GameObject. This can be a 3D
/// or UI element.
///
/// The targetObject is the object the user is pointing at.
/// The intersectionPosition is where the ray intersected with the targetObject.
/// The intersectionRay is the ray that was cast to determine the intersection.
public override void OnPointerEnter(RaycastResult rayastResult, Ray ray,
bool isInteractive) {
SetPointerTarget(rayastResult.worldPosition, isInteractive);
}
/// Called every frame the user is still pointing at a valid GameObject. This
/// can be a 3D or UI element.
///
/// The targetObject is the object the user is pointing at.
/// The intersectionPosition is where the ray intersected with the targetObject.
/// The intersectionRay is the ray that was cast to determine the intersection.
public override void OnPointerHover(RaycastResult rayastResult, Ray ray,
bool isInteractive) {
SetPointerTarget(rayastResult.worldPosition, isInteractive);
}
/// Called when the user's look no longer intersects an object previously
/// intersected with a ray projected from the camera.
/// This is also called just before **OnInputModuleDisabled** and may have have any of
/// the values set as **null**.
public override void OnPointerExit(GameObject previousObject) {
ReticleDistanceInMeters = RETICLE_DISTANCE_MAX;
ReticleInnerAngle = RETICLE_MIN_INNER_ANGLE;
ReticleOuterAngle = RETICLE_MIN_OUTER_ANGLE;
}
这些方法被调用。
总结:
GvrPointerInputModule分发事件,它里面会遍历所有的BaseRaycaster发射射线,然后更新gameObject,调用他们想要的回调比如OnPointEntry,然后更新光标Pointer,选中后变大,未选中变小。
最后一个问题,物体选中后变绿在那实现的?
上图,不解释。
菜单选中后的操作在实现呢?
需要提一下的是菜单的onClick事件是被GvrPointerGraphicRaycaster射线投射间接调用的。
如果自己写光标系统怎么办呢?
/// This script provides an implemention of Unity’s
BaseInputModule
class, so
/// that Canvas-based (uGUI) UI elements and 3D scene objects can be
/// interacted with in a Gvr Application.
///
/// This script is intended for use with either a
/// 3D Pointer with the Daydream Controller (Recommended for Daydream),
/// or a Gaze-based-Pointer (Recommended for Cardboard).
///
/// To use, attach to the scene’s EventSystem object. Be sure to move it above the
/// other modules, such as TouchInputModule and StandaloneInputModule, in order
/// for the Pointer to take priority in the event system.
///
/// If you are using a Canvas, set the Render Mode to World Space,
/// and add the GvrPointerGraphicRaycaster script to the object.
///
/// If you’d like pointers to work with 3D scene objects, add a GvrPointerPhysicsRaycaster to the main camera,
/// and add a component that implements one of the Event interfaces (EventTrigger will work nicely) to
/// an object with a collider.
///
/// GvrPointerInputModule emits the following events: Enter, Exit, Down, Up, Click, Select,
/// Deselect, UpdateSelected, and GvrPointerHover. Scroll, move, and submit/cancel events are not emitted.
///
/// To use a 3D Pointer with the Daydream Controller:
/// - Add the prefab GoogleVR/Prefabs/UI/GvrControllerPointer to your scene.
/// - Set the parent of GvrControllerPointer to the same parent as the main camera
/// (With a local position of 0,0,0).
///
/// To use a Gaze-based-pointer:
/// - Add the prefab GoogleVR/Prefabs/UI/GvrReticlePointer to your scene.
/// - Set the parent of GvrReticlePointer to the main camera.
///
这块内容是GvrPointerInputModule代码中注释的。