Touch Events in Unity3D
I’ve been working on Crazy Bugz for the past few days to take advantage of the 2D physics and sprites brought in by the latest version of Unity3D. Many things have been updated which reserves a post all by itself. For now, I want to discuss a discovery I made regarding the touch events. I’m not sure if this is iOS or Unity3D specific but I’ve built a work around that seems to be working for now š
For a demonstration, here’s a Unity3D packageĀ (requires 4.3). Build and test it on touch devices. I’ve only tested it for iOS devices. You can test it with Unity Remote but it has limitations like touch responsiveness which is critical for this demonstration.
First and foremost, I made a generic event handler for the different touch phases in TouchMonoBehavior.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public void Update () { for (int i = 0; i < Input.touchCount; i++) { Touch t = Input.GetTouch(i); switch (t.phase) { case TouchPhase.Began: OnTouchBegan(t); break; case TouchPhase.Ended: OnTouchEnded(t); break; case TouchPhase.Moved: OnTouchMoved(t); break; case TouchPhase.Stationary: OnTouchStay(t); break; } } } |
The OnTouch* event handlers are all declared public virtual void and passes the Touch parameter. Meaning, each event handler could be called multiple times per Update() depending on how many touches there are (Input.touchCount). We can treat each touch separately by taking note of the finger id that comes with the touch.
All the while, I thought the touch phases have the following state diagram
Unfortunately, upon testing over and over again, it is POSSIBLE to start with Moved or Ended! I haven’t noticed if it could start with Stationary since I don’t use it for my projects at the moment. But my point here is that the Began phase CAN BE SKIPPED! I’m not sure if this is intentional but it’s happening and it got me pulling my hair for the past couple of days.
I’m using an object pool in my project where the objects react to touch. When the user touches, an object is created, let’s call that Object 0. When the user touches again, another object is created called Object 1. If Object 0 gets disabled (as part of the game mechanic) and the user touches again, Object 0 will be re-intialized and treated as something new. Objects don’t get destroyed, rather, they become disabled. This is basically how object pooling works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
Transform GetTouchIdentifier () { // get an inactive touch identifier for (int i = 0; i < _touchPool.Count; i++) { if (!_touchPool[i].gameObject.activeSelf) { _touchPool[i].gameObject.SetActive(true); return _touchPool[i]; } } // all touch identifiers are used // create a new one Transform t = Instantiate(touchIdentifier.transform) as Transform; t.parent = transform; t.name = "Touch " + _lastIndex; _lastIndex++; _touchPool.Add(t); return t; } Transform GetTouchIdentifierWithFingerId (int fingerId) { foreach (Transform t in _touchPool) { if (t.GetComponent<TouchIdentifier>().fingerId == fingerId) return t; } // should not reach this point unless TouchPhase.Began has been skipped // which can happen, fyi return null; } |
Theoretically, every time a BeganĀ phase is encountered, a mirror is re-initialized in Crazy Bugz. The user can rotate or stretch this mirror by moving their finger which corresponds to theĀ Moved phase. When the user releases their finger, anĀ Ended phase is encountered and that mirror remains enabled until it gets disabled (shattered) by the laser. However, there are certain occasions where I touch and get a NullExceptionError. It turns out, my game is trying to look for a mirror with a specified finger id that was not created. This means, theĀ Began phase was skipped!
As a work around, if in case the Began phase has been skipped and goes directly to Moved phase, I would treat that as a Began phase. In the case the Began phase has been skipped and goes directly to Ended, I would simply ignore it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public override void OnTouchBegan (Touch touch) { Transform touchId = GetTouchIdentifier(); touchId.gameObject.SetActive(true); touchId.GetComponent<TouchIdentifier>().fingerId = touch.fingerId; touchId.GetComponent<TouchIdentifier>().timeCreated = Time.time; Vector3 pos = Camera.main.ScreenToWorldPoint(touch.position); pos.z = 0; touchId.position = pos; } public override void OnTouchMoved (Touch touch) { Transform touchId = GetTouchIdentifierWithFingerId(touch.fingerId); if (!touchId) { // TouchPhase.Moved has been encountered // without a corresponding TouchPhase.Began // the workaround is to recognize this event // as a TouchPhase.Began OnTouchBegan(touch); return; } Vector3 pos = Camera.main.ScreenToWorldPoint(touch.position); pos.z = 0; touchId.position = pos; } public override void OnTouchEnded (Touch touch) { Transform touchId = GetTouchIdentifierWithFingerId(touch.fingerId); // TouchPhase.Ended has been encountered // without a corresponding TouchPhase.Began if (!touchId) return; touchId.gameObject.SetActive(false); // TODO particles should fade out than suddenly disappear :D touchId.GetComponent<TouchIdentifier>().fingerId = -1; } |
Not exactly the best solution but this will have to do.
The ideal case? Well, Began phase shouldn’t be skipped… ever š