众所周知,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 和源码,相信自己可以搞定哒。

Last modification:October 18, 2018