原理
生产apk
首先新建一个Unity项目,写一个简单的游戏界面:
然后用Unity导出为apk:
我们的游戏运行起来之后,首先展示的就是这个主界面了:
所以猜测,如果将他转换为我们熟悉的Android项目,那么这个游戏界面就应该对应着MainActivity
了。接下来验证一下。
解析apk
使用apktool
工具,解析apk文件,然后打开其中的AndroidManifest.xml
文件,文件内容如下:
1 | <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"> |
果然,被标识为应用入口的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 | public class UnityPlayerActivity extends Activity { |
可以看到,这个Activity继承自Activity,持有一个UnityPlayer
对象的引用,并且在onCreate()
方法中调用UnityPlayer
的构造器,传入自己的引用,创建了这一对象,然后在调用setContentView()
方法将UnityPlayer
当做本Activity所显示的View。之后该Activity的所有生命周期方法,实际上都调用了UnityPlayer
的对应方法。那么来看看这个UnityPlayer
为何物:
1 | public class UnityPlayer extends FrameLayout implements com.unity3d.player.f { |
原来这个UnityPlayer
继承自FrameLayout
,所以他可以在刚才被当做参数传给setContentView()
方法。而在构造器中可以看到,将传入的Activity的引用,也就是Context的引用,用一个叫currentActivity
(记住这个名字)的变量保存起来了。
总结一下,Unity项目导出Android项目后,主界是UnityPlayerActivity
,是一个Activity。Activity所展示的界面,是通过一个叫做UnityPlayer
的FrameLayout
渲染的。所以我们如果想在Android端对这个主Activity做手脚,只需要继承UnityPlayerActivity
即可。
Android端的处理
模拟需求
经过上述分析,我们知道了大致的原理,接下来就是实际行动了。我们假设要在Android端写一个SDK,让Unity项目在接入SDK后生产的游戏,在启动后首先显示由Android端控制的SplashActivity
,之后再跳转至游戏的主界面。在游戏主界面中,通过点击按钮来调用Android端的方法。在Android端,再通过回调机制,调用Unity端的方法。以此,来实现两端的互相调用。
准备工作
首先创建一个Android项目。之后,在项目中创建一个Module,并选择Android Library:
这个Module就是我们的SDK项目了。
创建成功之后,先导入之前提到过的classes.jar。导入成功之后创建两个Activity,分别为SplashActivity
和UnityActivity
:
SplashActivity
作为应用的入口,提供一个闪屏的作用,UnityActivity
作为跳转之后的主Activity。因此,在AndroidManifest中这样注册他们:
1 |
|
注意,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 | public class SplashActivity extends Activity{ |
openActivity()
:
1 | public static void openActivity(Context context) { |
UnityActivity
因为继承自UnityPlayerActivity
,所以在onCreate()方法中无需设置界面。因为需要和Unity做交互,所以需要提供一些接口方法供外部调用:
1 | public class UnityActivity extends UnityPlayerActivity{ |
这里注意,同步接口很简单,正常写一个方法即可。如果要让为Unity提供异步接口,我的做法是开启一个线程模拟一些后台的耗时操作,在执行结束后,通过UnityPlayer提供的一个方法来通知Unity,这一方法就是UnityPlayer的静态方法UnitySendMessage()
方法:
UnityPlayer.UnitySendMessage("GameObject","payResult","pay success");
这个方法接受三个参数,第一个参数是GameObject的名称,即我们在Unity项目中定义的一个GameObject的名称。第二个参数是GameObject所关联脚本中需要被调用的方法,即我们实际上需要在Android中调用的Unity中的方法的方法名,最后一个参数是调用这个方法时传入的参数。如图是我在Unity中创建的GameObject:
以及所关联的脚本中的方法:
1 | public void payResult(string 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 | // 定义SDK包名称 |
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 | public void Login() { |
前两步的目的是获取到Android项目中UnityActivity的引用,获取到之后就可以任意调用其中的方法了,也就是直接调用AndroidJavaObject
的Call系列方法。之所以说是系列,是因为我们Android端的方法有很多类型,静态的、实例的、有返回值的、无返回值的、有参数的、无参数的,这些都可以用Call方法来搞定:
1 | //返回值为string的实例方法 |
最后,再设置一下构建时的设置,将package name替换为Android SDK的包名。然后就可以直接打包apk并运行了: