微信小游戏和白鹭引擎开发实践
前言
文章按照作者调研和开发顺序初步介绍和理解了微信小游戏和白鹭引擎,并产出了基于白鹭引擎的应用初始化程序egret-wechat-start。 以下是正文——
微信小游戏
官方文档
wx.createCanvas() 创建画布,
getContext获取对象后,剩下的就是对原生canvas接口的操作了。 理解到这一点之后,我们就会发现小游戏仅仅是封装了下创建画布的接口,剩下的就是用户需要在画布里用原生canvas绘制了,并没有提供其他方便开发的功能。到此我们再看看微信开发者工具创建小游戏项目时,初始化的一个飞机游戏的demo。
是如上图的一个很简单的游戏,说下这个游戏的大致实现逻辑:
canvas.addEventListener('click', (event)=>{
获取event对象x,y
获取 buttonA:x,y,width,height
判断是否点击
获取 buttonB:x,y,width,height
判断是否点击
})
一个弹窗上面的按钮点击,反而把弹框下面的按钮也点击到了,这不符合预期,那要解决这个问题,我们还需要一个层级管理器,根据层级判断谁应该触发,谁不应该触发。 目前就事件处理我们需要实现两个基础功能,事件监听池和元素对象层级管理器,因为事件只能绑定在canvas上,canvas事件触发以后,需要一个事件监听池来遍历监听池里的元素对象并判断谁被触发了(监听池也会随时增减监听对象),监听池获取的依然是一个对象集,层级管理器判断出对象集里最上层的元素进行触发。 想想功能好像越来越复杂了。 目前还没考虑完善,不仅仅是事件处理问题,还可能会有其它大大小小的问题。 用canvas原生开发,工作量可能会非常大。 所以这样看来,自己把这些实现了是不科学的,需要使用三方引擎开发才行。 因为两年前用过白鹭引擎,所以就事件监听和层级管理这个事情,我知道白鹭引擎已经实现了,除开事件,图形绘制,动画等等印象中白鹭都提供了,如果用引擎开发小游戏实现成本被大大降低。
白鹭引擎
- Egret Engine2D
- Texture Merger
- Egret 扩展库
- Egret Wing
Egret Engine2D
开发中主要的核心api
Texture Merger
Texture Merger 可将零散纹理拼合为整图,同时也可以解析SWF、GIF动画,制作Egret位图文本,导出可供Egret使用的配置文件。 我主要使用其中的精灵图功能,把图片集合到一张图上,并且会同时导出一个json的精灵图的在图片中的位置等配置信息
Egret 扩展库
扩展库在核心引擎功能之上提供了更高级的api,扩展库在引擎配置文件里配置好以后,会直接把方法和对象载入到egret全局对象中,目前我主要使用的扩展库有:
- RES: 资源管理库
- EUI: EUI是一套基于Egret核心显示列表的UI扩展库,它封装了大量的常用UI组件,能够满足大部分的交互界面需求,即使更加复杂的组件需求,您也可以基于EUI已有组件进行组合或扩展,从而快速实现需求。
- Game:这个库好像没有什么专门的定义,我主要使用了:ScrollView 滚动视图。 来处理需要滚动的页面
- Tween: 缓动动画库,类似于GreenSock库
Egret Wing
egret launcher
开始egret开发
你可以快速浏览一遍官方教程,以便更好对下文有所理解,http://developer.egret.com/cn/github/egret-docs/Engine2D/getStarted/helloWorld/index.html 。 文章不是教程所以会省略掉那些白鹭官网里的教程。 现在我们使用egret launcher创建一个初始化项目,初始化后的文件结构如下图,我展开了resource和src文件夹,因为我们需要操作的主要是这两个文件夹,resource文件夹主要是存放静态资源,我们的代码都在src里,白鹭使用的是typescript。
在wing工具里,我们可以马上开启调试,就可以在浏览器或者它自带的容器里预览效果。 main.ts是启动文件,main中首先使用await对resource中定义好的图片资源进行了预加载,所以预览开始后会出现loading效果,loading的绘制是写在src中LoadingUI.ts,图片加载完成以后,main里直接创建了下图2的页面,并且添加了一个按钮,点击后会出现一个弹窗。 效果如下图。
至此,初始化demo已经告诉了我们如何绘制图像和绑定事件了,如下图,我只截取了click按钮的代码,图像绘制首先需要创建一个相应的egret或者eui对象,比如eui.Button、egret.TextField、egret.Bitmap等等,然后给对象设置相应属性,比如label、x y坐标,width, height等。 再使用main的addChild载入到画布中(下面的this就是main对象,main继承于eui.UILayer)。 demo中的代码在载入loading的时候,使用了this.stage.addChild,直接addChild或者使用stage.addChild都可以载入到画布中。 白鹭封装的addEventListener方法和原生js的监听方法是一样的使用方法。
demo的代码说到这里总结一下,我们在main入口对象中可以使用addChild载入一个视图对象到画布中,比如文本,按钮等。 我们也可以在main里addChild一个视图容器A,视图容器A也可以添加文本按钮等,那我们在视图容器A中再次addChild视图容器B,那么这样就形成了层级嵌套main->A->B,如果想象成dom元素就是div.main->div.A->div.B的关系,我们用代码来对比一下:
class Main extends eui.UILayer { protected createChildren(): void { let A = new egret.DisplayObjectContainer(); this.addChild(A); let textA = new egret.TextField(); textA.text = 'text A Description'; A.addChild(textA); let B = new egret.DisplayObjectContainer(); A.addChild(B); let buttonB = new eui.Button(); buttonB.label = 'button B'; B.addChild(buttonB); } }
对应
<div class="main"> <div class="A"> <span>text A Description</span> <div class="B"> <button value="button B"></button> </div> </div> </div>
根据以上代码的理解和我们要做的需求(实现一个回合制游戏,这个游戏也有很多页面,首页就包含很多按钮和可能出现的弹窗,也有各种列表页,还有最关键的战斗页面)。 我在main里写一个initElement方法,创建基层容器,代码如下图,addChild默认根据先后顺序确定上下层关系,先载入的在下层。 首先最下层创建了一个背景层,接着是ScrollView和baseContent,页面容器会载入到他们之中,如果页面需要滚动会把页面视图对象载入到SV中,不需要滚动会载入到baseContent中,Layer和loading在更上层的位置。
基层容器准备好以后,我们可以创建一个首页页面。 我会创建3个文件:base.ts,Index_ui.ts,Index.ts。 Index继承Index_ui,Index_ui继承base。 所有的_ui都会继承base,base会定义通用方法和属性。 因为一个页面到最后可能代码量会比较大,甚至比较乱,所以才把一个页面拆分成page和page_ui,_ui里写视图相关代码,page里调用_ui的方法、处理请求和编写逻辑,达到视图和逻辑分离的效果。 当首页写好以后,需要创建一个简易路由,用路由提供的方法把Index添加到SV容器中。 我把路由直接写到了main中,changePage就是页面切换的方法,代码大致如下:
通过remove和add视图容器达到了切换页面的效果。 下面说说编写_ui页面的规则,下面是Index_ui的部分代码,el_layout提前把页面元素的布局信息提前定义并统一管理。 把Index逻辑页面需要操作的元素引用到$el对象里方便调用和操作。 把数据信息统一放在$data中。 创建页面视图元素之前,需要把第一个元素的y坐标传给 $firstEleY 这是为了后面pageContentCenter方法能获取到准确的页面内容高度,pageContentCenter要执行在所有页面元素创建完成之后,pageContentCenter会根据当前页面的高度再匹配当前设备的高度进行垂直居中。
class Index_ui extends Base {
public el_layout = {
indexbg: {x:0, y:0, w:750, h:1665},
gold: {x:300, y:100, w:300, h:39}
};
public constructor() {
super();
this.RES_index = RES.getRes('index');
this.RES_common = RES.getRes('common');
}
public RES_index;
public RES_common;
public $el = {
gold: Object(egret.TextField)
}
public $data = {
gold: '0'
}
public async createView() {
//背景
let RES_bg = new egret.Bitmap( RES.getRes('indexbg') );
$util.setLayout(RES_bg, this.el_layout['indexbg']);
RES_bg.fillMode = egret.BitmapFillMode.REPEAT;
this.$main.PageBg.addChild(RES_bg);
//顶部元素必传值
this.$firstEleY = this.el_layout.gold.y;
this.pageContentCenter(true);//根据内容计算处理居中
}
}
一个简易的开发封装的核心代码已经搭建好了,而后我们还需要封装一些其它工具类,如下图:配置文件($config)、封装拦截器($api)、滤镜($filter)、工具函数($util)、微信api封装(Wx)。 Platform.ts是白鹭自动生成的文件,根据它的规则自己写了一个Wx.ts文件,由于不同平台的接口形式各有不同,白鹭推荐开发者通过这种方式封装平台逻辑,以保证整体结构的稳定,白鹭推荐开发者将所有接口封装为基于 Promise 的异步形式。
和src同级的还有一个texture文件夹,里面是TextureMeger使用精灵图的相关文件,放在仓库里是方便后期管理。
简易的初始化demo,我已经更新到github上https://github.com/zimv/egret-wechat-start。 egret-resource是源码,egret-resource_wxgame是白鹭打包后的文件夹,它在开发者工具里运行。 egret-resource_wxgame应该在ignore里忽略,这里没有忽略是方便下载源码的朋友直接在开发者工具里运行demo。 当前程序使用白鹭引擎版本5.2.5。
demo里随便写了几个页面,看下效果:
坑
还有踩过很多坑,下面记录一下:
- 在公众号后台把设置里的服务类设置成游戏类,输入appId后会自动打开开发者工具游戏开发的界面
- 小游戏自定义字体微信支持程度差
- 部分功能和api需要注册的小程序才能使用,比如转发功能,目前注册了一个个人小游戏用于前期开发
- 使用wing工具编辑代码,编译调试,编译后的代码会存放在bin-debug文件夹里,我用的mac,项目菜单里有三个选项编译、调试和清理。我新增了一个xx文件,却在调试的时候一直报错,检查浏览器source里也没有新增的文件,bin-debug也没有,弄了很久,一直以为是自己代码写错了,最后意识到可能是编译器有问题,这个时候我点击了清理按钮,新增的文件就在bin-debug里出现了。应该是个bug,要多注意检查bin-debug里的文件是否有更新
- RES.getResByUrl是网络异步加载,需要提前addChild保证层级正常,请求完成再修改对象的texture属性,也可以通过addChildAt方法指定层级。
- TextField 字体size小于10会影响布局,文本是否换行取决于设置的元素高度
- webgl模式无法加载网络url图片
- scrollView有addChild方法,但是方法里的代码是直接抛错,表示不能用这个接口。它的子元素绑定touchStart move等事件会失效,所以目前又增加里一个baseContent,根据需求切换父容器
- measuredHeight这个测量接口只会测量最上面元素和最下面元素的实际高度,所以第一个元素如果y值大于0要注意配置$firstEleY
- 所有图片用工具压缩,会减少上传代码的大小和提升资源加载速度
结
当这一切都准备好以后,剩下的就是体力活啦,当然还有游戏最重要的核心玩法实现、动画和交互效果,这些可能是一个游戏实现难度最大的部分。仓库地址:https://github.com/zimv/egret-wechat-start 。