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这个类

// 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
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方法。

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

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

    public void destroy() {
        //...省略无用代码
        if (this.mProcessKillRequested) {
            if (this.m_UnityPlayerLifecycleEvents != null) {
                this.m_UnityPlayerLifecycleEvents.onUnityPlayerQuitted();
            } else {
                this.onUnityPlayerQuitted();
            }

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

        unloadNative();
    }

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

android:process=":e.unitry3d"

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

5. Android 自定义Unity显示形式

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

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

  1. 禁止UnityPlayerActivity中添加mUnityPlayer
    UnityPlayerActivityonCreate中注释setContentViewrequestFocus代码,因为要在子类中按需加载mUnityPlayer,防止多次设置View,就注释掉父类的相关代码。
  // 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();
  }

  1. 实现子类,将mUnityPlayer设置给布局
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();
    }

}

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

        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();

    }

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

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

例如:
启动界面:

    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

    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

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

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文件会被重置,导出前务必要备份。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272