根據上一篇轉盤抽獎,在開發消消樂遊戲上又擴展了一下目錄結構web
新增mvc思想 新增事件派發機制 添加波動平均算法 添加費雪耶茲算法 添加時間控制器 取消精靈構建類 導演類dirctor變成mvc入口 遊戲運行移到控制器control裏面
一、pixi.js和tweenMax.js。(這兩個主要用在視圖層,開發遊戲精靈,也能夠用原生canvas代替) 二、初步瞭解一下mvc的模式
時間控制器,主要是封裝一下游戲運行狀態和對requestAnimation進行封裝算法
// 時間控制器 class Timer { constructor() { this.showSpirt = []; this.START = 1; // 開始 this.END = 2; // 結束 this.PAUSE = 3; // 暫停 this.ERROR = 4; // 異常 this.state = this.START; this.lastTime = 0; this.timer = null; this.timeDown = null; this.totalTime = 30; } run(fn) { var self = this; var requestAnimation = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame; function ani() { fn(self.timeDown); if (self.state != self.END) { requestAnimation(ani); } } this.timeDown = new Date().getTime() + this.totalTime * 1000; requestAnimation(ani); } } export default Timer;
遊戲入口主要是初始化遊戲和懶加載添加所需插件canvas
import Director from './director'; import $loader from '../../../common/util/loader'; import Loading from '../../../core/comp/loading/loading'; /* * popHappy 消消樂 * */ class Game { constructor(dataStore, res) { this.gameManager = dataStore.gameManager; // 數據層,保存遊戲所有數據 this.dataStore = dataStore; this.resource = res; this.$container = dataStore.$container; this.load = Loading.getInstance(dataStore.$gameConfig.container); this.load.hideLoading(); } addJs() { return Promise.all([$loader.$loaderPixi(), $loader.$loaderTweenMax()]); } // 遊戲開始運行 start() { // 導演實例,遊戲執行核心 this.load.showLoading(); this.addJs().then(_ => { this.load.hideLoading(); this.director = new Director(this.dataStore); this.director.enter(); }); } } export default Game;
const config = { containWidth: 660, // 容器寬度 containHeight: 950, // 容器高度 containPaddingLeft: 20, // 左邊填充值 containPaddingTop: 20, // 上面填充值 containY: 100, // 容器Y軸座標 containCol: 6, // 網格的列數量 containRow: 9, // 網格的行數量 containColMargin: 6, // 網格列之間距離 containRowMargin: 4, // 網格行之間距離 spirtWidth: 110, // 精靈元素寬度 spirtHeight: 106 // 精靈元素高度 }; export default config;
事件派發機制,主要是派發事件,用作組件通訊數組
/** * @ author: leeenx * @ 事件封裝 * @ object.on(event, fn) // 監聽一個事件 * @ object.off(event, fn) // 取消監聽 * @ object.once(event, fn) // 只監聽一次事件 * @ object.dispacth(event, arg) // 觸發一個事件 */ export default class Events { constructor() { // 定義的事件與回調 this.defineEvent = {}; } // 註冊事件 register(event, cb) { if (!this.defineEvent[event]) { this.defineEvent[event] = [cb]; } else { this.defineEvent[event].push(cb); } } // 派遣事件 dispatch(event, arg) { if (this.defineEvent[event]) { /* eslint-disable */ { for ( let i = 0, len = this.defineEvent[event].length; i < len; ++i ) { this.defineEvent[event][i] && this.defineEvent[event][i](arg); } } } } // on 監聽 on(event, cb) { return this.register(event, cb); } // off 方法 off(event, cb) { if (this.defineEvent[event]) { if (typeof cb == 'undefined') { delete this.defineEvent[event]; // 表示所有刪除 } else { // 遍歷查找 for ( let i = 0, len = this.defineEvent[event].length; i < len; ++i ) { if (cb == this.defineEvent[event][i]) { this.defineEvent[event][i] = null; // 標記爲空 - 防止dispath 長度變化 // 延時刪除對應事件 setTimeout( () => this.defineEvent[event].splice(i, 1), 0 ); break; } } } } } // once 方法,監聽一次 once(event, cb) { let onceCb = () => { cb && cb(); this.off(event, onceCb); }; this.register(event, onceCb); } }
導演類,初始化mvc,遊戲初始化佈局入口,同時監聽遊戲結束業務邏輯。mvc
import Model from './core/Model'; import View from './core/View'; import Control from './core/Control'; import Director from '../../comp/director/director'; class EqxDir extends Director { constructor(dataStore) { let { gameManager, $gameConfig } = dataStore; super(gameManager); this.dataStore = dataStore; this.$gameConfig = $gameConfig; // 初始化mvc this.model = new Model(); this.view = new View(dataStore); // mv 由 c 控制 this.constrol = new Control(this.model, this.view); this.event = this.constrol.event; // 監聽遊戲結束,請求提交分數接口 this.event.on('game-over', score => { this.gameOver(score); }); } enter() { this.constrol.enter(); } } export default EqxDir;
視圖層:app
經過pixi.js初始化佈局頁面效果 經過tweenMax.js對精靈作動畫效果處理 updated函數,監聽model數據變化來處理視圖顯示
import config from '../config'; import HOST from '../../../../common/host'; import { tapstart, tapmove, tapend } from '../../../../core/common/util/compaty'; export default class View { constructor(dataStore) { // dataStore.$container.find('canvas').remove(); this.gameJson = dataStore.gameJson; this.$gameConfig = dataStore.$gameConfig; this.width = this.setCanvas(dataStore.$gameConfig.container).width; // 設置容器寬高 this.height = this.setCanvas(dataStore.$gameConfig.container).height; // 設置容器寬高 let app = new PIXI.Application({ width: this.width, height: this.height, // backgroundColor: 0xff0000, resolution: 1 }); Object.assign(this, app); this.view = app.view; dataStore.$container.prepend(app.view); // 表格尺寸 this.gridWidth = config.containWidth; this.gridHeight = config.containHeight; // 表格的行列數 this.col = config.containCol; this.row = config.containRow; // spirte this.spriteWidth = config.spirtWidth; this.spriteHeight = config.spirtHeight; // 磚塊數組 this.tiles = new Array(config.containRow * config.containCol); // 遊戲背景 let emptySprite = PIXI.Sprite.fromImage( HOST.FILE + this.gameJson.staticSpirts.BGIMG.imgUrl ); emptySprite.width = this.width; emptySprite.height = this.gameJson.staticSpirts.BGIMG.height; emptySprite.position.x = 0; emptySprite.position.y = 0; this.stage.addChild(emptySprite); // 繪製遊戲區域 this.area = new PIXI.Container(); this.area.width = 660; this.area.height = 950; this.area.x = 45; this.area.y = 150; // 繪製一個矩形 let rect1 = new PIXI.Graphics(); rect1.beginFill(0x000000, 0.6); rect1.lineStyle(); rect1.drawRect(0, 0, this.area._width, this.area._height); rect1.endFill(); this.area.addChild(rect1); this.area.mask = rect1; // 繪製遮罩 let rect2 = new PIXI.Graphics(); rect2.beginFill(0x000000, 0.6); rect2.lineStyle(); rect2.drawRect(0, 0, this.area._width, this.area._height); rect2.endFill(); this.area.addChild(rect2); // 遊戲單獨一個容器 this.game = new PIXI.Container(); // 添加到舞臺 this.game.addChild(this.area); // 添加到舞臺 this.stage.addChild(this.game); // this.paused this.paused = true; this.stage.addChild(this.drawScore(), this.drawTimer()); // 添加點擊事件 this.addClick(); // 添加監控 this.addWatch(); this.total = 0; this.time = 30; } init() { // 添加監控時間事件 this.event.on('view-time', time => { this.time = time; }); // 顯示遊戲界面 this.showGame(); // 開啓點擊 this.area.interactive = true; // 顯示磚塊 this.area.renderable = true; let arr = this.tiles.map((tile, index) => { let { col, row } = this.getColAndRow(tile.index); /* eslint-disable */ return this.topToDown.call(this, col, row, tile, index); }); Promise.all(arr).then(() => { // 派發下掉動做完成,開啓消消樂功能 this.event.dispatch('view-start'); }); } addWatch() { Reflect.defineProperty(this, 'total', { get: () => this._total || 0, set: value => { this._total = value; this.scoreLabel.text = value; } }); Reflect.defineProperty(this, 'time', { get: () => this._time || 30, set: value => { this._time = value; this.timeLabel.text = value; } }); } drawScore() { // 繪製頭像,分數組合和透明矩形 return scoreC; } drawTimer() { // 繪製時間,文本和遮罩 return scoreC; } addClick() { let isClick = false, initX, initY, initTime, cScale = this.$gameConfig['cScale'] || 1; // 添加移動開始事件 this.view.addEventListener(tapstart, event => { if (this.paused === true) return; initX = event.offsetX / cScale - this.area.x; initY = event.targetTouches[0].clientY - this.area.y; initTime = new Date().getTime(); }); this.view.addEventListener(tapmove, event => { // 暫停不觸發事件,移動過程當中,不出發移動事件 if (this.paused === true) return; let time = new Date().getTime(); if (time - initTime >= 30) { // 移動只觸發一次 if (isClick == true) return; isClick = true; // let x = event.offsetX / cScale - this.area.x; // let y = event.offsetY / cScale - this.area.y; let x = event.targetTouches[0].clientX - this.area.x; let y = event.targetTouches[0].clientY - this.area.y; let angel = getAngel({ x: initX, y: initY }, { x, y }); let orientation = 0; if (angel >= -45 && angel < 45) { orientation = 3; } else if (angel >= -135 && angel < -45) { orientation = 0; } else if (angel >= 45 && angel < 135) { orientation = 1; } else { orientation = 2; } let col = (initX / this.spriteWidth) >> 0, row = (initY / this.spriteHeight) >> 0; let position = col * this.row + row; this.event.dispatch('view-tap', { position, orientation }); } }); this.view.addEventListener(tapend, function(event) { // 暫停不觸發事件 setTimeout(() => { isClick = false; }, 600); }); // 計算角度 function getAngel(origin, target) { let rX = target['x'] - origin['x']; let rY = target['y'] - origin['y']; let angel = (Math.atan2(rY, rX) / Math.PI) * 180; return angel; } } // 初始化下掉動畫 topToDown(col, row, tile, i) { return new Promise(resolve => { TweenMax.to(tile.sprite, 0.5, { x: col * this.spriteWidth + this.spriteWidth / 2, y: row * this.spriteHeight + this.spriteHeight / 2, delay: ((i / this.col) >> 0) * 0.05, ease: Linear.easeNone, onComplete: () => { resolve(); } }); }); } // 獲取當前磚塊的橫縱位置 getColAndRow(index) { // let { index } = tile; let col = (index / this.row) >> 0; let row = index % this.row; return { col, row }; } // 生成對應的精靈 generateSpirt(clr = 5) { let imgObj = [ HOST.FILE + this.gameJson.dynamicSpirts[0], HOST.FILE + this.gameJson.dynamicSpirts[1], HOST.FILE + this.gameJson.dynamicSpirts[2], HOST.FILE + this.gameJson.dynamicSpirts[3], HOST.FILE + this.gameJson.dynamicSpirts[4] ]; /* eslint-disalbe */ let sprite = new PIXI.Sprite.fromImage(imgObj[clr]); sprite.width = this.spriteWidth; sprite.height = this.spriteHeight; sprite.x = 280; sprite.anchor.x = 0.5; sprite.anchor.y = 0.5; return sprite; } // 更新磚塊 update({ originIndex, index, clr, removed, score, type }) { if (originIndex === undefined || clr === undefined) return; let tile = this.tiles[originIndex]; // tile 不存在,生成對應磚塊 if (tile === undefined) { this.tiles[originIndex] = tile = { sprite: this.generateSpirt(clr), clr, originIndex, index, removed: false }; // 添加到舞臺 this.area.addChild(tile.sprite); } if (tile.removed !== removed) { this.bomb(removed, tile, index); } // index當前索引起生改變,表示位置發生改變 if (tile.index !== index) { this.updateTileIndex(tile, index, type); } // tile 存在,判斷顏色是否同樣 else if (tile.clr !== clr) { this.updateTileClr(tile, clr); } } // 磚塊位置變化 updateTileIndex(tile, index, type) { let { col, row } = this.getColAndRow(index || tile.originIndex); let x = col * this.spriteWidth; let y = row * this.spriteHeight; if (type == 2) { // 交換位置 TweenMax.to(tile.sprite, 0.2, { x: x + this.spriteWidth / 2, y: y + this.spriteHeight / 2, ease: Linear.easeNone }); } else if (tile.index < index) { // 遊戲過程,未消除的磚塊下落 TweenMax.to(tile.sprite, 0.2, { x: x + this.spriteWidth / 2, y: y + this.spriteHeight / 2, delay: 0, ease: Linear.easeNone }); } tile.index = index; } // 顏色發生改變 updateTileClr(tile, clr) { if (clr === undefined) return; tile.sprite = this.generateSpirt(clr); tile.clr = clr; } // 消除磚塊和添加磚塊 bomb(removed, tile, index) { if (removed === true) { // 遊戲過程,有動畫 縮小 TweenMax.to(tile.sprite, 0.2, { width: 0, height: 0, ease: Linear.easeNone, onComplete: () => { this.area.removeChild(tile.sprite); tile.sprite.width = this.spriteWidth; tile.sprite.height = this.spriteHeight; tile.removed = removed; this.total += 3; } }); } else { // 從上倒下下落動畫 this.area.addChild(tile.sprite); let { col, row } = this.getColAndRow(index); let x = col * this.spriteWidth; let y = row * this.spriteHeight; // 遊戲過程,有動畫 TweenMax.fromTo( tile.sprite, 0.2, { x: x + this.spriteWidth / 2, y: -this.spriteHeight * (this.row - row) + this.spriteHeight / 2, delay: 0, ease: Linear.easeNone }, { x: x + this.spriteWidth / 2, y: y + this.spriteHeight / 2, delay: 0, ease: Linear.easeNone, onComplete: () => { tile.removed = removed; } } ); } } // 顯示遊戲界面 showGame() { this.game.renderable = true; } // 設置容器寬高 setCanvas($container) { let container = $container.selector == 'body' ? $container : $container.parent(); let w = container.width(); let h = container.height(); let width = 750; let height = (h * width) / w; return { width, height }; } // 暫停按鈕 stop() { this.paused = true; } // 恢復渲染 resume() { this.paused = false; } }
數據層,主要作數據處理,包括磚塊數量、打散磚塊、改變位置、計算消除磚塊dom
import quickWave from '../libs/quickWave'; import shuffle from '../libs/shuffle'; import config from '../config'; export default class Model { constructor() { // 行列數 this.row = config.containRow; this.col = config.containCol; // 表格總數 6*9 this.gridCellCount = config.containCol * config.containRow; // 磚塊 this.tiles = new Array(this.gridCellCount); for (let i = 0; i < this.gridCellCount; ++i) { this.tiles[i] = { // 是否移除 removed: false }; } // 遊戲狀態 this.state = true; } // 填充數組 ---- count 表示幾種顏色 init() { // 色磚小計數 let subtotal = 0; // 波動均分色塊 let arr = quickWave(5, 4, 4); // 此處可優化,業務邏輯能夠放在均份內部 arr.forEach((count, clr) => { count += 11; // 色磚數量 while (count-- > 0) { let tile = this.tiles[subtotal++]; tile.clr = clr; } }); // 打散 tiles shuffle(this.tiles); // 存入 grid this.grid = this.tiles.map((tile, index) => { // 實時索引 tile.index = index; // 原索引 tile.originIndex = index; // 默認在舞臺上 tile.removed = false; // 欲消除狀態 tile.status = false; // 默認是消除換位 tile.type = 1; return tile; }); } // 消除磚塊 is() { let newGrid = [...this.grid]; // 豎消,判斷磚塊欲消除狀態 for (let i = 0; i < this.col; i++) { let xBox = newGrid.splice(0, this.row); this.setxBox(xBox); } // 橫消,判斷磚塊欲消除狀態 for (let i = 0; i < this.row; i++) { let xBox = []; for (let j = 0; j < this.row * this.col; j += this.row) { xBox.push(this.grid[i + j]); } this.setxBox(xBox); } // 經過欲消除狀態,改變在舞臺的呈現形式 status 賦值給removed this.grid.forEach(tile => { tile.removed = tile.status; }); // 消除磚塊後,磚塊的index值改變 this.changeIndex(); } setxBox(arr) { // 把欲消除的內容status標記爲true for (let i = 0; i < 5; i++) { let rBox = []; let xBox = []; let len = arr.length; arr.forEach((tile, index) => { if (tile.clr == i && index != len - 1) { // 不是最後一位,同一種顏色push到欲消除數組 xBox.push(tile); } else if ( tile.clr == i && index == len - 1 && xBox.length >= 2 ) { // 最後一位,而且內部可消除知足3個 放到欲消除數組,同時合併到結果數組裏面 xBox.push(tile); rBox = [...rBox, ...xBox]; } else if (xBox.length < 3) { // 刪除欲消除數組 xBox.length = 0; } else { // 把消除數組放到結果數組裏 rBox = [...rBox, ...xBox]; xBox.length = 0; } }); if (rBox.length > 2) { rBox.forEach(tile => { tile.status = true; }); } } } // 改變index changeIndex() { // 豎直移動 let newGrid = []; for (let i = 0; i < this.col; i++) { let xBox = this.grid.splice(0, this.row); newGrid = [...newGrid, ...this.setIBox(xBox)]; } this.timer && clearTimeout(this.timer); // 等消失以後在從新計算 this.timer = setTimeout(() => { this.grid = newGrid.map((tile, index) => { if (tile.removed == true) { tile.clr = (Math.random() * 5) >> 0; this.paused = true; } // 默認在舞臺上 tile.removed = false; // tile.originIndex = index; tile.status = false; tile.type = 1; // 實時索引 tile.index = index; return tile; }); if (this.paused == true && this.state) { setTimeout(() => { this.is(); this.paused = false; }, 500); } else { this.move = true; } }, 300); } // 把每一列的消除項添坑,並從新導出 setIBox(arr) { let len = arr.length; let newArr = []; for (let i = len - 1; i >= 0; i--) { if (arr[i].removed == true) { newArr.unshift(arr.splice(i, 1)[0]); } } arr = [...newArr, ...arr]; return arr; } // 更改兩個點座標 setTileDoubleIndex({ position, orientation }) { let obj = { 0: -1, 1: 1, 2: -9, 3: 9 }; let one = position; // 目標位置 let two = position + obj[orientation]; // 被交換位置 let topBorder = parseInt(one / this.row) * this.row; // 上邊界 let bottomBorder = parseInt(one / this.row) * this.row + this.row; // 底邊界 // 判斷替換不能出邊界,不能超過總邊界,若是是上下方向,不能超過當前上下邊界 if ( two < 0 || two > 53 || ((orientation == 0 || orientation == 1) && (two < topBorder || two >= bottomBorder)) ) { return; } // 兩個磚塊交換index, let tileOneIndex = this.grid[one].index; let tileTwoIndex = this.grid[two].index; this.grid[one].type = 2; this.grid[one].index = tileTwoIndex; this.grid[two].type = 2; this.grid[two].index = tileOneIndex; let tile = this.grid[one]; this.grid[one] = this.grid[two]; this.grid[two] = tile; // 校驗每個是否有消除狀態 if (this.checkOne(one) || this.checkOne(two)) { setTimeout(() => { this.is(); this.paused = false; this.move = false; }, 500); } else { // 不能消除,把替換的位置,在替換回來 setTimeout(() => { let tileOneIndex = this.grid[one].index; let tileTwoIndex = this.grid[two].index; this.grid[one].type = 2; this.grid[one].index = tileTwoIndex; this.grid[two].type = 2; this.grid[two].index = tileOneIndex; let tile = this.grid[one]; this.grid[one] = this.grid[two]; this.grid[two] = tile; this.paused = true; this.move = true; }, 200); } } /** * 檢測單個是否能夠消除 */ checkOne(position) { let clr = this.grid[position].clr; let obj = { 0: -1, 1: 1, 2: -9, 3: 9 }; let fanObj = { 0: 1, 1: 0, 2: 3, 3: 2 }; let topBorder = parseInt(position / this.row) * this.row; let bottomBorder = parseInt(position / this.row) * this.row + this.row; let statue = false; let index = 1; // 方向判斷是否能夠消除 function getOri(position, orientation, step) { // 知足3個跳出遞歸 if (index >= 3) { return; } let two = position + obj[orientation] * step; if ( two < 0 || two > 53 || ((orientation == 0 || orientation == 1) && (two < topBorder || two >= bottomBorder)) ) { // 若是出邊界不處理 } else if (this.grid[two].clr == clr) { index++; getOri.call(this, this.grid[two].index, orientation, 1); getOri.call(this, this.grid[two].index, fanObj[orientation], 2); } } for (let i in obj) { index = 0; getOri.call(this, position, i, 1); if (index >= 3) { statue = true; } } // 返回當前,校驗狀態 return statue; } /** * @ 檢查是否死局 * @ 非死局會返回一個索引值 * @ 死局返回 false */ check() { if (this.tileCount === 0) return false; return true; } }
包括:監聽每個磚塊屬性變化、註冊遊戲結束事件、初始化view和modelide
import Event from '../libs/Event'; import Timer from '../timer'; import { changeTimeStamp } from '../../../common/timeDown'; export default class Control { constructor(model, view) { this.model = model; this.view = view; // event事件 this.event = new Event(); // view 與control 共享一個event this.view.event = this.event; // timer let timer = new Timer(); // 數據綁定: model.tiles -> view.tiles model.tiles.forEach(tile => { Reflect.defineProperty(tile, 'index', { set: value => { if (value === tile._index) return false; Reflect.set(tile, '_index', value); // 與view同步數據 view.update(tile); }, get: () => Reflect.get(tile, '_index') }); Reflect.defineProperty(tile, 'clr', { set: value => { if (value === tile._clr) return false; Reflect.set(tile, '_clr', value); // 與view同步數據 view.update(tile); }, get: () => Reflect.get(tile, '_clr') }); Reflect.defineProperty(tile, 'removed', { set: value => { if (value === tile._removed) return false; Reflect.set(tile, '_removed', value); // 與view同步數據 view.update(tile); }, get: () => Reflect.get(tile, '_removed') || false }); }); // 監聽model數據運行格式 Reflect.defineProperty(model, 'move', { set: value => { if (value === model._paused) return false; Reflect.set(model, '_move', value); // 與view同步數據 if (value) { this.resume(); } else { this.stop(); } }, get: () => Reflect.get(model, '_move') || false }); // 監聽點擊事件 this.event.on('view-tap', moveObj => { // 暫停狀態下鎖屏 if (this.paused === true) return; // 消除 model 的磚塊 model.setTileDoubleIndex(moveObj); }); // 開啓消消樂功能 this.event.on('view-start', () => { setTimeout(() => { this.model.is(); timer.run(timeDown => { let data = changeTimeStamp(timeDown); let time = 0; if (data) { time = data.sec + '.' + data.ms.toString().substr(0, 2); } else { if (model.move === true) { timer.state = timer.END; time = '0.00'; model.state = false; model.move = false; // 派發遊戲結束 this.event.dispatch('game-over', view.total); } } this.event.dispatch('view-time', time); }); }, 500); }); } // 初關卡 init() { // 默認五個顏色 this.model.init(); // 磚塊動畫 this.view.init(); } // 指定關數 enter() { this.init(); } // 恢復遊戲 resume() { // 恢復渲染 this.view.resume(); // 標記恢復 this.paused = false; } // 暫停遊戲 stop() { // 恢復渲染 this.view.stop(); // 標記恢復 this.paused = true; } }
主要是快速分配方法,每次動態獲取當前數值的波峯和波谷
參考文獻:https://aotu.io/notes/2018/01...函數
快速隨機,若是用sort作隨機,第一:時間複雜度高,第二:並不算真正的隨機佈局
/* @ Fisher–Yates(費雪耶茲算法) */ export default function shuffle(a) { for (let i = a.length; i; i--) { let j = Math.floor(Math.random() * i); [a[i - 1], a[j]] = [a[j], a[i - 1]]; } return a; }