1.前提
依旧是需求驱动学习新的库。。。
需求是要做一个拼图的h5,需求如下:
1)共男女两名角色,男性角色为主要角色,女性角色为彩蛋,将两名角色都由上至下切成四等份,分别在15s的倒计时内随机掉落两名角色的相同身体部位(略惊悚?),点击屏幕则停止当前部件掉落,开始掉落下一部分的身体部件。若在倒计时之内部件掉出屏幕,则重复刷新当前部位的零件向下掉落;若倒计时结束则游戏失败。当身体四部分都拼好则游戏结束并计算得分,根据得分发放不同档位的游戏礼包。
2)分数的计算分为两部分,一部分是重合度,一部分是完成度。重合度,顾名思义就是身体零件与标准坐标的偏差值。完成度就是指男性角色拼合的完整度。
3)游戏结束后跳转至结果页,结果页根据不同的档位显示不同的文案,且根据拼出角色的性别做出不同的页面表现。同时生成相应的图片以便用户保存分享。
2.头脑风暴时间
最初的想法是使用canvas原生的api进行开发,后续的构想下来发现单纯使用原生api对于开发非常的累赘且不好控制,所以在隔壁前端大佬的怂恿下接触到pixi.js,看了一下github上的简易中文文档(点击我跳转)并尝试跟着demo打了一下代码,发现这个库提供的api非常好用,且对于性能的优化也不错(官方自己说的),于是就开始踩坑之路。
因为最后要把拼合的结果加入到生成的分享图片中(分享图片中的底和游戏界面的底不一样,不能直接将游戏界面导出图片,需要先将拼合的角色导出再与分享图片的底进行拼接),所以决定在游戏层添加两个canvas,一个负责绘制背景及一些动效(纯展示),另一个用于绘制身体零件的掉落效果。这样到最后可以直接将负责绘制身体零件的canvas通过pixi提供的api转成base64格式(png),上传到服务器中并拿到相应的url。拿到url后再新建一个canvas用于绘制分享的图片。
3.html结构划分
根据h5功能进行html结构的划分,共分为五部分:loading页,游戏进入页(主视觉渲染),游戏页,结果页和微信与游戏绑定页(用于后续礼包发放)
4.开始踩坑
主角来啦~
1) 首先我们要生成一个canvas并把他插入到页面中:
var app = new Application({
width: 750,
height: 1500,
antialias: true,
transparent: true,
resolution: 1
})
var stage = app.stage;
app.view.classList.add('stage');
$('.game-page').append(app.view);
现在添加的几个参数分别是:width:画布宽度、height:画布高度、antialias:抗锯齿、transparent:透明和resolution(视网膜屏幕配置选项)
application.stage中保存了舞台的各种信息,后续的各种部件都是添加到舞台内进行渲染。
这时候就可以在页面中看到一个black square拉。好的,项目结束可以回家了(并没有。)
2)接下来我们要将图片加载到纹理缓存中
PIXI.loader.add(imgArr).load(setup);
PIXI提供了一个loader,可以用于加载各种图片资源,loader.add()方法中可以接受一个字符串或一个数组,可以将资源地址放在一个数组中直接放在loader.add()中进行加载,loader会在图片加载完全后调用setup方法,我们就可以在setup方法中尽情的开始操作拉~
3) 先了解下一些生成精灵的方法
在pixi中,生成一个精灵非常的简单,例:
var sprite = new PIXI.Sprite(
PIXI.loader.resources["images/anyImage.png"].texture
);
但是!这是一整张图片作为一个精灵材质的方法,为了减少请求,我们通常会把一些小的图片进行合并生成雪碧图,那我们如何从雪碧图中读取一小部分作为精灵的材质呢!先贴代码:
function setup() {
//先从缓存中拿到材质
let texture = TextureCache["images/tileset.png"];
//新建一个Rectangle(矩形),四个参数分别是x坐标, y坐标,width和height
let rectangle = new Rectangle(192, 128, 64, 64);
//把这个矩形赋值给材质的frame
texture.frame = rectangle;
//这样你就能成功的在雪碧图中拿到你想要的部分了
let rocket = new Sprite(texture);
}
掌握勒这个方法以后我就兴致冲冲,一下子就根据坐标和宽高创建了10个不同rectangle,依次赋值给texture.frame再生成精灵。
结果刷新页面后,发现居然十个精灵都是一样的,当时我的内心是绝望的。后来猜测可能是因为引用的是同一缓存导致的(云里雾里@_@)后来想起js中的深度拷贝方法,误打误撞发现pixi也提供了texture.clone()方法,尝试在每次赋值给texture.frame赋值之前先把texture拷贝一边,居然成功了!于是把他封装成了一个方法,接受五个参数分别为材质,x坐标,y坐标和宽高,方法的返回直接是一个sprite对象,代码如下:
function produceSprite(texture, x, y, width, height) {
var itemSprite = texture.clone();
var rectangle = new PIXI.Rectangle(x, y, width, height);
itemSprite.frame = rectangle;
var block = new Sprite(itemSprite);
return block;
}
4)将散落在人间的身体部件添加到舞台内
在生成角色部件的同时先将他们的标准坐标赋值给finalY,这里的chara为1时表示是男性角色,为2时表示是彩蛋角色,代码如下:
function addCharacterBlocks() {
var posArr = [
{chara: 1, width: 512, height: 290, x: 0 , y: 0},
{chara: 2, width: 644, height: 290, x: 520, y: 0},
{chara: 1, width: 512, height: 230, x: 0 , y: 290},
{chara: 2, width: 644, height: 230, x: 520, y: 290},
{chara: 1, width: 512, height: 168, x: 0 , y: 520},
{chara: 2, width: 644, height: 168, x: 520, y: 520},
{chara: 1, width: 512, height: 227, x: 0 , y: 688},
{chara: 2, width: 644, height: 227, x: 520, y: 688}
]
var arrIndex = 0;
posArr.forEach(function(pos, index) {
var block = produceSprite(TextureCache['http://static.sdg-china.com/moon/pic/20181120_Pingtu/character.png'], pos.x, pos.y, pos.width, pos.height);
block.finalY = pos.y + 86;
block.finalTop = pos.y + 86 - pos.height;
block.finalBottom = pos.y + 86 + pos.height * 2;
if (index % 2) {
block.x = 68;
} else {
block.x = 134;
}
block.y = -pos.height;
block.vy = 0;
block.chara = pos.chara;
if (!(index % 2) && index !== 0) {
arrIndex++;
}
blocks[arrIndex].push(block);
})
blocks.forEach(function(block) {
block.forEach(function(b) {
charaContainer.addChild(b)
})
})
}
5 ) 大家一起动起来~
pixi中提供了ticker对象,用于根据显示机器的刷新率进行渲染,只需在sprite对象添加两个速度属性:vx,vy来控制他们的运动速度:vx被用来设置精灵在x轴(水平)的速度和方向。vy被用来设置精灵在y轴(垂直)的速度和方向。后续如果要控制精灵的运动与方向,只需要对他们的vx或vy属性进行操作就好。
app.ticker.add(function(delta) {
gameLoop(delta)
});
function gameLoop(delta){
cat.vx = 1;
cat.vy = 1;
//Apply the velocity values to the cat's
//position to make it move
cat.x += cat.vx;
cat.y += cat.vy;
}
6)点击事件
pixi似乎是屏蔽了原生的事件,不论是将事件添加到body上还是添加到canvas上,统统没有反应,原来是他已经提供了一套事件:
eventBlock.on('tap', function() {
getRandomBlockMove();
});
这里的getRandomBlockMove()是一个自定义的方法,当用户点击后将当前在运动的精灵对象添加到最终结果的数组中,以便后续分数的计算。同时使下一部分的身体部件开始运动。
7)分数计算
因为给之前生成的每个身体部件的Sprite都添加了finalY属性,所以这一步只需要把最后结果数组中的四部分最终的坐标和他们标准的坐标进行减法运算并取绝对值,就能拿到身体部件与标准坐标的偏差从而计算得分
8)遮罩
pixi也提供了遮罩的属性,可以用Graphics或Sprite作为container或Sprite的遮罩,但是Sprite作为遮罩只有当当前的渲染器是webgl才能够使用。接下来是创建一个graphics并将他作为遮罩的代码示例~
var headMask = new PIXI.Graphics();
headMask.clear();
headMask.beginFill(0xffffff);
headMask.drawCircle(0, 0, 33);
headMask.endFill();
//head是一个Sprite
head.mask = headMask;
9)生成图片
pixi也提供了十分好用的canvas转base64的方法:
//app为生成base64图片的应用,这个方法接受一个参数,就是需要被截图的容器;
app.renderer.plugins.extract.base64(charaContainer);
到这里,最初的设计问题就出来了,最初的构想是一个负责背景部分,一个负责人物运动。可是这样需要同时渲染两个canvas,对性能的要求会相对较高,所以后来就把两个app合并成一个了:将人物部件添加到一个container内,导出图片时直接将包裹人物物件的container作为参数,这样就能导出透明底的拼合人物了。
5.总结
项目结束后对pixi.js也算有了一个初步的认识,同时也领略到了canvas的魅力,中间也是踩了很多的坑(为什么还不出完整版的中文文档!!全程都在啃生肉文档),好在有隔壁前端大佬的帮助,有很多想不通的地方都是一起思考出解决方案。一切终于都结束了=。= 我的灵魂已经飞走了。。。最后贴一个项目的链接(点击我跳转)。
PS:感觉写的不够详细,后续还会再改进,感谢进来看我分享的各位大佬