人的记忆是有极限的,所以写博客记录一下。。。
最近开发的模拟经营类微信小游戏项目,同屏显示的精灵数太多,使用cocos creator开发性能有点差,所以不得不用Laya。
特地测试了一下:
cocos creator同屏(其实也不算全部同屏,算是大部分,因为引擎2.0版本以上已经去除了culling)数量在400+左右,其中400个精灵不停的切换父节点(因为3d游戏做成2d的需要改层级遮挡关系)在微信上运行IphoneX的FPS已经掉到30帧左右了,黑鲨1是60帧,IphoneX的FPS比黑鲨还低可能是ios处理js本来就比较慢的原因。
逻辑代码全部不变,全部转成了Laya之后,同屏700个精灵在微信上运行IphoneX的FPS是60帧,黑鲨也是60帧,再多点就没测试了,因为已经满足需求了。
所以就觉定用Laya了,使用的版本是2.1.1.1,在这之前已经使用过Laya开发了一款3D的小游戏,由于2D的UI界面比较少,所以踩的坑不多。这次的游戏是2D的,结果踩了一连串的坑。。。。。
首先吐槽一下可视化的UI编辑器,选中精灵按键盘的方向键是微调位置,但是我选中了层级窗口的节点,想按方向键上下切换节点,左右展开节点,但是每按一次方向键节点的位置就微调了1,这应该算是BUG吧,不知道为什么这么久还不修。。。
接下来是坑了:
1.很多情况下节点不填宽高默认auto是会出BUG的,比如坐标转换会出错,使用Dialog(这个组件比较特别,就算设置了宽高也会出现BUG,不知道是什么操作)如果创建了之后直接弹出(new Dialog().popup()),由于不能及时获取到宽高,弹窗位置不会居中,第二次弹出正常。要解决的话需要在构造函数设置一下宽高,有些弹窗还必须要再onAwake再设置一下宽高才能解决。
2.由于是从creator转过来的,所以锚点用习惯了anchor,然后坐标转换一直不对,悲剧了,折腾了一段时间之后断点进去看了函数实现发现Laya是用pivot的。。。。最后自己复制代码出来改装了一下:
/**局部坐标转换成全局坐标 应用锚点
* @param {pos} 需要转换的局部坐标
* @param {node} 局部坐标所在的容器
* @param {toGlobalNode} 需要转换到的上级容器 (默认舞台)
*/
export function localToGlobalAR(pos:Point,node:Laya.Sprite,toGlobalNode?:Laya.Sprite){
//坐标转换
let point = new Point(pos.x,pos.y);
toGlobalNode=toGlobalNode || Laya.stage;
while (node && !node.destroyed){
if (node==toGlobalNode)break ;
//锚点偏移计算(优先anchor 其次pivot)
if(node["anchorX"])node.pivotX = node["anchorX"] * node.width;
if(node["anchorY"])node.pivotY = node["anchorY"] * node.height;
point=node.toParentPoint(point);
node=node.parent as Laya.Sprite;
}
return point;
}
/**全局坐标转换成局部坐标 应用锚点
* @param {pos} 需要转换的全局坐标
* @param {node} 需要转换到局部坐标所在的容器
* @param {toGlobalNode} 全局坐标所在的上级容器 (默认舞台)
*/
export function globalToLocalAR(pos:Point,node:Laya.Sprite,toGlobalNode?:Laya.Sprite){
let point=new Point(pos.x,pos.y);
var list=[];
toGlobalNode=toGlobalNode || Laya.stage;
while (node && !node.destroyed){
if (node==toGlobalNode)break ;
list.push(node);
node = node.parent as Laya.Sprite;
};
var i=list.length-1;
while (i >=0){
node=list[i];
//锚点偏移计算(优先anchor 其次pivot)
if(node["anchorX"])node.pivotX = node["anchorX"] * node.width;
if(node["anchorY"])node.pivotY = node["anchorY"] * node.height;
point=node.fromParentPoint(point);
i--;
}
return point;
}
3.接上面,容器的scale为0时 转换出来的坐标会是NaN
4.导入bmfont后必须要重启项目才能在UI编辑器中预览,而且在使用这个UI界面之前必须要先加载bmfont。
5.bmfont是不能直接设置fontSize的,需要在加载完成后加上bmFont.autoScaleSize = true。示例如下
let mBitmapFont = new Laya.BitmapFont();
这里不需要扩展名,外部保证fnt与png文件同名
mBitmapFont.loadFont(url, Laya.Handler.create(this, function (font: Laya.BitmapFont) {
if (font) {
var name = url.match(/\/(.*?).fnt/)[1];
font.autoScaleSize = true;
Laya.Text.registerBitmapFont(name, font);
}
cp.runWith(font);
}, [mBitmapFont]));
6.bmfont在UI编辑器中设置fontSize是无效的,我才发现原来Laya在UI编辑器使用bmfont的正确姿势是直接设置scale。。。
7.bmfont在微信小游戏上运行会报错,必须要把domparserinone.js库勾上。
8.把节点放到Panel下实现滚动效果,节点的parent并不是Panel,而是被嵌套了一层Box,获取Panel需要.parent.parent。
9.使用了scrollRect时候子节点位置会根据scrollRect的x,y来,坐标转换又来坑了。。
10.3D的Animator重新播放要填参数play(name,layer,0),normalizedTime填0;
11.一个Tween实例调用了clear之后,再调用一次clear会清掉别的Tween(这是巨坑,莫名其妙的)。
12.使用了布局需要注意HBox会根据x坐标排序,VBox会根据y坐标排序。(cocos是根据节点顺序zIndex排序)。
13.最后是粒子,粒子实现原理是创建了一个网格 所有粒子都是渲染到这个网格上的,粒子运动的运算是在shader上,默认发送出来的粒子是不会根据节点移动(拖尾效果),而且不能清空已经被激活的粒子(源码可能是我没看仔细,找不到)只能等待生命周期结束(使用对象池要注意的问题,避免从对象池取出来后还残留着上次的生命周期没结束的粒子,销毁需要延迟等待什么周期自行结束后才放到对象池延迟时间是particle.emitter["setting"].duration * 1000),想要清空就直接destroy掉。
想要移除拖尾效果(应该说是移除全局拖尾效果,只留局部拖尾,也就是所有粒子都相对于父节点的坐标系)就重写一下Particle2D的customRender和_particleTemplate对象的addParticleArray加上节点移动时跟原位置的偏移。代码如下:
//粒子静态跟随对象(不拖尾)
class CustomParticleStaticFollow extends Laya.Script {
private staticFollowElement: Laya.Sprite;
private srcX: number;
private srcY: number;
private oldX: number;
private oldY: number;
onUpdate() {
let ele = this.getPointElement();
if (ele.x !== this.oldX ||
ele.y !== this.oldY) {
let par = this.owner as CustomParticle2D;
par.setOffset(ele.x - this.srcX, ele.y - this.srcY);
this.oldX = ele.x;
this.oldY = ele.y;
}
}
setStaticFollowElement(ele: Laya.Sprite) {
this.staticFollowElement = ele;
ele = this.getPointElement();
this.oldX = this.srcX = ele.x;
this.oldY = this.srcY = ele.y;
}
getPointElement(): any {
let rect = this.staticFollowElement.scrollRect;
if (rect) return rect;
else return this.staticFollowElement;
}
}
export class CustomParticle2D extends Laya.Particle2D {
private offset: Laya.Point;
/**静态跟随的对象 */
private staticFollowElement: Laya.Sprite;
constructor(setting: Laya.ParticleSetting) {
super(setting);
this.offset = new Laya.Point(0, 0);
}
/**自定义渲染 */
customRender(context, x, y) {
// this.transform
this["_matrix4"][0] = context._curMat.a;
this["_matrix4"][1] = context._curMat.b;
this["_matrix4"][4] = context._curMat.c;
this["_matrix4"][5] = context._curMat.d;
this["_matrix4"][12] = context._curMat.tx - this.offset.x;
this["_matrix4"][13] = context._curMat.ty - this.offset.y;
var sv = (this["_particleTemplate"]).sv;
sv.u_mmat = this["_matrix4"];
}
/**绑定静态跟随的对象(绑定坐标会改变的节点,跟着偏移,不拖尾)*/
bindStaticFollowElement(ele: Laya.Sprite) {
//绑定静态跟随的对象
let follow = (this.getComponent(CustomParticleStaticFollow) || this.addComponent(CustomParticleStaticFollow)) as CustomParticleStaticFollow;
follow.setStaticFollowElement(ele);
this.setOffset(0, 0);
return this;
}
clearStaticFollowElement() {
let com = this.getComponent(CustomParticleStaticFollow) as CustomParticleStaticFollow;
com && com.destroy();
this.setOffset(0, 0);
}
/**设置偏移 */
setOffset(x: number, y: number) {
this.offset.x = x;
this.offset.y = y;
}
setParticleSetting(setting: Laya.ParticleSetting) {
super.setParticleSetting(setting);
//重定向粒子 生成坐标(加上偏移)
let pt = this["_particleTemplate"];
pt.taddParticleArray = pt.addParticleArray;
pt.addParticleArray = function (position, velocity) {
if (this.posot) {
position[0] += this.posot.x;
position[1] += this.posot.y;
}
this.taddParticleArray(position, velocity);
}
pt.posot = this.offset;
}
}
使用方法是:
let setting = new Laya.ParticleSetting();
setting.textureName = "texture.png";//必须要设置一张图片 不然会报错
let par = new CustomParticle2D(setting);
par.load(url);
par.bindStaticFollowElement(map.owner.parent);