Using Unity3D's IPointerDownHandler approach, but with "the whole screen" - c#

In Unity say you need to detect finger touch (finger drawing) on something in the scene.
The only way to do this in modern Unity, is very simple:
Step 1. Put a collider on that object. ("The ground" or whatever it may be.) 1
Step 2. On your camera, Inspector panel, click to add a Physics Raycaster (2D or 3D as relevant).
Step 3. Simply use code as in Example A below.
(Tip - don't forget to ensure there's an EventSystem ... sometimes Unity adds one automatically, sometimes not!)
Fantastic, couldn't be easier. Unity finally handles un/propagation correctly through the UI layer. Works uniformly and flawlessly on desktop, devices, Editor, etc etc. Hooray Unity.
All good. But what if you want to draw just "on the screen"?
So you are wanting, quite simply, swipes/touches/drawing from the user "on the screen". (Example, simply for operating an orbit camera, say.) So just as in any ordinary 3D game where the camera runs around and moves.
You don't want the position of the finger on some object in world space, you simply want abstract "finger motions" (i.e. position on the glass).
What collider do you then use? Can you do it with no collider? It seems fatuous to add a collider just for that reason.
What we do is this:
I just make a flat collider of some sort, and actually attach it under the camera. So it simply sits in the camera frustum and completely covers the screen.
(For the code, there is then no need to use ScreenToWorldPoint, so just use code as in Example B - extremely simple, works perfectly.)
My question, it seems a bit odd to have to use the "under-camera colldier" I describe, just to get touches on the glass.
What's the deal here?
(Note - please don't answer involving Unity's ancient "Touches" system, which is unusable today for real projects, you can't ignore .UI using the legacy approach.)
Code sample A - drawing on a scene object. Use ScreenToWorldPoint.
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class FingerMove:MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
public void OnPointerDown (PointerEventData data)
{
Debug.Log("FINGER DOWN");
prevPointWorldSpace =
theCam.ScreenToWorldPoint( data.position );
}
public void OnDrag (PointerEventData data)
{
thisPointWorldSpace =
theCam.ScreenToWorldPoint( data.position );
realWorldTravel =
thisPointWorldSpace - prevPointWorldSpace;
_processRealWorldtravel();
prevPointWorldSpace = thisPointWorldSpace;
}
public void OnPointerUp (PointerEventData data)
{
Debug.Log("clear finger...");
}
Code sample B ... you only care about what the user does on the glass screen of the device. Even easier here:
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
public class FingerMove:MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
private Vector2 prevPoint;
private Vector2 newPoint;
private Vector2 screenTravel;
public void OnPointerDown (PointerEventData data)
{
Debug.Log("FINGER DOWN");
prevPoint = data.position;
}
public void OnDrag (PointerEventData data)
{
newPoint = data.position;
screenTravel = newPoint - prevPoint;
prevPoint = newPoint;
_processSwipe();
}
public void OnPointerUp (PointerEventData data)
{
Debug.Log("FINEGR UP...");
}
private void _processSwipe()
{
// your code here
Debug.Log("screenTravel left-right.. " + screenTravel.x.ToString("f2"));
}
}
1 If you're just new to Unity: at that step very likely, make it a layer called say "Draw"; in physics settings make "Draw" interact with nothing; in step two with the Raycaster just set the layer to "Draw".

First of all, you need to understand that there are just 3 ways to detect click on an Object with the OnPointerDown function:
1.You need a UI component to in order to detect click with the OnPointerDown function. This applies to other similar UI events.
2.Another method to detect a click with the OnPointerDown function on a 2D/Sprite GameObject is to attach Physics2DRaycaster to the Camera and then OnPointerDown will be called when it is clicked. Note that a 2D Collider must be attached to it.
3.If this is a 3D Object with a Collider not 2D Collider, you must have PhysicsRaycaster attached to the camera in order for the OnPointerDown function to be called.
Doing this with the first method seems more reasonable instead of having a large collider or 2D collider covering the screen. All you do is to create a Canvas, Panel GameObject, and attach Image component that stretches across the whole screen to it.
Dude I just don't see using .UI as a serious solution: imagine we're
doing a big game and you're leading a team that is doing all the UI (I
mean buttons, menus and all). I'm leading a team doing the walking
robots. I suddenly say to you "oh, by the way, I can't handle touch
("!"), could you drop in a UI.Panel, don't forget to keep it under
everything you're doing, oh and put one on any/all canvasses or
cameras you swap between - and pass that info back to me OK!" :) I
mean it's just silly. One can't essentially say: "oh, Unity doesn't
handle touch"
Not really hard like the way you described it. You can write a long code that will be able to create a Canvas, Panel and an Image. Change the image alpha to 0. All you have to do is attach that code to the Camera or an empty GameObject and it will perform all this for you automatically on play mode.
Make every GameObject that wants to receive event on the screen subscribe to it, then use ExecuteEvents.Execute to send the event to all the interfaces in the script attached to that GameObject.
For example, the sample code below will send OnPointerDown event to the GameObject called target.
ExecuteEvents.Execute<IPointerDownHandler>(target,
eventData,
ExecuteEvents.pointerDownHandler);
Problem you will run into:
The hidden Image component will block other UI or GameObject from receiving raycast. This is the biggest problem here.
Solution:
Since it will cause some blocking problems, it is better to make the Canvas of the Image to be on top of everything. This will make sure that it is now 100 blocking all other UI/GameObject. Canvas.sortingOrder = 12; should help us do this.
Each time we detect an event such as OnPointerDown from the Image, we will manually send resend the OnPointerDown event to all other UI/GameObjects beneath the Image.
First of all, we throw a raycast with GraphicRaycaster(UI), Physics2DRaycaster(2D collider), PhysicsRaycaster(3D Collider) and store the result in a List.
Now, we loop over the result in the List and resend the event we received by sending artificial event to the results with:
ExecuteEvents.Execute<IPointerDownHandler>(currentListLoop,
eventData,
ExecuteEvents.pointerDownHandler);
Other problems you will run into:
You won't be able to send emulate events on the Toggle component with GraphicRaycaster. This is a bug in Unity. It took me 2 days to realize this.
Also was not able to send fake slider move event to the Slider component. I can't tell if this is a bug or not.
Other than these problems mentioned above, I was able to implement this. It comes in 3 parts. Just create a folder and put all the scripts in them.
SCRIPTS:
1.WholeScreenPointer.cs - The main part of the script that creates Canvas, GameObject, and hidden Image. It does all the complicated stuff to make sure that the Image always covers the screen. It also sends event to all the subscribe GameObject.
public class WholeScreenPointer : MonoBehaviour
{
//////////////////////////////// SINGLETON BEGIN ////////////////////////////////
private static WholeScreenPointer localInstance;
public static WholeScreenPointer Instance { get { return localInstance; } }
public EventUnBlocker eventRouter;
private void Awake()
{
if (localInstance != null && localInstance != this)
{
Destroy(this.gameObject);
}
else
{
localInstance = this;
}
}
//////////////////////////////// SINGLETON END ////////////////////////////////
//////////////////////////////// SETTINGS BEGIN ////////////////////////////////
public bool simulateUIEvent = true;
public bool simulateColliderEvent = true;
public bool simulateCollider2DEvent = true;
public bool hideWholeScreenInTheEditor = false;
//////////////////////////////// SETTINGS END ////////////////////////////////
private GameObject hiddenCanvas;
private List<GameObject> registeredGameobjects = new List<GameObject>();
//////////////////////////////// USEFUL FUNCTIONS BEGIN ////////////////////////////////
public void registerGameObject(GameObject objToRegister)
{
if (!isRegistered(objToRegister))
{
registeredGameobjects.Add(objToRegister);
}
}
public void unRegisterGameObject(GameObject objToRegister)
{
if (isRegistered(objToRegister))
{
registeredGameobjects.Remove(objToRegister);
}
}
public bool isRegistered(GameObject objToRegister)
{
return registeredGameobjects.Contains(objToRegister);
}
public void enablewholeScreenPointer(bool enable)
{
hiddenCanvas.SetActive(enable);
}
//////////////////////////////// USEFUL FUNCTIONS END ////////////////////////////////
// Use this for initialization
private void Start()
{
makeAndConfigWholeScreenPinter(hideWholeScreenInTheEditor);
}
private void makeAndConfigWholeScreenPinter(bool hide = true)
{
//Create and Add Canvas Component
createCanvas(hide);
//Add Rect Transform Component
//addRectTransform();
//Add Canvas Scaler Component
addCanvasScaler();
//Add Graphics Raycaster Component
addGraphicsRaycaster();
//Create Hidden Panel GameObject
GameObject panel = createHiddenPanel(hide);
//Make the Image to be positioned in the middle of the screen then fix its anchor
stretchImageAndConfigAnchor(panel);
//Add EventForwarder script
addEventForwarder(panel);
//Add EventUnBlocker
addEventRouter(panel);
//Add EventSystem and Input Module
addEventSystemAndInputModule();
}
//Creates Hidden GameObject and attaches Canvas component to it
private void createCanvas(bool hide)
{
//Create Canvas GameObject
hiddenCanvas = new GameObject("___HiddenCanvas");
if (hide)
{
hiddenCanvas.hideFlags = HideFlags.HideAndDontSave;
}
//Create and Add Canvas Component
Canvas cnvs = hiddenCanvas.AddComponent<Canvas>();
cnvs.renderMode = RenderMode.ScreenSpaceOverlay;
cnvs.pixelPerfect = false;
//Set Cavas sorting order to be above other Canvas sorting order
cnvs.sortingOrder = 12;
cnvs.targetDisplay = 0;
//Make it child of the GameObject this script is attached to
hiddenCanvas.transform.SetParent(gameObject.transform);
}
private void addRectTransform()
{
RectTransform rctrfm = hiddenCanvas.AddComponent<RectTransform>();
}
//Adds CanvasScaler component to the Canvas GameObject
private void addCanvasScaler()
{
CanvasScaler cvsl = hiddenCanvas.AddComponent<CanvasScaler>();
cvsl.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
cvsl.referenceResolution = new Vector2(800f, 600f);
cvsl.matchWidthOrHeight = 0.5f;
cvsl.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
cvsl.referencePixelsPerUnit = 100f;
}
//Adds GraphicRaycaster component to the Canvas GameObject
private void addGraphicsRaycaster()
{
GraphicRaycaster grcter = hiddenCanvas.AddComponent<GraphicRaycaster>();
grcter.ignoreReversedGraphics = true;
grcter.blockingObjects = GraphicRaycaster.BlockingObjects.None;
}
//Creates Hidden Panel and attaches Image component to it
private GameObject createHiddenPanel(bool hide)
{
//Create Hidden Panel GameObject
GameObject hiddenPanel = new GameObject("___HiddenPanel");
if (hide)
{
hiddenPanel.hideFlags = HideFlags.HideAndDontSave;
}
//Add Image Component to the hidden panel
Image pnlImg = hiddenPanel.AddComponent<Image>();
pnlImg.sprite = null;
pnlImg.color = new Color(1, 1, 1, 0); //Invisible
pnlImg.material = null;
pnlImg.raycastTarget = true;
//Make it child of HiddenCanvas GameObject
hiddenPanel.transform.SetParent(hiddenCanvas.transform);
return hiddenPanel;
}
//Set Canvas width and height,to matach screen width and height then set anchor points to the corner of canvas.
private void stretchImageAndConfigAnchor(GameObject panel)
{
Image pnlImg = panel.GetComponent<Image>();
//Reset postion to middle of the screen
pnlImg.rectTransform.anchoredPosition3D = new Vector3(0, 0, 0);
//Stretch the Image so that the whole screen is totally covered
pnlImg.rectTransform.anchorMin = new Vector2(0, 0);
pnlImg.rectTransform.anchorMax = new Vector2(1, 1);
pnlImg.rectTransform.pivot = new Vector2(0.5f, 0.5f);
}
//Adds EventForwarder script to the Hidden Panel GameObject
private void addEventForwarder(GameObject panel)
{
EventForwarder evnfwdr = panel.AddComponent<EventForwarder>();
}
//Adds EventUnBlocker script to the Hidden Panel GameObject
private void addEventRouter(GameObject panel)
{
EventUnBlocker evtrtr = panel.AddComponent<EventUnBlocker>();
eventRouter = evtrtr;
}
//Add EventSystem
private void addEventSystemAndInputModule()
{
//Check if EventSystem exist. If it does not create and add it
EventSystem eventSys = FindObjectOfType<EventSystem>();
if (eventSys == null)
{
GameObject evObj = new GameObject("EventSystem");
EventSystem evs = evObj.AddComponent<EventSystem>();
evs.firstSelectedGameObject = null;
evs.sendNavigationEvents = true;
evs.pixelDragThreshold = 5;
eventSys = evs;
}
//Check if StandaloneInputModule exist. If it does not create and add it
StandaloneInputModule sdlIpModl = FindObjectOfType<StandaloneInputModule>();
if (sdlIpModl == null)
{
sdlIpModl = eventSys.gameObject.AddComponent<StandaloneInputModule>();
sdlIpModl.horizontalAxis = "Horizontal";
sdlIpModl.verticalAxis = "Vertical";
sdlIpModl.submitButton = "Submit";
sdlIpModl.cancelButton = "Cancel";
sdlIpModl.inputActionsPerSecond = 10f;
sdlIpModl.repeatDelay = 0.5f;
sdlIpModl.forceModuleActive = false;
}
}
/*
Forwards Handler Event to every GameObject that implements IDragHandler, IPointerDownHandler, IPointerUpHandler interface
*/
public void forwardDragEvent(PointerEventData eventData)
{
//Route and send the event to UI and Colliders
for (int i = 0; i < registeredGameobjects.Count; i++)
{
ExecuteEvents.Execute<IDragHandler>(registeredGameobjects[i],
eventData,
ExecuteEvents.dragHandler);
}
//Route and send the event to UI and Colliders
eventRouter.routeDragEvent(eventData);
}
public void forwardPointerDownEvent(PointerEventData eventData)
{
//Send the event to all subscribed scripts
for (int i = 0; i < registeredGameobjects.Count; i++)
{
ExecuteEvents.Execute<IPointerDownHandler>(registeredGameobjects[i],
eventData,
ExecuteEvents.pointerDownHandler);
}
//Route and send the event to UI and Colliders
eventRouter.routePointerDownEvent(eventData);
}
public void forwardPointerUpEvent(PointerEventData eventData)
{
//Send the event to all subscribed scripts
for (int i = 0; i < registeredGameobjects.Count; i++)
{
ExecuteEvents.Execute<IPointerUpHandler>(registeredGameobjects[i],
eventData,
ExecuteEvents.pointerUpHandler);
}
//Route and send the event to UI and Colliders
eventRouter.routePointerUpEvent(eventData);
}
}
2.EventForwarder.cs - It simply receives any event from the hidden Image and passes it to the WholeScreenPointer.cs script for processing.
public class EventForwarder : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler
{
WholeScreenPointer wcp = null;
void Start()
{
wcp = WholeScreenPointer.Instance;
}
public void OnDrag(PointerEventData eventData)
{
wcp.forwardDragEvent(eventData);
}
public void OnPointerDown(PointerEventData eventData)
{
wcp.forwardPointerDownEvent(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
wcp.forwardPointerUpEvent(eventData);
}
}
3.EventUnBlocker.cs - It unblocks the the rays the hidden Image is blocking by sending fake event to any Object above it. Be it UI, 2D or 3D collider.
public class EventUnBlocker : MonoBehaviour
{
List<GraphicRaycaster> grRayCast = new List<GraphicRaycaster>(); //UI
List<Physics2DRaycaster> phy2dRayCast = new List<Physics2DRaycaster>(); //Collider 2D (Sprite Renderer)
List<PhysicsRaycaster> phyRayCast = new List<PhysicsRaycaster>(); //Normal Collider(3D/Mesh Renderer)
List<RaycastResult> resultList = new List<RaycastResult>();
//For Detecting button click and sending fake Button Click to UI Buttons
Dictionary<int, GameObject> pointerIdToGameObject = new Dictionary<int, GameObject>();
// Use this for initialization
void Start()
{
}
public void sendArtificialUIEvent(Component grRayCast, PointerEventData eventData, PointerEventType evType)
{
//Route to all Object in the RaycastResult
for (int i = 0; i < resultList.Count; i++)
{
/*Do something if it is NOT this GameObject.
We don't want any other detection on this GameObject
*/
if (resultList[i].gameObject != this.gameObject)
{
//Check if this is UI
if (grRayCast is GraphicRaycaster)
{
//Debug.Log("UI");
routeEvent(resultList[i], eventData, evType, true);
}
//Check if this is Collider 2D/SpriteRenderer
if (grRayCast is Physics2DRaycaster)
{
//Debug.Log("Collider 2D/SpriteRenderer");
routeEvent(resultList[i], eventData, evType, false);
}
//Check if this is Collider/MeshRender
if (grRayCast is PhysicsRaycaster)
{
//Debug.Log("Collider 3D/Mesh");
routeEvent(resultList[i], eventData, evType, false);
}
}
}
}
//Creates fake PointerEventData that will be used to make PointerEventData for the callback functions
PointerEventData createEventData(RaycastResult rayResult)
{
PointerEventData fakeEventData = new PointerEventData(EventSystem.current);
fakeEventData.pointerCurrentRaycast = rayResult;
return fakeEventData;
}
private void routeEvent(RaycastResult rayResult, PointerEventData eventData, PointerEventType evType, bool isUI = false)
{
bool foundKeyAndValue = false;
GameObject target = rayResult.gameObject;
//Make fake GameObject target
PointerEventData fakeEventData = createEventData(rayResult);
switch (evType)
{
case PointerEventType.Drag:
//Send/Simulate Fake OnDrag event
ExecuteEvents.Execute<IDragHandler>(target, fakeEventData,
ExecuteEvents.dragHandler);
break;
case PointerEventType.Down:
//Send/Simulate Fake OnPointerDown event
ExecuteEvents.Execute<IPointerDownHandler>(target,
fakeEventData,
ExecuteEvents.pointerDownHandler);
//Code Below is for UI. break out of case if this is not UI
if (!isUI)
{
break;
}
//Prepare Button Click. Should be sent in the if PointerEventType.Up statement
Button buttonFound = target.GetComponent<Button>();
//If pointerId is not in the dictionary add it
if (buttonFound != null)
{
if (!dictContains(eventData.pointerId))
{
dictAdd(eventData.pointerId, target);
}
}
//Bug in Unity with GraphicRaycaster and Toggle. Have to use a hack below
//Toggle Toggle component
Toggle toggle = null;
if ((target.name == "Checkmark" || target.name == "Label") && toggle == null)
{
toggle = target.GetComponentInParent<Toggle>();
}
if (toggle != null)
{
//Debug.LogWarning("Toggled!: " + target.name);
toggle.isOn = !toggle.isOn;
//Destroy(toggle.gameObject);
}
break;
case PointerEventType.Up:
//Send/Simulate Fake OnPointerUp event
ExecuteEvents.Execute<IPointerUpHandler>(target,
fakeEventData,
ExecuteEvents.pointerUpHandler);
//Code Below is for UI. break out of case if this is not UI
if (!isUI)
{
break;
}
//Send Fake Button Click if requirement is met
Button buttonPress = target.GetComponent<Button>();
/*If pointerId is in the dictionary, check
*/
if (buttonPress != null)
{
if (dictContains(eventData.pointerId))
{
//Check if GameObject matches too. If so then this is a valid Click
for (int i = 0; i < resultList.Count; i++)
{
GameObject tempButton = resultList[i].gameObject;
if (tempButton != this.gameObject && dictContains(eventData.pointerId, tempButton))
{
foundKeyAndValue = true;
//Debug.Log("Button ID and GameObject Match! Sending Click Event");
//Send/Simulate Fake Click event to the Button
ExecuteEvents.Execute<IPointerClickHandler>(tempButton,
new PointerEventData(EventSystem.current),
ExecuteEvents.pointerClickHandler);
}
}
}
}
break;
}
//Remove pointerId since it exist
if (foundKeyAndValue)
{
dictRemove(eventData.pointerId);
}
}
void routeOption(PointerEventData eventData, PointerEventType evType)
{
UpdateRaycaster();
if (WholeScreenPointer.Instance.simulateUIEvent)
{
//Loop Through All GraphicRaycaster(UI) and throw Raycast to each one
for (int i = 0; i < grRayCast.Count; i++)
{
//Throw Raycast to all UI elements in the position(eventData)
grRayCast[i].Raycast(eventData, resultList);
sendArtificialUIEvent(grRayCast[i], eventData, evType);
}
//Reset Result
resultList.Clear();
}
if (WholeScreenPointer.Instance.simulateCollider2DEvent)
{
//Loop Through All Collider 2D (Sprite Renderer) and throw Raycast to each one
for (int i = 0; i < phy2dRayCast.Count; i++)
{
//Throw Raycast to all UI elements in the position(eventData)
phy2dRayCast[i].Raycast(eventData, resultList);
sendArtificialUIEvent(phy2dRayCast[i], eventData, evType);
}
//Reset Result
resultList.Clear();
}
if (WholeScreenPointer.Instance.simulateColliderEvent)
{
//Loop Through All Normal Collider(3D/Mesh Renderer) and throw Raycast to each one
for (int i = 0; i < phyRayCast.Count; i++)
{
//Throw Raycast to all UI elements in the position(eventData)
phyRayCast[i].Raycast(eventData, resultList);
sendArtificialUIEvent(phyRayCast[i], eventData, evType);
}
//Reset Result
resultList.Clear();
}
}
public void routeDragEvent(PointerEventData eventData)
{
routeOption(eventData, PointerEventType.Drag);
}
public void routePointerDownEvent(PointerEventData eventData)
{
routeOption(eventData, PointerEventType.Down);
}
public void routePointerUpEvent(PointerEventData eventData)
{
routeOption(eventData, PointerEventType.Up);
}
public void UpdateRaycaster()
{
convertToList(FindObjectsOfType<GraphicRaycaster>(), grRayCast);
convertToList(FindObjectsOfType<Physics2DRaycaster>(), phy2dRayCast);
convertToList(FindObjectsOfType<PhysicsRaycaster>(), phyRayCast);
}
//To avoid ToList() function
void convertToList(GraphicRaycaster[] fromComponent, List<GraphicRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
//To avoid ToList() function
void convertToList(Physics2DRaycaster[] fromComponent, List<Physics2DRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
//To avoid ToList() function
void convertToList(PhysicsRaycaster[] fromComponent, List<PhysicsRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
//Checks if object is in the dictionary
private bool dictContains(GameObject obj)
{
return pointerIdToGameObject.ContainsValue(obj);
}
//Checks if int is in the dictionary
private bool dictContains(int pointerId)
{
return pointerIdToGameObject.ContainsKey(pointerId);
}
//Checks if int and object is in the dictionary
private bool dictContains(int pointerId, GameObject obj)
{
return (pointerIdToGameObject.ContainsKey(pointerId) && pointerIdToGameObject.ContainsValue(obj));
}
//Adds pointerId and its value to dictionary
private void dictAdd(int pointerId, GameObject obj)
{
pointerIdToGameObject.Add(pointerId, obj);
}
//Removes pointerId and its value from dictionary
private void dictRemove(int pointerId)
{
pointerIdToGameObject.Remove(pointerId);
}
public enum PointerEventType
{
Drag, Down, Up
}
}
Usage:
1.Attach the WholeScreenPointer script to an empty GameObject or the Camera.
2.To receive any event in the scene, simply implement IDragHandler, IPointerDownHandler, IPointerUpHandler in any script then call WholeScreenPointer.Instance.registerGameObject(this.gameObject); once. Any event from the screen will now be sent to that script. Don't forget to unregister in the OnDisable() function.
For example, attach Test to any GameObject you want to receive touch events:
public class Test : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler
{
void Start()
{
//Register this GameObject so that it will receive events from WholeScreenPointer script
WholeScreenPointer.Instance.registerGameObject(this.gameObject);
}
public void OnDrag(PointerEventData eventData)
{
Debug.Log("Dragging: ");
}
public void OnPointerDown(PointerEventData eventData)
{
Debug.Log("Pointer Down: ");
}
public void OnPointerUp(PointerEventData eventData)
{
Debug.Log("Pointer Up: ");
}
void OnDisable()
{
WholeScreenPointer.Instance.unRegisterGameObject(this.gameObject);
}
}
NOTE:
You only need to call WholeScreenPointer.Instance.registerGameObject(this.gameObject); if you want to receive event anywhere on the screen. If you just want to receive event from current Object, then you don't have to call this. If you do, you will receive multiple events.
Other Important functions:
Enable WholeScreen Event - WholeScreenPointer.Instance.enablewholeScreenPointer(true);
Disable WholeScreen Event - WholeScreenPointer.Instance.enablewholeScreenPointer(false);
Finally, this can be improved more.

The question and the answer I am going to post seems pretty much opinion based. Nevertheless I am going to answer as best as I can.
If you are trying to detect pointer events on the screen, there is nothing wrong with representing the screen with an object. In your case, you use a 3D collider to cover the entire frustum of the camera. However, there is a native way to do this in Unity, using a 2D UI object that covers the entire screen. The screen can be best represented by a 2D object. For me, this seems like a natural way to do it.
I use a generic code for this purpose:
public class Screen : MonoSingleton<Screen>, IPointerClickHandler, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler, IScrollHandler {
private bool holding = false;
private PointerEventData lastPointerEventData;
#region Events
public delegate void PointerEventHandler(PointerEventData data);
static public event PointerEventHandler OnPointerClick = delegate { };
static public event PointerEventHandler OnPointerDown = delegate { };
/// <summary> Dont use delta data as it will be wrong. If you are going to use delta, use OnDrag instead. </summary>
static public event PointerEventHandler OnPointerHold = delegate { };
static public event PointerEventHandler OnPointerUp = delegate { };
static public event PointerEventHandler OnBeginDrag = delegate { };
static public event PointerEventHandler OnDrag = delegate { };
static public event PointerEventHandler OnEndDrag = delegate { };
static public event PointerEventHandler OnScroll = delegate { };
#endregion
#region Interface Implementations
void IPointerClickHandler.OnPointerClick(PointerEventData e) {
lastPointerEventData = e;
OnPointerClick(e);
}
// And other interface implementations, you get the point
#endregion
void Update() {
if (holding) {
OnPointerHold(lastPointerEventData);
}
}
}
The Screen is a singleton, because there is only one screen in the context of the game. Objects(like camera) subscribe to its pointer events, and arrange theirselves accordingly. This also keeps single-responsibility intact.
You would use this as appending it to an object that represents the so called glass (surface of the screen). If you think buttons on the UI as popping out of the screen, glass would be under them. For this, the glass has to be the first child of the Canvas. Of course, the Canvas has to be rendered in screen space for it to make sense.
One hack here, which doesn't make sense is to add an invisible Image component to the glass, so it would receive events. This acts like the raycast target of the glass.
You could also use Input (Input.touches etc.) to implement this glass object. It would work as checking if the input changed in every Update call. This seems like a polling-based approach to me, whereas the above one is an event-based approach.
Your question seems as if looking for a way to justify using the Input class. IMHO, Do not make it harder for yourself. Use what works. And accept the fact that Unity is not perfect.

Related

Unity Mirror: inbuilt buttons not working on client side of the server

I am very new to unity networking and I am trying to make a game where the user can click multiple buttons in order to spawn pieces to a board. I've hit a lot of roadblocks while doing this and I've managed to figure out a lot of them but I seem to be stuck on this one. the buttons click as they should on the host computer however, on the client's computer nothing happens. the button doesn't even highlight when clicked which is an inbuilt unity feature
I have used this code to spawn in all the button prefabs into the code. the prefabs have a prebuilt network identity and a network transform component i have tried with or without client authority and nothing changes.
public class BoardSetupScript : NetworkBehaviour
{
public GameObject[] SpawnList;
public bool isPlayer1; //used to determine which set of objects need to be placed.
public GameObject self; //makes sure the set up only happens once
// Start is called before the first frame update
void Start()
{
if (self.transform.position.x < 0) //the two players are spawned on either ends, to tell which one is which we use the position of object
{
isPlayer1 = true;
}
else
{
isPlayer1 = false;
}
}
// Update is called once per frame
void Update()
{
if (isLocalPlayer) //checks that only the player that has authority over this object can execute this function
{
if ((Input.GetKeyDown(KeyCode.Space))&(oncespawned==false))
{
oncespawned =true;
SpawnAllObjects();
}
}
}
void SpawnAllObjects()
{
Cmd_SpawnAllObjects();
}
[Command]
void Cmd_SpawnAllObjects() //object is positioned so that this spawns all the buttons in the correct place, depending on which player the box belongs to. used for loops for efficiency
{
if (isPlayer1 == true)
{
for (int Index=0; Index<10; Index++)
{
Rpc_SpawnObject(Index,Index);
}
}
if (isPlayer1 == false)
{
for (int Index=10; Index<20; Index++)
{
Rpc_SpawnObject(Index, Index - 10);
}
}
}
[ClientRpc]
void Rpc_SpawnObject(int indexOfObject,int relativePos) //instantiate adds the object to the game and then spawn sets it up as part of the network.
{
SpawnList[indexOfObject] = (GameObject)Instantiate(SpawnList[indexOfObject], new Vector3(this.transform.position.x,this.transform.position.y - relativePos, 0), Quaternion.identity);
NetworkIdentity SpawnableIdentity = SpawnList[indexOfObject].GetComponent<NetworkIdentity>();
NetworkServer.Spawn(SpawnList[indexOfObject]);
if (connectionToClient != null) //to make sure that Assign Client authority doesnt crash the server
{
SpawnableIdentity.AssignClientAuthority(connectionToClient); //gives the player who owns the player object control of this object
SpawnList[indexOfObject].transform.SetParent(self.transform,true);
}
}
}
here is the code for the functions the buttons are supposed to execute.
public class NetworkBoardStats : NetworkBehaviour
{
[SyncVar]
public bool p1turn=true;
[SyncVar]
public int p1gold;
[SyncVar]
public int p2gold;
Start()
{
}
// Update is called once per frame
void Update()
{
}
public void p22EndTurn()
{
if (isLocalPlayer)
{
Rpc_p2EndTurn();
}
}
public void p1EndTurn()
{
if (isLocalPlayer)
{
Rpc_p1EndTurn();
}
}
[ClientRpc]
public void Rpc_p1EndTurn()
{
if (p1turn == true)
{
p1turn = false;
p2gold += 5;
}
}
[ClientRpc]
public void Rpc_p2EndTurn() //ends player 2's turn over the network.
{
Debug.Log("p2EndTurn has been activated ");
if (p1turn == false)
{
p1turn = true;
p1gold += 5;
Debug.Log("p2EndTurn has been executed");
}
}
}
i have put all the buttons i want to spawn in the spawnable prefabs list
my only theory is to why this is could be that i set up the prefabs wrong as i have given the canvas the button is attached to the network identity and transform and not the button itself but you cant have multiple identities on the same object so i dont know how you would do that.
any help would be appreciated or if you have a better way of making a bunch of buttons that spawn pieces to fight across a network that would be appreciated too thanks!
apologies for weird formatting stack overflow was being finicky about my code

Raycast not detecting custom Unity UI button script

I have written a custom class to create buttons which can have an animator. But objects of this class are not detectable by raycast but normal Unity UI buttons are detectable by raycast. I'm looking for a solution which can be solved through code.
public class AnimatedButton : UIBehaviour, IPointerClickHandler
{
[Serializable]
private class ButtonClickedEvent : UnityEvent
{
}
public bool Interactable = true;
[SerializeField]
private ButtonClickedEvent onClick = new ButtonClickedEvent();
private Animator animator;
private bool blockInput;
protected override void Start()
{
base.Start();
animator = GetComponent<Animator>();
}
public virtual void OnPointerClick(PointerEventData eventData)
{
if (!Interactable || eventData.button != PointerEventData.InputButton.Left)
return;
if (!blockInput)
{
blockInput = true;
Press();
// Block the input for a short while to prevent spamming.
StartCoroutine(BlockInputTemporarily());
}
}
public void Press()
{
if (!IsActive())
return;
animator.SetTrigger("Pressed");
StartCoroutine(InvokeOnClickAction());
}
private IEnumerator InvokeOnClickAction()
{
yield return new WaitForSeconds(0.1f);
onClick.Invoke();
}
private IEnumerator BlockInputTemporarily()
{
yield return new WaitForSeconds(0.5f);
blockInput = false;
}
}
below code is used to find the gameobject by firing a raycast
private bool checkButtonClick()
{
bool flag = false;
PointerEventData pointer = new PointerEventData(EventSystem.current);
List<RaycastResult> raycastResult = new List<RaycastResult>();
pointer.position = Input.mousePosition;
EventSystem.current.RaycastAll(pointer, raycastResult);
foreach (RaycastResult result in raycastResult)
{
if (result.gameObject.GetComponent<Button>() != null || result.gameObject.GetComponent<AnimatedButton>() != null)
{
Debug.Log("Button Name : " + result.gameObject.name);
}
}
raycastResult.Clear();
return flag;
}
Only objects of type "Button" are printed with this log and objects of type "AnimatedButton" are not detected. What could be the issue here and how to solve it?
You're close. But it's not UIBehaviour, it's actually UnityEngine.UI.Graphic, derived from UIBehaviour, that is searched for by the GraphicRaycaster. So you should be able to derive from Graphic, and your new button type will be found in the raycast results. Here's the Unity docs relating to the Graphic class. Quoting that page:
Base class for all visual UI Component.
When creating visual UI components you should inherit from this class.

Hierarchy issue Unity

Im making a simple game in Unity 3d, basically you have a backdround object with more gameobjects on top, what you have to do is drag those game objects into some trash cans, as soon as you hit the correct trash can with the correct object said object gets deleted, the idea of the game is that when you put all the objects in the corect cans you win the game
I have the drag and acceptance all made but i cant figure out how to make the script so that when theres no more objects, you win the game.
I've been trying to use Hierarchy on an empty game object thats the parent for the objects that you have to destroy, but i dont know how to actually delete the objects so that the code can register it, any help?
The code for the drag and the code im using for the trash cans:
using UnityEngine;
using UnityEngine.EventSystems;
public class arrastrar : MonoBehaviour, IDragHandler
{
public float z = 0.0f;
public void OnDrag(PointerEventData eventData)
{
Vector3 mousePosition = Input.mousePosition;
mousePosition.z = z;
transform.position = Camera.main.ScreenToWorldPoint(mousePosition);
}
}
And the code inside the trashcans
using System.Collections;
using UnityEngine;
using System.Collections.Generic;
public class botarBasura: MonoBehaviour
{
void OnCollisionEnter(Collision other)
{
if (other.gameObject.name == "cubo")
Destroy(other.gameObject);
} else if (other.gameObject.name == "escombros")
{
other.gameObject.transform.position = new Vector3(-1, 2.5f, -2); //If its not the correct trash can it returns the object to the starting position
}
} else if ( other.gameObject.name == "botellas")
{
other.gameObject.transform.position = new Vector3(0, 2.5f, -2);
}
}
}
You can use a scriptable object to keep track of your object. Objects just need to add and remove themselves to a list in the scriptable object in their OnEnable() and OnDisable() callbacks. When an object removes itself you can have code check to see if there are no more objects left and trigger an event that something in your scene will react to.
(Dragable Object)
public class DragableObject : MonoBehaviour{
//reference to the scriptable object
public DragableObjectSet RuntimeSet;
void OnEnable(){
RuntimeSet.Add(this);
}
void OnDisable(){
RuntimeSet.Remove(this);
//you can put code checking if the runtime set is empty here
//or you can put it in the DragableObjectSet Remove method
}
}
Scriptable object example code
public DragableObjectSet : ScriptableObject
{
public List<DragableObject> RuntimeSet = new List<DragableObject>();
public void Add(DragableObject obj){
RuntimeSet.Add(obj);
}
public void Remove(DragableObject obj){
RuntimeSet.Remove(obj);
if(RuntimeSet.Count() == 0)
//some code here raising an event (if desired)
}
}
This type of object is called a runtime set and is detailed in this talk around the 40 minute mark.
https://www.youtube.com/watch?v=raQ3iHhE_Kk
A solution could possibly be using a singleton:
Create a new empty GameObject and add this code:
using UnityEngine;
public class TrashCounter : MonoBehaviour {
public static TrashCounter instance = null;
public int counter = 0;
void Awake(){ instance = this; }
}
Now, wherever you create an object (or in the Start() method of any object code), you just need to do TrashCounter.instance.counter++ and you will have your trash count.
After that, you just need to update this counter everytime you delete an object by doing TrashCounter.instance.counter-- and after you ask if(TrashCounter.instance.counter == 0) WinGame().
You can get an array of all active & not-yet-destroyed GameObjects with some enabled component with Object.FindObjectsOfType<ComponentType>();. Once you have that, you could then look at the length of that array.
So, you could do something like this in the code that destroys/deactivates the draggable gameobject:
GameObject otherObject = /* get draggable object to possibly destroy */;
if ( /* test to remove otherObject */ )
{
// deactivate the draggable object
otherObject.setActive = false;
// get all other draggable objects
DraggableObject[] remainingDraggableObjects = Object.FindObjectsOfType<DraggableObject>();
if (remainingDraggableObjects.Length == 0)
{
// there are no more active gameobjects with enabled DraggableObject component
// handle win condition here.
}
}
In your specific example, it could look like this:
void OnCollisionEnter(Collision other)
{
if (other.gameObject.name == "cubo")
{
other.gameObject.setActive = false; // better than destroying in most cases
// get all other draggable objects
arrastrar[] remainingObjects = Object.FindObjectsOfType<arrastrar>();
if (remainingObjects.Length == 0)
{
// there are no more active gameobjects with enabled arrastrar component
// handle win condition here.
}
}
// ...

Unity3D, have BOTH 3D and UI Raycaster simultaneously respond to touch?

These days in Unity you very easily detect touch on objects using a raycaster...
public class DragThreeDee:MonoBehaviour,IPointerDownHandler
IPointerUpHandler, IBeginDragHandler,
IDragHandler, IEndDragHandler
{
public void OnPointerDown (PointerEventData data)
{ etc
So, regarding your UI layer, it correctly ignores touch on the UI layer and so on.
What if you want the following to happen,
you want to collect (simultaneously, in the same frame, both) a touch which is over both a 3D object, and, a UI item?
(Imagine you have, say, a few transparent UI buttons on the screen; you also have ordinary 3D robots running around. User clicks on a point on the screen where there's a UI button, and "underneath", a robot. BOTH the robot's script as above, and the button, should respond.)
How would you do that? I tried extensively using separate cameras, separate EventSystem, adjusting the layers, blocking masks and so on. I can't find a way to do it.
How to do this in Unity?
Looks complicated but can be done.
1.Route the data from PointerEventData in all of the UI components you want to unblock 3D GameObjects raycast on.
2.Get all the instance of PhysicsRaycaster with FindObjectsOfType<PhysicsRaycaster>(). In terms of performance, it would make sense to cache this.
3.Perform a Raycast with PhysicsRaycaster.Raycast which will return all GameObjects with Collider attached to it.
4.Use ExecuteEvents.Execute to send the appropriate event to the result stored in the RaycastResult.
RaycastForwarder script:
public class RaycastForwarder : MonoBehaviour
{
List<PhysicsRaycaster> rayCast3D = new List<PhysicsRaycaster>();
List<RaycastResult> rayCast3DResult = new List<RaycastResult>();
private static RaycastForwarder localInstance;
public static RaycastForwarder Instance { get { return localInstance; } }
private void Awake()
{
if (localInstance != null && localInstance != this)
{
Destroy(this.gameObject);
}
else
{
localInstance = this;
}
}
public void notifyPointerDown(PointerEventData eventData)
{
findColliders(eventData, PointerEventType.Down);
}
public void notifyPointerUp(PointerEventData eventData)
{
findColliders(eventData, PointerEventType.Up);
}
public void notifyPointerDrag(PointerEventData eventData)
{
findColliders(eventData, PointerEventType.Drag);
}
private void findColliders(PointerEventData eventData, PointerEventType evType)
{
UpdateRaycaster();
//Loop Through All Normal Collider(3D/Mesh Renderer) and throw Raycast to each one
for (int i = 0; i < rayCast3D.Count; i++)
{
//Send Raycast to all GameObject with 3D Collider
rayCast3D[i].Raycast(eventData, rayCast3DResult);
sendRayCast(eventData, evType);
}
//Reset Result
rayCast3DResult.Clear();
}
private void sendRayCast(PointerEventData eventData, PointerEventType evType)
{
//Loop over the RaycastResult and simulate the pointer event
for (int i = 0; i < rayCast3DResult.Count; i++)
{
GameObject target = rayCast3DResult[i].gameObject;
PointerEventData evData = createEventData(rayCast3DResult[i]);
if (evType == PointerEventType.Drag)
{
ExecuteEvents.Execute<IDragHandler>(target,
evData,
ExecuteEvents.dragHandler);
}
if (evType == PointerEventType.Down)
{
ExecuteEvents.Execute<IPointerDownHandler>(target,
evData,
ExecuteEvents.pointerDownHandler);
}
if (evType == PointerEventType.Up)
{
ExecuteEvents.Execute<IPointerUpHandler>(target,
evData,
ExecuteEvents.pointerUpHandler);
}
}
}
private PointerEventData createEventData(RaycastResult rayResult)
{
PointerEventData evData = new PointerEventData(EventSystem.current);
evData.pointerCurrentRaycast = rayResult;
return evData;
}
//Get all PhysicsRaycaster in the scene
private void UpdateRaycaster()
{
convertToList(FindObjectsOfType<PhysicsRaycaster>(), rayCast3D);
}
private void convertToList(PhysicsRaycaster[] fromComponent, List<PhysicsRaycaster> toComponent)
{
//Clear and copy new Data
toComponent.Clear();
for (int i = 0; i < fromComponent.Length; i++)
{
toComponent.Add(fromComponent[i]);
}
}
public enum PointerEventType
{
Drag, Down, Up
}
}
RayCastRouter script:
public class RayCastRouter : MonoBehaviour, IPointerDownHandler,
IPointerUpHandler,
IDragHandler
{
public void OnDrag(PointerEventData eventData)
{
RaycastForwarder.Instance.notifyPointerDrag(eventData);
}
public void OnPointerDown(PointerEventData eventData)
{
RaycastForwarder.Instance.notifyPointerDown(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
RaycastForwarder.Instance.notifyPointerUp(eventData);
}
}
Usage:
A.Attach RaycastForwarder to an empty GameObject.
B.Attach RayCastRouter to any UI component such as Image that you don't want to block a 3D GameObject. That's it. Any UI component with RayCastRouter attached to it will be able to allow a 3D GameObject behind it receive a raycast.
An event will now be sent to 3D Object that has a script that implements functions from IPointerDownHandler, IPointerUpHandler and IDragHandler interface.
Don't forget to attach Physics Raycaster to the camera.

how to make image appear when player is dead in unity

I need to make an image pop up when the player has died or crashed but i do not know how to do it, i'm trying to make a game in unity using c#
but i have made a code that will tell show the user an image before they start (tap to start image) and all i want to do is display another one that tell to user to start again
does the code have to be similar to this or do i have to start from scratch?
public class StartScreenScript : MonoBehaviour {
static bool sawOnce = false;
// Use this for initialization
void Start () {
if(!sawOnce) {
GetComponent<SpriteRenderer>().enabled = true;
Time.timeScale = 0;
}
sawOnce = true;
}
// Update is called once per frame
void Update () {
if(Time.timeScale==0 && (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0)) ) {
Time.timeScale = 1;
GetComponent<SpriteRenderer>().enabled = false;
}
}
}
this code show the an image telling the user to tap the screen and the image then goes away until the user closes the game then comes back on however i want to display a "you are dead image" every time the player dies can someone please help me
p.s this is for a 2d game
Well one way to do it would be using Unity3D's GUI.DrawTexture that given a texture draws it at a given position. Here is a sample call to the method.
GUI.DrawTexture(new Rect(leftAnchor, topAnchor, textureWidth, textureHeight), textureSource);
This is my approach and it works in most cases:
Create a GameObject that will work as a dead screen. Apply a sprite
or whatever telling the user to click to restart.
Add the previous GameObject to the PlayerController so he can
instantiate it.
When the player is dead call PlayerController.ShowDeadScreen()
When the user clicks inside DeadScreen GameObject it will call your
PlayerController.PlayAgain function and destroy itself. So you must handle everything
the game need to be restarted.
PlayerController example code
public class PlayerController : MonoBehaviour {
public GameObject deadScreen;
void Start() { }
void Update() { }
public void ShowDeadScreen()
{
// show DeadScreen GameObject on the center of the screen
GameObject go = Instantiate(deadScreen, new Vector(0, 0, 0), Quaternation.Identity) as GameObject;
go.playerController = this;
}
public void PlayAgain()
{
// handle game restart
}
}
DeadScreen example code
public class DeadScreen : MonoBehaviour {
public PlayerController playerController;
void Start() { }
void Update() { }
void OnMouseDown()
{
// when user clicks inside this GameObject start the game again
playerController.PlayAgain();
Destroy(this.gameObject);
}
}

Categories