美文网首页
用cocos2d-js制作WearOS表盘

用cocos2d-js制作WearOS表盘

作者: 0晨鹤0 | 来源:发表于2018-10-18 19:37 被阅读0次

    这只是一次实验

    众所周知,cocos2d 属于游戏引擎,本身就不适合运行在手表,而直接把他当做表盘当然就更不合适了。这里只是一个突发奇想,借助 cocos 强大的渲染与跨平台特性,看看能否做出一款表盘。

    要做到这一步需要解决两个问题:

    1. 将 cocos 编译到 Android.
    2. 将容器由 Activity 转到表盘。

    倒弄了一天,仅仅是成功运行起来了,还有许许多多的问题需要解决,例如效率问题、内存释放问题等等。

    当前环境:

    • Win10
    • Cocos2d-js 3.17
    • Android Studio 3.2
    • Wear OS 2.x

    完成本项目需要了解 Android 开发知识,最好还了解 WearOS 表盘开发。

    编译到 Android

    其实是这一步就足够再写一篇文章了,有许多隐藏的坑,牵扯到 Android NDK, JNI 等许多知识。不过这并不是本文的重点,简单写一下。

    为了不影响 Cocos2d 自带的 demo,我们要编译出 .so 库文件,然后新建一个工程引用。

    打开 Android Studio,点击 File - Open,选择 cocos 项目根目录下 frameworks/runtime-src/proj.android 即可打开自带的 Android 工程。注意,默认情况下只编译 armeabi-v7a 的库,这个只能用于手机而不能用于模拟器。为了调试方便我们要他把 x86 也给编译了。修改 proj.android/gradle.properties 文件,找到 PROP_APP_ABI 新增一个 x86 格式:

    # List of CPU Archtexture to build that application with
    # Available architextures (armeabi-v7a | arm64-v8a | x86)
    # To build for multiple architexture, use the `:` between them
    # Example - PROP_APP_ABI=armeabi-v7a:arm64-v8a:x86
    PROP_APP_ABI=armeabi-v7a:x86
    

    点击 Build - Rebuild project 就开始编译啦~ 编译很慢,十几分钟吧。

    编译成功后可以在 proj.android/app/build/intermediates/ndkBuild/debug/obj/local 下找到各个平台的 so 文件。与此同时也可以在 intermediates/assets/debug 下找到后边需要的 js 文件。

    配置新工程

    创建

    我们新建一个 Android 工程来制作表盘。注意只勾选 Wear OS 就可以了,选择 Watch Face 模板来简化配置。

    选择平台
    选择模板

    自动生成的代码我们不需要,只保留一个最基本的类框架就行。

    // 只保留这三行就够了
    import android.support.wearable.watchface.CanvasWatchFaceService;
    
    public class MyWatchFace extends CanvasWatchFaceService {
    
    }
    

    复制 cocos 文件

    cocos 的文件主要有3部分需要复制:

    1. 编译好的 so 文件。
    2. Java 源码与库。
    3. js 文件。

    复制 so

    切换到 Project 视图,在 app/src/main/ 创建 jniLibs 文件夹,然后把之前编译好的 so 文件复制过来。

    目录结构

    然后在 manifest 里加上下面代码:

    <application>
        <!--others-->
        <!-- 加入下面代码,用于指明 cocos 的库名 -->
        <meta-data
            android:name="android.app.lib_name"
            android:value="cocos2djs" />
    </application>
    

    复制 java

    在 cocos 工程目录 frameworks/cocos2d-x/cocos/platform/android/java/src 下可以找到 java 源码。把 comorg 这俩文件夹直接复制到 Android 工程的 src/main/java 下。然后在 module 的 build.gradle 里加入下面配置:

    android {
        //...
        defaultConfig {
            //...
        }
        buildTypes {
            //...
        }
        //加入下面的代码,用于指定 aidl 源码目录。aidl 用于进程通信,这里不深究。
        sourceSets.main {
            aidl.srcDir 'src/main'
        }
    }
    

    然后复制 frameworks/cocos2d-x/cocos/platform/android/java/libs 下的文件到 Anddroid 工程的 app/libs 目录下。

    复制 js 文件

    我们知道 cocos2d-js 是 js 与 cpp 的整合。引擎本身是 cpp 编写的,但是游戏逻辑则是 js 编写的。前面的编译仅仅是编译了引擎的 cpp 部分,下面要把真正的控制程序逻辑的 js 文件复制过来,否则打开后会黑屏。

    前面说过,编译 so 之后可以在 intermediates/assets/debug 找到需要的文件。也可以手动在 cocos 工程目录找到,下面是需要复制的文件:

    • res:资源文件
    • src:js 源码
    • main.js:入口文件
    • project.json:工程配置
    • frameworks/cocos2d-x/cocos/scripting/js-bindings/script:引擎 js 源码

    在 Android 工程文件列表里右键,选择 New - Folder - Assets Folder 可以快速创建资源文件目录,然后把上述文件复制进去就好了。

    Assets

    这样,我们就复制完了所需的全部 cocos 文件。准备工作刚刚完成,下面开始敲代码吧~

    基础知识

    为了将 cocos 做成表盘,我们需要大致了解 cocos 在 Android 上的原理以及表盘的工作原理。

    Cocos 原理

    为了弄清 Cocos 在 Android 上的工作,我们可以参考自带的 proj.android 工程。

    打开源码发现只有一个 AppActivity,继承了 Cocos2dxActivity,跟踪进去看看,发现实现了 Cocos2dxHelperListener 接口。

    重点关注 onCreate() 函数。首先调用 onLoadNativeLibraries() 加载了 so 库。接着调用 Cocos2dxHelper.init(this) 初始化了 helper、获取了 GLContext 参数。

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 省略了一些表盘不相关的代码
        onLoadNativeLibraries(); // 加载 so 库
        Cocos2dxHelper.init(this); //初始化helper
        this.mGLContextAttrs = getGLContextAttrs();
        this.init(); // 创建 surface
        //初始化EngineDataManager
        Cocos2dxEngineDataManager.init(this, mGLSurfaceView);
    }
    

    然后看看 init() 函数:

    public void init() {
    
        // 省略了一些创建布局的代码
    
        // 创建一个 SurfaceView
        this.mGLSurfaceView = this.onCreateView();
        // 加入布局
        mFrameLayout.addView(this.mGLSurfaceView);
        // 设置渲染器
        this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
        this.mGLSurfaceView.setCocos2dxEditText(edittext);
        // 设置布局
        setContentView(mFrameLayout);
    }
    

    同样的,onCreateView() 就是创建并设置了一下 SurfaceView.

    至此我们发现,cocos 主要就是创建了一个 SurfaceView 并添加到布局,然后猜想应该是把这个 Surface 传给了 cpp 来进行绘制。也就是说和 Android 的控件体系是无关的,而是采用了一种更加底层的方式渲染。

    表盘原理

    从创建的模板工程可以看出,表盘并不是一个窗口(Activity),而是一个服务(Service),它继承了 CanvasWatchFaceService。然后内部创建了一个 Engine,通过 onDraw() 回调方法拿到 Canvas 并绘制。既然不是窗口,也就意味着 cocos 的 demo 并不能直接套过来,而 canvas 似乎也和 cocos 没啥关系。

    只好继续跟踪进 CanvasWatchFaceService,发现它继承了 WatchFaceService,同样有个 engine。关注最底下的 draw() 方法:

    private void draw(SurfaceHolder holder) {
        this.mDrawRequested = false;
        Canvas canvas = holder.lockCanvas();
        if (canvas != null) {
            try {
                // !这里出现了 Surface
                this.onDraw(canvas, holder.getSurfaceFrame());
            } finally {
                holder.unlockCanvasAndPost(canvas);
    
        }
    }
    

    而它是这样被调用的:Engine.this.draw(Engine.this.getSurfaceHolder()); OK,我们似乎找到了,表盘渲染的背后其实也是一个 Surface,于是目标就很明确了,只需要把这个 Surface 交给 cocos,应该就可以了。

    终于可以写代码了

    准备了那么长时间,相信大家都快不耐烦了吧。不过要是没有之前的准备,下面的工作将会无从下手哦。

    基本整合

    首先自然是让我们的表盘服务直接继承 WatchFaceService. 首先要加载 so 库,可以把 demo 源码直接搬过来:

    @Override
    public void onCreate() {
        super.onCreate();
        onLoadNativeLibraries(); // 加载so
    }
    
    private void onLoadNativeLibraries() {
        try {
            ApplicationInfo ai = getPackageManager().getApplicationInf(getPackageName(), PackageManager.GET_META_DATA);
            Bundle bundle = ai.metaData;
            String libName = bundle.getString("android.app.lib_name");
            System.loadLibrary(libName);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    然后创建一个 Engine 内部类,继承 Engine 并实现 Cocos2dxHelperListener,同时在 onCreateEngine() 里实例化并返回。

    为了能让我们的 Surface 和 cocos 关联起来,再创建一个 Engine 的内部类,继承自 Cocos2dxGLSurfaceView

    public class MySurfaceView extends Cocos2dxGLSurfaceView{
        public MySurfaceView(Context context) {
            super(context);
        }
    
        /**
         * 重写了父类方法,返回 Engine 提供的 Surface.
         */
        @Override
        public SurfaceHolder getHolder() {
            // getSurfaceHolder() 函数是 Engine 自带的
            return getSurfaceHolder();
        }
    
        // 新增函数,非重写。
        public void onDestroy(){
            super.onDetachedFromWindow();
        }
    }
    

    仿照着写个 createView:

    public MySurfaceView createView() {
        MySurfaceView glSurfaceView = new MySurfaceView(MyWatchFace.this);
        if(this.mGLContextAttrs[3] > 0)
            glSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
    
        Cocos2dxActivity.Cocos2dxEGLConfigChooser chooser = new Cocos2dxActivity.Cocos2dxEGLConfigChooser(this.mGLContextAttrs);
    
        glSurfaceView.setEGLConfigChooser(chooser);
        return glSurfaceView;
    }
    

    这里有个问题,Cocos2dxActivity.Cocos2dxEGLConfigChooser 是 private,为了能顺利实例化,我们需要把它改成 public static 的。

    最后完成 onCreaterunOnGLThread

    /*声明一些变量*/
    private int[] mGLContextAttrs;
    private MySurfaceView mSurfaceView;
    private Cocos2dxRenderer mRenderer;
    private int screenHeight;
    private int screenWidth;
    
    @Override
    public void onCreate(SurfaceHolder holder) {
        super.onCreate(holder);
        screenWidth = getResources().getDisplayMetrics().widthPixels;
        screenHeight = getResources().getDisplayMetrics().heightPixels;
        mGLContextAttrs = Cocos2dxActivity.getGLContextAttrs(); // 这个函数也要改成 public
        mSurfaceView = createView();
        mRenderer = new Cocos2dxRenderer();
        mRenderer.setScreenWidthAndHeight(screenWidth, screenHeight);
        mSurfaceView.setCocos2dxRenderer(mRenderer);
        Cocos2dxHelper.init(MyWatchFace.this, this); // 重点关注
        Cocos2dxEngineDataManager.init(MyWatchFace.this, mGlSurfaceView);
    }
    
    @Override
    public void runOnGLThread(Runnable pRunnable) {
        mSurfaceView.queueEvent(pRunnable);
    }
    

    改造 Cocos2dxEngineDataManager

    默认的 Cocos2dxEngineDataManager.init() 只能传入 Activity,我们要把它改造成 init(Context,Cocos2dxHelperListener) 的形式。

    首先注意的 Manager 内部保存了一个 sActivity 的 Activity 变量,经过检查,其大部分用途可以用 Context 代替。所以直接改成 Contenxt。修改之后会多出来几个错误。

    1. 有个函数 public static Activity getActivity() 需要返回这个变量,将其返回值也改为 Context.
    2. 有个函数 public static int getDPI() 用到了 Activity 获取 Windowmanager. 不过看起来这个函数并没有真正使用。为了以防万一,还是改成下面的方式获取。或者直接屏蔽掉。
    WindowManager wm = (WindowManager) sActivity.getSystemService(Context.WINDOW_SERVICE);
    

    此时先试着编译一下,发现还有2个文件产生了错误。

    两个错误文件

    第一个同样是 WindowManager 的问题,按上述方法替换就好。

    至于第二个,我们先给 Cocos2dxHelper 新增一个函数:

    public static Cocos2dxHelperListener getCocos2dxHelperListener(){
        return sCocos2dxHelperListener;
    }
    

    然后把出错的 Cocos2dxHelper.getActivity().runOnUiThread() 替换成 Cocos2dxHelper.getCocos2dxHelperListener().runOnGLThread() 就可以啦。

    错误!
    UiThreadGLThread 是两个线程,不可混用。UI 线程是 Android 的主线程,用于刷新 View,响应操作等。而 GL 线程是 Surface 的刷新线程,只有 GL 线程才拥有 GL 的上下文环境,但不能操作系统原生控件。

    之所以这样写后边可以跑起来是因为没有实际用到这一函数。因为现在已经不研究这个了所以这里也无法提供正确的方案。

    最后别忘写一个重载函数给其他 cocos 类调用:

    public static void init(final Activity activity) {
        init(activity, (Cocos2dxHelperListener) activity);
    }
    

    这样就完成了我们 Engine 的 onCreate()函数。最后再在 Engine 销毁时释放一下资源:

    @Override
    public void onDestroy() {
        super.onDestroy();
        mSurfaceView.onDestroy();
        Cocos2dxHelper.end();
        Cocos2dxHelper.terminateProcess();
    }
    

    运行

    吁,终于完成了(:зゝ∠)

    编译运行装进去,然后切换下表盘看看吧。cocos2d 已经成功把画面渲染出来了。右下角还有帧率,只是因为屏幕原因显示不全。

    表盘

    再说一遍,只是能用,还有许许多多的问题没有解决,包括我们改的一些源码,可能还会有其他副作用。累死啦,以后再研究吧。

    最后,遇到崩溃不要怕,多看 Log 和源码,相信自己可以搞定哒。

    相关文章

      网友评论

          本文标题:用cocos2d-js制作WearOS表盘

          本文链接:https://www.haomeiwen.com/subject/esgszftx.html