Unity2019与Android混合开发

0. 开始前的版本对齐

Unity版本:Unity2019.3.4f1
AndroidStudio版本:3.5.3

1. Unity – 准备项目

  1. 新建项目

  2. 打开File -> Build Setting

    File -> Build Setting

  3. 切换工程模式
    首先选择Android Platform,然后点击Switch Platform切换工程模式。

切换工程模式

  1. 导出Android工程
    勾上Export Project,否则下方的Export按钮会是一个Build,点击后Unity会直接导出一个Apk文件,而并不是一个Android Studio项目。
    点击Export后,选择保存位置后会成功输出一个Android Studio项目,此时Unity的操作告一段落。

导出工程

2. Android 打开项目

在使用Android studio 打开项目时,会跳出一个选择SDK的选项,此处我选择使用Android Studio’s SDK。Project’s SDK是Unity提供的,我觉得用此SDK可能对原生开发会有一定的影响。我并没有使用Project’s SDK进行验证。

sdk 选择

然后在弹出的Gradle 同步提示框中点击OK后项目就开始同步,如果无错误就可以进行开发了

3. Android 项目结构

Gradle同步完成后,可以看到以下目录(从Android视图切换为了Project)

项目列表

其中launcher为平时Android开发中app主module,推荐在launcher主module中开发新的逻辑。(java目录需要自行创建)。

unityLibrary为Unity生成的子module。
在unityLibrary中包含一个UnityPlayerActivity的示例Activity,在不进行修改任何代码的时候默认启动的Activity就是这个UnityPlayerActivity。(可以在AndroidManifest中看到将这个activity配置成了启动Acitivity)

image.png

而在unityLibrary module中的lib目录中可以看到有一个unity-classes.jar,一个非常重要的类UnityPlayer就是来自这个jar包。如果之前已经在Unity项目中添加过一些Android插件,在lib目录下也会出现这些其他的lib包

那么我们来看下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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
package com.unity3d.player;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.os.Process;

public class UnityPlayerActivity extends Activity implements IUnityPlayerLifecycleEvents
{
protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

// Override this in your custom UnityPlayerActivity to tweak the command line arguments passed to the Unity Android Player
// The command line arguments are passed as a string, separated by spaces
// UnityPlayerActivity calls this from 'onCreate'
// Supported: -force-gles20, -force-gles30, -force-gles31, -force-gles31aep, -force-gles32, -force-gles, -force-vulkan
// See https://docs.unity3d.com/Manual/CommandLineArguments.html
// @param cmdLine the current command line arguments, may be null
// @return the modified command line string or null
protected String updateUnityCommandLineArguments(String cmdLine)
{
return cmdLine;
}

// Setup activity layout
@Override protected void onCreate(Bundle savedInstanceState)
{
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);

String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
getIntent().putExtra("unity", cmdLine);

mUnityPlayer = new UnityPlayer(this, this);
setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();
}

// When Unity player unloaded move task to background
@Override public void onUnityPlayerUnloaded() {
moveTaskToBack(true);
}

// When Unity player quited kill process
@Override public void onUnityPlayerQuitted() {
Process.killProcess(Process.myPid());
}

@Override protected void onNewIntent(Intent intent)
{
// To support deep linking, we need to make sure that the client can get access to
// the last sent intent. The clients access this through a JNI api that allows them
// to get the intent set on launch. To update that after launch we have to manually
// replace the intent with the one caught here.
setIntent(intent);
mUnityPlayer.newIntent(intent);
}

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

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

// Resume Unity
@Override protected void onResume()
{
super.onResume();
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); }
}

其中UnityPlayer mUnityPlayer就是Unity最终绘制内容的View(是一个FrameLayout),而UnityPlayerActivity 将这个View设置为自己的根View,进行显示。所以也可以自定义一个任意大小的布局,将mUnityPlayer当做正常的View 添加到布局中,进行自定义大小的控制。
UnityPlayerActivity 也重写了onResumeonPause等进行了对mUnityPlayer生命周期的管理。

4. Android与Unity跳转

一般情况下,混合开发都是会先启动原生界面,然后通过点击原生的中button根据业务逻辑跳转至包含Unity的Activity。这样我们就不能将UnityPlayerActivity设置为第一个启动的Activity

  1. 取消UnityPlayerActivity默认启动
    AndroidManifest文件中删除或注释掉UnityPlayerActivity配置的下intent-filter
    删除intent-filter
    小伙伴如果之前已经在Unity中导入了其他Android插件,那么这个AndroidManifest中显示的Activity应该是插件中自定义的Activity,而不是UnityPlayerActivity,注释掉相应的代码即可。
  2. 页面跳转
    通过常规的startActivity即可启动UnityPlayerActivity
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    findViewById(R.id.btn_button1).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

    Intent intent = new Intent(HomeActivity.this, UnityPlayerActivity.class);

    startActivity(intent);

    }
    });
    但是,当你finish到这个UnityPlayerActivity时你会发现,即使还有Activity显示,应用还是自动关闭了。这个问题是因为在UnityPlayerActivity中的onDestroy方法中调用了mUnityPlayerdestroy方法。
    1
    2
    3
    4
    5
    6
    7
    // Quit Unity
    @Override protected void onDestroy ()
    {
    mUnityPlayer.destroy();
    super.onDestroy();
    }

我们点进mUnityPlayer.destroy()看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void destroy() {
//...省略无用代码
if (this.mProcessKillRequested) {
if (this.m_UnityPlayerLifecycleEvents != null) {
this.m_UnityPlayerLifecycleEvents.onUnityPlayerQuitted();
} else {
this.onUnityPlayerQuitted();
}

Process.killProcess(Process.myPid()); // 结束自己的进程
}

unloadNative();
}

发现在mProcessKillRequested true的时候,会进行一个杀自己进程的操作,而我们一般app都是一个进程,就会导致我们的app被kill掉。
解决办法就是在AndroidManifest配置一下UnityPlayerActivityUnityPlayerActivity以一个新的进程启动。

1
android:process=":e.unitry3d"

Android多进程总结一:生成多进程(android:process属性)

5. Android 自定义Unity显示形式

由于业务的需求决定,混合开发中的Unity不一定为全屏幕显示或者可能需要多个Unity界面,那么就需要继承UnityPlayerActivity进行自定义一个显示Unity的界面。

当我们的业务需求决定了我们需要实现一个UnityPlayerActivity的子类进行扩展功能的时候,需要进行以下步骤:

  1. 禁止UnityPlayerActivity中添加mUnityPlayer
    UnityPlayerActivityonCreate中注释setContentViewrequestFocus代码,因为要在子类中按需加载mUnityPlayer,防止多次设置View,就注释掉父类的相关代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Setup activity layout
    @Override protected void onCreate(Bundle savedInstanceState)
    {
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    super.onCreate(savedInstanceState);

    String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
    getIntent().putExtra("unity", cmdLine);

    mUnityPlayer = new UnityPlayer(this, this);
    //setContentView(mUnityPlayer);
    //mUnityPlayer.requestFocus();
    }

  2. 实现子类,将mUnityPlayer设置给布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class UnityActivity extends UnityPlayerActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.layout_unity);

    FrameLayout frameLayout = findViewById(R.id.framelayout);
    frameLayout.addView(mUnityPlayer);

    mUnityPlayer.requestFocus();
    }

    }

1
2
3
4
5
6
7
8
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();

}

这个地方需要注意两点:1.如果之前导入过插件,这里一定要继承自插件中实现的UnityPlayerActivity子类,否则,插件的方法不会被调用。2. 记得要将实现的Activity配置为新的进程。

如果想启动不同的Unity界面,也不需要实现多个Activity子类,和Unity开发约定下通信规则,确定好发送什么参数启动什么页面,在Activity启动后调用相关的方法,发送约定好的参数即可。

例如:
启动界面:

1
2
3
4
5
6
7
8
9
findViewById(R.id.btn_button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {

Intent intent = new Intent(HomeActivity.this, UnityActivity.class);
intent.putExtra("panelName","LunchPanel");
startActivity(intent);
}
});

UnityActivity

1
2
3
4
5
6
7
8
9
10
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();

String panelName = getIntent().getStringExtra("panelName");

UnityPlayer.UnitySendMessage("UIRoot","openPanel",panelName);//unity方法
}

6. 使用Fragment当做Unity显示的载体

目前我试出来的方案就是将mUnityPlayerFragment将要挂载的Activity中进行创建并进行生命周期的管理。

Activity

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class HomeActivity extends FragmentActivity {
protected UnityPlayer mUnityPlayer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_activity);
mUnityPlayer = new UnityPlayer(this, null);
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().add(R.id.fl,new UnityFragment(mUnityPlayer)).commit();


}


@Override protected void onNewIntent(Intent intent)
{
// To support deep linking, we need to make sure that the client can get access to
// the last sent intent. The clients access this through a JNI api that allows them
// to get the intent set on launch. To update that after launch we have to manually
// replace the intent with the one caught here.
setIntent(intent);
mUnityPlayer.newIntent(intent);
}

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

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

// Resume Unity
@Override protected void onResume()
{
super.onResume();
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); }

}

fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UnityFragment extends Fragment{

private UnityPlayer mUnityPlayer;


public UnityFragment(UnityPlayer unityPlayer) {
mUnityPlayer = unityPlayer;
}


@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

return mUnityPlayer;
}

}

7. Unity与Android之间的通讯

此内容网络上已有较多文章,本文不再叙述。

8. 注意事项

  1. 当Unity与Android同时开发时,每次从Unity导出新的项目覆盖之前的老代码的时候主launcher中的AndroidManifest文件会被重置,导出前务必要备份。

文章可能因为个人能力原因出现错误,忘谅解。希望能够指出。

作者

Hanani

发布于

2021-03-26

更新于

2022-06-16

许可协议