一、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 ├── src │ ├── css │ ├── img │ ├── index.html │ ├── js │ │ └── index.js │ ├── json │ ├── lib │ └── sprite ├── webpack.config.build.js └── webpack.config.dev.js
|
项目的构建工具使用的是Webpack, Webpack的配置可以查看源码webapck.config.dev.js,为避免文章篇幅过长,这里将不会详细介绍Webpack的配置过程,Webpck的配置介绍可以查看Webpack的官方文档https://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.WEBGL
、Phaser.CANVAS
、Phaser.HEADLESS
(不进行渲染)。
parent
:游戏界面挂载的DOM节点,可以为DOM id,或者标签。
state
:游戏state对象,默认值为null,游戏的state对象一般包含方法(preload、create、update、render)。
transparent
:是否设置游戏背景为透明,默认值为false。
antialias
:是否显示图片抗锯齿。默认值为true。
physicsConfig
:游戏物理引擎配置。
window.customGame = new Phaser.Game(750 , 750 / window.innerWidth * window.innerHeight , Phaser.CANVAS , 'container');
|
<div id="container"></div>
|
这样就可以在页面上看到我们的Canvas界面。
3.功能划分
在项目中,为了将项目模块化,将加载资源逻辑和游戏逻辑分开,在src/js
中新建load.js
存放加载资源逻辑,新建play.js
存放游戏逻辑。在这里的两个模块以游戏场景的形式存在。
场景(state)在Phaser中是可以更快地获取公共函数,比如camera、cache、input等,表现形式为js自定义对象或者函数存在,只要存在preload、create、update这三个方法中地任意一个,就是一个Phaser场景。
在Phaser场景中,总共有五个方法:init
、preload
、create
、update
、render
。前三个的执行循序为: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.js
customGame.state.start('Load');
|
三、资源加载
const load = { } module.exports = load;
|
1.画面初始化
进入页面前,需要进行一些游戏画面的初始化。在这里进行初始化的原因在于在场景里才能使用一些设置的方法。
(1)添加画布背景色
customGame.stage.backgroundColor = '#4f382b';
|
(2)设置屏幕适配模式
由于不同设备屏幕尺寸不同,需要根据需求设置适合的适配模式。可通过game.scale.scaleMode
设置适配模式,适配模式Phaser.ScaleManager
有五种:
NO_SCALE
:不进行任何缩放
EXACT_FIT
:对画面进行拉伸撑满屏幕,比例发生变化,会有缩放变形的情况
SHOW_ALL
:在比例不变、缩放不变形的基础上显示所有的内容,通常使用这种模式
RESIZE
:适配画面的宽度不算高度,不进行缩放,不变形
USER_SCALE
: 根据用户的设置变形
在这里的适配模式选择的是SHOW_ALL
:
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){ $('.J_loading .progress').text(`${progress}%`) }
|
onFileComplete
第一个参数为每个资源加载完的事件,第二个参数为指定该事件的上下文。
全部资源加载完成事件
通过onLoadComplete
方法来监听全部资源加载完成事件。
customGame.load.onLoadComplete.addOnce(this.loadComplete , this);
|
第一个参数为加载完成事件,第二个参数为指定该事件的上下文。
以上就是预加载的主要实现。
四、游戏逻辑
游戏逻辑大致可以分为四个部分,分别为画面初始化、物件选择面板的创建、元素的编辑、生成长图。
1.画面初始化
初始化的页面主要有墙面、桌子和电视机,主要是创建这三个物件。在此之前,先介绍下用到的两个概念。
sprite :可用于展示绝大部分的可视化的对象。
const newObject = game.add.sprite(0,0,spriteName , frame);
|
group :用于包含一系列对象的容器,方便批量操作对象,比如移动、旋转、放大等。
const group1 = game.add.group();
group1.add(newObject);
|
接下来是实例,创建墙面、桌子和电视机:
const play = { create : function(){ this.createEditPage(); }, createEditPage : function(){ this.mobilityGroup = customGame.add.group(); 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++;
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(); this.createNewyear(); this.createEditContent(); this.createEditTab(); this.createFinishBtn(); } ...
|
新年快乐框、tab标题、完成按钮的实现可以查看源码,这里主要着重介绍tab内容的实现。
物件选择面板主要有四个tab类:
四个tab类创建方式相同,因此取较为复杂的人物tab类为例介绍实现方法。
这里插播一些新的API:
graphics: 可以用来绘画,比如矩形、圆形、多边形等图形,还可以用来绘画直线、圆弧、曲线等各种基本物体。
const graphicObject = game.add.graphics(0,100);
graphicObject.beginFill(0x000000); graphicObject.drawRect(0,0,100 , 100);
|
编辑框的实现:
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); 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 );
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; },
|
右侧的内容需要考虑的是不同内容的位置、尺寸和显示数量不一定的问题,因此需要抽取出不同的设置作为参数传入:
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(){ ... 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。
6.在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; 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; },
|
绘制虚线框使用了BitmapData
api实现,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); 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/
参考文档
1.https://phaser.io/