Developer Tip: How to Use NFC with Unity 3D in Android Devices

A guest blog post by Pablo Martínez, Twinsprite

NFC gaming platform

Unity 3D is a cross-platform game creation system developed by Unity Technologies, including a game engine and integrated development environment (IDE). It is used to develop 2D, 2.5D and 3D video games for web sites, desktop platforms, consoles, and mobile devices.

With the goal of democratizing the building of computer games, Unity has built a big community, over 1MM developers. These developers now use Unity, which is responsible for 100,000+ games on the iPhone and Android.

In this post we are going to create a Java plugin for Unity 3D that uses the NFC reader of an Android device to read a NFC tag with NDEF format, that could be attached to a toy or physical object you want to interact with in your game.

Prerequisites

  • Unity 4.x
  • Android SDK (API 10 or higher installed)
  • Eclipse IDE

Java Code

Open Unity and create a new project called NFCPlugin. Under the Assets folder crate following directories structure Assets/Plugins/Android/com/twinsprite/nfcplugin and inside the nfcplugin folder create the file NFCPluginText.java using an external text editor.

This is the content of the Java file:

package com.twinsprite.nfcplugin;
import java.nio.charset.Charset;
import java.util.Arrays;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.primitives.Bytes;
import com.unity3d.player.UnityPlayerActivity;
public class NFCPluginTest extends UnityPlayerActivity {
    
    public static final String MIME_TEXT_PLAIN = "text/plain";
    
    private static final BiMap URI_PREFIX_MAP = ImmutableBiMap.builder()
            .put((byte) 0x00, "")
            .put((byte) 0x01, "http://www.")
            .put((byte) 0x02, "https://www.")
            .put((byte) 0x03, "http://")
            .put((byte) 0x04, "https://")
            .put((byte) 0x05, "tel:")
            .put((byte) 0x06, "mailto:")
            .put((byte) 0x07, "ftp://anonymous:anonymous@")
            .put((byte) 0x08, "ftp://ftp.")
            .put((byte) 0x09, "ftps://")
            .put((byte) 0x0A, "sftp://")
            .put((byte) 0x0B, "smb://")
            .put((byte) 0x0C, "nfs://")
            .put((byte) 0x0D, "ftp://")
            .put((byte) 0x0E, "dav://")
            .put((byte) 0x0F, "news:")
            .put((byte) 0x10, "telnet://")
            .put((byte) 0x11, "imap:")
            .put((byte) 0x12, "rtsp://")
            .put((byte) 0x13, "urn:")
            .put((byte) 0x14, "pop:")
            .put((byte) 0x15, "sip:")
            .put((byte) 0x16, "sips:")
            .put((byte) 0x17, "tftp:")
            .put((byte) 0x18, "btspp://")
            .put((byte) 0x19, "btl2cap://")
            .put((byte) 0x1A, "btgoep://")
            .put((byte) 0x1B, "tcpobex://")
            .put((byte) 0x1C, "irdaobex://")
            .put((byte) 0x1D, "file://")
            .put((byte) 0x1E, "urn:epc:id:")
            .put((byte) 0x1F, "urn:epc:tag:")
            .put((byte) 0x20, "urn:epc:pat:")
            .put((byte) 0x21, "urn:epc:raw:")
            .put((byte) 0x22, "urn:epc:")
            .put((byte) 0x23, "urn:nfc:").build();
            
    private NfcAdapter mNfcAdapter;
    
    private PendingIntent pendingIntent;
    
    IntentFilter[] mIntentFilter;
    
    String[][] techListsArray;
    
    private static String value = "";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // Foreground Dispatch: 1. Creates a PendingIntent object so the Android system can populate it with the details of the tag when it is scanned.
        pendingIntent = PendingIntent.getActivity(NFCPluginTest.this, 0,
                new Intent(NFCPluginTest.this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
        // Foreground Dispatch: 2. Declare intent filters to handle the intents that you want to intercept 
        mIntentFilter = new IntentFilter[] { new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) };
        // Foreground Dispatch: 3. Set up an array of tag technologies that your application wants to handle.
        techListsArray = new String[][] { new String[] { NfcF.class.getName() } };
        
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            Log.e(NFCPluginTest.class.toString(), "This device doesn't support NFC.");
            finish();
            return;
        }
        if (!mNfcAdapter.isEnabled()) {
            Log.e(NFCPluginTest.class.toString(), "NFC is disabled.");
        } else {
            Log.i(NFCPluginTest.class.toString(), "NFC reader initialized.");
        }
    }
    @Override
    public void onResume() {
        super.onResume();
        // Enables the foreground dispatch when the activity regains focus.
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, mIntentFilter, techListsArray);
    }
    @Override
    public void onPause() {
        super.onPause();
        // Disables the foreground dispatch when the activity loses focus.
        mNfcAdapter.disableForegroundDispatch(this);
    }
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handleIntent(intent);
    }
    private void handleIntent(Intent intent) {
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        if (tag != null) {
            // Parses through all NDEF messages and their records and picks text and uri type.
            Parcelable[] data = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
            String s = "";
            if (data != null) {
                try {
                    for (int i = 0; i < data.length; i++) {
                        NdefRecord[] recs = ((NdefMessage) data[i]).getRecords();
                        for (int j = 0; j < recs.length; j++) {
                            if (recs[j].getTnf() == NdefRecord.TNF_WELL_KNOWN
                                    && Arrays.equals(recs[j].getType(), NdefRecord.RTD_TEXT)) {
                                /*
                                * See NFC forum specification for
                                * "Text Record Type Definition" at 3.2.1
                                * 
                                * http://www.nfc-forum.org/specs/
                                * 
                                * bit_7 defines encoding bit_6 reserved for
                                * future use, must be 0 bit_5..0 length of IANA
                                * language code
                                */
                                byte[] payload = recs[j].getPayload();
                                String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16";
                                int langCodeLen = payload[0] & 0077;
                                s += new String(payload, langCodeLen + 1, payload.length - langCodeLen - 1,
                                        textEncoding);
                            } else if (recs[j].getTnf() == NdefRecord.TNF_WELL_KNOWN
                                    && Arrays.equals(recs[j].getType(), NdefRecord.RTD_URI)) {
                                /*
                                * See NFC forum specification for
                                * "URI Record Type Definition" at 3.2.2
                                * 
                                * http://www.nfc-forum.org/specs/
                                * 
                                * payload[0] contains the URI Identifier Code
                                * payload[1]...payload[payload.length - 1]
                                * contains the rest of the URI.
                                */
                                byte[] payload = recs[j].getPayload();
                                String prefix = (String) URI_PREFIX_MAP.get(payload[0]);
                                byte[] fullUri = Bytes.concat(prefix.getBytes(Charset.forName("UTF-8")),
                                        Arrays.copyOfRange(payload, 1, payload.length));
                                s += new String(fullUri, Charset.forName("UTF-8"));
                            }
                        }
                    }
                } catch (Exception e) {
                    value = e.getMessage();
                    Log.e(NFCPluginTest.class.toString(), e.getMessage());
                }
            }
            Log.i(NFCPluginTest.class.toString(), s);
            value = s;
        }
    }
    public static String getValue() {
        return value;
    }
}

This activity uses the Foreground Dispatch System to intercept the ACTION_TAG_DISCOVERED intent and prevents other NFC Readers installed in the device from launching.

The intents received in the onNewIntent(Intent intent) method are handled in handleIntent(Intent intent)

This method checks if the intent contains the data of a NFC tag NDEF formatted. Iterates over its NDEF messages, and extracts the Text and URI records to the s String

In a text record the bit 7 defines the encoding, the bit 6 is reserved for future use and must be 0, and the 5..0 bits define the length of the ISO/IANA language code (Examples: ‘fi’,’en-US’,’fr-CA’,’jp’). The remaining bytes in the record is the text data.

In a URI record the first byte defines the URI identifier code, we use the URI_PREFIX_MAP BiMap to map the uri protocols. The remaining data in the record completes the stored uri.

The method getValue() will be called from a Unity script to retrieve the readed NFC tag data.

Now configure the Unity project as an Android project. In Unity, create the Scenes folder under Assets, and go to File > Build Settings, select Android as the platform and click on Switch Platform. Then hit Add Current to set the application active scene, you will be prompted to save the scene, save the file as Assets/Scenes/NFCPlugin.unity.

Then select Player Settings… fill the Company Name (Twinsprite) and the Product Name (NFCPlugin). Click Other Settings and change the Bundle Identifier to match the pacakage name com.twinsprite.nfcplugin.

Switch back to your text editor and create a the AndroidManifest.xml inside Assets/Plugins/Android with the following content:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.twinsprite.nfcplugin"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="10" />
    <uses-permission android:name="android.permission.NFC" >
    </uses-permission>
    <uses-feature
        android:name="android.hardware.nfc"
        android:required="true" >
    </uses-feature>
    <application android:label="@string/app_name">
        <activity android:name=".NFCPluginTest"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

In the manifest file we provide access to the NFC hardware. Note that the package and activity android:name values are consistent with the Java class package and name.

In the java file we are using classes of the Google Guava library, so we need to copy the guava-17.0.jar inside our Unity project folder Assets/Plugins/Android/libs

Now we are going to create the Ant file that builds and compile the Java program. In Assets/Plugins/Android create the file build.xml as follows.

<?xml version="1.0" encoding="UTF-8"?>
<project name="CompileNFCPluginTutorialAndroidJava">
    <!-- Change this in order to match your configuration -->
    <property name="sdk.dir" value="/Users/pablomartinez/android-sdks/"/>
    <property name="target" value="android-18"/>
    <property name="unity.androidplayer.jarfile" value="/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar"/>
    <!-- Source directory -->
    <property name="source.dir" value="/Users/pablomartinez/Documents/development/workspaces/unity/NFCPlugin/Assets/Plugins/Android" />
    <!-- Libraries directory  -->
    <property name="libs.dir" value="/Users/pablomartinez/Documents/development/workspaces/unity/NFCPlugin/Assets/Plugins/Android/libs" />
    <!-- Output directory for .class files-->
    <property name="output.dir" value="/Users/pablomartinez/Documents/development/workspaces/unity/NFCPlugin/Assets/Plugins/Android/classes"/>
    <!-- Name of the jar to be created -->
    <property name="output.jarfile" value="/Users/pablomartinez/Documents/development/workspaces/unity/NFCPlugin/Assets/Plugins/Android/NFCPluginTutorial.jar"/>
      <!-- Creates the output directories if they don't exist yet. -->
    <target name="-dirs"  depends="message">
        <echo>Creating output directory: ${output.dir} </echo>
        <mkdir dir="${output.dir}" />
    </target>
   <!-- Compiles this project's .java files into .class files. -->
    <target name="compile" depends="-dirs"
                description="Compiles project's .java files into .class files">
        <javac encoding="ascii" target="1.6" debug="true" destdir="${output.dir}"  verbose="${verbose}" includeantruntime="false" >
            <src path="${source.dir}" />
            <classpath>
                <pathelement location="${sdk.dir}\platforms\${target}\android.jar"/>
                <pathelement location="${unity.androidplayer.jarfile}"/>
                <fileset dir="${libs.dir}" includes="*.jar"/>
            </classpath>
        </javac>
    </target>
    <target name="build-jar" depends="compile">
        <zip zipfile="${output.jarfile}"
            basedir="${output.dir}" />
    </target>
    <target name="clean-post-jar">
         <echo>Removing post-build-jar-clean</echo>
         <delete dir="${output.dir}"/>
    </target>
    <target name="clean" description="Removes output files created by other targets.">
        <delete dir="${output.dir}" verbose="${verbose}" />
    </target>
    <target name="message">
     <echo>Android Ant Build for Unity NFC Plugin</echo>
        <echo>   message:   Displays this message.</echo>
        <echo>   clean:     Removes output files created by other targets.</echo>
        <echo>   compile:   Compiles project's .java files into .class files.</echo>
        <echo>   build-jar: Compiles project's .class files into .jar file.</echo>
    </target>
</project>

You need to edit following properties:

  • sdk.dir: set your Android SDK directory.
  • target: set the Android SDK target verion you are using.
  • unity.androidplayer.jarfile: set your classes.jar file path, located in PlaybackEngines/AndroidPlayer/bin under your Unity installation directory.
  • source.dir, libs.dir, output.dir and output.jarfile: change /Users/pablomartinez/Documents/development/workspaces/unity/NFCPlugin/ for your Unity project base path.

Now open the Eclipse IDE and go to File > New Project and search for Java Project from Existing Ant Buildfile. Select the build.xml file inside your Unity project and finish.

This is the eclipse project tree

Eclipse Project

Do secondary button click on build.xml select Run As > Ant Build…

Eclipse Project

In the target tab check the build-jar and clean-post-jar targets and click the Run button.

Now the NFCPluginTutorial.jar file we need is inside the Assets/Plugins/Android folder of our Unity project.

Unity Code

Now we are going to create a C# script that access the NFC tag value via the getValue() method in the NFCPluginTest Android activity.

Create the Assets/Script folder and inside it the NFCExample.cs C# script, as follows.

using UnityEngine;
using System.Collections;
public class NFCExample : MonoBehaviour
{
    public GUIText tag_output_text;
    AndroidJavaClass pluginTutorialActivityJavaClass;
    
    void Start ()
    {
        AndroidJNI.AttachCurrentThread ();
        pluginTutorialActivityJavaClass = new AndroidJavaClass ("com.twinsprite.nfcplugin.NFCPluginTest");
    }
    
    void Update ()
    {
        string value = pluginTutorialActivityJavaClass.CallStatic<string> ("getValue");
        tag_output_text.text = "Value:\n" + value;
    }
}

Link the new script to the Main Camera object, and create a new GUI Text Game Object to display the NFC tag value. Now drag the GUT Text object to the script tag_output_text parameter

Finally build and run the project in your Android device.

The source code of this tutorial is available in the unity-android-nfc-plugin repository.

About our guest blogger:

Pablo Martinez is a Java developer and Twinsprite Co-founder. Twinsprite is a tech company that offers business-to-business services and tools that enable the implementation of mobile games, which interact with physical objects like toys.

Share
Tagged:Tags:

Leave A Comment?

You must be logged in to post a comment.