一、Phaser介绍
二、整体框架搭建
三、资源加载
四、游戏逻辑
五、完成
六、总结
参考文档

最近用Phaser做了一个全家福拼图h5的项目,这篇文章将会从零开始讲解如何用Phaser实现,最终效果如下:

源码:https://github.com/ZENGzoe/phaser-puzzle.git demo:https://zengzoe.github.io/phaser-puzzle/dist/

一、Phaser介绍

Phaser是一个开源的HTML5游戏框架,支持桌面和移动HTML5游戏,支持Canvas和WebGL渲染。官方文档齐全,上手也比较容易。

Phaser的功能主要还有预加载、物理引擎、图片精灵、群组、动画等。

更多详细内容可以查看Phaser官网,我的学习过程是主要是边看Phaser案例的实现,边看API文档查看用法。

二、整体框架搭建

1.目录结构

目录初始结构如下:

.
├── package.json
├── postcss.config.js           //postcss配置
├── src                         //主要代码目录
│   ├── css
│   ├── img
│   ├── index.html
│   ├── js
│   │   └── index.js            //入口文件
│   ├── json                    //json文件目录
│   ├── lib                     //其他库
│   └── sprite                  //sprite雪碧图合成目录
├── webpack.config.build.js     //webpack生成distw文件配置
└── webpack.config.dev.js       //webpack编译配置
复制代码

项目的构建工具使用的是Webpack, Webpack的配置可以查看源码webapck.config.dev.js,为避免文章篇幅过长,这里将不会详细介绍Webpack的配置过程,Webpck的配置介绍可以查看Webpack的官方文档webpack.github.io/。

2.创建游戏

(1)库引入

index.html引入Phaser官网下载的Phaser库。

<script src="js/phaser.min.js"></script>
复制代码

(2)创建游戏

Phaser中通过Phaser.Game来创建游戏界面,也是游戏的核心。可以通过创建的这个游戏对象,添加更多生动的东西。

Phaser.Game(width, height, renderer, parent, state, transparent, antialias, physicsConfig)有八个参数:

width :游戏界面宽度,默认值为800。
height :游戏界面高度,默认值为600。
renderer :游戏渲染器,默认值为Phaser.AUTO,随机选择其他值:Phaser.WEBGLPhaser.CANVASPhaser.HEADLESS(不进行渲染)。
parent :游戏界面挂载的DOM节点,可以为DOM id,或者标签。
state :游戏state对象,默认值为null,游戏的state对象一般包含方法(preload、create、update、render)。
transparent :是否设置游戏背景为透明,默认值为false。
antialias :是否显示图片抗锯齿。默认值为true。
physicsConfig :游戏物理引擎配置。


//index.js//以750宽度视觉搞为准
//选择是canvas渲染方式
window.customGame = new Phaser.Game(750 , 750 / window.innerWidth * window.innerHeight , Phaser.CANVAS , 'container');复制代码
//index.html
<div id="container"></div>
复制代码

这样就可以在页面上看到我们的Canvas界面。

3.功能划分

在项目中,为了将项目模块化,将加载资源逻辑和游戏逻辑分开,在src/js中新建load.js存放加载资源逻辑,新建play.js存放游戏逻辑。在这里的两个模块以游戏场景的形式存在。

场景(state)在Phaser中是可以更快地获取公共函数,比如camera、cache、input等,表现形式为js自定义对象或者函数存在,只要存在preload、create、update这三个方法中地任意一个,就是一个Phaser场景。

在Phaser场景中,总共有五个方法:initpreloadcreateupdaterender。前三个的执行循序为:init => preload => create。

init :在场景中是最先执行的方法,可以在这里添加场景的初始化。

preload :这个方法在init后触发,如果没有init,则第一个执行,一般在这里进行资源的加载。

create :这个方法在preload后触发,这里可以使用预加载中的资源。

update :这是每一帧都会执行一次的更新方法。

render :这是在每次物件渲染之后都会执行渲染方法。

用户自定义场景可以通过game.state.add方法添加到游戏中,如在项目中,需要将预加载模块和游戏逻辑模块加入到游戏中:

//index.js...
const load = require('./load');
const play = require('./play');customGame.state.add('Load' , load);
customGame.state.add('Play' , play);
复制代码

game.state.add第一个参数为场景命名,第二个参数为场景。

此时我的游戏场景就有Load和Play。游戏中首先要执行的是Load场景,可以通过game.state.start方法来开始执行Load场景。

//index.jscustomGame.state.start('Load');
复制代码

三、资源加载

//load.jsconst load = {
}
module.exports = load;
复制代码

1.画面初始化

进入页面前,需要进行一些游戏画面的初始化。在这里进行初始化的原因在于在场景里才能使用一些设置的方法。

(1)添加画布背景色

//load.js
customGame.stage.backgroundColor = '#4f382b';复制代码

(2)设置屏幕适配模式

由于不同设备屏幕尺寸不同,需要根据需求设置适合的适配模式。可通过game.scale.scaleMode设置适配模式,适配模式Phaser.ScaleManager有五种:

NO_SCALE :不进行任何缩放

EXACT_FIT :对画面进行拉伸撑满屏幕,比例发生变化,会有缩放变形的情况

SHOW_ALL :在比例不变、缩放不变形的基础上显示所有的内容,通常使用这种模式

RESIZE :适配画面的宽度不算高度,不进行缩放,不变形

USER_SCALE : 根据用户的设置变形

在这里的适配模式选择的是SHOW_ALL

//load.js
customGame.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
复制代码

2.资源预加载

Phaser中通过game.load进行加载资源的预加载,预加载的资源可以为图片、音频、视频、雪碧图等等,这个游戏的资源只有普通图片和雪碧图,其他类型的加载方式可查看官网文档Phaser. Loader。

(1)预加载

普通图片

customGame.load.image('popup' , '../img/sprite.popup.png');
复制代码

普通图片使用的是game.load.image(图片key名,图片地址);

雪碧图

customGame.load.atlasJSONHash('tvshow' , '../img/tvshow.png' , '' , this.tvshowJson);
复制代码

雪碧图的合成工具我使用的是texturepacker,选择的是输出文件模式是Phaser(JSONHash),因此使用的是atlasJSONHash方法。第一个参数为图片key名,第二个参数为资源地址,第三个参数为图片数据文件地址,第四个参数为图片数据json或xml对象。

(2)图片跨域

如果图片资源和画布不是同源的,需要设置图片可跨域。

customGame.load.crossOrigin = 'anonymous';
复制代码

(3)监听加载事件

单个资源加载完成事件

通过onFileComplete方法来监听每个资源加载完的事件,可以用来获取加载进度。

customGame.load.onFileComplete.add(this.loadProgress , this);function loadProgress(progress){//progress为获取的资源进度百分比$('.J_loading .progress').text(`${progress}%`)
}
复制代码

onFileComplete第一个参数为每个资源加载完的事件,第二个参数为指定该事件的上下文。

全部资源加载完成事件

通过onLoadComplete方法来监听全部资源加载完成事件。

customGame.load.onLoadComplete.addOnce(this.loadComplete , this);
复制代码

第一个参数为加载完成事件,第二个参数为指定该事件的上下文。

以上就是预加载的主要实现。

四、游戏逻辑

游戏逻辑大致可以分为四个部分,分别为画面初始化、物件选择面板的创建、元素的编辑、生成长图。

1.画面初始化

初始化的页面主要有墙面、桌子和电视机,主要是创建这三个物件。在此之前,先介绍下用到的两个概念。

sprite :可用于展示绝大部分的可视化的对象。

//创建新图像
//spriteName为预加载资源的唯一key,frame为雪碧图内的frame名,可通过雪碧图的json获得
const newObject = game.add.sprite(0,0,spriteName , frame);复制代码

group :用于包含一系列对象的容器,方便批量操作对象,比如移动、旋转、放大等。

//创建组
const group1 = game.add.group();
//向组内添加新对象newObject
group1.add(newObject);
复制代码

接下来是实例,创建墙面、桌子和电视机:

//play.js
const play = {create : function(){this.createEditPage();  //创建编辑页},createEditPage : function(){this.mobilityGroup = customGame.add.group();    //创建mobilityGroup组,用于存放游戏中的物件this.createWall();      //创建墙this.createTableSofa('sofatable1.png');     //创建沙发this.createTelevision('television1.png');   //创建电视机},createWall : function(){const wall = customGame.add.sprite(0,this.gameHeightHf + 80,'wall1.png');wall.anchor.set(0 , 0.5);  wall.name = 'wall';this.mobilityGroup.add(wall);},createTableSofa : function(spriteName){const tableSofa = customGame.add.sprite(this.gameWidthHf , this.gameHeightHf + 20, 'tableSofa' , spriteName );tableSofa.anchor.set(0.5,0.5);tableSofa.name = 'tableSofa';tableSofa.keyNum = this.keyNum++;   //设置唯一key值this.mobilityGroup.add(tableSofa);},
}
module.exports = play;
复制代码

createTelevision创建同createTableSofa,可通过源码查看。 object.anchor.set(0,0) 设置对象偏移位置的基准点,默认是左上角的位置(0,0),如果是右下角则是(1,1),对象的中间点是(0.5,0.5); object.name = 'name'设置对象的名称,可通过group.getByName(name)从组中获取该对象。

这样就会在页面上创建一个这样的画面:

2.物件选择面板的创建

物件选择面板的主要逻辑可以分为几部分:创建左侧tab和批量创建元素、tab切换、元素滑动和新增元素。

(1)创建左侧tab和批量创建元素

物件选择面板可以分为新年快乐框、tab标题、tab内容、完成按钮四个部分。

...
createEditPage : function(){...this.createEditWrap();          //创建编辑面板
},
createEditWrap : function(){this.editGroup = customGame.add.group();    //editGroup用于存放面板的所有元素this.createNewyear();           //创建新年快乐框this.createEditContent();       //创建tab内容this.createEditTab();           //创建tab标题this.createFinishBtn();         //创建完成按钮
}
...
复制代码

新年快乐框、tab标题、完成按钮的实现可以查看源码,这里主要着重介绍tab内容的实现。

物件选择面板主要有四个tab类:

四个tab类创建方式相同,因此取较为复杂的人物tab类为例介绍实现方法。

这里插播一些新的API:

graphics: 可以用来绘画,比如矩形、圆形、多边形等图形,还可以用来绘画直线、圆弧、曲线等各种基本物体。

//新建图形,第一个参数为x轴位置,第二个参数为y轴位置
const graphicObject = game.add.graphics(0,100);
//画一个黑色的矩形
graphicObject.beginFill(0x000000);  //设置矩形的颜色
graphicObject.drawRect(0,0,100 , 100);   //设置矩形的x,y,width,height
复制代码

编辑框的实现:

//index.js
createEditContent : function(){const maskHeight = this.isIPhoneXX ? (this.gameHeight - 467) : (this.gameHeight - 430);const editContent = customGame.add.graphics(0 , this.gameHeight); //遮罩const mask = customGame.add.graphics(0, maskHeight);    mask.beginFill(0x000000);mask.drawRect(0,0,this.gameWidth , 467); //tab内容背景editContent.beginFill(0xffffff);editContent.drawRect(0,0,this.gameWidth , 350);editContent.mask = mask;this.editGroup.add(editContent);this.editContent = editContent;//创建人物this.createPostContent();
},
复制代码

editContent添加了遮罩是为了在子元素滑动的时候,可以遮住滑出的内容。

人物选择内容框分为左侧tab和右侧内容。左侧tab主要是文字,通过Phaser的text api实现,右侧通过封装的createEditListDetail方法批量生成。

createPostContent : function(){const postContent = customGame.add.group(this.editContent);//左侧背景const leftTab = customGame.add.graphics(0,0);const leftTabGroup = customGame.add.group(leftTab)leftTab.beginFill(0xfff7e0);leftTab.drawRect(0,0,155 , 350);//左侧选中背景const selected = customGame.add.graphics(0,0);selected.beginFill(0xffffff);selected.drawRect(0,0,155,70);selected.name = 'selected';//左侧文字const text = customGame.add.text(155/2 , 23 , "站姿\n坐姿\n瘫姿\n不可描述" , {font : "24px" , fill : "#a55344" , align : "center"});text.lineSpacing = 35;text.anchor.set(0.5 , 0);//左侧文字区域this.createLeftBarSpan(4 ,leftTabGroup );//右侧sprite合集const standSpriteSheet = {number : 12,info : [{ name : 'stand' , spriteSheetName : 'stand' , number : 8 , startNum : 0} , { name : 'stand2' , spriteSheetName : 'stand' , number : 4 , startNum : 8}]};const sitSpriteSheet = { name : 'sit', spriteSheetName : 'sit' , number : 12};const stallSpriteSheet = { name : 'stall' , spriteSheetName : 'stall' , number : 13};const indescribeSpriteSheet = { name : 'indescribe' , spriteSheetName : 'indescribe' , number : 12};// 右侧合集const standGroup = customGame.add.group();const sitGroup = customGame.add.group();const stallGroup = customGame.add.group();const indescribeGroup = customGame.add.group();//右侧生成const stallSpecialSize = {'stall0.png' : 0.35,'stall9.png' : 0.35,'stall12.png' : 0.8};const standSpecialSize = {'stand8.png' : 0.6,'stand9.png' : 0.6,'stand10.png' : 0.6,'stand11.png' : 0.6,}  this.createEditListDetail(standSpriteSheet , 0.37 , standGroup , 105 , 220 , 25 , 20 , 40 , 17 , 160 , 590 , standSpecialSize , 4);this.createEditListDetail(sitSpriteSheet , 0.42 , sitGroup , 105 , 220, 25 , 20, 40 , 17, 160 , 590 , null , 4);this.createEditListDetail(stallSpriteSheet , 0.4 , stallGroup , 170 , 194, 25 , 15, 33 , 30, 160, 590 , stallSpecialSize , 3);this.createEditListDetail(indescribeSpriteSheet , 0.4 , indescribeGroup , 105 , 220, 25 , 20, 40 , 17, 160 , 590 , null , 4);leftTabGroup.addMultiple([selected,text]);postContent.addMultiple([leftTab,sitGroup,standGroup,stallGroup,indescribeGroup])this.postContent = postContent;this.postLeftTab = leftTabGroup;this.sitGroup = sitGroup;this.standGroup = standGroup;this.stallGroup = stallGroup;this.indescribeGroup = indescribeGroup;
},
复制代码

右侧的内容需要考虑的是不同内容的位置、尺寸和显示数量不一定的问题,因此需要抽取出不同的设置作为参数传入:

/*** * @param {*} spriteSheet  spriteSheet雪碧图信息* @param {*} scaleRate    图像显示的缩放* @param {*} group        新建图像存放的组* @param {*} spriteWidth  图像显示区域尺寸的宽度* @param {*} spriteHeight 图像显示区域尺寸的高度* @param {*} verticalW     图像显示区域的横向间距* @param {*} horizentalH   图像显示区域的纵向间距* @param {*} startX        整块图像区域的x偏移量* @param {*} startY        整块图像区域的y偏移量* @param {*} groupleft     左侧tab的宽度* @param {*} groupWidth    整块区域的宽度* @param {*} specialSize   特殊元素的缩放尺寸,由于元素的尺寸缩放标准不一,因此需要设置特殊元素的缩放尺寸* @param {*} verticalNum   列项数量*/
createEditListDetail : function(spriteSheet , scaleRate , group , spriteWidth , spriteHeight , verticalW , horizentalH , startX , startY , groupleft ,groupWidth , specialSize , verticalNum){let { name , spriteSheetName , number } = spriteSheet; const hv = number % verticalNum == 0 ? number : number + (verticalNum-number%verticalNum);const box = customGame.add.graphics(groupleft,0,group);box.beginFill(0xffffff);box.drawRect(0,0,groupWidth,startY + (spriteHeight + horizentalH) * parseInt(hv/verticalNum) + horizentalH);        box.name = 'box';//由于元素的体积过大,部分元素集不能都合并成一张雪碧图,因此需要区分合并成一张和多张都情况if(spriteSheet.info){let i = 0;spriteSheet.info.map((item , index) => {let { name , spriteSheetName , number} = item;for(let j = 0 ; j < number ; j++){createOne(i, name , spriteSheetName);i++;}})}else{for(let i = 0 ;  i < number ; i++ ){createOne(i, name , spriteSheetName)}}function createOne(i , name , spriteSheetName){const x = startX + (spriteWidth+verticalW) * (i%verticalNum) + spriteWidth/2,y = startY + (spriteHeight + horizentalH) * parseInt(i/verticalNum) + spriteHeight/2;  const item = customGame.add.sprite(x , y , name , `${spriteSheetName}${i}.png`);let realScaleRate = scaleRate;if(spriteWidth/item.width >= 1.19){realScaleRate = 1;}if(specialSize && specialSize[`${spriteSheetName}${i}.png`]){realScaleRate = specialSize[`${spriteSheetName}${i}.png`];}item.anchor.set(0.5);item.scale.set(realScaleRate);item.inputEnabled = true;box.addChild(item);}
},
复制代码

到这里就搭好了游戏的全部画面,接下来是tab的切换。

(2)tab切换

tab的切换逻辑是显示指定的内容,隐藏其他内容。通过组的visible属性设置元素的显示和隐藏。

//显示
newObject.visible = true;
//隐藏
newObject.visible = false;
复制代码

除此之外,tab的切换还涉及到元素的点击事件,绑定事件前需要激活元素的inputEnabled属性,在元素的events属性上添加点击事件:

newObject.inputEnabled = true;
newObject.events.onInputDown.add(clickHandler , this);  //第一个参数为事件的回调函数,第二个参数为绑定的上下文
复制代码

以人物选择内容框的左侧tab切换为例

给左侧tab添加点击事件:

createPostContent : function(){...//组内批量添加点击事件,用setAll设置属性,用callAll添加事件leftTabGroup.setAll('inputEnabled' , true);leftTabGroup.callAll('events.onInputDown.add' , 'events.onInputDown' , this.switchPost , this);
},
switchPost : function(e){const item = e.name || '';if(!item) return;let selectedTop = 0;switch(item){case 'text0' :selectedTop = 0;this.standGroup.visible = true;this.sitGroup.visible = false;this.stallGroup.visible = false;this.indescribeGroup.visible = false;break;case 'text1' :selectedTop = 70;this.standGroup.visible = false;this.sitGroup.visible = true;this.stallGroup.visible = false;this.indescribeGroup.visible = false;break;case 'text2' :selectedTop = 140;this.standGroup.visible = false;this.sitGroup.visible = false;this.stallGroup.visible = true;this.indescribeGroup.visible = false;break;case 'text3' :selectedTop = 210;this.standGroup.visible = false;this.sitGroup.visible = false;this.stallGroup.visible = false;this.indescribeGroup.visible = true;}//设置选中框的位置this.postLeftTab.getByName('selected').y = selectedTop;
},
复制代码

(3)元素滑动和新增元素

这里把元素滑动和新增元素放在一起是考虑到组内元素的滑动操作和点击操作的冲突,元素的滑动是通过拖拽实现,如果组内元素添加了点击事件,点击事件优先于父元素的拖拽事件,当手指触摸到子元素时,无法触发拖拽事件。如果忽略子元素的点击事件,则无法捕获子元素的点击事件。

因此给元素添加滑动的逻辑如下:

1.触发滑动的父元素的拖拽功能,并且禁止横向拖拽,允许纵享拖拽。

2.给元素添加物理引擎(因为要给元素一个惯性的速度)。

3.结合onDragStart、onDragStop和onInputUp三个事件的触发判断用户的操作是点击还是滑动,如果是滑动,则三个事件都会触发,并且onInputUp的事件优先于onDragStop,如果是点击,则只会触发InputUp。

4.在onDragUpdate设置边界点,如果用户滑动超过一定边界点则只能滑动到边界点。

5.在onDragStop判断用户滑动的距离和时间计算出手势停止时,给定元素的速度。

6.在onDragStart判断是否有因惯性正在移动的元素,如果有则让该元素停止运动,让移动速度为0。

7.在update里让移动元素的速度减少直至为0停下来模拟惯性。

addScrollHandler : function(target){let isDrag = false; //判断是否滑动的标识let startY , endY , startTime , endTime;const box = target.getByName('box');box.inputEnabled = true;box.input.enableDrag();box.input.allowHorizontalDrag = false;  //禁止横向拖拽box.input.allowVerticalDrag = true;     //允许纵向拖拽box.ignoreChildInput = true;            //忽略子元素事件box.input.dragDistanceThreshold = 10;       //滑动阈值//允许滑动到底部的最高值const maxBoxY = -(box.height - 350);       //给父元素添加物理引擎customGame.physics.arcade.enable(box);box.events.onDragUpdate.add(function(){//滑到顶部,禁止继续往下滑if(box.y > 100){box.y = 100;}else if(box.y < maxBoxY - 100){//滑到底部,禁止继续往上滑box.y = maxBoxY - 100;}endY = arguments[3];endTime = +new Date();} , this);box.events.onDragStart.add(function(){isDrag = true;startY = arguments[3];startTime = +new Date();if(this.currentScrollBox){//如果当前有其他正在滑动的元素,取消滑动this.currentScrollBox.body.velocity.y = 0;this.currentScrollBox = null;}} , this);box.events.onDragStop.add(function(){isDrag = false;//指定可以点击滑动的区域box.hitArea = new Phaser.Rectangle(0,-box.y,box.width,box.height + box.y);//向下滑动到极限,给极限到最值位置动画if(box.y > 0){box.hitArea = new Phaser.Rectangle(0, 0 , box.width , box.height);customGame.add.tween(box).to({ y : 0} , 100 , Phaser.Easing.Linear.None, true , 0 , 0);return;}//向上滑动到极限,给极限到最值位置动画if(box.y < maxBoxY){box.hitArea = new Phaser.Rectangle(0, -maxBoxY , box.width , box.height - maxBoxY);customGame.add.tween(box).to({ y : maxBoxY} , 100 , Phaser.Easing.Linear.None , true , 0, 0);return;}//模拟滑动停止父元素仍滑动到停止的惯性//根据用户的滑动距离和滑动事件计算元素的惯性滑动速度const velocity = (Math.abs(Math.abs(endY) - Math.abs(startY)) / (endTime - startTime)) * 40;//scrollFlag标识父元素是向上滑动还是向下滑动if(endY > startY){// 向下box.body.velocity.y = velocity;box.scrollFlag = 'down';}else if(endY < startY){ //向上box.body.velocity.y = -velocity;box.scrollFlag = 'up';}   this.currentScrollBox = box;         } , this);box.events.onInputUp.add(function(e , p ){if(isDrag) return;const curX = p.position.x - e.previousPosition.x;const curY = p.position.y - e.previousPosition.y;//根据点击区域,判断用户点击的是哪个元素const idx = e.wrapData.findIndex((val , index , arr) => {return curX >= val.minX && curX <= val.maxX && curY >= val.minY && curY <= val.maxY;})if(idx == -1) return;const children = e.children[idx];//添加新元素到画面this.addNewMobilityObject(children.key , children._frame.name);} , this);
},
dealScrollObject : function(){if(this.currentScrollBox && this.currentScrollBox.body.velocity.y !== 0){const currentScrollBox = this.currentScrollBox,height = currentScrollBox.height,width = currentScrollBox.width;const maxBoxY = -(height - 350);if(currentScrollBox.y > 0){currentScrollBox.hitArea = new Phaser.Rectangle(0, 0 , width , height);customGame.add.tween(currentScrollBox).to({ y : 0} , 100 , Phaser.Easing.Linear.None, true , 0 , 0);currentScrollBox.body.velocity.y = 0;return;}if(currentScrollBox.y < maxBoxY){currentScrollBox.hitArea = new Phaser.Rectangle(0, -maxBoxY , width , height - maxBoxY);customGame.add.tween(currentScrollBox).to({ y : maxBoxY} , 100 , Phaser.Easing.Linear.None , true , 0, 0);currentScrollBox.body.velocity.y = 0;return;}currentScrollBox.hitArea = new Phaser.Rectangle(0,-currentScrollBox.y,width,height + currentScrollBox.y);if(currentScrollBox.scrollFlag == 'up'){currentScrollBox.body.velocity.y += 1.5;if(currentScrollBox.body.velocity.y >= 0){currentScrollBox.body.velocity.y = 0;}}else if(currentScrollBox.scrollFlag == 'down'){currentScrollBox.body.velocity.y -= 1.5;if(currentScrollBox.body.velocity.y <= 0){currentScrollBox.body.velocity.y = 0;}}}
},
update : function(){this.dealScrollObject();
}
复制代码

每次元素移动都要设置hitArea属性,用来设置元素的点击和滑动区域。这是因为元素的mask不可见区域还是可点击和滑动的,需要手动设置。

新增元素:

addNewMobilityObject : function(key , name){//默认新元素的位置在屏幕居中位置取随机值const randomPos = 30 * Math.random();const posX = Math.random() > 0.5 ? this.gameWidthHf + randomPos : this.gameWidthHf - randomPos;const posY = Math.random() > 0.5 ? this.gameHeightHf + randomPos : this.gameHeightHf - randomPos;const newOne = customGame.add.sprite(posX , posY , key , name);newOne.anchor.set(0.5);newOne.keyNum = this.keyNum++;this.mobilityGroup.add(newOne);
},
复制代码

3.元素编辑

新添加的元素或点击画面区内的元素,会有这样的编辑框出现,使得该元素可进行删除缩放操作。

绘制编辑框

addNewMobilityObject : function(){...//绑定选中元素this.bindObjectSelected(newOne);//让新建元素成为当前选中元素this.objectSelected(newOne);
},
bindObjectSelected : function(target){target.inputEnabled = true;target.input.enableDrag(false , true);//绘制编辑框target.events.onDragStart.add(this.objectSelected , this );
},
objectSelected : function(e, p){if(e.name == 'wall' || e.name == this.selectedObject) return;//如果点击的元素是当前选中元素,则不进行任何操作if(this.selectWrap && e.keyNum == this.selectWrap.keyNum) return;//去掉当前选中元素状态this.deleteCurrentWrap();const offsetNum = 10 , width = e.width,height = e.height, offsetX = -width/2 ,offsetY = -height / 2,boxWidth = width + 2*offsetNum , boxHeight = height + 2*offsetNum; const dashLine = customGame.add.bitmapData(width + 2*offsetNum , height + 2*offsetNum);const wrap = customGame.add.image(e.x + offsetX - offsetNum, e.y + offsetY - offsetNum, dashLine)wrap.name = 'wrap';wrap.keyNum = e.keyNum;//绘制虚线dashLine.ctx.shadowColor = '#a93e26';dashLine.ctx.shadowBlur = 20;dashLine.ctx.beginPath();dashLine.ctx.lineWidth = 6;dashLine.ctx.strokeStyle = 'white';dashLine.ctx.setLineDash([12 , 12]);dashLine.ctx.moveTo(0,0);dashLine.ctx.lineTo(boxWidth , 0);dashLine.ctx.lineTo(boxWidth , boxHeight);dashLine.ctx.lineTo(0 , boxHeight);dashLine.ctx.lineTo(0,0);dashLine.ctx.stroke();dashLine.ctx.closePath();wrap.bitmapDatas = dashLine;//删除按钮const close = customGame.add.sprite(- 27, -23,'objects','close.png');close.inputEnabled = true;close.events.onInputDown.add(this.deleteObject , this , null , e , e._frame.name);wrap.addChild(close);//放大按钮const scale = customGame.add.sprite(boxWidth - 27 , -23 , 'objects' , 'scale.png');scale.inputEnabled = true;scale.events.onInputDown.add(function(ev , pt){//判断用户是否要缩放元素this.isOnTarget = true;this.onScaleTarget = e;this.onScaleTargetValue = e.scale.x;} , this);wrap.addChild(scale);this.selectWrap = wrap;
},
复制代码

绘制虚线框使用了BitmapDataapi实现,BitmapData对象可以有canvas context的操作,可以作为图片或雪碧图的texture。

create : function(){...this.bindScaleEvent();
},
bindScaleEvent : function(){this.isOnTarget = false;    //判断是否按了当前选中元素的缩放按钮this.onScaleTarget = null;      //选中元素this.objectscaleRate = null;        //通过滑动位置计算出得缩放倍数this.onScaleTargetValue = null;     //选中元素当前的缩放倍数customGame.input.addMoveCallback(function(e){if(!this.isOnTarget) return;const currentMoveX = arguments[1] == 0 ? 1 : arguments[1];const currentMoveY = arguments[2] == 0 ? 1 : arguments[2];if(!this.objectscaleRate){this.objectscaleRate = currentMoveX / currentMoveY;return;}const currentRate = currentMoveX / currentMoveY;//元素的缩放要以上一次缩放后的倍数被基础进行缩放let scaleRate = currentRate / this.objectscaleRate - 1 + this.onScaleTargetValue;scaleRate = scaleRate <= 0.25 ? 0.25 : scaleRate >=2 ? 2 : scaleRate;this.onScaleTarget.scale.set(scaleRate);const dashLine = this.selectWrap.bitmapDatas;const onScaleTarget = this.onScaleTarget;const scaleBtn = this.selectWrap.getChildAt(1);const offsetNum = 10 , width = onScaleTarget.width,height = onScaleTarget.height, offsetX = -width/2 ,offsetY = -height / 2,boxWidth = width + 2*offsetNum , boxHeight = height + 2*offsetNum; //元素需要缩放,编辑框只缩放尺寸,不缩放按钮和虚线实际大小,因此每次缩放都要重新绘制虚线框dashLine.clear(0,0,this.selectWrap.width , this.selectWrap.height);dashLine.resize(width + 2*offsetNum , height + 2*offsetNum)this.selectWrap.x = onScaleTarget.x + offsetX - offsetNum, this.selectWrap.y = onScaleTarget.y + offsetY - offsetNum;scaleBtn.x = this.selectWrap.width - 30;dashLine.ctx.shadowColor = '#a93e26';dashLine.ctx.shadowBlur = 20;dashLine.ctx.shadowOffsetX = 0;dashLine.ctx.shadowOffsetY = 0;dashLine.ctx.beginPath();dashLine.ctx.lineWidth = 6;dashLine.ctx.strokeStyle = 'white';dashLine.ctx.setLineDash([12 , 12]);dashLine.ctx.moveTo(0,0);dashLine.ctx.lineTo(boxWidth , 0);dashLine.ctx.lineTo(boxWidth , boxHeight);dashLine.ctx.lineTo(0 , boxHeight);dashLine.ctx.lineTo(0,0);dashLine.ctx.stroke();dashLine.ctx.closePath();} , this);customGame.input.onUp.add(function(){this.isOnTarget = false;this.onScaleTarget = null;this.objectscaleRate = null;this.onScaleTargetValue = null;} , this);
},
复制代码

由于元素的缩放都会改变尺寸,编辑框的只缩放虚线框尺寸,不改变按钮的尺寸大小,因此每次缩放都要清楚编辑框,重新绘制编辑框。

4.生成长图

生成长图较为简单,只需要通过game.canvas.toDataURL生成。

createFinishBtn : function(){...finishBtn.events.onInputUp.add(this.finishPuzzle , this);
},
finishPuzzle : function(){//显示结果页$('.J_finish').show();//删除编辑框this.deleteCurrentWrap();//隐藏选择元素面板this.editGroup.visible = false;//创建底部结果二维码等this.createResultBottom();//隐藏选择元素面板和创建底部结果二维码需要时间,需要间隔一段时候后再生成长图setTimeout(() => {this.uploadImage();} , 100);
},
uploadImage : function(){const dataUrl = customGame.canvas.toDataURL('image/jpeg' , 0.7);//todo 可以在此将图片上传到服务器再更新到结果页this.showResult(dataUrl);
},
showResult : function(src){$('.J_finish .result').attr('src' , src).css({ opacity : 1});$('.J_finish .btm').css({opacity : 1});$('.J_finish .load').hide();
},
复制代码

五、总结

以上是这个h5的主要实现过程,由于代码细节较多,部分代码未贴出,需要配合源码阅读~~

源码:https://github.com/ZENGzoe/phaser-puzzle.git demo:https://zengzoe.github.io/phaser-puzzle/dist/

参考文档

phaser.io/

如何用Phaser实现一个全家福拼图H5相关推荐

  1. 如何用Python做一个三阶拼图?

    转载自品略图书馆 http://www.pinlue.com/article/2020/08/0911/3411120197743.html 用python做一个三阶拼图 程序介绍 先上图让大家感受一 ...

  2. 如何用OKR搞垮一个团队?

    作者| Mr.K   整理| Emma 来源| 技术领导力(ID:jishulingdaoli) 这几天一大堆读者在后台留言催更: "老K又偷懒了,看了这么多天恰饭文,我都忍了!再不写干货, ...

  3. 如何用 css 画一个心形

    如何用 css 画一个心形 (How to draw hearts using CSS) 用两个长方形切圆角倾斜位移并合并为一个心形 第一步 画一个长方形 (Draw a rectangle) 这个长 ...

  4. 怎么用python制作简单的程序-神级程序员教你如何用python制作一个牛逼的外挂!...

    玩过电脑游戏的同学对于外挂肯定不陌生,但是你在用外挂的时候有没有想过如何做一个外挂呢?(当然用外挂不是那么道义哈,呵呵),那我们就来看一下如何用python来制作一个外挂.... 我打开了4399小游 ...

  5. 手把手教你如何用Python制作一个电子相册?末附python教程

    这里简单介绍一下python制作电子相册的过程,主要用到tkinter和pillow这2个库,tkinter用于窗口显示照片,pillow用来处理照片,照片切换分为2种方式,一种是自动切换(每隔5秒) ...

  6. h5策划书_一个好的H5营销活动设计要如何进行策划

    一个好的H5营销活动想要脱颖而出可以抓住用户的痛点,从用户需求的角度来抓住H5营销的内容,而一个好的H5营销活动设计也可以为活动增加吸引力,吸引到更多的用户关注.那么,一个好的H5营销活动设计要如何进 ...

  7. js打乱数组的顺序_如何用 js 实现一个类似微信红包的随机算法

    如何用 js 实现一个类似微信红包的随机算法 js, 微信红包, 随机算法 "use strict"; /** * * @author xgqfrms * @license MIT ...

  8. 如何用控制台启动一个wcf服务

    快速阅读 如何用控制台启动一个wcf服务,已经wcf的配置和在类库中如何实现 . wcf类库 用vs新建一个类库,引用system.ServiceModel 定义接口实现服务契约和操作契约 定义方法实 ...

  9. 贪吃蛇博弈算法python_算法应用实践:如何用Python写一个贪吃蛇AI

    原标题:算法应用实践:如何用Python写一个贪吃蛇AI 前言 这两天在网上看到一张让人涨姿势的图片,图片中展示的是贪吃蛇游戏, 估计大部分人都玩过.但如果仅仅是贪吃蛇游戏,那么它就没有什么让人涨姿势 ...

最新文章

  1. 如何不让右下角出现“windows安全报警”
  2. 后缀数组求最长重复子串
  3. linux aemv7,无法在我的Ubuntu machin中安装“xlwings”
  4. Jquery Highcharts 参数配置说明
  5. GitHub直接查看HTML【项目网站一种制作方法】
  6. 线程的组成 java 1615387415
  7. 【我的物联网成长记5】如何进行物联网大数据分析?
  8. 【ElasticSearch】ElasticSearch 7.x 默认不在支持指定索引类型 Failed to parse mapping [_doc]: Root mapping definitio
  9. C语言调用自定义交换函,C语言函数篇 - personal page of Msingwen - OSCHINA - 中文开源技术交流社区...
  10. 软件测试用例.范文,软件测试用例模板范文
  11. proteus设计教程-数码管使用方法
  12. ssdp java_SSDP 简单服务发现协议
  13. 【前端教程】如何监控网页崩溃?
  14. Nacos 服务治理(服务注册中心)
  15. 计算机组成原理推荐书籍
  16. 04-MPI几个基本函数
  17. Android使用第三方字体
  18. 计算机毕业设计Java教育培训机构信息管理系统(源码+系统+mysql数据库+lW文档)
  19. Python3 --- Tornado之模板
  20. height、min-height、max-height中听谁的?

热门文章

  1. WEB2.0相关概念
  2. 景观格局指数计算方法及代表的生态学意义(待补充)
  3. Python爬虫:利用Python爬取网站上加 密 的 视 频
  4. 伤感 html代码,让对方瞬间心酸的文案,伤感入体,痛彻心扉!
  5. 个人支付宝、微信、云闪付收款
  6. matlab角点检测fast_AGAST角点检测算法:比FAST和FAST-ER更快
  7. ucsd大学音乐计算机,音乐留学│综合名校UCSD音乐制作专业详解!
  8. 基于HarmonyOS分布式小游戏之你画我猜
  9. C++基本语法知识查漏补缺(一)
  10. java毕业设计——基于java+jsp+Servlet的B2C网上拍卖系统设计与实现(毕业论文+程序源码)——网上拍卖系统