Unity与Android通信的中间件(二)

一 、前言

最近都好忙好忙,感觉很累,曾好几次想继续把关于Unity和Android相互通信的这部分技术分享的博客写完,但是实在是无法提起写博客的精神,所以就一拖再拖,从一月份拖到了五月份,好在当时的思路想法都还在,今天就让这部分博客画个句号吧。

在前一篇文章:Unity与Android通信的中间件(一) 里我说了我写这个插件主要是为了解决两个问题:一个是降低android端代码和unity端代码的耦合度,还有一个是降低android端的接入复杂度。降低android端代码和unity端代码的耦合度,在那篇文章里已经解释清楚了,并且提供了源码,大家直接去看源码就可以了,今天就接着来讲降低android端的接入复杂度的问题。

二 、实现思路

其实降低android端的接入复杂度的问题根本不算问题,这个肯定是仁者见仁智者见智,我在这里只是提供一种我所想到的实现思路,说白了就是封装一些基础代码,让开发的人可以添加少量的代码即可完成Unity和Android相互通信的功能。由于我本身技术的问题,封装的可能就是写laji,希望大家轻拍,谢谢。

2.1 解决android端的接入复杂度高的问题

Unity最终提供给Android端的其实就是个View,所谓降低android端的接入复杂度,就是让Android端可以装载View的一些模块可以简单、快速的实现显示Unity端视图的功能,比如ViewGroup、Dialog、Activity、Fragment,等等。基于以上考虑,所以我只是封装了Activity、Fragment的基类,至于ViewGroup、Dialog,完全可以在Fragment和Activity里显示,我就不做过多示例了。

2.1.1 Fragment和Activity都需要实现的接口——IBaseView

/**
 * Description:Basic interface of all {@link Activity}
 * or
 * {@link Fragment}
 * or
 * {@link android.app.Fragment}
 * <p>
 * Creator:yankebin
 * CreatedAt:2018/12/18
 */
public interface IBaseView {

    /**
     * Return the layout resource
     *
     * @return Layout Resource
     */
    @LayoutRes
    int contentViewLayoutId();

    /**
     * Call after {@link Activity#onCreate(Bundle)}
     * or
     * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}
     * or
     * {@link android.app.Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}
     *
     * @param params
     * @param contentView
     */
    void onViewCreated(@NonNull Bundle params, @NonNull View contentView);

    /**
     * Call after
     * {@link Activity#onDestroy()} (Bundle)}
     * or
     * {@link Fragment#onDestroyView()}
     * or
     * {@link android.app.Fragment#onDestroyView()}
     */
    void onVieDestroyed();
}

这个类很简单的,只是统一下Activity、Fragment、v4包的Fragment的一些抽象方法,方便封装BaseActivity、BaseFragment、BaseFragmentV4。比如contentViewLayoutId方法,就是让开发者可以直接返回布局的id,而不用自己去写加载布局的代码。

2.1.2 显示Unity端视图的模块的基类——BaseActivity、BaseFragment、BaseFragmentV4

/**
 * Description:Activity基类
 * Creator:yankebin
 * CreatedAt:2018/12/18
 */
public abstract class BaseActivity extends AppCompatActivity implements IBaseView {
    protected View mainContentView;
    protected Unbinder unbinder;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mainContentView =  getLayoutInflater().inflate(contentViewLayoutId(),
                (ViewGroup) getWindow().getDecorView(), false);
        setContentView(mainContentView);
        unbinder = ButterKnife.bind(this, mainContentView);
        Bundle bundle = getIntent().getExtras();
        if (null == bundle) {
            bundle = new Bundle();
        }
        if (null != savedInstanceState) {
            bundle.putAll(savedInstanceState);
        }
        onViewCreated(bundle, mainContentView);
    }

    @Override
    protected void onDestroy() {
        onVieDestroyed();
        if (null != unbinder) {
            unbinder.unbind();
        }
        mainContentView = null;
        super.onDestroy();
    }
}

/**
 * Description:app包下Fragment作为基类封装
 * Creator:yankebin
 * CreatedAt:2018/12/18
 */
@Deprecated
public abstract class BaseFragment extends Fragment implements IBaseView {
    protected Unbinder unbinder;
    protected View mainContentView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        if (null == mainContentView) {
            mainContentView = inflater.inflate(contentViewLayoutId(), container, false);
            unbinder = ButterKnife.bind(this, mainContentView);
            Bundle bundle = getArguments();
            if (null == bundle) {
                bundle = new Bundle();
            }
            if (null != savedInstanceState) {
                bundle.putAll(savedInstanceState);
            }
            onViewCreated(bundle, mainContentView);
        } else {
            unbinder = ButterKnife.bind(this, mainContentView);
        }
        return mainContentView;
    }

    @Override
    public void onDetach() {
        mainContentView = null;
        super.onDetach();
    }

    @Override
    public void onDestroyView() {
        onVieDestroyed();
        if (null != unbinder) {
            unbinder.unbind();
        }
        if (mainContentView != null && mainContentView.getParent() != null) {
            ((ViewGroup) mainContentView.getParent()).removeView(mainContentView);
        }
        super.onDestroyView();
    }
}

/**
 * Description:V4包下Fragment作为基类封装
 * Creator:yankebin
 * CreatedAt:2018/12/18
 */
public abstract class BaseFragmentV4 extends Fragment implements IBaseView {
    protected Unbinder unbinder;
    protected View mainContentView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        if (null == mainContentView) {
            mainContentView = inflater.inflate(contentViewLayoutId(), container, false);
            unbinder = ButterKnife.bind(this, mainContentView);
            Bundle bundle = getArguments();
            if (null == bundle) {
                bundle = new Bundle();
            }
            if (null != savedInstanceState) {
                bundle.putAll(savedInstanceState);
            }
            onViewCreated(bundle, mainContentView);
        } else {
            unbinder = ButterKnife.bind(this, mainContentView);
        }
        return mainContentView;
    }

    @Override
    public void onDetach() {
        mainContentView = null;
        super.onDetach();
    }

    @Override
    public void onDestroyView() {
        onVieDestroyed();
        if (null != unbinder) {
            unbinder.unbind();
        }
        if (mainContentView != null && mainContentView.getParent() != null) {
            ((ViewGroup) mainContentView.getParent()).removeView(mainContentView);
        }
        super.onDestroyView();
    }
}

这三个类主要是简化了开发者加载布局相关的代码,以及简化生命周期回调的方法数量,让开发者只关心该关注的生命周期回调。

2.1.3 显示Unity端视图的模块的基类进一步封装——UnityPlayerActivity、UnityPlayerFragment、UnityPlayerFragmentV4

UnityPlayerActivity:

/**
 * Desription:Base Unity3D Player Activity.
 * <BR/>
 * If you want to implement Fragment to load the Unity scene, then the Fragment needs to refer to {@link UnityPlayerFragment} and {@link UnityPlayerFragmentV4}
 *    To implement, then overload the {@link #generateIOnUnity3DCallDelegate(UnityPlayer, Bundle)} method to return the conforming Fragment.
 * <BR/>
 * Creator:yankebin
 * <BR/>
 * CreatedAt:2018/12/1
 */
public abstract class UnityPlayerActivity extends BaseActivity implements IGetUnity3DCall, IOnUnity3DCall, IUnityPlayerContainer {
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code
    protected IOnUnity3DCall mOnUnity3DCallDelegate;

    @Override
    @CallSuper
    public void onViewCreated(@NonNull Bundle params, @NonNull View contentView) {
        initUnityPlayer(params);
    }

    /**
     * Initialize Unity3D related
     *
     * @param bundle {@link Bundle}
     */
    protected void initUnityPlayer(@NonNull Bundle bundle) {
        mUnityPlayer = new UnityPlayer(this);
        mOnUnity3DCallDelegate = generateIOnUnity3DCallDelegate(mUnityPlayer, bundle);
        if (null != mOnUnity3DCallDelegate) {
            if (mOnUnity3DCallDelegate instanceof Fragment) {
                getSupportFragmentManager().beginTransaction().replace(unityPlayerContainerId(),
                        (Fragment) mOnUnity3DCallDelegate, ((Fragment) mOnUnity3DCallDelegate)
                                .getClass().getName()).commit();
            } else if (mOnUnity3DCallDelegate instanceof android.app.Fragment) {
                getFragmentManager().beginTransaction().replace(unityPlayerContainerId(),
                        (android.app.Fragment) mOnUnity3DCallDelegate, ((android.app.Fragment) mOnUnity3DCallDelegate)
                                .getClass().getName()).commit();
            } else if (mOnUnity3DCallDelegate instanceof View) {
                final ViewGroup unityContainer = findViewById(unityPlayerContainerId());
                unityContainer.addView((View) mOnUnity3DCallDelegate);
            } else {
                throw new IllegalArgumentException("Not support type : " + mOnUnity3DCallDelegate.toString());
            }
        } else {
            mOnUnity3DCallDelegate = this;
            final ViewGroup unityContainer = findViewById(unityPlayerContainerId());
            unityContainer.addView(mUnityPlayer);
            mUnityPlayer.requestFocus();
        }
    }

    @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);
    }

    // Quit Unity
    @Override
    protected void onDestroy() {
        if (null != mUnityPlayer) {
            mUnityPlayer.quit();
        }
        super.onDestroy();
    }

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

    // Resume Unity
    @Override
    protected void onResume() {
        super.onResume();
        if (null != mUnityPlayer) {
            mUnityPlayer.resume();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (null != mUnityPlayer) {
            mUnityPlayer.start();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (null != mUnityPlayer) {
            mUnityPlayer.stop();
        }
    }

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

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

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

    // Notify Unity of the focus change.
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (null != mUnityPlayer) {
            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) {
            if (null != mUnityPlayer) {
                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) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return super.onKeyUp(keyCode, event);
        }
        if (null != mUnityPlayer) {
            return mUnityPlayer.injectEvent(event);
        }
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return super.onKeyDown(keyCode, event);
        }
        if (null != mUnityPlayer) {
            return mUnityPlayer.injectEvent(event);
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (null != mUnityPlayer) {
            return mUnityPlayer.injectEvent(event);
        }
        return super.onTouchEvent(event);
    }

    /*API12*/
    public boolean onGenericMotionEvent(MotionEvent event) {
        if (null != mUnityPlayer) {
            return mUnityPlayer.injectEvent(event);
        }
        return super.onGenericMotionEvent(event);
    }

    @Nullable
    @Override
    public Context gatContext() {
        return this;
    }

    @NonNull
    @Override
    public IOnUnity3DCall getOnUnity3DCall() {
        //Perhaps this method is called after Unity is created after the activity is created,
        // so there is no problem for the time being.
        return mOnUnity3DCallDelegate;
    }

    @Nullable
    protected IOnUnity3DCall generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,
                                                            @Nullable Bundle bundle) {
        return null;
    }
}

别看这个类有200多行,其实绝大部分代码都是重载,是为了满足Unity端的需求,真正要关注的方法就那么三四个,只有generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,@Nullable Bundle bundle)这个方法需要继承的实现者去关注,这个方法的作用就是生成真正去加载和显示Unity端使视图的模块,不关你是View也好,Fragment也好,只要你实现了IOnUnity3DCall接口和IUnityPlayerContainer接口,你就可以加载和显示Unity端的视图。

默认情况下,继承至UnityPlayerActivity的类就是加载和显示Unity端视图的模块,除非你重写generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,@Nullable Bundle bundle)方法,返回合适的代理,这个可以从initUnityPlayer(@NonNull Bundle bundle) 方法里面直观的看出来。

如果你还想再进一步,实现了IOnUnity3DCall接口和IUnityPlayerContainer接口的代理模块还想让其他模块来显示Unity短的View,那么实现了IOnUnity3DCall接口和IUnityPlayerContainer接口的代理模块就可以在实现IGetUnity3DCall接口,重写generateIOnUnity3DCallDelegate(@NonNull UnityPlayer unityPlayer,@Nullable Bundle bundle)方法,返回合适的代理。当然,可能大多数情况下我们也不需要这么做吧。

基于以上的原理,要让Fragment作为显示Unity短的View的模块的方法就呼之欲出了:

v4包下的Fragment

/**
 * Description:The base Unity3D fragment, the Activity holding this Fragment needs to implement the {@link IGetUnity3DCall} interface and implement {@link IGetUnity3DCall#getOnUnity3DCall()}
 * 方法返回此Fragment的实例
 * Creator:yankebin
 * CreatedAt:2018/12/1
 */
public abstract class UnityPlayerFragmentV4 extends BaseFragmentV4 implements IOnUnity3DCall, IUnityPlayerContainer {
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

    public void setUnityPlayer(@NonNull UnityPlayer mUnityPlayer) {
        this.mUnityPlayer = mUnityPlayer;
    }

    @Override
    @CallSuper
    public void onViewCreated(@NonNull Bundle params, @NonNull View contentView) {
        if (null != mUnityPlayer) {
            final ViewGroup unityContainer = contentView.findViewById(unityPlayerContainerId());
            unityContainer.addView(mUnityPlayer);
            mUnityPlayer.requestFocus();
        }
    }

    @Nullable
    @Override
    public Context gatContext() {
        return getActivity();
    }
}

app包下的Fragment

/**
 * Description:The base Unity3D fragment, the Activity holding this Fragment needs to implement the {@link IGetUnity3DCall} interface and implement {@link IGetUnity3DCall#getOnUnity3DCall()}
 *   Method returns an instance of this Fragment
 * Creator:yankebin
 * CreatedAt:2018/12/1
 */
public abstract class UnityPlayerFragment extends BaseFragment implements IOnUnity3DCall, IUnityPlayerContainer {
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

    public void setUnityPlayer(@NonNull UnityPlayer mUnityPlayer) {
        this.mUnityPlayer = mUnityPlayer;
    }

    @Override
    @CallSuper
    public void onViewCreated(@NonNull Bundle params, @NonNull View contentView) {
        if (null != mUnityPlayer) {
            final ViewGroup unityContainer = contentView.findViewById(unityPlayerContainerId());
            unityContainer.addView(mUnityPlayer);
            mUnityPlayer.requestFocus();
        }
    }

    @Nullable
    @Override
    public Context gatContext() {
        return getActivity();
    }
}

两者的实现完全一致,只是为了让开发者少封装一种Fragment。我想,当你们看到这里的时候,心中对如何让一个ViewGroup或者Dialog显示Unity端的View的方法已经很清晰了吧。

2.2 唠叨的结语

在我的Demo里,让一个Activity加载和控制并且响应Unity做的一个小游戏只需要不到100行代码,看起来是不是已经很简单啦?就像我在开头所说的一样,降低android端的接入复杂度其实是一个仁者见仁智者见智的问题,我所能做到的简化大概就是这个样子,但是我相信这绝对是一个很low的封装,大神们肯定可以用各种各样的设计模式来设计出牛b的架构,所以我只是在这里给出我的一种见解,希望大家轻拍。

还有就是,其实做Unity的难点一直都不在于Android这端,也不在于两端的通信,我做这个项目只适合入门级开发者闲时无事来看一看,扩宽点知识面,了解点新东西,顺便练练自己的文笔,哈哈哈哈哈。

提前透露下吧,后面可能会写几篇关于基于Google chromium内核的Android Chrome浏览器相关的内容,主要涉及源码改造和嵌入ijkplayer实现视频播放的功能(类似腾讯X5内核的多媒体播放功能),希望大家多多支持。

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