using System; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.VR; namespace Framework.Actors.Shooter { /// Uses a camera's look direction as GUI cursor /// /// This basically allows the player to look at buttons (or other interactive elements in /// the game world that respond to Unity's input event system) to select them rather /// than moving a cursor around on the screen. Works great in combination with the /// red dot pointer component. /// public class LookInputModule : PointerInputModule { /// Button that will be interpreted as a click on the pointed-at item public string SubmitButton = "Submit"; /// Game object the pointer is currently hovering over public GameObject ActivePointee; /// /// Called once per update cycle (which sucks) to update the state of the input devices /// public override void Process() { updateActivePointees(); forwardButtonPresses(); if (this.pointerEventData.hovered.Count == 0) { this.ActivePointee = null; } else { this.ActivePointee = this.pointerEventData.hovered[0]; } } /// /// Updates the pointed-to game object and sends out exit/enter notifications /// private void updateActivePointees() { // Reuse the pointer event data instance to avoid feeding the garbage collector if (this.pointerEventData == null) { this.pointerEventData = new PointerEventData(eventSystem); } // Pointer is always at the center of the view this.pointerEventData.position = getCenterOfView(); this.pointerEventData.delta = Vector2.zero; // Do a raycast to get the game objects currently hit by the "pointer" // The RaycastAll() will clear the list and ensure only the current results are in it // (by the way great work in the BaseInputModule exposing a poorly documented // static field supposed to be used for this. Couldn't you write this exact method // and/or an observable pointee collection that is exposed? To user-friendly, I guess) base.eventSystem.RaycastAll(this.pointerEventData, base.m_RaycastResultCache); // Now look for the first game object that has been hit (the list is sorted // by depth, but some of the results may not belong to any game objects somehow...) this.pointerEventData.pointerCurrentRaycast = FindFirstRaycast(base.m_RaycastResultCache); // Send out notifications when a game object has been entered by the cursor // or the cursor has left it again. This stupid method checks if the mouse pointer is // locked(!!) sabotaging by default any attempt to highlight the game object being // pointed at. Except that when ProcessMove() calls HandlePointerExitAndEnter(), // the latter method tries to recover the exact object ProcessMove() erased... ProcessMove(this.pointerEventData); } // Taken from Unity's public UI / event system sources. This makes me want to cry :-(( #region Unholy crap /// Responds to a cursor movement /// Pointer event for the cursor /// /// This method needs to be overriden because the default implementation blindly /// assumes all cursor input will be from the mouse cursor and simply ignores events /// if the mouse is locked. Look input and VR controllers are too exotic, I guess... /// protected override void ProcessMove(PointerEventData pointerEvent) { GameObject newEnterTarget = pointerEvent.pointerCurrentRaycast.gameObject; this.HandlePointerExitAndEnter(pointerEvent, newEnterTarget); } /// /// Updates the mouse state by doing something weird with pointer event data states cached /// in a dictionary per mouse button that then do not get sent out but instead do crazy /// flip flops just to fill a mouse state object /// /// An id with no documented purpose that is never used at all /// The mouse state that is in the m_mouseState object variable anyway protected override PointerInputModule.MouseState GetMousePointerEventData(int id) { // WHAT. THE. FUCK. IS. THIS ?! // // This crap was found in the public Unity event system code and supposedly // sends out mouse events to the UI. Not only that, all of the event system // code looks like this crap. // // Run for your lives. // Get the pointer state from some dictionary of pointer states per pointer id // Furthermore, each mouse button is a pointer... somehow. Also, this looks like // the .NET TryGet() pattern but it isn't, so don't think it is. PointerEventData leftButtonEvent; // Piss-poor API design. Why does the caller need to know if this is a new instance?! bool createdNew = GetPointerData(kMouseLeftId, out leftButtonEvent, create: true); leftButtonEvent.Reset(); if(createdNew) { // This value gets overwritten and the assignment is bullshit. However, it was // found exactly this way in the event system code which is so chock-full of // unexpected side effects that we don't dare to remove a completely useless line. leftButtonEvent.position = base.input.mousePosition; // Why? It's overwritten... } // Our pointer is in the center and that's where it is. Vector2 middleOfScreen = getCenterOfView(); leftButtonEvent.position = middleOfScreen; leftButtonEvent.delta = Vector2.zero; // It never moves. // Erm, do some raycast and assume the pointer event is for the left mouse button leftButtonEvent.button = PointerEventData.InputButton.Left; this.eventSystem.RaycastAll(leftButtonEvent, this.m_RaycastResultCache); RaycastResult firstRaycast = BaseInputModule.FindFirstRaycast(this.m_RaycastResultCache); leftButtonEvent.pointerCurrentRaycast = firstRaycast; this.m_RaycastResultCache.Clear(); // Then, copy all that eventamaching and make it the right mouse button PointerEventData rightButtonEvent; GetPointerData(kMouseRightId, out rightButtonEvent, create: true); CopyFromTo(leftButtonEvent, rightButtonEvent); rightButtonEvent.button = PointerEventData.InputButton.Right; // And do it again, this time making it the middle mouse button. Three shall be // the number of mouse buttons you support and the number of mouse buttons you // support shall be three. PointerEventData middleButtonEvent; GetPointerData(kMouseMiddleId, out middleButtonEvent, create: true); CopyFromTo(leftButtonEvent, middleButtonEvent); middleButtonEvent.button = PointerEventData.InputButton.Middle; // And now that we have made fancy button events, they go into the mouse state. // So hey, we actually have a handy mouse state structure, except that it's been // designed by the trainee, carries a boat-load of nested data, each mouse button // is represented as a whole separate mouse that could move on its own and has its // own mouse wheel... ...oh hell, just look at the PointerEventData class. Arf. this.mouseState.SetButtonState( PointerEventData.InputButton.Left, StateForMouseButton(0), leftButtonEvent ); this.mouseState.SetButtonState( PointerEventData.InputButton.Right, StateForMouseButton(1), rightButtonEvent ); this.mouseState.SetButtonState( PointerEventData.InputButton.Middle, StateForMouseButton(2), middleButtonEvent ); return this.mouseState; } /// /// Massages and broadcasts random events in response to a click or touch event until /// the UI elements cry out in despair. Ensure sufficient blood/alcohol level before reading /// /// /// Pointer event that will be completely overwritten and has no reason to be passed /// as a parameter. Garbage avoidal for the clueless. /// /// Whether the touch / mouse button has been pressed /// Whether the touch / mouse button has been released protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released) { // You who gaze upon this, abandon all hope, let go of you dreams // and leave your sanity at the door. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - GameObject pointedToObject = pointerEvent.pointerCurrentRaycast.gameObject; if(pressed) { pointerEvent.eligibleForClick = true; // What? pointerEvent.delta = Vector2.zero; pointerEvent.dragging = false; pointerEvent.useDragThreshold = true; Vector2 middleOfScreen = new Vector2(Screen.width / 2.0f, Screen.height / 2.0f); pointerEvent.pressPosition = middleOfScreen; pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast; // Isn't this stale? this.DeselectIfSelectionChanged(pointedToObject, pointerEvent); if(pointerEvent.pointerEnter != pointedToObject) { this.HandlePointerExitAndEnter(pointerEvent, pointedToObject); pointerEvent.pointerEnter = pointedToObject; } // Send the click even through some hierarchy (WTF? c-o-r pattern? documentation?!) // and get the first object that handled the event back, if any. GameObject reactingObject = ExecuteEvents.ExecuteHierarchy( pointedToObject, pointerEvent, ExecuteEvents.pointerDownHandler ); // So if no object reacted to the click, we just act like the pointed at object // reacted to the click because... we suck? if(reactingObject == null) { reactingObject = ExecuteEvents.GetEventHandler(pointedToObject); } // We assume that pointerEvent.clickTime is stale and still contains the time of // the last time the user clicked, so if less than 0.3 seconds have passed since then, // it's a follow-up click. We support only three mouse buttons, but we do clicks, // double-clicks, triple-clicks, quadruple-clicks, quintuple-clicks... float wallClockTime = Time.unscaledTime; if(reactingObject == pointerEvent.lastPress) { if((double)(wallClockTime - pointerEvent.clickTime) < 0.3) { ++pointerEvent.clickCount; } else { pointerEvent.clickCount = 1; } pointerEvent.clickTime = wallClockTime; } else { pointerEvent.clickCount = 1; } // Remember this run's clicked objects and click time pointerEvent.pointerPress = reactingObject; pointerEvent.rawPointerPress = pointedToObject; pointerEvent.clickTime = wallClockTime; // Drag & drop events pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler(pointedToObject); if((UnityEngine.Object) pointerEvent.pointerDrag != (UnityEngine.Object) null) { ExecuteEvents.Execute( pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag ); } } if(released) { // Deliver the pointer up event. ExecuteEvents.Execute( pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler ); // A click event is generated when the mouse is released... on whatever object // the mouse was now pointing at. This appears to be a failed imitation of // the established click-hold-move-off feature in mainstream GUI environments // that will go berserk (send click events to anything the mouse is released on) GameObject handler = ExecuteEvents.GetEventHandler(pointedToObject); if( (UnityEngine.Object) pointerEvent.pointerPress == (UnityEngine.Object) handler && pointerEvent.eligibleForClick ) { ExecuteEvents.Execute( pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler ); } else if((pointerEvent.pointerDrag != null) && pointerEvent.dragging) { // Drag & drop handling ExecuteEvents.ExecuteHierarchy( pointedToObject, pointerEvent, ExecuteEvents.dropHandler ); } // State update. Remember that we delivered a click event and don't want to do // that again should more mouse buttons be released. Because. pointerEvent.eligibleForClick = false; pointerEvent.pointerPress = null; pointerEvent.rawPointerPress = null; // More drag & drop handling 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; } } /// Stores the current state of the mouse because it is funny private MouseState mouseState = new MouseState(); #endregion // Unholy crap /// Forwards click notifications to the element currently being looked at private void forwardButtonPresses() { //bool isLeftMouseButtonHeld = UnityEngine.Input.GetMouseButton(0); bool wasLeftMouseButtonPressed = UnityEngine.Input.GetMouseButtonDown(0); bool wasLeftMouseButtonReleased = UnityEngine.Input.GetMouseButtonDown(0); // Reuse the pointer event data instance to avoid feeding the garbage collector if (this.pointerEventData == null) { this.pointerEventData = new PointerEventData(eventSystem); } // Call a method copied from the StandardInputModule. Reading this code // is enough to never want to rely on UGUI or Unity's EventSystem ever again, // so better just see this as the end of the journey and what's behind // this call must never be talked about or mentioned to anyone :-(( ProcessTouchPress( this.pointerEventData, wasLeftMouseButtonPressed, wasLeftMouseButtonReleased ); } /// Returns the center of the user's active view /// The center of the user's active view private static Vector2 getCenterOfView() { if(VRSettings.enabled && VRSettings.isDeviceActive) { return new Vector2(VRSettings.eyeTextureWidth / 2.0f, VRSettings.eyeTextureHeight / 2.0f); } else { return new Vector2(Screen.width / 2.0f, Screen.height / 2.0f); } } /// /// Reused pointer event notification that gets broadcast through the event system /// private PointerEventData pointerEventData; } } // namespace Framework.Actors.Shooter