根据上一篇转盘抽奖,在开发消消乐游戏上又扩展了一下目录结构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; }