вторник, 13 ноября 2012 г.

Вызов функций Android из Unity3d

Unity3D - мощная система разработки 3D приложений, - поддерживает Android. Документация, к сожалению, очень скудная, да и сам процесс разработки - не сахар... Тем не менее, возможность создать Android-приложение есть.

В целом процесс разработки выглядит так. Вы создаете Android-проект, экспортируете его в jar-файл. Далее, помещаете этот jar файл в директорию проекта Unity: Assets\Plugins\Android\xxx.jar. В эту же директорию кладете AndroidManifest.xml, jar-файлы сторонних библиотек, которые задействованы в вашем проекте и ресурсы (папка res с той же структурой, что в обычном Android-проекте).

Следующий шаг - вызов функций, реализованных на стороне Android, из Unity. Вот на этом вопросе я и хочу остановиться поподробнее, т.к. здесь не все тривиально и есть подводные камни.

Тестовый проект

Для удобства работы я создал тестовый проект Unity2AndroidTest (который, кстати, может служить в примером реализации Android-плагина для Unity3D). В него входят:
  • u2aTest - Android-проект - плагин для Unity; содержит тестовые активити MyActivity и класс MyClass. В MyActivity и MyClass реализован набор функций-заглушек, которые ничего не делают, только фиксируют в логе факт их вызова. Функций реализовано множество разных: с разным набором параметров, статические и нестатические.
  • unity - маленький проект на Unity, реализующий приложение с одной кнопкой. Нажимаешь на кнопку - происходит вызов функций u2aTest множеством различных способов.

Перекрытие Activity

Начнем с задачи перекрытия Activity. Unity внутри себя способна реализовать "главную" Activity. Но мы можем подменить ее собственной с нужным функционалом. Делается это так:
//file: MyMainActivity.java
package com.example.u2aTest;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;

public class MyMainActivity extends UnityPlayerActivity {
  @Override protected void onCreate(Bundle icicle) { 
    super.onCreate(icicle);
    Kernel.logInfo("MyMainActivity.onCreate");
  }
}
//file: Assets\Plugins\Android\Android\AndroidManifest.xml

<application android:icon="@drawable/app_icon" android:label="@string/app_name">
 <activity android:name=".MyMainActivity" 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>
Здесь хочу обратить внимание на то, что у нас ДВА файла AndroidManifest.xml. Первый находится в Android-проекте - он нас совершенно не интересует. Второй - в Assets\Plugins\Android\Android. Вот во втором и следует объявлять активити, сервисы, permissions и т.д. - имеено он используется Unity при сборке приложения. Функция Kernel.logInfo - это обертка над функцией записи сообщения в лог, она сводится к простейшему коду: Log.i(TAG_LOG, message);

Итак, теперь у нас есть Activity. Если все сделано правильно, то при запуске Unity-проекта на девайсе мы увидим в логе строчку "MyMainActivity.onCreate" - наша активити запустилась. Теперь добавим в Activity две функции:
//file: MyMainActivity.java
package com.example.u2aTest;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;

public class MyMainActivity extends UnityPlayerActivity {
  @Override protected void onCreate(Bundle icicle) { 
    super.onCreate(icicle);
    Kernel.logInfo("MyMainActivity.onCreate");
  }

  public void testVoid() {
    Kernel.logInfo("MyMainActivity.testVoid");  
  }
  public static void testVoidStatic() {
    Kernel.logInfo("MyMainActivity.testVoidStatic");  
  }
}

Обращение к функциям Current Activity

Как вызвать функции testVoid и testVoidStatic на стороне Unity? Для начала нам потребуются объекты AndroidJavaClass и AndroidJavaObject

public class Caller {
  private readonly AndroidJavaClass _ActivityClass;
  private readonly AndroidJavaObject _ActivityObject;
  private readonly AndroidJavaClass _MyActivityClass; 

  public Caller() {
    _ActivityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    _MyActivityClass = new AndroidJavaClass("com.example.u2aTest.MyMainActivity"); 

    _ActivityObject = _ActivityClass.GetStatic<AndroidJavaObject>("currentActivity");
    // AndroidJavaObject my_activity_object = _MyActivityClass.GetStatic<AndroidJavaObject>("currentActivity"); //A0: не работает
  }
}
Мы создаем два объекта типа AndroidJavaClass:
  • _ActivityClass - для доступа к свойствам класса "com.unity3d.player.UnityPlayer"
  • _MyActivityClass - для доступа к свойствам класса "com.example.u2aTest.MyMainActivity"
Далее, через ActivityClass мы получаем доступ к полю currentActivity, которая указывает на экземпляр MyMainActivity, создаваемый при запуске приложения. Любопытно, что через MyActivityClass у нас доступа к currentActivity нет.

Теперь переходим к вызову функций testVoid и testVoidStatic:
public class Caller {
  //...................

  public void MakeTestCalls() {
    _ActivityObject.Call("testVoid"); // А1
   //_ActivityClass.CallStatic("testVoidStatic"); //А2: не работает
   _MyActivityClass.CallStatic("testVoidStatic"); //А3
   //_ActivityObject.CallStatic("testVoid"); //А4: работает, но не на всех девайсах
  }
}
Через объект _ActivityObject мы спокойно можем вызывать функции MyMainActivity. Статические функции MyMainActivity доступны только через _MyActivityClass, через _ActivityClass доступа к ним нет. Вообщем все закономерно: доступ к статическим функциям через AndroidJavaClass, доступ к нестатическим функциям - через AndroidJavaObject. Вот только _ActivityObject (почему-то) позволяет вызывать и статические функции тоже, хотя работает это не на всех девайсах. По-моему - это косяк реализации, не должна такая функциональность работать. Тем не менее, на форумах такой код встречается.

Вызов функций с параметрами

Функции testVoid и testVoidStatic ничего не возвращают и не принимают параметры. Рассмотрим более интересные варианты:
//file: MyMainActivity.java
package com.example.u2aTest;
import android.os.Bundle;
import com.unity3d.player.UnityPlayerActivity;

public class MyMainActivity extends UnityPlayerActivity {

public String testStringString(String paramValue) {
 Kernel.logInfo("MyMainActivity.testStringString:" + paramValue);
 return paramValue;
}

public int testInt(int intValue) {
 Kernel.logInfo("MyMainActivity.testInt:" + intValue);
 return intValue;
}

public static String testStringStringStatic(String paramValue) {
 Kernel.logInfo("MyMainActivity.testStringStringStatic:" + paramValue);
 return paramValue;
}
public static int testIntStatic(int intValue) {
 Kernel.logInfo("MyMainActivity.testIntStatic:" + intValue);
 return intValue;
}

}
public class Caller {
  //...................

  public void MakeTestCalls2() {
   int iresult = _ActivityObject.Call<int>("testInt", 999);
   String sresult = _ActivityObject.Call<String>("testStringString", "s2");      
   iresult = _MyActivityClass.CallStatic<int>("testIntStatic", 999);
   sresult =_MyActivityClass.CallStatic<String>("testStringStringStatic", "s1");
  }
}
Все работает. Особых отличий тут нет. Важно только не забывать использовать generic-варианты функций Call и CallStatic, а так же соблюдать соответствие типов C# и Java.

Подводные камни Thread

А что, если попробовать вызвать Android-функции из потока, созданного на стороне Unity? Пробуем:
public class Caller {
  //...................

  public void MakeTestCallsFromThread() {
   System.Threading.Thread t = new System.Threading.Thread(this.thread_proc);
   t.Start();
   t.Join();
  }

  private void thread_proc() { 
   _ActivityObject.Call("testVoid");
   _MyActivityClass.CallStatic("testVoidStatic"); 

   int iresult = _ActivityObject.Call<int>("testInt", 999);
   String sresult = _ActivityObject.Call<String>("testStringString", "s2");      
   iresult = _MyActivityClass.CallStatic<int>("testIntStatic", 999);
   sresult =_MyActivityClass.CallStatic<String>("testStringStringStatic", "s1");
  }
}
... и вот здесь нас ждем много интересных и неординарных ошибок. Вот некоторые из них:
W/System.err(4850): java.lang.NoSuchMethodError: no static method with name='testVoid' signature='()V' in class Lcom/unity3d/player/UnityPlayer;
W/System.err(4850):  at com.unity3d.player.UnityPlayer.nativeRender(Native Method)                                                             
W/System.err(4850):  at com.unity3d.player.UnityPlayer.onDrawFrame(Unknown Source)                                                             
W/System.err(4850):  at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1470)                                              
W/System.err(4850):  at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1224)                                                     


W/System.err(4850): java.lang.NoClassDefFoundError: com/unity3d/player/ReflectionHelper              
W/System.err(4850):  at dalvik.system.NativeStart.run(Native Method)                                
W/System.err(4850): Caused by: java.lang.ClassNotFoundException: com.unity3d.player.ReflectionHelper 
W/System.err(4850):  at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:61)      
W/System.err(4850):  at java.lang.ClassLoader.loadClass(ClassLoader.java:501)                       
W/System.err(4850):  at java.lang.ClassLoader.loadClass(ClassLoader.java:461)                       
                                                       


I/Unity(3092): (Filename: /Applications/buildAgent/work/14194e8ce88cdf47/Runtime/ExportGenerated/AndroidManaged/UnityEngineDebug.cpp Line: 43)
I/com.example.u2aTest(3092): MyMainActivity.testVoid                                                                                          
E/dalvikvm(3092): JNI ERROR (app bug): accessed stale local reference 0x1d200001 (index 0 in a table of size 0)                               
E/dalvikvm(3092): VM abortin                                                                                                
Последняя ошибка приводит к полному краху приложения. Печально, но факт - из потока функции Android не вызвать.

Выводы

Вызов функций Android из Unity не составляет особых проблем, если проводить вызов из основного потока. Из дополнительных потоков функции Android НЕ ВЫЗЫВАЮТСЯ. Подозреваю, что это особенность реализации Unity или фишка JNI. Я, к сожалению, не знал о такой особенности и потратил кучу времени, чтобы выяснить в чем дело. Надеюсь, кому-нибудь эта статья сэкономит время. Удачи.

Скачать исходные коды примера Android-плагина для Unity3D: 20121113_Unity2AndroidTest.7z

10 комментариев:

  1. вы могли бы написать как из андроид проекта корректно собирать jar файл

    ОтветитьУдалить
    Ответы
    1. Вручную собирать можно с помощью функции Export. Щелкаем в Eclipse по названию проекта правой кнопкой мыши, выбираем Export... Далее "Java\JAR file", Next. Указываем путь, все прочие настройки - по умолчанию. Finish.

      Если ваш проект использует сторонние библиотеки, то в настройках проекта, в разделе Java Build Paths, на вкладке Order and Export, следует отметить все эти библиотеки. Галочка для библиотеки unity-классов classes.jar не требуется.

      В unity 4 добавили интеграцию с Eclipse проектами, так что ручной экспорт, возможно, уже не так актуален. Но я еще не разбирался, руки не дошли, пока работаю по старому.

      Удалить
    2. Спасибо за статью, все работает

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Здравствуйте!

    У меня, наверное, глупый вопрос, но пока не могу сам понять: я хотел сделать что-нибудь более наглядное, чем запись в лог, и добавил тестовый метод:

    public void myTestVoid()
    {
    Kernel.logInfo("MyMainActivity.myTestVoid");
    Toast.makeText(getApplicationContext(), "Test!", Toast.LENGTH_LONG).show();
    }

    При вызове этого метода запись в лог идет, но тост не появляется. С аналогичным результатом пробовал добавить новый view на экран.

    Подскажите, пожалуйста, можно ли вызвать тост из юнити в андроид приложении?

    ОтветитьУдалить
    Ответы
    1. Извиняюсь за беспокойство, разобрался сам. Вызывать тосты надо из UI потока :)

      Удалить
    2. Да, есть такая проблема. Помимо тостов через UI-поток приходиться, например, показывать диалоги и изменять флаги окна activity. Так что я в активити завел для этих целей хендлер. Как то так:


      public class MyActivity extends UnityPlayerActivity {
      /** Хендлер для показа диалогов. Unity вызывает функции из не-Gui потока,
      * поэтому напрямую вызывать диалог не получается.
      * http://stackoverflow.com/questions/4911686/alertdialog-error-with-events
      * */
      private Handler _Handler = new Handler();

      ....

      /** Показать диалог с предложением установить LogCollector.
      * Приходится использовать _Handler, т.к. вызов идет не из GUI-потока*/
      public void showLogCollectorDialog(MyActivity activity) {
      _Handler.post(new CallerThroughHandler(new ICallerThroughHandler() {
      @Override public void update() {
      collectAndSendLog(MyActivity.this);
      }
      } ));
      }

      ....
      }


      Теперь если что-нибудь вдруг не срабатывает - первым делом проверяю через хендлер :)

      Удалить
  4. Такой вопрос, скачав пример и ничего не модифицируя, получаю ошибку в логе андроида:

    11-21 16:04:37.274: E/AndroidRuntime(5020): java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.u2aTest/com.example.u2aTest.MyMainActivity}: java.lang.ClassNotFoundException: com.example.u2aTest.MyMainActivity

    какие могут быть решения проблемы?

    ОтветитьУдалить
    Ответы
    1. Проверил. Скачал архив примера, скомпилировал плагин в eclipse, сделал экспорт jar в "unity-test\unity\Assets\Plugins\Android\", открыл юнити-проект в Unity 4, согласился на upgrade, сделал в Unity "build & run" - приложение запустилось на девайсе, без ошибок в логе.

      Проверьте, как вы делаете экспорт jar-файла. При экспорте вылазит диалоговое окно "JAR Export", на втором шаге "JAR File Specification" проверьте, все ли суб-галочки у вас для выбранного проекта включены? У меня включены все: src, gen, assets, bin, libs, res + все галочки справа.

      Вот тот же самый проект, обновленный до Unity4 и со всеми временными файлам. Попробуйте открыть его в Unity и сделать Build&Run:

      https://www.dropbox.com/s/gwizt6xp0hh78qu/unity-test.7z

      Версия unity / Android Sdk последние?

      Удалить
  5. Добрый день, подскажите, если я сделаю два своих плагина, каждый будет с активити, у первого я в манифесте объявлю как мне реализовать работу и со вторым и с первым активити? есть пример какой-нибудь?

    ОтветитьУдалить