Unity3D New Input System: Is it really so hard to stop UI clickthroughs (or figure out if cursor is over a UI object)? - unity3d

Even the official documentation has borderline insane recommendations to solve what is probably one of the most common UI/3D interaction issues:
If I click while the cursor is over a UI button, both the button (via the graphics raycaster) and the 3D world (via the physics raycaster) will receive the event.
The official manual:
https://docs.unity3d.com/Packages/com.unity.inputsystem#1.2/manual/UISupport.html#handling-ambiguities-for-pointer-type-input essentially says "how about you design your game so you don't need 3D and UI at the same time?".
I cannot believe this is not a solved problem. But everything I've tried failed. EventSystem.current.currentSelectedGameObject is sticky, not hover. PointerData is protected and thus not accessible (and one guy offered a workaround via deriving your own class from Standalone Input Module to get around that, but that workaround apparently doesn't work anymore). The old IsPointerOverGameObject throws a warning if you query it in the callback and is always true if you query it in Update().
That's all just mental. Please someone tell me there's a simple, obvious solution to this common, trivial problem that I'm just missing. The graphics raycaster certainly stores somewhere if it's over a UI element, right? Please?

I've looked into this a fair bit and in the end, the easiest solution seems to be to do what the manual says and put it in the Update function.
bool pointerOverUI = false;
void Update()
{
pointerOverUI = EventSystem.current.IsPointerOverGameObject();
}

Your frustration is well founded: there are NO examples of making UI work with NewInput which I've found. I can share a more robust version of the Raycaster workaround, from Youtube:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
/* Danndx 2021 (youtube.com/danndx)
From video: youtu.be/7h1cnGggY2M
thanks - delete me! :) */
public class SCR_UiInteraction : MonoBehaviour
{
public GameObject ui_canvas;
GraphicRaycaster ui_raycaster;
PointerEventData click_data;
List<RaycastResult> click_results;
void Start()
{
ui_raycaster = ui_canvas.GetComponent<GraphicRaycaster>();
click_data = new PointerEventData(EventSystem.current);
click_results = new List<RaycastResult>();
}
void Update()
{
// use isPressed if you wish to ray cast every frame:
//if(Mouse.current.leftButton.isPressed)
// use wasReleasedThisFrame if you wish to ray cast just once per click:
if(Mouse.current.leftButton.wasReleasedThisFrame)
{
GetUiElementsClicked();
}
}
void GetUiElementsClicked()
{
/** Get all the UI elements clicked, using the current mouse position and raycasting. **/
click_data.position = Mouse.current.position.ReadValue();
click_results.Clear();
ui_raycaster.Raycast(click_data, click_results);
foreach(RaycastResult result in click_results)
{
GameObject ui_element = result.gameObject;
Debug.Log(ui_element.name);
}
}
}
So, just drop into my "Menusscript.cs"?
But as a pattern, this is terrible for separating UI concerns. I'm currently rewiring EVERY separately-concerned PointerEventData click I had already working, and my question is, Why? I can't even find how it's supposed to work: to your point there IS no official guide at all around clicking UI, and it does NOT just drop-on-top.
Anyway, I haven't found anything yet which makes new input work easily on UI, and definitely not found how I'm going to sensibly separate Menuclicks from Activityclicks while keeping game & ui assemblies separate.
Good luck to us all.

Unity documentation for this issue with regard to Unity.InputSystem can be found at https://docs.unity3d.com/Packages/com.unity.inputsystem#1.3/manual/UISupport.html#handling-ambiguities-for-pointer-type-input.
IsPointerOverGameObject() can always return true if the extent of your canvas covers the camera's entire field of view.
For clarity, here is the solution which I found worked best (accumulated from several other posts across the web).
Attach this script to your UI Canvas object:
public class CanvasHitDetector : MonoBehaviour {
private GraphicRaycaster _graphicRaycaster;
private void Start()
{
// This instance is needed to compare between UI interactions and
// game interactions with the mouse.
_graphicRaycaster = GetComponent<GraphicRaycaster>();
}
public bool IsPointerOverUI()
{
// Obtain the current mouse position.
var mousePosition = Mouse.current.position.ReadValue();
// Create a pointer event data structure with the current mouse position.
var pointerEventData = new PointerEventData(EventSystem.current);
pointerEventData.position = mousePosition;
// Use the GraphicRaycaster instance to determine how many UI items
// the pointer event hits. If this value is greater-than zero, skip
// further processing.
var results = new List<RaycastResult>();
_graphicRaycaster.Raycast(pointerEventData, results);
return results.Count > 0;
}
}
In class containing the method which is handling the mouse clicks, obtain the reference to the Canvas UI either using GameObject.Find() or a public exposed variable, and call IsPointerOverUI() to filter clicks when over UI.

Reply to #Milad Qasemi's answer
From the docs you have attached in your answer, I have tried the following to check if the user clicked on a UI element or not.
// gets called in the Update method
if(Input.GetMouseButton(0) {
int layerMask = 1 << 5;
// raycast in the UI layer
RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, Mathf.Infinity, layerMask);
// if the ray hit any UI element, return
// don't handle player movement
if (hit.collider) { return; }
Debug.Log("Touched not on UI");
playerController.HandlePlayerMovement(x);
}
The raycast doesn't seem to detect collisions on UI elements. Below is a picture of the Graphics Raycaster component of the Canvas:

Reply to #Lowelltech
Your solution worked for me except that instead of Mouse I used Touchscreen
// Obtain the current touch position.
var pointerPosition = Touchscreen.current.position.ReadValue();

An InputSytem is a way to receive new inputs provided by Unity. You can't use existing scripts there, and you'll run into problems like the original questioner. Answers with code like "if(Input.GetMouseButton(0)" are invalid because they use the old system.

Related

Unity - How do I click on a specific object instead of just the whole screen in a different object?

I'm coding a top down game, with point and click movement. Currently you are able to click on the map, but you can also click outside the map to move there. I added colliders to the walls, but you still try and go outside. Code example:
if (Input.GetMouseButtonDown(1)) {'move'}
But what I want is something like this:
if (Input.GetMouseButtonDown(1) on MAP) //map is the object
So I want to be able to only click on the map, and if you click outside the map, it won't do anything. Thanks!
My script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public float speed = 10f;
Vector2 lastClickedPos;
//Rect inRect = new Rect(82.80f, -83.20f, 164.90f, 163.29f);
bool moving;
private void Update()
{
if (Input.GetMouseButtonDown(1) && GameObject.CompareTag("clickedOn")){ // && inRect.Contains(Input.mousePosition)
lastClickedPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
moving = true;
}
if (moving && (Vector2)transform.position != lastClickedPos)
{
float step = speed * Time.deltaTime;
transform.position = Vector2.MoveTowards(transform.position, lastClickedPos, step);
}
else
{
moving = false;
}
}
}
I think you can resolve your issue using tags or layers. I'll just list how to setup tags since it has a lot less setup vs. doing layers.
First off you'll need to create a tag and since Unity has good documentation on stuff like this I'll just link it here: https://docs.unity3d.com/Manual/Tags.html
Once you created your tag and tagged the ground/environment/area you want to have be clickable with the tag then you just need to find the object you want to collided with and use CompareTag so to put that in an example here is what your if statement could look like:
if (Input.GetMouseButtonDown(1) && collidedObject.CompareTag("TagNameGoesHere"))
{
//Movement goes here
}
Just to note I've named the gameObject that was found to collidedObject but you can name it whatever you want.
Hopefully this helps, let me know if I need to clarify something, it has been awhile since I've done a stackoverflow answer so I may have left something out.
Edit: Alright so adding onto this, you'll need to also look into how to do raycasting to check what object you click on so you can determine if it's a spot you can move to.
I've just tested this in a project just to make sure I understand it (I've used raycast a lot but never really done point to click movement before).
Essentially I've broken down things into 3 statements, which you can add together into one if statement but it's more so I can explain everything in detail:
if (Input.GetMouseButtonDown(1))
What you use currently, we want to make sure we only do the next few checks when we click
if (Physics.Raycast(playerCamera.ScreenPointToRay(Input.mousePosition), out hit, Mathf.Infinity))
So this here, is essentially drawing a line from a position, in this bit we are using the player camera which is just a regular camera reference and converting a point on screen into a ray which we then set the length to be Mathf.Infinity (this can be whatever float, I've just used this for an example) and then we output the hit object to hit which is a RaycastHit struct.
if (hit.collider != null && hit.collider.CompareTag("TagNameGoesHere"))
Now we finally check to see if the collider is not null (in case we hit the sky or something, which shouldn't happen for you in a top down game) and also that the object has right tag. Again you need to setup the tags which I've listed above and make sure you set the correct game objects in scene to have the correct tag. After which you should be able to move to the position (using the position you've setup and such is fine)
So it should look something like this:
if (Input.GetMouseButtonDown(1))
{
if (Physics.Raycast(playerCamera.ScreenPointToRay(Input.mousePosition), out rayCastHit, Mathf.Infinity))
{
if (hit.collider != null && hit.collider.CompareTag("TagNameGoesHere"))
{
//Movement goes here
}
}
}
Make sure to also put a reference to the camera and RayCastHit objects (these are the playerCamera and rayCastHit variables in the above). For my example script that I created I made them global variables.

Unity3D How can I select multiple objects in 3D with a drag and select / lasso select?

I am struggling to find a good tutorial or informations that would allow me to select multiple objects in 3D in a user friendly manner.
So far, the best tutorial I found is this one : https://sharpcoderblog.com/blog/unity-3d-rts-style-unit-selection. The tutorial works by using the transform.position of the selectable objects and checking if it within the user's selection.
What I wish is to have the user be able to select a unit even if it is only partially within the user's selection such as most RTS games do ( both in 2D and 3D ).
One possibility would be to create a temporary mesh using the camera's clipping distances and the user's selection and then check for collisions but I was not able to find any tutorials using this method nor do I know if it is the best approach to the subject.
If I understand correctly you want to
somehow start a selection
collect every object that was "hit" during the collection
somehow end the collection
Couldn't you simply use raycasting? I will assume simple mouse input for now but you could basically port this to whatever input you have.
// Just a little helper class for an event in the Inspector you can add listeners to
[SerializeField]
public class SelectionEvent : UnityEvent<HashSet<GameObject>> { }
public class SelectionController : MonoBehaviour
{
// Adjust via the Inspector and select layers that shall be selectable
[SerializeField] private LayerMask includeLayers;
// Add your custom callbacks here either via code or the Inspector
public SelectionEvent OnSelectionChanged;
// Collects the current selection
private HashSet<GameObject> selection = new HashSet<GameObject>();
// Stores the current Coroutine controlling he selection process
private Coroutine selectionRoutine;
// If possible already reference via the Inspector
[SerializeField] private Camera _mainCamera;
// Otherwise get it once on runtime
private void Awake ()
{
if(!_mainCamera) _mainCamera = Camera.main;
}
// Depending on how exactly you want to start and stop the selection
private void Update()
{
if(Input.GetMouseButtonDown(0))
{
StartSelection();
}
if(Input.GetMouseButtonUp(0))
{
EndSelection();
}
}
public void StartSelection()
{
// if there is already a selection running you don't wanr to start another one
if(selectionRoutine != null) return;
selectionRoutine = StartCoroutine (SelectionRoutine ());
}
public void EndSelection()
{
// If there is no selection running then you can't end one
if(selectionRoutine == null) return;
StopCoroutine (selectionRoutine);
selectionRoutine = null;
// Inform all listeners about the new selection
OnSelectionChanged.Invoke(new HashSet<GameObject>(selection);
}
private IEnumerator SelectionRoutine()
{
// Start with an empty selection
selection.Clear();
// This is ok in a Coroutine as long as you yield somewhere within it
while(true)
{
// Get the ray shooting forward from the camera at the mouse position
// for other inputs simply replace this according to your needs
var ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
// Check if you hit any object
if(Physics.Raycast(ray, out var hit, layerMask = includeLayers ))
{
// If so Add it once to your selection
if(!selection.Contains(hit.gameObject)) selection.Add(hit.gameObject);
}
// IMPORTANT: Tells Unity to "pause" here, render this frame
// and continue from here in the next frame
// (without this your app would freeze in an endless loop!)
yield return null;
}
}
}
Ofcourse you could do it directly in Update in this example but I wanted to provide it in a way where you can easily exchange the input method according to your needs ;)
From UX side you additionally might want to call a second event like OnSelectionPreviewUpdate or something like this every time you add a new object to the selection in order to be able to e.g. visualize the selection outcome.
I might have understood this wrong and it sounds like you rather wanted to get everything inside of a drawn shape.
This is slightly more complex but here would be my idea for that:
Have a dummy selection Rigidbody object that by default is disabled and does nothing
don't even have a renderer on it but a mesh filter and mesh collider
while you "draw" create a mesh based on the input
then use Rigidbody.SweepTestAll in order to check if you hit anything with it
Typed on smartphone but I hope the idea gets clear
I think I would try to create a PolygonCollider2D because it is quite simple comparing to creating a mesh. You can set its path (outline) by giving it 2D points like location of your pointer/mouse. Use the SetPath method for it. You can then use one of its methods to check if another point in space overlaps with that collider shape.
While the PolygonCollider2D interacts with 2D components you can still use its Collider2D.OverlapPoint method to check positions/bounds of your 3D objects after translating it to 2D space.
You can also use its CreateMesh method to create a mesh for drawing your selection area on the screen.
You can read more about the PolygonCollider2D here.
Hope it makes sens and hope it helps.

Instance object does not work properly with animator in Unity 3d

I am making games using Unity and I have some problems and I will ask questions.
The animator of an object instantiated using a prefab does not work properly, and precisely a specific event is a problem. The object placed in the hierarchy is fine. However, certain events do not work for objects instantiated using a script.
this is code.
public Animator guestmove;
public void Jump_motion()
{
if (tag == "Boy")
{
guestmove.SetTrigger("Jump");
}
}
public void Angry_motion()
{
guestmove.SetTrigger("Angry");
}
Here, we implement the event by pressing a button.
I changed the code to work when the tags match, but the objects I placed in the hierarchy also do not work.
This is the code that creates the instance.
if (currentlyObject > 0){
boyObject = Instantiate(boy, tableObject.transform.position, tableObject.transform.rotation);
boyObject.transform.Translate(new Vector3(0, -3, -11));
girlObject = Instantiate(girl, tableObject.transform.position, tableObject.transform.rotation);
girlObject.transform.Translate(new Vector3(1.5f, -3, -11));
}
I kept searching for the relevant data, but I could not find any cases of similar problems. I do not know where the problem is, please help me.
this is link
(https://drive.google.com/file/d/1SKbSIfFQM4-n8l-ZBBvZb_3SuNx-kd5-/view?usp=sharing)
Use the transition to triggers from Any State, this should work.
And where are your functions called?

is there anyway to detect which part of hand is touching the object in interaction engine (Leap Motion & Unity3D)

I am trying to build a application which includes using leap motion interaction engine to move the object in unity 3d. However, i also need to find out which fingers are touching the object when interacting with object in unity. Is there anyway i can do that? Thanks in advance!!
Strictly speaking, the Grasping logic of the Interaction Engine has to check this very thing in order to initiate or release grasps, but it doesn't have a friendly API for accessing this information.
A more convenient way to express this, even though it's not the most efficient way, would be to detect when a hand is intersecting with the interaction object and checking the distance between each fingertip and the object.
All InteractionControllers that are intersecting with a given InteractionBehaviour can be accessed via its contactingControllers property; using the Query library included in Leap tools for Unity, you can convert a bunch of Interaction Controller references to a bunch of Leap Hands without too much effort, and then perform the check:
using Leap.Unity;
using Leap.Unity.Interaction;
using Leap.Unity.Query;
using UnityEngine;
public class QueryFingertips : MonoBehaviour {
public InteractionBehaviour intObj;
private Collider[] _collidersBuffer = new Collider[16];
private float _fingertipRadius = 0.01f; // 1 cm
void FixedUpdate() {
foreach (var contactingHand in intObj.contactingControllers
.Query()
.Select(controller => controller.intHand)
.Where(intHand => intHand != null)
.Select(intHand => intHand.leapHand)) {
foreach (var finger in contactingHand.Fingers) {
var fingertipPosition = finger.TipPosition.ToVector3();
// If the distance from the fingertip and the object is less
// than the 'fingertip radius', the fingertip is touching the object.
if (intObj.GetHoverDistance(fingertipPosition) < _fingertipRadius) {
Debug.Log("Found collision for fingertip: " + finger.Type);
}
}
}
}
}

Unity3D OffNavMesh jump issue

I have set up Unity navigation meshes (four planes), navigation agent (sphere) and set up automatic and manual off mesh links. It should now jump between meshes. It does jump between meshes, but it does that in straight lines.
In other words, when agent comes to an edge, instead of actually jumping up (like off mesh link is drawn) it just moves straight in line but a bit faster. I tried moving one plane higher than others, but sphere still was jumping in straight line.
Is it supposed to be like this? Is it possible to set up navigation to jump by some curve? Or should I try to implement that myself?
I came by this question, and had to dig through the Unity sample. I just hope to make it easier for people by extracting the important bits.
To apply your own animation/transition across a navmesh link, you need to tell Unity that you will handle all offmesh link traversal, then add code that regularly checks to see if the agent is on an offmesh link. Finally, when the transition is complete, you need to tell Unity you've moved the agent, and resume normal navmesh behaviour.
The way you handle link logic is up to you. You can just go in a straight line, have a spinning wormhole, whatever. For jump, unity traverses the link using animation progress as the lerp argument, this works pretty nicely. (if you're doing looping or more complex animations, this doesn't work so well)
The important unity bits are:
_navAgent.autoTraverseOffMeshLink = false; //in Start()
_navAgent.currentOffMeshLinkData; //the link data - this contains start and end points, etc
_navAgent.CompleteOffMeshLink(); //Tell unity we have traversed the link (do this when you've moved the transform to the end point)
_navAgent.Resume(); //Resume normal navmesh behaviour
Now a simple jump sample...
using UnityEngine;
[RequireComponent(typeof(NavMeshAgent))]
public class NavMeshAnimator : MonoBehaviour
{
private NavMeshAgent _navAgent;
private bool _traversingLink;
private OffMeshLinkData _currLink;
void Start()
{
// Cache the nav agent and tell unity we will handle link traversal
_navAgent = GetComponent<NavMeshAgent>();
_navAgent.autoTraverseOffMeshLink = false;
}
void Update()
{
//don't do anything if the navagent is disabled
if (!_navAgent.enabled) return;
if (_navAgent.isOnOffMeshLink)
{
if (!_traversingLink)
{
//This is done only once. The animation's progress will determine link traversal.
animation.CrossFade("Jump", 0.1f, PlayMode.StopAll);
//cache current link
_currLink = _navAgent.currentOffMeshLinkData;
//start traversing
_traversingLink = true;
}
//lerp from link start to link end in time to animation
var tlerp = animation["Jump"].normalizedTime;
//straight line from startlink to endlink
var newPos = Vector3.Lerp(_currLink.startPos, _currLink.endPos, tlerp);
//add the 'hop'
newPos.y += 2f * Mathf.Sin(Mathf.PI * tlerp);
//Update transform position
transform.position = newPos;
// when the animation is stopped, we've reached the other side. Don't use looping animations with this control setup
if (!animation.isPlaying)
{
//make sure the player is right on the end link
transform.position = _currLink.endPos;
//internal logic reset
_traversingLink = false;
//Tell unity we have traversed the link
_navAgent.CompleteOffMeshLink();
//Resume normal navmesh behaviour
_navAgent.Resume();
}
}
else
{
//...update walk/idle animations appropriately ...etc
Its recommended to solve your problems via animation. Just create a Jump animation for your object, and play it at the correct time.
The position is relative, so if you increase the Y-position in your animation it will look like the object is jumping.
This is also how the Unity sample is working, with the soldiers running around.
Not sure what version of unity you are using but you could also try this, I know it works just fine in 4:
string linkType = GetComponent<NavMeshAgent>().currentOffMeshLinkData.linkType.ToString();
if(linkType == "LinkTypeJumpAcross"){
Debug.Log ("Yeah im in the jump already ;)");
}
also just some extra bumf for you, its best to use a proxy and follow the a navAgent game object:
Something like:
AIMan = this.transform.position;
AI_Proxy.transform.position = AIMan;
And also be sure to use:
AI_Proxy.animation["ProxyJump"].blendMode = AnimationBlendMode.Additive;
If you are using the in built unity animation!
K, that's my good deed for this week.
Fix position in update()
if (agent.isOnOffMeshLink)
{
transform.position = new Vector3(transform.position.x, 0f, transform.position.z);
}