Advanced Unity mobile scripting
Personalizando una Pantalla de Bienvenida (Splash Screen) en Android

Construcción de plugins para Android

Esta página describe los Plugins en Código Nativo para Android.

Construir un plugin para Android

Para construir un plugin para Android, debe en primer lugar obtener el NDK de Android y familiarizarse con los pasos para construir una librería compartida.

Si estás usando C++ (.cpp) para implementar el plugin, debes asegurarte que las funciones sean declaradas con enlazamientos en C para evitar problemas de resolución de nombres.

extern "C" {
  float FooPluginFunction ();
}

Using Your Plugin from C#

Una vez construida, la librería compartida debe ser copiada a la carpeta Assets->Plugins->Android. Entonces Unity la encontrará por nombre cuando sea definida una función como la siguiente en el script de C#:-

[DllImport ("PluginName")]
private static extern float FooPluginFunction ();

Favor notar que el PluginName no debe incluir el prefijo (‘lib’) ni la extensión (‘.so’) que hay en el nombre de archivo. Se debe envolver todos los métodos de código nativo con una capa adicional de código en C#. Este código debe revisar a Application.platform y llamar a los métodos nativos solo cuando la aplicación está ejecutándose en el dispositivo actual; algunos valores de prueba pueden ser devueltos desde el código en C# cuando está ejecutándose en el editor. Se pueden también usar las definiciones de plataforma para controlar la compilación de código dependiente de la plataforma.

Proyectos de librerías Android

You can drop pre-compiled Android library projects into the Assets->Plugins->Android folder. Pre-compiled means all .java files must have been compiled into jar files located in either the bin/ or the libs/ folder of the project. AndroidManifest.xml from these folders will get automatically merged with the main manifest file when the project is built.

Para más detalles, ver Proyectos de librerías Android.

Despliegue

For cross platform deployment, your project should include plugins for each supported platform (ie, libPlugin.so for Android, Plugin.bundle for Mac and Plugin.dll for Windows). Unity automatically picks the right plugin for the target platform and includes it with the player.

Para una plataforma Android especifica (armv7, x86), las librerías (lib*.so) deberían ser colocados en lo siguiente:

Assets/Plugins/Android/libs/x86/

Assets/Plugins/Android/libs/armeabi-v7a/

Uso de plugins Java

El mecanismo de plugins Android también permite que Java sea usado para permitir la interacción con el sistema operativo Android.

Construir un plugin Java para Android

There are several ways to create a Java plugin but the result in each case is that you end up with a .jar file containing the .class files for your plugin. One approach is to download the JDK, then compile your .java files from the command line with javac. This will create .class files which you can then package into a .jar with the jar command line tool. Another option is to use the Eclipse IDE together with the ADT.

Note: Unity espera que los plugins Java sean construidos usando el JDK v1.6. Si se usa la v1.7, hay que incluir “-source 1.6 -target 1.6” en las opciones de la línea de comando del compilador.

Usar su plugin Java desde código nativo

Una vez ha sido construido el plugin Java (.jar), hay que copiarlo a la carpeta Assets->Plugins->Android en proyecto de Unity. Unity empacará los archivos .class junto con el resto del código Java y luego accede al código usando el Java Native Interface (JNI). JNI es usado tanto para llamar a código nativo desde Java, como para interactuar con Java (o con JavaVM) desde el código nativo.

Para encontrar el código Java desde el lado nativo se requiere acceder a la máquina virtual (VM) de Java. Afortunadamente, este acceso puede ser obtenido con facilidad mediante la adición de una función como la siguiente a su código en C/C++:

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  JNIEnv* jni_env = 0;
  vm->AttachCurrentThread(&jni_env, 0);
  return JNI_VERSION_1_6;
}

Esto es todo lo que se necesita para empezar a usar Java desde C/C##. Lo referente a explicar los pormenores de JNI está más allá del alcance de este documento. Sin embargo, el utilizarlo involucra usualmente encontrar la definición de clase, resolver el método constructor (<init>) y crear una nueva instancia de objeto, como se muestra en este ejemplo:-

jobject createJavaObject(JNIEnv* jni_env) {
  jclass cls_JavaClass = jni_env->FindClass("com/your/java/Class");         // find class definition
  jmethodID mid_JavaClass = jni_env->GetMethodID (cls_JavaClass, "<init>", "()V");      // find constructor method
  jobject obj_JavaClass = jni_env->NewObject(cls_JavaClass, mid_JavaClass);     // create object instance
  return jni_env->NewGlobalRef(obj_JavaClass);                      // return object with a global reference
}

Usar un plugin Java con clases ayudantes

AndroidJNIHelper y AndroidJNI se pueden usar para solventar algunas de las dificultades al utilizar el JNI raso.

AndroidJavaObject y AndroidJavaClass automatizan una gran cantidad de tareas y también realizan cache para hacer llamados a Java más rápido. La combinación de AndroidJavaObject y AndroidJavaClass se construye encima de AndroidJNI y AndroidJNIHelper, pero también tiene una considerable cantidad de lógica por sí mismo (para manejar la automatización). Estas clases tambien vienen en una versión ‘estática’ para acceder a miembros estáticos de clases Java.

Puedes elegir cualquier enfoque que prefieras, ya sea JNI crudo a través de métodos de clase de AndroidJNI, o AndroidJNIHelper junto con AndroidJNI y al final con AndroidJavaObject/AndroidJavaClass para una máxima automatización y conveniencia.

UnityEngine.AndroidJNI es un envoltorio para los llamados JNI disponibles en C (como fue descrito antes). Todos los métodos en esta clase son estáticos y tienen transformación uno a uno con la JNI.UnityEngine.AndroidJNIHelper proporciona funcionalidad de ayuda que es utilizada por el próximo nivel, pero está expuesta a través de métodos públicos dado que pueden ser útiles en algunos casos especiales.

Las instancias de UnityEngine.AndroidJavaObject y UnityEngine.AndroidJavaClass tienen transformación uno a uno hacia una instancia de java.lang.Object y java.lang.Class (o hacia sus subclases) en el lado de Java, respectivamente. Proporcionan en esencia 3 tipos de interacción con el lado de Java:-

  • Llamar a un método (Call)
  • Obtener el valor de un campo (Get)
  • Colocar el valor de un campo (Set)

El Call se divide en dos categorías: Hacer Call a un método ‘void’, y hacer Call a un método que retorne un tipo que no sea void. Un tipo genérico es usado para representar el tipo que retornan estos métodos, el cual devuelve un tipo que no es void. Los Get y __Set__siempre toman un tipo genérico que representa al tipo del campo.

Ejemplo 1

//The comments describe what you would need to do if you were using raw JNI
 AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); 
 // jni.FindClass("java.lang.String"); 
 // jni.GetMethodID(classID, "<init>", "(Ljava/lang/String;)V"); 
 // jni.NewStringUTF("some_string"); 
 // jni.NewObject(classID, methodID, javaString); 
 int hash = jo.Call<int>("hashCode"); 
 // jni.GetMethodID(classID, "hashCode", "()I"); 
 // jni.CallIntMethod(objectID, methodID);


Here, we’re creating an instance of java.lang.String, initialized with a string of our choice and retrieving the hash value for that string.

El constructor AndroidJavaObject toma al menos 1 parámetro, el nombre de la clase para la que uno quiere construir una instancia. Cualquier parámetro que siga al nombre de la clase será para el llamado al constructor dentro del objeto, en este caso la cadena “some_string”. El Call realizado posteriormente a hashCode() retorna un ‘int’ el cual es el motivo por el que se usa esto como un parámetro con tipo genérico para el método Call.

Note: No se puede instanciar una clase Java anidada usando dotted notation. Las clases interiores deben usar el separador $, y éste debería funcionar tanto con formato “.” como con “/”. De este modo, \[android.view.ViewGroup$LayoutParams__ o __android/view/ViewGroup$LayoutParams__ podrían ser utilizados, en donde una clase __LayoutParams__ está anidada en una clase __ ViewGroup\].

Ejemplo 2

Uno de los ejemplos de plugin de arriba muestra cómo obtener el directorio de caché para la aplicación actual. Esta es la forma como se puede hacer lo mismo con C# sin usar ningún plugin:-

 AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); 
 // jni.FindClass("com.unity3d.player.UnityPlayer"); 
 AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); 
 // jni.GetStaticFieldID(classID, "Ljava/lang/Object;"); 
 // jni.GetStaticObjectField(classID, fieldID); 
 // jni.FindClass("java.lang.Object"); 

 Debug.Log(jo.Call<AndroidJavaObject>("getCacheDir").Call<string>("getCanonicalPath")); 
 // jni.GetMethodID(classID, "getCacheDir", "()Ljava/io/File;"); // or any baseclass thereof! 
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.FindClass("java.io.File"); 
 // jni.GetMethodID(classID, "getCanonicalPath", "()Ljava/lang/String;"); 
 // jni.CallObjectMethod(objectID, methodID); 
 // jni.GetStringUTFChars(javaString);


En este caso, empezamos con AndroidJavaClass en vez de AndroidJavaObject porque queremos acceder a un miembro estático de com.unity3d.player.UnityPlayer en lugar de crear un nuevo objeto (una instancia es creada automáticamente por el Android UnityPlayer). Entonces, accedemos al campo estático “currentActivity” pero esta vez usamos AndroidJavaObject como el parámetro genérico. Es así porque el actual tipo de campo (android.app.Activity) es una subclase de java.lang.Object, y cualquier otro tipo no primitivo debe ser accedido como un AndroidJavaObject. Las excepciones a esta regla son las cadenas, las cuales pueden ser accedidas directamente aún cuando no representan un tipo de datos primitivo en Java.

Luego de esto, es solo asunto de recorrer el Activity por medio de getCacheDir() para obtener el objeto File que representa el directorio de caché, y después se llama a getCanonicalPath() para obtener la representación en string.

Por supuesto, hoy en día no se necesita todo esto para obtener el directorio de caché dado que Unity proporciona acceso a la caché de la aplicación y al directorio de archivos a través de Application.temporaryCachePath y Application.persistentDataPath.

Ejemplo 3

Por último, he aquí un truco para pasar datos desde Java a un código de script usando UnitySendMessage.

using UnityEngine; 
public class NewBehaviourScript : MonoBehaviour { 

    void Start () { 
        AndroidJNIHelper.debug = true; 
        using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { 
            jc.CallStatic("UnitySendMessage", "Main Camera", "JavaMessage", "whoowhoo"); 
        } 
    } 

    void JavaMessage(string message) { 
        Debug.Log("message from java: " + message); 
    }
} 


La clase Java com.unity3d.player.UnityPlayer ahora tiene un método estático UnitySendMessage, equivalente a la función UnitySendMessage de iOS desde el lado nativo. Puede ser usada en Java para pasar datos al código de script.

Aquí, sin embargo, se le llama directamente desde el código de script, el cual transmite el mensaje dese el lado de Java. Entonces éste llama de regreso al código nativo/Unity para entregar el mensaje al objeto llamado “Main Camera”. Este objeto tiene un script adjunto que contiene un método llamado “JavaMessage”.

Buenas prácticas al usar plugins de Java con Unity

Como esta sección está dirigida principalmente a personas que no han tenido experiencia con JNI, Java y Android, se asume que ha sido usado el enfoque de AndroidJavaObject/AndroidJavaClass para interactuar con el código Java desde Unity.

Lo primero que hay que notar es que cualquier operación que sea realizada sobre un AndroidJavaObject o AndroidJavaClass es computacionalmente costosa (puesto que se trata del enfoque JNI raso). Se recomienda mucho el mantener el número de transiciones entre el código managed y el código nativo/Java en un mínimo, en aras del desempeño y también de la claridad del código.

Puedes tener un método Java que haga todo el trabajo real y luego usar AndroidJavaObject / AndroidJavaClass para comunicarse con este método y obtener el resultado. Sin embargo, vale la pena tener en mente que las clases ayudantes de JNI intentan cachear tantos datos como sea posible para mejorar el desempeño.

//The first time you call a Java function like 
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string"); // somewhat expensive
int hash = jo.Call<int>("hashCode"); // first time - expensive
int hash = jo.Call<int>("hashCode"); // second time - not as expensive as we already know the java method and can call it directly


El recolector de basura de Mono debe lanzar todas las instancias creadas de AndroidJavaObject y AndroidJavaClass después de ser usadas, pero es aconsejable mantenerlas en una declaración using(){} a fin de asegurar que sean borradas tan pronto como sea posible. Si no se hace esto, no puedes estar seguro de si serán destruidas. Si colocas AndroidJNIHelper.debug en true, verás un registro de la actividad del recolector de basura en el debug output.

//Getting the system language with the safe approach
void Start () { 
    using (AndroidJavaClass cls = new AndroidJavaClass("java.util.Locale")) { 
        using(AndroidJavaObject locale = cls.CallStatic<AndroidJavaObject>("getDefault")) { 
            Debug.Log("current lang = " + locale.Call<string>("getDisplayLanguage")); 

        } 
    } 
}


Puedes también llamar al método .Dispose() directamente para verificar que no hay objetos de Java aún existentes. El objeto actual en C# podría vivir un poco más pero será recolectado por Mono posteriormente.

Escribir extensiones para el código de UnityPlayerActivity en Java

Con Unity Android es posible extender la clase estándar de UnityPlayerActivity (la clase primaria en Java para el Unity Player en Android, similar a AppController.mm en Unity iOS).

An application can override any and all of the basic interaction between Android OS and Unity Android. You can enable this by creating a new Activity which derives from UnityPlayerActivity (UnityPlayerActivity.java can be found at /Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/src/com/unity3d/player on Mac and usually at C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\src\com\unity3d\player on Windows).

To do this, first locate the classes.jar shipped with Unity Android. It is found in the installation folder (usually C:\Program Files\Unity\Editor\Data (on Windows) or /Applications/Unity (on Mac)) in a sub-folder called PlaybackEngines/AndroidPlayer/Variations/mono or il2cpp/Development or Release/Classes/. Then add classes.jar to the classpath used to compile the new Activity. The resulting .class file(s) should be compressed into a .jar file and placed in the Assets->Plugins->Android folder. Since the manifest dictates which activity to launch it is also necessary to create a new AndroidManifest.xml. The AndroidManifest.xml file should also be placed in the Assets->Plugins->Android folder (placing a custom manifest completely overrides the default Unity Android manifest).

La nueva actividad podría lucir como el siguiente ejemplo, OverrideExample.java:

package com.company.product;

import com.unity3d.player.UnityPlayerActivity;

import android.os.Bundle;
import android.util.Log;

public class OverrideExample extends UnityPlayerActivity {

  protected void onCreate(Bundle savedInstanceState) {

    // call UnityPlayerActivity.onCreate()
    super.onCreate(savedInstanceState);

    // print debug message to logcat
    Log.d("OverrideActivity", "onCreate called!");
  }

  public void onBackPressed()
  {
    // instead of calling UnityPlayerActivity.onBackPressed() we just ignore the back button event
    // super.onBackPressed();
  }
}

Y así es como podría quedar su respectivo AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
    <activity android:name=".OverrideExample"
             android:label="@string/app_name"
             android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
  </application>
</manifest>

UnityPlayerNativeActivity

It is also possible to create your own subclass of UnityPlayerNativeActivity. This has much the same effect as subclassing UnityPlayerActivity, but with improved input latency. Because touch/motion events are processed in native code, Java views do normally not see those events. There is, however, a forwarding mechanism in Unity which allows events to be propagated to the DalvikVM. To access this mechanism, you need to modify the manifest file as follows:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
    <activity android:name=".OverrideExampleNative"
             android:label="@string/app_name"
             android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
  <meta-data android:name="android.app.lib_name" android:value="unity" />
  <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
  </application>
</manifest> 


Nótese que el atributo “.OverrideExampleNative” en el elemento de la actividad y los dos elementos meta-data adicionales. El primer meta-data es una instrucción para usar la librería de Unity libunity.so. El segundo habilita a los eventos para que sean pasados a la subclase personalizada de UnityPlayerNativeActivity.

Ejemplos

Ejemplo de plugin nativo

Un ejemplo simple del uso de un plugin en código nativo puede ser encontrado aquí

This sample demonstrates how C code can be invoked from a Unity Android application. The package includes a scene which displays the sum of two values as calculated by the native plugin. Please note that you will need the Android NDK to compile the plugin.

Advanced Unity mobile scripting
Personalizando una Pantalla de Bienvenida (Splash Screen) en Android