lite 发表于 2020-11-25 09:19

如何把 Unity 项目 嵌入 Android

本教程将分为两部分展开,第一部分为安卓应用的一些基本知识,如果你已经对这些有了一定了解,则可以直接跳到第二部分。


一、Android 基础知识



Android 由 Google 率先发起,这是基于 Linux 的一个全面的开源平台。作为一个强大的开发框架,Android 包含结合 Java 和 XML 构建应用所需的全部特性。
一个典型的 Android 应用由布局、活动、额外的资源组成
布局决定一个应用的外观,它决定了应用长什么样,比如添加一些按钮、文本域、标签等 GUI 组件,并把它们按照一定规则摆放在界面上,一般是 xml 格式的文件。


活动就是一些特殊的 JAVA 类,它们决定应用做什么,一般情况下每一个活动都会对应一个布局,活动可以对布局进行一系列的操作,掌管着应用与用户交互的部分。比如在布局中包含了一个按钮,那么就要在 Java 代码中定义这个按钮被按下后做什么。当然不是所有 Java 类都要包含一个布局,有一些特殊的活动并没有界面,它们只是在后台提供一些服务,还有一些 Java 类作为工具类出现,和普通 Java 工程中的类相似。


除了活动和布局,一个应用通常包含许多额外的资源,比如视频、图像、字体等应用中用到的数据,我们可以为应用增加任何额外的文件。
Android 平台有很多不同的组件,它们帮助我们控制应用的外观和行为,下图展示了这些组件如何集成在一起:


虽然看起来很复杂,但是这些强大的库都可以通过 Android 中的 API 来访问,所以我们可以使用 Java 代码比较轻松地构建一个应用程序。


搭建环境
我们在开始之前需要搭建 Android 的开发环境,这一步需要安装 Android Studio ,同时还要配置 Java 环境,Android Studio 包含了开发 Android 应用需要的所有工具。


首先需要安装 Java 环境,可以在 www.oracle.com 获取 JDK 和 JRE,安装完毕后可以从developer.android.com/studio/ 下载Android Studio,这个页面还提供了安装说明,按照说明安装好后就可以开始构建应用了。


构建应用
打开 Android Studio ,会出现以下界面:


应用名称及包名设置
第一栏是应用名,第二栏是域名,第三栏是工程存放的位置,按照要求填好后会自动生成应用的包名,一般是 域名反过来+应用名 的格式,包名是一个 APP 的唯一标识,如果安装两个包名相同的 APP ,后安装的会把原有的替换掉。




设备支持设置
这个页面可以选择 Android 应用运行的平台,我们选择第一项,API 15 表示应用使用的 API 版本,不同版本的 Android 系统都对应一个 API 层次,越旧的设备的数字就越低,我们一般不会直接选择最高层次,因为这样的应用可能在大多数设备上都无法运行。




活动选择页面


这个页面包含一些布局,我们可以根据实际需要选择它们,可以很快构建出我们想要的布局,我们这里选择 Empty Activity。


活动设置
这个页面对刚刚创建的活动进行一些设置,我们把这个页面设置为主活动,也就是整个应用的入口。文件名按默认就好。


点击 finish 按钮,整个工程就会开始开始构建,稍等片刻后就会出现工程的主界面,我们在左侧把项目展示方式改为 project:


修改工程目录展示方式
此时我们已经创建了一个可以运行的 APP,点击工具栏的 Run 按钮即可测试你的 APP ,有两种方式,一种是使用 IDE 自带的模拟器进行测试,另一种是通过 USB 连接到实体机进行测试。 对于这个教程的项目,我建议在实体机上进行测试,因为 unity 项目在运行时会消耗大量的资源。


点击绿色按钮可以运行应用
用 USB 连接手机,手机上需要打开 USB 调试模式,然后点击运行按钮,稍等片刻之后手机便会打开刚刚创建的应用,效果如下图:


默认的 helloworld 界面


在进行下一步之前,我们需要了解项目的目录结构:


在最外层的文件夹中,我们只需要关注 app 文件夹下的内容,libs 用来存放一些库文件,src 包含你编写和编辑的源代码。在 main 文件夹下,java/ 文件夹包含Java代码,创建的所有活动都存放在这里。res/文件夹存放了各种系统资源。


其中 layout 下存放的就是布局文件,values 下 color.xml 用于管理颜色,string.xml 包含字符串值,包括应用名和默认文本值等字符串,布局和活动可以在这里查找文本。style.xml 定义了应用的基本样式,可以保证应用外观的统一。


还有最重要的 AndroidManifest.xml 文件,每个应用都应该在根目录包含它,这个清单文件包含了应用的基本信息,比如它包含哪些组件、必要的库以及需要向系统申请的权限等。在打开一个活动时系统会先在这个文件中查找是否存在这个活动,所以我们创建的每一个活动都应该在这个文件里进行“登记”。
还有一个 Java 文件叫 R.java,它由编译工具自动生成,用于跟踪应用中的资源,比如我们需要在代码中获取布局中的按钮对象,可以使用findViewById(R.id.btn0) 获取按钮对象。




调整应用


这是activity_main.xml文件的代码,第一行声明xml文件的版本和字符编码格式,下面共有两个标签,外层的标签标识了该布局使用的布局方式,内层则是一个文本域,在标签里可以设置标签的一些属性,我们现在对这个文件进行修改,首先删掉除第一行外所有的代码并新建一个相对布局:


然后在标签里添加一个按钮,并把它的id设为“btn0”:


完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hxuanyu.testdemo.MainActivity">

    <Button
      android:id="@+id/button_1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerInParent="true"
      android:text="开始扫描"/>


</RelativeLayout>此时,我们可以在编辑器左下角的选项卡上切换视图:


点击Design按钮就可以看到刚刚做的布局预览效果了,在这里还可以进行各种组件的添加和移动,不过不建议这样做,因为这样摆放出来的效果可能只适用于预览窗口,在实际运行中会出现意想不到的偏差。我们要通过代码更加精确地控制组件的位置。






这是MainActivity.java 的代码,也就是主活动的代码,我们看到它复写了父类的 onCreate 方法,事实上每个活动都必须有这一步,每个活动都以这个方法作为入口运行。
想要对布局中的按钮进行操作,需要创建一个按钮对象,并把它和布局中的按钮联系起来并添加一个点击事件:


点击按钮后,会弹出一个消息提示。以下是完整代码:
package com.hxuanyu.testdemo;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      Button button1 = (Button)findViewById(R.id.button_1);
      button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this,"正在启动扫描程序,请稍候^_^",Toast.LENGTH_LONG).show();
            }
      });
    }
}

现在运行一下 APP ,可以通过点击按钮弹出一个消息提示:




至此,Android 部分就先告一段落,接下来我们开始着手把unity嵌入这个项目。




二、Unity 项目与安卓项目合并



在unity中完成开发后,我们需要导出unity项目,打开 buildsetting 窗口,点击左侧的playersetting按钮,对导出的包进行一些设置,有两处必须改动,第一处是CompanyName:


第二处是包名,一定要与刚才创建的安卓应用包名一致 :


进行一些其他设置后我们需要把构建工具改为 gradle ,因为默认的工具无法导出Android项目。同时还要把 Export Project 勾上:


然后点击 Export ,选择导出文件夹,便得到如下文件:


这是一个 Eclipse 的Android项目,所以不能直接导入 AndroidStudio,二者的区别仅仅是文件夹目录结构不同,所以我们可以手动提取我们需要的文件到 AndroidStudio 项目中。
PS:为了描述方便把Unity项目称作项目一,Android项目称为项目二。首先把项目一的libs文件夹下的 jar 包复制到项目二的 app/libs 目录下:


复制完成后回到 AndroidStudio ,找到这个文件夹,右键单击其中一个文件,在菜单中点击Add As Library,弹出窗口后点确定即可把库文件引入。




然后把项目一的 src/main 目录下的assets文件夹和 jniLibs 文件夹复制到项目二的 app/src/main 目录下:


紧接着把项目一Java目录下的 UnityPlayerActivity.java 复制到项目二存放活动的目录下:


最后我们需要编辑 AndroidManifist.xml文件,添加一些标签以向系统申请一些权限,同时用 activity 标签对刚刚引入的 UnityPlayerActivity 进行注册,具体代码如下:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hxuanyu.testdemo">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-sdk
      android:minSdkVersion="14"
      android:targetSdkVersion="22" />
    <uses-feature android:name="android.hardware.camera" />
    <supports-screens
      android:anyDensity="true"
      android:largeScreens="true"
      android:normalScreens="true"
      android:smallScreens="true"
      android:xlargeScreens="true" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-feature android:glEsVersion="0x00020000" />
    <uses-feature
      android:name="android.hardware.camera.autofocus"
      android:required="true" />
    <uses-feature
      android:name="android.hardware.camera.front"
      android:required="false" />
    <uses-permission
      android:name="android.permission.READ_EXTERNAL_STORAGE"
      android:maxSdkVersion="18" />
    <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" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <application
      android:allowBackup="true"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/AppTheme">
      <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
      </activity>
      <activity
            android:name=".UnityPlayerActivity"
            android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection"
            android:label="@string/app_name"
            android:screenOrientation="fullSensor">
            <meta-data
                android:name="unityplayer.UnityActivity"
                android:value="true" />
            <meta-data
                android:name="unityplayer.ForwardNativeEventsToDalvik"
                android:value="false" />
      </activity>
      <uses-library
            android:name="com.ti.s3d"
            android:required="false" />
      <!-- To support the ODG R7 in stereo mode we must add the following library. -->
      <uses-library
            android:name="com.osterhoutgroup.api.ext"
            android:required="false" />

      <meta-data
            android:name="unity.build-id"
            android:value="178fe355-969e-45e2-9ca7-8281ce009fa6" />
      <meta-data
            android:name="unity.splash-mode"
            android:value="0" />
      <meta-data
            android:name="unity.splash-enable"
            android:value="True" />
    </application>

</manifest>

其实就是把项目一需要申请的权限复制到了项目二中,注意只能有一个主活动,也就是只能有一个 activity标签里包含 intent-filter 标签,这个标签的作用是设置该活动为主活动。


修改完毕后,在主活动的按钮监听器里添加两行代码:
Intent intent = new Intent(MainActivity.this,UnityPlayerActivity.class);
startActivity(intent);

这两行代码使用 intent 作为载体启动了另外一个活动,即刚刚导入的UnityPlayerActivity,我们发现这个活动并没有对应的布局,是因为布局可以在 Java 代码中动态创建,Unity导出的这个活动采用的就是这种方法。
此时运行项目,点击“开始扫描”按钮,系统会询问是否授予一些权限:


全部选择允许,之后会运行 Unity 项目中的活动,但是你很快会发现点击返回键没反应,只能强制关闭程序,而且通过直接调用的方式并不能满足我们的更多需求,所以我们通过新建一个活动作为 “容器” 把 unity 项目装进去。
新建一个 Java 类,命名为 ScanActivity ,为了能嵌入 Unity 活动,需要继承 UnityPlayerActivity 类:


在 onCreate() 方法中创建一个新的布局,这里使用相对布局,然后用 addView() 把父类中继承过来的 mUnityPlayer 对象的 View 放进去,获取 View 的方法为 mUnityPlayer.getView()


其中mview_params 可以对布局进行设置。为了和最初的直接调用的方式形成对比,我们需要添加一些布局规则:
mview_params.width=600;
mview_params.height=600;
mview_params.addRule(RelativeLayout.CENTER_IN_PARENT);在运行之前,我们需要把主活动中的按钮点击事件启动的活动修改为 ScanActivity:




现在运行应用,点击 “开始扫描” 按钮后,出现下面的界面:


我们已经把它做成了一个小组件,可以通过代码改变它的位置和大小,但是还有一个问题,点击返回按钮没反应,为了解决这个问题,我们要添加一个按钮点击事件,通过复写父类的 onKeyDown() 方法,对手机返回键进行监听,在监测到点击事件后,调用 mUnityPlayer.quit() 方法结束当前活动:




现在运行应用,在按下返回键后应用可以正常返回上一个活动,但是会有一点卡顿,这方面我还在寻找解决办法,有想法的小伙伴可以在评论区说出你的建议。
正常情况下关闭一个活动应该使用 finish 方法,但是在这里如果直接调用finish方法会直接退出应用,可能因为 UnityPlayerAcyivity 在退出时会直接 kill 掉整个进程,经过很久的尝试后发现 quit() 可以成功返回,不过可能还有更好的方法,正在摸索中。




以上就是把unity项目嵌入Android中的过程,可能还有更完美的方法,如果你有好的想法可以在评论区提出建议或者直接私信交流传授你的经验(我在Android方面是小白)




最后附上这个项目的源码:
链接: https://pan.baidu.com/s/1i4Z298p
密码: 6666
页: [1]
查看完整版本: 如何把 Unity 项目 嵌入 Android