一、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.WEBGL
、Phaser.CANVAS
、Phaser.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場景中,總共有五個方法: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.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;
},
復制代碼
繪制虛線框使用了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);//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/