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