将Unity功能嵌入Android界面及交互

5,568 阅读4分钟

将Unity功能嵌入Android界面及交互

一、完成Unity功能

以一个车模型功能为例,该功能点击车门、车窗、尾门可以打开关闭对应部位。

image.png 首先将其导出,在Unity开发工具的File ——> Build Settings选项配置如下

image.png 然后选择Export按钮,将代码导入事先创建好的文件夹,导出目录如下,整个文件夹可以在Android Studio中运行,但我们当前仅需要unityLibrary这个目录

image.png

二、将unityLibrary目录放入Android Studio

所使用Android Studio版本为2020.3.1 Patch2,适配期间做了一点结构更改。

1、将unityLibrary放入Android项目根目录

image.png

2、配置settings.gradle添加unityLibrary,整体如下

rootProject.name = "carmodel"
include ':app',':unityLibrary'

3、配置全局build.gradle添加flatDir,整体配置如下,

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.2"
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter() 
        flatDir{
            dirs "${project(':unityLibrary').projectDir}/libs"
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

3、在app的build.gradle里,在dependencies中添加unity相关依赖,增加架构设置ndk及packagingOptions,整体配置如下,

plugins {
    id 'com.android.application'
}

android {
    compileSdk 30

    defaultConfig {
        applicationId "com.saicmotor.hmi.carmodel"
        minSdk 21
        targetSdk 30
        versionCode 1
        versionName "1.0"

        ndk {
            abiFilters 'armeabi-v7a'
        }

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

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

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation project(':unityLibrary')
    implementation files('../unityLibrary/libs/unity-classes.jar')

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

4、删除unityLibrary下build.gradle的noCompress,整体配置如下,

apply plugin: 'com.android.library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

android {
    compileSdkVersion 30
    buildToolsVersion '31.0.0'

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 30
        ndk {
            abiFilters 'armeabi-v7a'
        }
        versionCode 1
        versionName '0.1'
        consumerProguardFiles 'proguard-unity.txt'
    }

    lintOptions {
        abortOnError false
    }

    aaptOptions {
        //noCompress = ['.ress', '.resource', '.obb'] + unityStreamingAssets.tokenize(', ')
        ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
    }

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

5、删除unityLibrary中AndroidManifest.xml中如下只有启动页才需要的配置

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

6、在app下strings.xml中增加game_view_content_description字符串,

<resources>
    <string name="app_name">CarModelTest</string>
    <string name="game_view_content_description">Game view</string>
</resources>

三、配置unity功能在Android界面的展示

1、目前将unity界面作为fragment全屏显示在主界面上,配置app下的activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".CarModelActivity">

    <FrameLayout
        android:id="@+id/fm"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

2、在app下的主界面CarModelActivity.java将unity界面绑定到FrameLayout

1)mUnityPlayer = new UnityPlayer(this)创建UnityPlayer并将CarModelActivity赋值给unity内置currentActivity,以便后续unity调用Android方法;

2)将mUnityPlayer绑定到FrameLayout;

3)重写生命周期函数并添加mUnityPlayer相应的调用,如下

package com.example.carmodel;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;

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

public class CarModelActivity extends AppCompatActivity {
    private FrameLayout fm;
    private UnityPlayer mUnityPlayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fm = findViewById(R.id.fm);
        mUnityPlayer = new UnityPlayer(this);
        fm.addView(mUnityPlayer.getView());
        UnityPlayer.UnitySendMessage("Car02_Door_FrontLeft", "getDoorLock", "1");

    }

    public void setDoorLock(int doorLockStatus) {
    }

    public void setWindowState(int windowStatus) {

    }

    public void setFrontLightState(int frontLightState) {

    }

    public void setBrakeLightState(int brakeLightState) {

    }

    // Quit Unity
    @Override protected void onDestroy ()
    {
        mUnityPlayer.destroy();
        super.onDestroy();
    }

    @Override protected void onStop()
    {
        super.onStop();

        if (!MultiWindowSupport.getAllowResizableWindow(this))
            return;

        mUnityPlayer.pause();
    }

    @Override protected void onStart()
    {
        super.onStart();

        if (!MultiWindowSupport.getAllowResizableWindow(this))
            return;

        mUnityPlayer.resume();
    }

    // Pause Unity
    @Override protected void onPause()
    {
        super.onPause();

        if (MultiWindowSupport.getAllowResizableWindow(this))
            return;

        mUnityPlayer.pause();
    }

    // Resume Unity
    @Override protected void onResume()
    {
        super.onResume();

        if (MultiWindowSupport.getAllowResizableWindow(this))
            return;

        mUnityPlayer.resume();
    }

    // Low Memory Unity
    @Override public void onLowMemory()
    {
        super.onLowMemory();
        mUnityPlayer.lowMemory();
    }

    // Trim Memory Unity
    @Override public void onTrimMemory(int level)
    {
        super.onTrimMemory(level);
        if (level == TRIM_MEMORY_RUNNING_CRITICAL)
        {
            mUnityPlayer.lowMemory();
        }
    }

    // This ensures the layout will be correct.
    @Override public void onConfigurationChanged(Configuration newConfig)
    {
        super.onConfigurationChanged(newConfig);
        mUnityPlayer.configurationChanged(newConfig);
    }

    // Notify Unity of the focus change.
    @Override public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);
        mUnityPlayer.windowFocusChanged(hasFocus);
    }

    // For some reason the multiple keyevent type is not supported by the ndk.
    // Force event injection by overriding dispatchKeyEvent().
    @Override public boolean dispatchKeyEvent(KeyEvent event)
    {
        if (event.getAction() == KeyEvent.ACTION_MULTIPLE)
            return mUnityPlayer.injectEvent(event);
        return super.dispatchKeyEvent(event);
    }

    // Pass any events not handled by (unfocused) views straight to UnityPlayer
    @Override public boolean onKeyUp(int keyCode, KeyEvent event)     { return mUnityPlayer.injectEvent(event); }
    @Override public boolean onKeyDown(int keyCode, KeyEvent event)   { return mUnityPlayer.injectEvent(event); }
    @Override public boolean onTouchEvent(MotionEvent event)          { return mUnityPlayer.injectEvent(event); }
    /*API12*/ public boolean onGenericMotionEvent(MotionEvent event)  { return mUnityPlayer.injectEvent(event); }

}

到了这里,就可以把整个工程像普通Android工程一样,安装到Android设备上看效果了,我这里安装到了手机真机上。

四、Android调用Unity

在我需要调用unity代码的地方通过UnityPlayer.UnitySendMessage来调用

参数一是unity中的一个gameobject名称;

参数二为这个gameobject身上捆绑的脚本中的一个方法;

参数三这个对应方法上的参数,只能传一个字符串参数,不能传多个,如果需要传多个参数,只能分几个函数调用,或者字符串用'|'之类的字符合并传递再拆分接收,没有参数时传入空字符串,举例如下,

UnityPlayer.UnitySendMessage("Car02_Door_FrontLeft", "getDoorLock", "1");
UnityPlayer.UnitySendMessage("AndroidObject", "generatePayloadMessage", "");

就这样可以调用unity中getDoorLock函数,如下

public void getDoorLock(string lockStateStr)
{
    Debug.Log("Door, getDoorLock, lockStateStr :" + lockStateStr);
    int lockState = System.Int32.Parse(lockStateStr);
    if (lockState == DOOR_LOCK_CLOSE)
    {
        CloseDoor();
    }
    else if (lockState == DOOR_LOCK_OPEN)
    {
        OpenDoor();
    }
    m_LockState = lockState;
}  

五、Unity调用Android

在此之前我们将CarModelActivity赋值给unity内置currentActivity,那么unity就可以通过调用currentActivity来调用CarModelActivity中的方法

假如CarModelActivity中有如下方法

public void setDoorLock(int doorLockStatus) {
}

unity中就可以通过如下方式来调用Android

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

if (jo == null)
{
    Debug.LogError("Failed to obtain Android Activity from Unity Player class.");
    return;
}

jo.Call("setDoorLock", m_LockState);

还有其他调用方式

jo.Call(method ,parameter );//调用实例方法 
jo.Get(method ,parameter );//获取实例变量(非静态) 
jo.Set(method ,parameter );//设置实例 变量(非静态) 
jo.CallStatic(method ,parameter );//调用静态变量(非静态) 
jo.GetStatic (method ,parameter );//获取静态变量 
jo.SetStatic (method ,parameter );//设置静态变量 

运行到相关设备上即可交互。