将Unity功能嵌入Android界面及交互
一、完成Unity功能
以一个车模型功能为例,该功能点击车门、车窗、尾门可以打开关闭对应部位。
首先将其导出,在Unity开发工具的File ——> Build Settings选项配置如下
然后选择Export按钮,将代码导入事先创建好的文件夹,导出目录如下,整个文件夹可以在Android Studio中运行,但我们当前仅需要unityLibrary这个目录
二、将unityLibrary目录放入Android Studio
所使用Android Studio版本为2020.3.1 Patch2,适配期间做了一点结构更改。
1、将unityLibrary放入Android项目根目录
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 );//设置静态变量
运行到相关设备上即可交互。