用cocos2d-js制作WearOS表盘
这只是一次实验
众所周知,cocos2d 属于游戏引擎,本身就不适合运行在手表,而直接把他当做表盘当然就更不合适了。这里只是一个突发奇想,借助 cocos 强大的渲染与跨平台特性,看看能否做出一款表盘。
要做到这一步需要解决两个问题:
- 将 cocos 编译到 Android.
- 将容器由 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 文件。
<!--more-->
配置新工程
创建
我们新建一个 Android 工程来制作表盘。注意只勾选 Wear OS 就可以了,选择 Watch Face 模板来简化配置。
自动生成的代码我们不需要,只保留一个最基本的类框架就行。
// 只保留这三行就够了
import android.support.wearable.watchface.CanvasWatchFaceService;
public class MyWatchFace extends CanvasWatchFaceService {
}
复制 cocos 文件
cocos 的文件主要有3部分需要复制:
- 编译好的 so 文件。
- Java 源码与库。
- 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 源码。把 com
和 org
这俩文件夹直接复制到 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
可以快速创建资源文件目录,然后把上述文件复制进去就好了。
这样,我们就复制完了所需的全部 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 的。
最后完成 onCreate
和 runOnGLThread
:
/*声明一些变量*/
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
。修改之后会多出来几个错误。
- 有个函数
public static Activity getActivity()
需要返回这个变量,将其返回值也改为 Context. - 有个函数
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()
就可以啦。
错误!
UiThread
和GLThread
是两个线程,不可混用。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 和源码,相信自己可以搞定哒。