Unity - Detecting Google Cardboard Click

From NoskeWiki
Jump to navigation Jump to search

About

NOTE: This page is a daughter page of: Unity


Unity3D is a multi-platform 3D game engine. Google Cardboard is a cheap virtual reality solution which lets you turn any phone into the viewer, and on the side of the Google Cardboard version 1 device is a special magnetic button. The way it works: almost all smartphones have a compass, so when you put a magnet near the phone, the compass is affected. The code below is for Unity to detect when the button has been pushed.


Please note that Google Cardboard version 2 has a touch button instead of a magnet, because the magnet didn't seem to work on all phones and was prone to calibration issues.


Acknowledgements: This code was modified from some amazing source code by Casey Borders which you can download from github. Casey's C# "MagnetSensor" class inherits MonoBehaviour and has it's own listener, meaning you must drag it onto a GameObject and add a listener class. This version I've added some comments, removed the listener and made it a standalone class with static function, meaning you don't need to drag it onto the scene - but you do have to call "EnableSensor(true)" once then "CheckIfPressed()" continually from elsewhere to detect when it is set off.



Detecting Google Cardboard Magnetic Button Click - Singleton Implementation

CardboardMagnetSensor.cs:

// CardboardMagnetSensor.cs
// http://andrewnoske.com/wiki/Unity_-_Detecting_Google_Cardboard_Click
// Acknowledgement:
// This is modified from code at https://github.com/CaseyB/UnityCardboardTrigger
// by Casey Borders which uses MonoBehaviour and a Listener. This modified
// version is designed to be more 'singleton'.
//
// UPDATE: A fellow developer just pointed out that the original code by
// Casey Borders has a licence for any source or binary copies which I
// I missed: https://github.com/CaseyB/UnityCardboardTrigger/blob/master/LICENSE
// I will copy here:
// ==============================================================================
// Copyright (c) 2014, CaseyB
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// 
// * Redistributions of source code must retain the above copyright notice, this
//   list of conditions and the following disclaimer.
// 
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// ==============================================================================
//
//
// Usage:
//
//   public class MyMonoClassWhichUsesTheSensor : MonoBehaviour {
//     void Start () {
//       CardboardMagnetSensor.SetEnabled(true);
//     }
//     void Update() {
//       if (CardboardMagnetSensor.CheckIfWasClicked()) {
//         Debug.Log("Cardboard trigger was just clicked");
//         Application.Quit();
//         CardboardMagnetSensor.ResetClick();
//       }
//     }
//   }
using UnityEngine;
using System.Collections.Generic;

public class CardboardMagnetSensor {
  // Constants:
  private const int WINDOW_SIZE = 40;
  private const int NUM_SEGMENTS = 2;
  private const int SEGMENT_SIZE = WINDOW_SIZE / NUM_SEGMENTS;
  private const int T1 = 30, T2 = 130;

  // Variables:
  private static bool wasClicked;           // Flips to true once set off.
  private static bool sensorEnabled;        // Is sensor active.
  private static List<Vector3> sensorData;  // Keeps magnetic sensor data.
  private static float[] offsets;           // Offsets used to detect click.


  // Call this once at beginning to enable detection.
  public static void SetEnabled(bool enabled) {
    Reset();
    sensorEnabled = enabled;
    Input.compass.enabled = sensorEnabled;
  }

  // Reset variables.
  public static void Reset() {
    sensorData = new List<Vector3>(WINDOW_SIZE);
    offsets = new float[SEGMENT_SIZE];
    wasClicked = false;
    sensorEnabled = false;
  }

  // Poll this once every frame to detect when the magnet button was clicked
  // and if it was clicked make sure to call "ResetClick()"
  // after you've dealt with the action, or it will continue to return true.
  public static bool CheckIfWasClicked() {
    UpdateData();
    return wasClicked;
  }
  
  // Call this after you've dealt with a click operation.
  public static void ResetClick() {
    wasClicked = false;
  }

  // Updates 'sensorData' and determines if magnet was clicked.
  private static void UpdateData() {
    Vector3 currentVector = Input.compass.rawVector;
    Common.SetDebugText("compasRawVector = " + currentVector);
    if (wasClicked) {
      Common.PrependDebugText("CLICKED!!!!!");
    }
    
    if (currentVector.x == 0 && currentVector.y == 0 && currentVector.z == 0) {
      Common.SetDebugText("No compass enabled");
      return;
    }

    if(sensorData.Count >= WINDOW_SIZE) sensorData.RemoveAt(0);
    sensorData.Add(currentVector);
    
    // Evaluate model:
    if(sensorData.Count < WINDOW_SIZE) return;
    
    float[] means = new float[2];
    float[] maximums = new float[2];
    float[] minimums = new float[2];
    
    Vector3 baseline = sensorData[sensorData.Count - 1];
    
    for(int i = 0; i < NUM_SEGMENTS; i++) {
      int segmentStart = 20 * i;
      offsets = ComputeOffsets(segmentStart, baseline);
      
      means[i] = ComputeMean(offsets);
      maximums[i] = ComputeMaximum(offsets);
      minimums[i] = ComputeMinimum(offsets);
    }
    
    float min1 = minimums[0];
    float max2 = maximums[1];

    // Determine if button was clicked.
    if(min1 < T1 && max2 > T2) {
      sensorData.Clear();
      wasClicked = true;  // Set button clicked to true.
      // NOTE: 'wasClicked' will now remain true until "ResetClick()" is called.
    }
  }
  
  private static float[] ComputeOffsets(int start, Vector3 baseline) {
    for(int i = 0; i < SEGMENT_SIZE; i++) {
      Vector3 point = sensorData[start + i];
      Vector3 o = new Vector3(point.x - baseline.x, point.y - baseline.y, point.z - baseline.z);
      offsets[i] = o.magnitude;
    }
    return offsets;
  }
  
  private static float ComputeMean(float[] offsets) {
    float sum = 0;
    foreach(float o in offsets) {
      sum += o;
    }
    return sum / offsets.Length;
  }
  
  private static float ComputeMaximum(float[] offsets) {
    float max = float.MinValue;
    foreach(float o in offsets) {
      max = Mathf.Max(o, max);
    }
    return max;
  }

  private static float ComputeMinimum(float[] offsets) {
    float min = float.MaxValue;
    foreach(float o in offsets) {
      min = Mathf.Min(o, min);
    }
    return min;
  }
}


And then to use this class, create something like the file below... attach it to any object in your scene and modify where it says "PEFORM ACTION".

CardboardTriggerControlMono.cs:

using UnityEngine;
using System.Collections;

public class CardboardTriggerControlMono : MonoBehaviour {
  public bool magnetDetectionEnabled = true;
  
  void Start() {
    CardboardMagnetSensor.SetEnabled(magnetDetectionEnabled);
    // Disable screen dimming:
    Screen.sleepTimeout = SleepTimeout.NeverSleep;
  } 
  
  void Update () {
    if (!magnetDetectionEnabled) return;
    if (CardboardMagnetSensor.CheckIfWasClicked()) {
      Debug.Log("DO SOMETHING HERE");  // PERFORM ACTION.
      CardboardMagnetSensor.ResetClick();
    }
  }
}


Detecting Google Cardboard Magnetic Button Click - Shorter Version

Acknowledgements: This code by Andrew Whyte from Secret Ingredient Games who found the code by Casey and I and made a great reimplementation. I have left the original, but this one might be better. Thans Andrew! Check out his amazing mobile game: tilt golf


/*     -- MIT/X11 like license --

Copyright (c) 2014 Paramita ltd, (Secret Ingredient Games)

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, 
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, 
subject to the following conditions:
    
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software source.

Users download a free copy of Secret Ingredient Games':
https://play.google.com/store/apps/details?id=com.secretingredientgames.tiltGolf
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

//
//  Google Cardboard click code in C# for Unity.
//  Author: Andrew Whyte
//
using UnityEngine;
using System.Collections;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

//public static XmlDocument XmlDoc;
//public static XmlNodeList xnl;
//public TextAsset TA;

public class magneticClick {
  //  Concept: two FIR filters,  running on Magnetics and tilt.
  //  If the tilt hasn't changed, but the compass has, then the magnetic field moved
  //  without device this is the essence of a cardboard magnet click.
  private Vector3 lastTiltVector;
  public float tiltedBaseLine = 0f;
  public float magnetBaseLine = 0f;

  public float tiltedMagn = 0f;
  public float magnetMagn = 0f;

  private int N_SlowFIR = 25;
  private int N_FastFIR_magnet = 3;
  private int N_FastFIR_tilted = 5;  // Clicking the magnet tends to tilt the device slightly.


  public float threshold = 1.0f;
  
  bool click = false;
  bool clickReported = false;

  public void init() {
    Input.compass.enabled = true;

    // Note that init is platform specific to unity.
    magnetMagn = Input.compass.rawVector.magnitude;
    magnetBaseLine = Input.compass.rawVector.magnitude;
    tiltedBaseLine = Input.acceleration.magnitude;
    tiltedMagn = Input.acceleration.magnitude;
  }

  public void magUpdate(Vector3 acc,  Vector3 compass) {
    // Call this function in the Update of a monobehaviour as follows:
    // <magneticClickInstance>.magUpdate(Input.acceleration, Input.compass.rawVector);

    // we are interested in the change of the tilt not the actual tilt.
    Vector3 TiltNow = acc;
    Vector3 motionVec3 = TiltNow - lastTiltVector;
    lastTiltVector = TiltNow;

    // update tilt and compass "fast" values
    tiltedMagn = ((N_FastFIR_tilted-1) * tiltedMagn + motionVec3.magnitude) / N_FastFIR_tilted;
    magnetMagn = ((N_FastFIR_magnet-1) * magnetMagn + compass.magnitude) / N_FastFIR_magnet;

    // update the "slow" values
    tiltedBaseLine = ( (N_SlowFIR-1) * tiltedBaseLine + motionVec3.magnitude) / N_SlowFIR;
    magnetBaseLine = ( (N_SlowFIR-1) * magnetBaseLine + compass.magnitude) / N_SlowFIR;

    if( tiltedMagn < 0.2 && (magnetMagn / magnetBaseLine) > 1.1  ) {
      if( clickReported == false) {
        click = true;
      }
      clickReported = true;
    } else  {
      clickReported = false;
    }
  }

  public bool clicked()  {
    // Basic premise is that the magnitude of magnetic field should change while the 
    // device is steady.  This seems to be suiltable for menus etc.

    // Clear the click by reading (so each 'click' returns true only once)
    if(click == true) {
      click = false;
      return true;
    } else {
      return false;
    }
  }
}


Update: The new Google cardboard design, released in June 2015, uses a touch button instead of a magnet. Good to know!


Other Pages

  • Unity - main Unity article


Links