Android SDK for Unity

原理

生产apk

首先新建一个Unity项目,写一个简单的游戏界面:

然后用Unity导出为apk:

我们的游戏运行起来之后,首先展示的就是这个主界面了:

所以猜测,如果将他转换为我们熟悉的Android项目,那么这个游戏界面就应该对应着MainActivity了。接下来验证一下。

解析apk

使用apktool工具,解析apk文件,然后打开其中的AndroidManifest.xml文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="28" android:compileSdkVersionCodename="9" android:installLocation="preferExternal" package="com.levent_j.sdk_jar" platformBuildVersionCode="28" platformBuildVersionName="9">
<supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:xlargeScreens="true"/>
<application android:banner="@drawable/app_banner" android:debuggable="false" android:icon="@mipmap/app_icon" android:isGame="true" android:label="@string/app_name" android:theme="@style/UnityThemeSelector">
<activity android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode" android:hardwareAccelerated="false" android:label="@string/app_name" android:launchMode="singleTask" android:name="com.unity3d.player.UnityPlayerActivity" android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true"/>
</activity>
<meta-data android:name="unity.build-id" android:value="25b7b97f-87c9-4c20-8185-b8dbd0337926"/>
<meta-data android:name="unity.splash-mode" android:value="0"/>
<meta-data android:name="unity.splash-enable" android:value="true"/>
</application>
<uses-feature android:glEsVersion="0x00020000"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false"/>
</manifest>

果然,被标识为应用入口的MainActivity出现了,就是这里的UnityPlayerActivity
这个UnityPlayerActivity,放在Unity安装目录下的一个classes.jar包中。因此我们如果想在Android端做一些事情,就一定需要依赖这个classes.jar文件。所以,我们需要复制这一文件,把他当做一个外部依赖包,导入到Android项目中。

这一文件的具体位置不同系统不一样,不过一般都是在Unity的安装目录下。比如Mac OS下,具体路径为:

1
/Applications/Unity/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes

复制这一目录下的classes.jar,然后在Android项目中导入。导入之后,就可以查看其中的代码了。

UnityPlayerActivity是什么

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
public class UnityPlayerActivity extends Activity {
protected UnityPlayer mUnityPlayer;

public UnityPlayerActivity() {
}

protected void onCreate(Bundle var1) {
this.requestWindowFeature(1);
super.onCreate(var1);
this.mUnityPlayer = new UnityPlayer(this);
this.setContentView(this.mUnityPlayer);
this.mUnityPlayer.requestFocus();
}

protected void onNewIntent(Intent var1) {
this.setIntent(var1);
}

protected void onDestroy() {
this.mUnityPlayer.quit();
super.onDestroy();
}

protected void onPause() {
super.onPause();
this.mUnityPlayer.pause();
}

protected void onResume() {
super.onResume();
this.mUnityPlayer.resume();
}

//忽略了其他的冗余代码
}

可以看到,这个Activity继承自Activity,持有一个UnityPlayer对象的引用,并且在onCreate()方法中调用UnityPlayer的构造器,传入自己的引用,创建了这一对象,然后在调用setContentView()方法将UnityPlayer当做本Activity所显示的View。之后该Activity的所有生命周期方法,实际上都调用了UnityPlayer的对应方法。那么来看看这个UnityPlayer为何物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UnityPlayer extends FrameLayout implements com.unity3d.player.f {
public static Activity currentActivity = null;
private Camera2Wrapper q = null;
private Context r;
private SurfaceView s;

public UnityPlayer(Context var1) {
super(var1);
if (var1 instanceof Activity) {
currentActivity = (Activity)var1;
this.c = currentActivity.getRequestedOrientation();
}

a(currentActivity);
this.r = var1;
if (currentActivity != null && this.i()) {
this.m = new l(this.r, com.unity3d.player.l.a.a()[this.getSplashMode()]);
this.addView(this.m);
}
//忽略其他代码
}
//忽略其他代码
}

原来这个UnityPlayer继承自FrameLayout,所以他可以在刚才被当做参数传给setContentView()方法。而在构造器中可以看到,将传入的Activity的引用,也就是Context的引用,用一个叫currentActivity(记住这个名字)的变量保存起来了。

总结一下,Unity项目导出Android项目后,主界是UnityPlayerActivity,是一个Activity。Activity所展示的界面,是通过一个叫做UnityPlayerFrameLayout渲染的。所以我们如果想在Android端对这个主Activity做手脚,只需要继承UnityPlayerActivity即可。

Android端的处理

模拟需求

经过上述分析,我们知道了大致的原理,接下来就是实际行动了。我们假设要在Android端写一个SDK,让Unity项目在接入SDK后生产的游戏,在启动后首先显示由Android端控制的SplashActivity,之后再跳转至游戏的主界面。在游戏主界面中,通过点击按钮来调用Android端的方法。在Android端,再通过回调机制,调用Unity端的方法。以此,来实现两端的互相调用。

准备工作

首先创建一个Android项目。之后,在项目中创建一个Module,并选择Android Library:

这个Module就是我们的SDK项目了。

创建成功之后,先导入之前提到过的classes.jar。导入成功之后创建两个Activity,分别为SplashActivityUnityActivity

SplashActivity作为应用的入口,提供一个闪屏的作用,UnityActivity作为跳转之后的主Activity。因此,在AndroidManifest中这样注册他们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.levent_j.sdk_jar">

<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".DemoApp">
<activity android:name=".SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

</activity>
<activity android:name=".UnityActivity">
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>
</application>

</manifest>

注意,SplashActivity要加上作为应用入口的<intent-filter>标签,而UnityActivity作为游戏界面,需要加上<meta-data>标签:

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

###Activity如何处理

因为SplashActivity就是一个普通的Activity,所以继承Activity就可以了,而UnityActivity则需要继承UnityPlayerActivity

public class SplashActivity extends Activity

public class UnityActivity extends UnityPlayerActivity

由于SplashActivity是闪屏界面,需要跳转到主界面,所以延时几秒直接跳转也好,点击按钮跳转也好,只要最后通过Intent启动了UnityActivity即可。我是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SplashActivity extends Activity{
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_splash);

new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
openActivity();
}
}
}).start();
}

private void openActivity() {
UnityActivity.openActivity(this);
}
}

openActivity():

1
2
3
4
public static void openActivity(Context context) {
Intent intent = new Intent(context,UnityActivity.class);
context.startActivity(intent);
}

UnityActivity因为继承自UnityPlayerActivity,所以在onCreate()方法中无需设置界面。因为需要和Unity做交互,所以需要提供一些接口方法供外部调用:

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
public class UnityActivity extends UnityPlayerActivity{

private Handler handler;

@SuppressLint("HandlerLeak")
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
//创建好一个Handle
handler = new Handler(){
@Override
public void handleMessage(Message msg) {
payResult();
}
};
}

//提供一个同步的 登陆接口
public String login(){
return "jar login success";
}

//提供一个同步的 退出登录的接口
public String logout(){
return "jar logout success";
}

//提供一个异步的 支付接口
public void pay(String amount){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
handler.sendEmptyMessage(0);
}
}
}).start();
}

public void payResult(){
UnityPlayer.UnitySendMessage("GameObject","payResult","pay success");
}

}

这里注意,同步接口很简单,正常写一个方法即可。如果要让为Unity提供异步接口,我的做法是开启一个线程模拟一些后台的耗时操作,在执行结束后,通过UnityPlayer提供的一个方法来通知Unity,这一方法就是UnityPlayer的静态方法UnitySendMessage()方法:

UnityPlayer.UnitySendMessage("GameObject","payResult","pay success");

这个方法接受三个参数,第一个参数是GameObject的名称,即我们在Unity项目中定义的一个GameObject的名称。第二个参数是GameObject所关联脚本中需要被调用的方法,即我们实际上需要在Android中调用的Unity中的方法的方法名,最后一个参数是调用这个方法时传入的参数。如图是我在Unity中创建的GameObject:

以及所关联的脚本中的方法:

1
2
3
 public void payResult(string result) {
mContent.text = result;
}

两个Activity写好,就算可以了,当然也可以额外再加一个自定义的Application,因为有些时候可能会需要在这里做一些全局的初始化的工作。接下里就需要导出了。

导出sdk

将编写好的代码导出为可以用的SDK,实际上是导出为jar包或者aar包。两者的区别在于,导出jar包的话,需要再将项目中的AndroidManifest文件和res目录一起,作为资源文件,copy并导入到Unity项目中,略微有点麻烦。而导出为aar包的话,直接将aar包导入即可,比较简单,但是在导入前需要通过压缩软件打开而不是解压aar文件,删除其中的libs目录下的classes.jar文件。如果不这么做会在Unity打包时出现冲突异常。按理说,我们在一开始直接导入classes.jar包时,如果选择了compileOnly形式的话,就可以避免这一步骤,但是我试了试好像行不通。所以虽然麻烦点,导出jar包这一方案还是很好的。

首先,需要编辑Module目录下的build.gradle文件,加入导出自己jar包的脚本代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义SDK包名称
def SDK_BASENAME = "UnitySDKDemo"
// 定义SDK包版本
def SDK_VERSION = "_V1.0.0"
// SDK包生成地址
def SDK_PATH = "build/libs"
// 删除之前的Jar包 保证每一次生成的都是最新的
task deleteOldJar(type: Delete) {
delete SDK_PATH + SDK_BASENAME + SDK_VERSION + '.jar'
}
task exportJar(type: Copy) {
// 从源地址拷贝
from('build/intermediates/packaged-classes/release/')
// 存放
into(SDK_PATH)
// 导入
include('classes.jar')
// 重命名
rename('classes.jar', SDK_BASENAME + SDK_VERSION + '.jar')
}
// 执行脚本文件
exportJar.dependsOn(deleteOldJar, build)

sync之后,在右侧的gradle task列表中,就出现了上面定义的名为exportJar的Task:

找到这个task,双击执行后,等待build:

然后找到Module的build/libs目录,我们的jar包就导出成功了。

Unity的接入

在Unity项目的Project窗口中可以看到项目目录,在Assests目录下新建一个目录Plugins,在Plugins目录下新建一个目录Android,再在Android目录下新建一个目录libs目录,如图:

这个Plugins目录就是我们接入iOS或者Android所需的插件目录。接下来直接把刚才的jar包拖到这个Plugins/Android/libs目录下。注意一定要手动拖进来,因为Unity会自动创建一个相关的文件,这一文件如果自己打开文件管理器复制粘贴的话是无法自动生成的。然后,再用同样的方法将Module中的AndroidManifest.xml文件和res目录拖到Plugins/Android目录下:

现在Andorid的插件已经导入成功了,需要再编辑Unity的脚本来使用了。由于前面我们看到,UnityPLayer对象有一个名叫currentActivity的变量,保存着对Activity的引用,所以如果要调用Activity中的方法,只需要获取到这个变量即可,因此在Unity端需要调用Android端方法的地方,这样处理即可:

1
2
3
4
5
6
7
8
9
public void Login() {
Debug.Log("login");

AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");

string result = jo.Call<string>("login");
mContent.text = result;
}

前两步的目的是获取到Android项目中UnityActivity的引用,获取到之后就可以任意调用其中的方法了,也就是直接调用AndroidJavaObject的Call系列方法。之所以说是系列,是因为我们Android端的方法有很多类型,静态的、实例的、有返回值的、无返回值的、有参数的、无参数的,这些都可以用Call方法来搞定:

1
2
3
4
5
6
7
8
//返回值为string的实例方法
string result = jo.Call<string>("login");
mContent.text = result;

//无返回值,但是需要传参的实例方法
string money = mInput.text;
jo.Call("pay",money);
mContent.text = "支付中……";

最后,再设置一下构建时的设置,将package name替换为Android SDK的包名。然后就可以直接打包apk并运行了: