unity-与Android交互(unity、android studio)


前篇

因为现在eclipse已经停止维护了,官方推荐是用AS来构建Android app,然后导出 aar 包给unity使用
可以用cocos2dx的方式去理解,也是用一个 MainActivity 去继承 unity封装好的 UnityPlayerActivity ,当前应用就是的主线程就是跑在 MainActivity

本文是导出 aar 的形式, 其实也是提取里面的 jar 包.

编写的原生 java 代码是以 库 的方式引入到 unity 中, 所以在 as 的模块中是 库模块, 只要在 mainTemplate.gradle 指定启动的 activity 为 库模块 中的 activity 就可以就用到自己写的代码.

Android Studio 的必要条件库

  • SDK Tools

  • SDK Platforms 根据需要下载


生成库 (arr包)

1、使用AS构建一个app工程

  1. File->New->New Project
  2. 包名 Package Name 一定要和unity中打包参数 Bundle Identifier 中的包名一致,这里用 com.test.yangx
  3. 设置 mini sdk
  4. 选个 Empty Activity
  5. 默认的 MainActivity即可,然后 Finish

2、导入 unity 的 jar 到AS工程中

  • 在 unity5 中,在 D:\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes 的路径下有个 classes.jar
    把这个 classes.jar 丢进AS工程的 libs
    这里写图片描述
  • 工程引用这个 classes.jar
    • 右键 工程-> Open Module Settings
    • 这里写图片描述

3、编写 MainActivity 代码

完整代码如下

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.test.yangx;

import android.app.AlertDialog;
import android.os.Vibrator;
import android.os.Bundle;
import android.widget.Toast;

import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;

public class MainActivity extends UnityPlayerActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

}

public String ShowDialog(final String _title, final String _content){

runOnUiThread(new Runnable() {
@Override
public void run() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle(_title).setMessage(_content).setPositiveButton("Down", null);
builder.show();
}
});

return "Java return";
}

// 定义一个显示Toast的方法,在Unity中调用此方法
public void ShowToast(final String mStr2Show){
// 同样需要在UI线程下执行
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(),mStr2Show, Toast.LENGTH_LONG).show();
}
});
}


// 定义一个手机振动的方法,在Unity中调用此方法
public void SetVibrator(){
Vibrator mVibrator=(Vibrator)getSystemService(VIBRATOR_SERVICE);
mVibrator.vibrate(new long[]{200, 2000, 2000, 200, 200, 200}, -1); //-1:表示不重复 0:循环的震动
}

// 第一个参数是unity中的对象名字,记住是对象名字,不是脚本类名
// 第二个参数是函数名
// 第三个参数是传给函数的参数,目前只看到一个参数,并且是string的,自己传进去转吧
public void callUnityFunc(String _objName , String _funcStr, String _content)
{
UnityPlayer.UnitySendMessage(_objName, _funcStr, "Come from:" + _content);
}
}

4、修改 AndroidManifest.xml

这个 AndroidManifest.xml 可以使用unity中默认的, UNITY_PATH\Editor\Data\PlaybackEngines\AndroidPlayer\Apk\AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.asdasdasd">
<!-- package 一定不要和正式报名一致 -->

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true">
<activity android:name="com.test.yangx.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 一定要加上这句 -->
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>
</application>

</manifest>
  • package 一定不要和正式报名一致!!!

    package 一定不要和正式报名一致!!!

    package 一定不要和正式报名一致!!!

    否则会引发这个报错 [报错: Program type already present: com.xxx.BuildConfig](#报错: Program type already present: com.xxx.BuildConfig)

    因为 unity 会用 编辑器 中指定的报名 来打包, 并不会用这个 AndroidManifest.xml 中的 package


5、修改 build.gradle

  1. apply plugin: 'com.android.application' 修改为 apply plugin: 'com.android.library' , 这样才能导出一个 aar 包, 不然 application 构建的是 apk

  2. 删除掉这句代码 applicationId "com.test.yangx"

  3. 完整代码

    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
    apply plugin: 'com.android.library' // 修改为库

    android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
    minSdkVersion 19
    targetSdkVersion 23
    // applicationId "com.test.yangx // 注释掉
    versionCode 1
    versionName "1.0"
    }
    buildTypes {
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    }
    }

    dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.3.0'
    compile files('libs/classes.jar')
    }
    • 假如加入了相关依赖, 需要 refresh 一下, 才能用 alt + enterimport 相关的包, 比如加了 Google, Facebook 的依赖 ( implementation 是新的写法, 上面 compile 是老的写法 )
      1
      2
      3
      4
      dependencies {
      implementation 'com.google.android.gms:play-services-auth:17.0.0'
      implementation 'com.facebook.android:facebook-login:[5,6)'
      }

6、生产 aar

有两种方式可以生成aar包

  1. Build->Build APK.

  2. gradle 可视化任务中, 双击 build

!!!(如果遇到把报错, 可以尝试清空一下工程 build -> clean project , 重新构建.)!!!

成功会在 app\build\outputs\aar 目录下出现一个 app-debug.aar

然后用 解压软件打开这个包,删掉 libs 下的 classes.jar (没错,就是之前重unity中拷过来的, 因为unity打包时会重新把自带的 classes.jar 打进去,如果不删掉它会打包报错,冲突)

然后这就是最终需要的 aar 包, 其实用到只需要里面的两个东西就行

  1. 然后把这个 aar 里的 AndroidManifest.xml (指定继承了 UnityPlayerActivity 类的类 ) 和 classes.jar (自己编写的代码类) 文件 丢进 unity 的 Assets\Plugins\Android 目录下

    如果 libs 有使用到第三方库的话, 也需要将其丢进 Assets\Plugins\Android 目录下

如果 build 遇到资源验证报错, 可以暂且无视, 设置因为在 AndroidManifest.xml 中配置了资源, 但 as 工程中有没有, 最后还是用 unity 编辑器中指定的资源


7、写个c# 测试

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using UnityEngine;
using System.Runtime.InteropServices;
using UnityEngine.UI;

public class testDll : MonoBehaviour {

private Text mText;

void Start()
{
//int ret = MyAddFunc(200, 200);
//Debug.LogFormat("--- ret:{0}", ret);
mText = GameObject.Find("MsgText").GetComponent<Text>();
}

public void MyShowDialog()
{
// Android的Java接口
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
// 参数
string[] mObject = new string[2];
mObject[0] = "Jar4Android";
mObject[1] = "Wow,Amazing!It's worked!";
// 调用方法
string ret = jo.Call<string>("ShowDialog", mObject);
setMsg(ref ret);
}

public void MyShowToast()
{
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
jo.Call("ShowToast", "Showing on Toast");
}

/// <summary>
/// 测试 unity->java->unity
/// </summary>
public void MyInteraction()
{
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); // 固定写死指定 unity 的 Java 类
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity"); // 固定写死指定 unity 的 Java 类
jo.Call("callUnityFunc", "U2J2U", "BeCallFunc", "yangx");
}

public void BeCallFunc(string _content)
{
setMsg(ref _content);
}

private void setMsg(ref string _str)
{
mText.text = _str;
}
}

8、最后 unity 打包 apk

修改测试参数
mini sdk 一致
这里写图片描述


9、done

装在模拟器上测试以下
这里写图片描述


AndroidManifest.xml

unity 打包后会生成最终的 AndroidManifest.xml 文件, 在项目路径下的 Temp\StagingArea 中, 其中 AndroidManifest-main.xml 是你自定义的, AndroidManifest.xml 才是最终的.


代码中获取 strings.xml

假设主包名是 com.its.xxx.xxx

代码 读取自定义的 strings.xml 文件 ( Assets\Plugins\Android\res\values\strings.xml ), 比如 strings.xml

1
2
3
<resources>
<string name="facebook_app_id">123123123123123123</string>
</resources>

不要使用这个方式去获取, 也就是 库包名.R

1
2
import com.its.demo.asdasd.R;
R.string.facebook_app_id

会报错找不到 com.its.demo.asdasd.R, 因为 unity 不会把这个文件打包进去, 会把 自定义的 strings.xml 合并到 主 strings.xml 中, 所以理论上应该是

1
2
import com.its.xxx.xxx.R;
R.string.facebook_app_id

所以得曲线救国, 使用这种方式获取

1
2
3
4
5
public static String GetStringVaule(Activity ctx, String key) {
Resources res = ctx.getResources();
// String appPackageName = ctx.getApplication().getPackageName(); // 可以这样获取主包名
return res.getString(res.getIdentifier(key, "string", "com.its.xxx.xxx"));
}
  • ctx : 主activity (也就是 UnityPlayerActivity 的实例 )
  • “string” : 是指获取 strings.xml 中的值, 而不是指定获取 string 类型. ( layout:布局文件资源的ID, drawable:图片资源的ID, string:字符串资源 )
  • “com.its.xxx.xxx” : 是你的应用的包名, 并不是 [4、修改 AndroidManifest.xml](#4、修改 AndroidManifest.xml) 这里面的包名

附:

AndroidManifest.xml 中就这样使用, unity 自动合并 AndroidManifest.xml 后, 就可以获取到自定义的 strings.xml 的值

1
2
<meta-data
android:value="@string/facebook_app_id" />

Android 6.0 (Marshmallow)中的运行时权限

如果您的应用程序运行在Android 6.0 (Marshmallow)或更高版本的设备上,并且还针对Android API级别23或更高,则您的应用程序使用Android运行时权限系统。

Android运行时权限系统要求应用程序的用户在应用程序运行时授予权限,而不是在应用程序首次安装时授予。当应用程序运行时,用户通常可以在应用程序需要时授予或拒绝每个权限(例如,在拍照之前请求相机权限)。这允许应用程序在没有权限的情况下以有限的功能运行。

Unity不支持运行时权限系统,所以你的应用程序会提示用户在启动时允许Android所谓的“危险”权限。有关更多信息,请参阅Android的危险权限文档。

提示用户允许危险的权限是确保插件在丢失权限时不会导致崩溃的唯一方法。但是,如果您不希望Unity Android应用程序在启动时请求权限,您可以在应用程序或活动部分的清单中添加以下内容。

1
<meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" />

添加此选项将完全抑制在启动时显示的权限对话框,但是必须小心处理运行时权限,以避免崩溃。此方法只建议高级Android应用程序开发人员使用。


unity android 默认的 keystore

Note: For security reasons, Unity does not save the passwords on this page. The unsigned debug keystore is located by default at ~/.android/debug.keystore on MacOS and %USERPROFILE%\\.android\debug.keystore on Windows.

https://docs.unity3d.com/Manual/class-PlayerSettingsAndroid.html


自定义 gradle

mainTemplate.gradle 文件复制一份到工程目录 Assets/Plugins/Android 下, 然后自行修改就行, 比如 引入 Google gms 库

1
2
3
4
dependencies {
implementation 'com.google.android.gms:play-services-auth:17.0.0'
...
}

unity 的 build.gradle 分析

Temp\gradleOut\build.gradle, 是 unity 根据自定义的 mainTemplate.gradle 生产出来的 android 的 build.gradle 文件

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN

buildscript {
repositories {
mavenCentral()
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:3.4.0'
}
}

allprojects {
repositories {
mavenCentral()
google()
jcenter()
flatDir {
dirs 'libs'
}
}
}

apply plugin: 'com.android.application'


dependencies { // 依赖的的
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(name: 'play-services-auth-17.0.0', ext:'aar') // 依赖 libs 下的某个aar
implementation project(':unity-android-resources') // 依赖 module, (该名称必须匹配使用 settings.gradle 文件中的 include: 定义的库名称)。 在构建您的应用时,构建系统会编译库模块,并将生成的编译内容打包到 APK中。
implementation 'com.android.billingclient:billing:2.0.1' // 远程二进制library依赖
}

android {
compileSdkVersion 28
buildToolsVersion '29.0.1'

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

defaultConfig {
minSdkVersion 16
targetSdkVersion 28
applicationId 'com.its.rummy'
ndk {
abiFilters 'armeabi-v7a'
}
versionCode 1
versionName '0.1.1'
}

lintOptions {
abortOnError false
}

aaptOptions {
noCompress = ['.unity3d', '.ress', '.resource', '.obb', 'dataconfig/cfg_checkin.bytes', 'dataconfig/cfg_checkindate.bytes', 'dataconfig/cfg_globalconfig.bytes', 'dataconfig/cfg_language.bytes', 'dataconfig/cfg_rate.bytes', 'dataconfig/cfg_roomtype13.bytes', 'dataconfig/cfg_skin.bytes', 'dataconfig/dataconfig_list.json', 'dataconfig/version_list.json', 'lua/lua', 'lua/lua_list.json', 'lua/version_list.json', 'resource/asset_list.json', 'resource/resource', 'resource/resource_list.json', 'resource/version_list.json', 'resource/assets/code/shader.assetbundle', 'resource/assets/res/audio/cop.assetbundle', 'resource/assets/res/common/materials.assetbundle', 'resource/assets/res/common/textures.assetbundle', 'resource/assets/res/effect/commontexture.assetbundle', 'resource/assets/res/prefabs/common/audiosource.assetbundle', 'resource/assets/res/prefabs/common/maincamera.assetbundle', 'resource/assets/res/prefabs/spine/heguan.assetbundle', 'resource/assets/res/prefabs/ui/ui_root_ex.assetbundle', 'resource/assets/res/prefabs/ui/cards/cardgroup.assetbundle', 'resource/assets/res/prefabs/ui/cards/carditem.assetbundle', 'resource/assets/res/prefabs/ui/cards/groupslot.assetbundle', 'resource/assets/res/prefabs/ui/checkin/pnlcheckin.assetbundle', 'resource/assets/res/prefabs/ui/common/btnclose.assetbundle', 'resource/assets/res/prefabs/ui/common/mask.assetbundle', 'resource/assets/res/prefabs/ui/common/pnlloading.assetbundle', 'resource/assets/res/prefabs/ui/common/pnltips.assetbundle', 'resource/assets/res/prefabs/ui/common/pnlwaiting.assetbundle', 'resource/assets/res/prefabs/ui/common/pnlwindowframe.assetbundle', 'resource/assets/res/prefabs/ui/help/helpitem.assetbundle', 'resource/assets/res/prefabs/ui/help/pnlhelp.assetbundle', 'resource/assets/res/prefabs/ui/invite/pnlinvite.assetbundle', 'resource/assets/res/prefabs/ui/login/pnldebugpanel.assetbundle', 'resource/assets/res/prefabs/ui/login/pnllogin.assetbundle', 'resource/assets/res/prefabs/ui/mail/pnlmail.assetbundle', 'resource/assets/res/prefabs/ui/main/pnlmain.assetbundle', 'resource/assets/res/prefabs/ui/register/pnlregister.assetbundle', 'resource/assets/res/prefabs/ui/room/chipsitem.assetbundle', 'resource/assets/res/prefabs/ui/room/playeritem.assetbundle', 'resource/assets/res/prefabs/ui/room/pnlroom.assetbundle', 'resource/assets/res/prefabs/ui/room/effects/win_chouma.assetbundle', 'resource/assets/res/prefabs/ui/room/effects/win_chouma2.assetbundle', 'resource/assets/res/prefabs/ui/setting/pnlrateus.assetbundle', 'resource/assets/res/prefabs/ui/setting/pnlsetting.assetbundle', 'resource/assets/res/prefabs/ui/settle/pnlsettle.assetbundle', 'resource/assets/res/prefabs/ui/settle/settleitem.assetbundle', 'resource/assets/res/prefabs/ui/shop/pnlshop.assetbundle', 'resource/assets/res/refs/ui_loading.assetbundle', 'resource/assets/res/refs/ui_room.assetbundle', 'resource/assets/res/scenes/scene_main.assetbundle', 'resource/assets/res/scenes/scene_room.assetbundle', 'resource/assets/res/scenes/common/scene_login.assetbundle', 'resource/assets/res/ui/atlas/cards.assetbundle', 'resource/assets/res/ui/atlas/checkin.assetbundle', 'resource/assets/res/ui/atlas/common.assetbundle', 'resource/assets/res/ui/atlas/help.assetbundle', 'resource/assets/res/ui/atlas/invite.assetbundle', 'resource/assets/res/ui/atlas/loading.assetbundle', 'resource/assets/res/ui/atlas/login.assetbundle', 'resource/assets/res/ui/atlas/mail.assetbundle', 'resource/assets/res/ui/atlas/main.assetbundle', 'resource/assets/res/ui/atlas/register.assetbundle', 'resource/assets/res/ui/atlas/room.assetbundle', 'resource/assets/res/ui/atlas/setting.assetbundle', 'resource/assets/res/ui/atlas/settle.assetbundle', 'resource/assets/res/ui/atlas/shop.assetbundle', 'resource/assets/res/ui/fonts/default.assetbundle']
}

signingConfigs {
release {
storeFile file('E:/its_rummy/rummy.keystore')
storePassword 'Itengshe.1'
keyAlias 'its_rummy_android'
keyPassword 'Itengshe.1'
}
}

buildTypes {
debug {
minifyEnabled false
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
jniDebuggable true
}
release {
minifyEnabled false
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
signingConfig signingConfigs.release
}
}

packagingOptions {
doNotStrip '*/armeabi-v7a/*.so'
}

bundle {
language {
enableSplit = false
}
density {
enableSplit = false
}
abi {
enableSplit = true
}
}
}


遇到的小坑

  1. unity导出apk, File->Build Run, 当导出apk时,可能遇到问题 Unable to find unity activity in manifest. You need to make sure orientation attribute is set to fullSensor manually.

    需在AndroidManifest中增加一行:
    <meta-data android:name="unityplayer.UnityActivity" android:value="true" />

  2. 删除 AndroidManifest.xml 中 app 的主题 (使用默认的就行 android:theme="@style/UnityThemeSelector" ) ,否则 unity 打包 apk 是关联的主题会报找不到错误

  3. 删除生产的arr文件里的libs下的classes.jar,这个是之前从u3d中拷过去的,打包时会重新打进去,所以要删除,不删除打包会报错

另外还可以参考: Unity接入九游SDK学习与踩坑 - https://www.cnblogs.com/MuniuTian/p/11133341.html

报错: Program type already present: com.xxx.BuildConfig

参考: http://rx1226.pixnet.net/blog/post/347963956-%5Bandroid%5D-program-type-already-present-buildconfig

是因为有2个module在 AndroidManifest.xml 里面具有一样的package name,修改不同名字即可。

被这个坑的有点久

报错: Duplicate class android.support.v4 and support-compat:27.0.2

参考: https://stackoverflow.com/questions/55909804/duplicate-class-android-support-v4-app-inotificationsidechannel-found-in-modules

从这里得知去修改本地的 gradle - https://7dot9.com/2017/12/20/how-to-fix-unity-gradle-build-with-heap-size-error/

类冲突, 解决办法是在本地的 gradle 中加入配置. 路径: C:\Users\xxxx\.gradle\gradle.properties

1
2
android.useAndroidX=true
android.enableJetifier=true

使用 as 新建的原生工程就不会报错, 是因为在项目中的 gradle.properties 有配置这两个参数

还有一种解决方法没尝试, 是在 代码中动态重写这个配置, 参考: https://stackoverflow.com/questions/54186051/is-there-a-way-to-change-the-gradle-properties-file-in-unity

报错: NoClassDefFoundError … R$string

代码中你可能使用的是

1
2
import com.its.demo.asdasd.R;
R.string.facebook_app_id

要换成这样的方式去获取

1
2
3
4
5
public static String GetStringVaule(Activity ctx, String key) {
Resources res = ctx.getResources();
// String appPackageName = ctx.getApplication().getPackageName(); // 可以这样获取主包名
return res.getString(res.getIdentifier(key, "string", "com.its.xxx.xxx"));
}

参考: [代码中获取 strings.xml](#代码中获取 strings.xml)

报错参考: https://forum.unity.com/threads/problem-with-r-class-in-custom-unity-player-activity.139741/