This one has annoyed me for years: Unity’s OnMouseDown has never really worked. (NB: it works perfectly if you make a scene with a single cube; but that is not a game. It breaks completely as soon as you add multiple objects, colliders, etc to a scene)
The main reason to use a 3rd party engine is to avoid re-inventing the wheel; it’s tragic that Unity can’t even do mouse-input out of the box. Let’s fix it!
The problem
OnMouseDown is “supposed to” be invoked on all the Components of a GameObject whenever the player moves the mouse over that GameObject on screen, and clicks.
In reality:
- OnMouseDown requires the physics engine. WTF? Yeah, I know: bad design decision right there (*)
- OnMouseDown conflicts with the physics engine: you cannot have mouse-clicks without having physics objects. Workaround: you use-up some of your limited layers + tags to “isolate” the physics for clicks from physics for reality
- OnMouseDown randomly decides which GameObject to invoke — this is the Killer Bug. (**)
- OnMouseDown will only tell ONE GameObject that the mouse has been clicked. If there were multiple objects under the mouse, a RANDOM selection of them will be ignored. — this is the other Killer Bug
In addition, there’s some obvious stuff that OnMouseDown needs in order to be useful. Without these, I’ve generally found it about as useful as a chocolate teapot:
- If the “hit” object is marked as a trigger (i.e. it’s a NON PHYSICAL collider), then mouse clicks ought to “pass through” it to the objects behind. Unity ought to send events to BOTH objects
- Optionally, the “hit” object should be able to say “I am consuming this mouse click”. This has been standard behaviour for API’s / UI’s for decades. It’s disappointing that Unity doesn’t do it.
(**) – Educated guess: this is because Physics.RayCast() is so badly named/designed, and explicitly returns hits in “random” order. This is perhaps so counter-intuitive that even Unity’s internal programmers were confused by it
(*) – my guess is that OnMouseDown was never intended to be used by game developers. Rather, it was a quick hack that someone thought “hey, I could abuse our physics engine, and get a simplistic mouse handler for free! Let’s try it!”. I appreciate the hack; it’s a nice idea. But it’s out of place as the sole implementation for mouse-handling in a production game-engine!
The solution
If you’re not used to fixing Unity, here’s the high-level overview:
- Implement a Scene-wide hack that runs on Update (because we have to check this every frame!)
- Use a manual Physics.RayCast from the mouse pointer
- Check everything along the ray
- Check in order, starting from the nearest hit
- Allow each successive hit to optionally consume the hit
- Deliver to multiple objects
- For each object, the source code should be the same as OnMouseDown: i.e. you simply write a method with the right name, and everything is automatic
- Using Reflection, we look for the magic method name on each object
- When something is hit, don’t just check the object, check it’s parent and grandparents
- Why? Because in Unity, many bugs require you to “parent” one object inside another in order to fix them. Unity doesn’t fix the bugs because this workaround is “known” and “recommended”. So … we have to assume that our script that handles OnMouseDown might not be on the same object that has the Collider that receives the hit
N.B.:
This should not be implemented using Physics; as noted at the start, that’s a terrible idea. But since Unity has zero support for using rendering-without-physics, this is the only reasonably cheap way to write the code. Writing a full solution would require adding the complete “rendering-feedback API” that Unity is missing. I didn’t have time for that.
The Code
You can do this better. If you’d like this done better (easier to re-use, import to projects, etc) let me know and I’ll do an Asset Store package at some point. At the moment, this is a fast, quick, hack that works “good enough” for small projects.
Sampsa Lehtonen pointed out on twitter that this has a stupid bug: if a gameobject has multiple components that ALL want the event, only the first one receives it. That was not intended. I recommend fixing it
[csharp]
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
/** Unity OnMouseDown has never worked properly, except in trivial scenes with only 1/few objects
c.f. */
public class FixUnityBrokenMouseDown : MonoBehaviour
{
void Update ()
{
if (Input.GetMouseButtonDown (0))
{
Ray mouseRay = Camera.main.ScreenPointToRay (Input.mousePosition);
List<RaycastHit> orderedHits = new List<RaycastHit> (Physics.RaycastAll (mouseRay));
orderedHits.Sort ((h1,h2) => h1.distance.CompareTo (h2.distance));
bool hasBeenConsumed = false;
foreach (RaycastHit hit in orderedHits)
{
if (hasBeenConsumed)
{
break;
}
ComponentWithMethod target = ComponentThatCanReceiveMethod (hit.collider.gameObject, "FixedOnMouseDown");
if (target.component != null)
{
object result = target.method.Invoke (target.component, null);
bool didConsume = (result == null)? ( true /** assume consumption if not specified */ ) : (bool)result;
if (!didConsume)
{
continue;
} else
{
hasBeenConsumed = true;
break;
}
}
}
}
}
struct ComponentWithMethod
{
public Component component;
public MethodInfo method;
}
private ComponentWithMethod ComponentThatCanReceiveMethod (GameObject go, string methodName)
{
foreach (Component subComponent in go.GetComponents ( typeof(Component)))
{
MethodInfo info = subComponent.GetType ().GetMethod ( methodName, BindingFlags.Public | BindingFlags.Instance);
if (info != null)
{
ComponentWithMethod result = new ComponentWithMethod ();
result.component = subComponent;
result.method = info;
return result;
}
}
/**
didn’t find aything on this object or its components.
So … check again on parent object. Keep going till you find a match or fail
*/
if (go.transform.parent != null)
{
return ComponentThatCanReceiveMethod (go.transform.parent.gameObject, methodName);
}
return new ComponentWithMethod();
}
}
[/csharp]
Example Usage
You can do it the simplest way:
[csharp]
public class MyComponent : MonoBehaviour
{
public void FixedOnMouseDown()
{
// works exactly like OnMouseDown.
//
// Except: it actually works.
}
}
[/csharp]
Alternatively, you can make the method return a bool. If “true” (the default) it will consume the event, and nothing else will receive the mouse click.
If “false” … the click will be passed on to the next thing along the raycast. This is very useful for transparent objects.
It’s also useful for objects that might have other context for whether they should “accept” the mouse-click (e.g. “if the player is too far away from the treasure chest, ignore the click”)
[csharp]
public class MyComponent : MonoBehaviour
{
public bool FixedOnMouseDown()
{
// Return "false" to allow other classes to see the mouse-click too
//
// Except: it actually works.
if( collider.isTrigger ) // e.g. use the collider’s isTrigger state!
return false;
else
return true;
}
}
[/csharp]