如何用Phaser實現一個全家福拼圖H5

1、Phaser介紹
2、總體框架搭建
3、資源加載
4、遊戲邏輯
5、完成
6、總結
參考文檔
css

最近用Phaser作了一個全家福拼圖h5的項目,這篇文章將會從零開始講解如何用Phaser實現,最終效果以下:html


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

1、Phaser介紹

Phaser是一個開源的HTML5遊戲框架,支持桌面和移動HTML5遊戲,支持Canvas和WebGL渲染。官方文檔齊全,上手也比較容易。git

Phaser的功能主要還有預加載、物理引擎、圖片精靈、羣組、動畫等。github

更多詳細內容能夠查看Phaser官網,個人學習過程是主要是邊看Phaser案例的實現,邊看API文檔查看用法。web

2、總體框架搭建

1.目錄結構

目錄初始結構以下:json

.
├── 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/canvas


2.建立遊戲

(1)庫引入

index.html引入Phaser官網下載的Phaser庫。api

<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.js

customGame.state.start('Load');
複製代碼

3、資源加載

//load.js

const 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);
複製代碼

第一個參數爲加載完成事件,第二個參數爲指定該事件的上下文。

以上就是預加載的主要實現。


4、遊戲邏輯

遊戲邏輯大體能夠分爲四個部分,分別爲畫面初始化、物件選擇面板的建立、元素的編輯、生成長圖。

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();
},
複製代碼

5、總結

以上是這個h5的主要實現過程,因爲代碼細節較多,部分代碼未貼出,須要配合源碼閱讀~~

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


參考文檔

phaser.io/

相關文章
相關標籤/搜索