行业知识
2048小游戏(前端三板斧详细笔记,JavaScript初步详细教程)
目录
项目源码:GitHub - Atopos-suyu/256game: 这是改编 的2048小游戏,为防止有人通关不成功,让他们体验下通关的满足感,写了256小游戏
2048设计稿:腾讯 CoDesign - 腾讯 CoDesign
页面效果展示:
游戏开始界面:
游戏结束页面:
游戏胜利页面:
实战需求:
实战技术知识点:
静态页面开发
2048静态页面一:
静态页面二
我们分析下其相同点和不同点:
2048模型设计,随机渲染
2048移动处理
2048合并处理
2048动画效果
2048本地存储
项目源码:GitHub - Atopos-suyu/256game: 这是改编 的2048小游戏,为防止有人通关不成功,让他们体验下通关的满足感,写了256小游戏
2048设计稿:腾讯 CoDesign - 腾讯 CoDesign
页面效果展示:
游戏开始界面:
游戏结束页面:
游戏胜利页面:
实战需求:
1.游戏是一个4x4的方格,每个方格我们称作为一个Tile或者Cel。
2.游戏开始能随机出现2个Tile,每个的值90%可能为2,10%可能为4。
3.可以通过上、下、左、右键盘操作,每个Tle按照方向移动到不可移动为止。
4.如果移动以后两个Tile的内容值一样,则进行合并。
5.每个Tile移动会有100ms的移动动画。
6.每个Tile的出现有个短暂的放大效果。
7.每次Tile的合并有个短暂的放大回弹效果。
8.J顶部Score记录当前分数,BestScore记录有史以来最高
分,每次合并都会产生分数的变化,分数计算规则为:分数
=原来分数+合并后的值。
9.游戏将时时刻刻记录进度,刷新页面重现游戏进度。
10.当某个Tile的值为2048,游戏胜利。
11.当每个方格都有值,并且相邻两个方格无法再进行合并,则游戏结束。
实战技术知识点:
1.静态页面渲染:需要HTML、CSS基础知识,包括学习的SCSS知识。
2.开始游戏等事件处理:需要使用DOM监听事件。
3.Tle移动处理:需要监听键盘事件(暂时不处理H5中手势事件的情况)。
4.Tile动态随机添加:需要使用DOM动态操作。
5.Tile移动,合并:需要使用Javascript列表,对象,方法等数据结构和常用技巧。
6.Tile动画:需要使用CSS的transform和animation等动画效果。
7.本地缓存:需要使用Javascript localStorage浏览器缓存。
8…
静态页面开发
2048静态页面一:
优课达-2048 整个页面的CSS文件较大,为了更加清晰的理解CSS文件。我们利用scss@import特性对文件进行分离,如下文件目录。|-- images|-- style|-- index.scss // scss入口文件 + footer|-- nav.scss // 头部区域文件|-- main.scss // 主体区域文件|-- desc.scss // 描述区域文件|-- index.html$field-width: 290px;$grid-spacing: 10px;$grid-row-cells: 4;$tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells;$tile-border-radius: 3px;main { margin-top: 20px; margin-left: auto; margin-right: auto; box-sizing: border-box; width: $field-width; height: $field-width; position: relative; padding: $grid-spacing; background: #bbada0; border-radius: 8px; .game-grid { .grid-row { .grid-cell { } } }}静态页面二
为了方便以后Tile方块动态渲染,我们加入2、4、8到2048,11个方块静态页面,最终效果如下:
我们分析下其相同点和不同点:
1.它们都有一样的大小,圆角,动效。所以我们需要设置一个统一的class为tile
2.它们每个数字颜色和字体大小都不同,因此我们需要为每个值设置单独的样式,class为ti1e-(x)(x为2、4、8、16.2048)。
3.它们的位置可以总结为*行(roW),列(column),因此我们可以使用绝对定位进行布局class为title-position-(row)-(column)
4.每个元素都有移动(translate)和放缩(scale)动画,因为两个动画都是transform的一个属性,会出现冲突。因此我们将每个Tile分为外框tile和tile-inner两个部分,tile用于元素移动,title-inner用于元素放缩。
2.tile-container { position: absolute; left: 0; top: 0; .tile { position: absolute; width: $tile-size; height: $tile-size; border-radius: 4px; transition: transform 100ms ease-in-out; } .tile-inner { width: 100%; height: 100%; line-height: $tile-size; background: #eee4da; text-align: center; font-weight: bold; font-size: 34px; color: #776e65; }//通过scss的循环和变量,动态生成4行4列的Tile Position样式 @for $x from 1 through $grid-row-cells { @for $y from 1 through $grid-row-cells { .tile-position-#{$x}-#{$y} { $xPos: $grid-spacing + floor(($tile-size + $grid-spacing) * ($y - 1)); $yPos: $grid-spacing + floor(($tile-size + $grid-spacing) * ($x - 1)); transform: translate($xPos, $yPos); } }}.tile.tile-2 .tile-inner { background: #eee4da;}}main .tile-container { position: absolute; left: 0; top: 0;}main .tile-container .tile { position: absolute; width: 60px; height: 60px; border-radius: 4px; transition: transform 100ms ease-in-out;}main .tile-container .tile-inner { width: 100%; height: 100%; line-height: 60px; background: #eee4da; text-align: center; font-weight: bold; font-size: 34px; color: #776e65;}main .tile-container .tile-position-1-1 { transform: translate(10px, 10px);}main .tile-container .tile-position-1-2 { transform: translate(80px, 10px);}main .tile-container .tile-position-1-3 { transform: translate(150px, 10px);}main .tile-container .tile-position-1-4 { transform: translate(220px, 10px);}main .tile-container .tile-position-2-1 { transform: translate(10px, 80px);}main .tile-container .tile-position-2-2 { transform: translate(80px, 80px);}main .tile-container .tile-position-2-3 { transform: translate(150px, 80px);}main .tile-container .tile-position-2-4 { transform: translate(220px, 80px);}main .tile-container .tile-position-3-1 { transform: translate(10px, 150px);}main .tile-container .tile-position-3-2 { transform: translate(80px, 150px);}main .tile-container .tile-position-3-3 { transform: translate(150px, 150px);}main .tile-container .tile-position-3-4 { transform: translate(220px, 150px);}main .tile-container .tile-position-4-1 { transform: translate(10px, 220px);}main .tile-container .tile-position-4-2 { transform: translate(80px, 220px);}main .tile-container .tile-position-4-3 { transform: translate(150px, 220px);}main .tile-container .tile-position-4-4 { transform: translate(220px, 220px);}main .tile-container .tile-merged .tile-inner { z-index: 20; animation: pop 200ms ease 100ms; animation-fill-mode: backwards;}main .tile-container .tile-new .tile-inner { animation: appear 200ms ease-in-out; animation-delay: 100ms; animation-fill-mode: backwards;}main .tile-container .tile.tile-2 .tile-inner { background: #eee4da;}main .tile-container .tile.tile-4 .tile-inner { background: #ede0c8;}main .tile-container .tile.tile-8 .tile-inner { color: #f9f6f2; background: #f2b179;}main .tile-container .tile.tile-16 .tile-inner { color: #f9f6f2; background: #f59563;}main .tile-container .tile.tile-32 .tile-inner { color: #f9f6f2; background: #f67c5f;}main .tile-container .tile.tile-64 .tile-inner { color: #f9f6f2; background: #f65e3b;}main .tile-container .tile.tile-128 .tile-inner { color: #f9f6f2; background: #edcf72; font-size: 30px;}main .tile-container .tile.tile-256 .tile-inner { color: #f9f6f2; background: #edcc61; font-size: 30px;}main .tile-container .tile.tile-512 .tile-inner { color: #f9f6f2; background: #edc850; font-size: 30px;}main .tile-container .tile.tile-1024 .tile-inner { color: #f9f6f2; background: #edc53f; font-size: 22px;}main .tile-container .tile.tile-2048 .tile-inner { color: #f9f6f2; background: #edc22e; font-size: 22px;}2048模型设计,随机渲染
2048对象设计
Tile对象
// tile.jsfunction Tile(position, value) { this.row = position.row; this.column = position.column; this.value = value;}Grid对象
// grid.jsfunction Grid(size = 4) { this.size = size;}//grid.jsfunction Grid(size = 4) { this.size = size; this.cells = []; this.init(size);}// prototype 设置方法Grid.prototype.init = function (size) { for (let row = 0; row//grid.jsfunction Grid(size = 4, state) { this.size = size; this.cells = []; this.init(size); //调用init方法进行初始化 // 如果有之前的进度,则恢复 if (state) { this.recover(state); }} //Grid对象表示一个游戏棋盘,大小为size*size的方格,参数state恢复游戏进度Grid.prototype.recover = function({ size, cells }) { this.size = size; //通过遍历cells二维数组,如果某个单元格存在内容,则新建一个Tile对象并放在Grid对象的位置上 for (let row = 0; row 0) { //如果存在空闲方格,利用Math.random()随机获取其中的某一个 return cells[Math.floor(Math.random() * cells.length)]; }};// 获取某个位置的TileGrid.prototype.get = function(position) { if (this.outOfRange(position)) { return null; } return this.cells[position.row][position.column];};// 判断某个位置是否超出边界Grid.prototype.outOfRange = function(position) { return ( position.row = this.size || position.column = this.size );};//满足以下任何条件,则判断trueGrid.prototype.serialize = function() { const cellState = []; // cellState 是一个二维数组,分别存储整个Grid信息。 // 如果该位置有Tile, 则返回 Tile序列化结果 // 如果该位置没有Tile,则存储null for (let row = 0; row引入js
... ...2048对象渲染
Render对象用于页面渲染
//render.jsfunction Render() {}// 渲染整个gridRender.prototype.render = function(grid) { for (let row = 0; row渲染每一个Tile
//render.jsfunction Render() { this.tileContainer = document.querySelector('.tile-container');}// 渲染单个tileRender.prototype.renderTile = function(tile) { // 创建一个tile-inner const tileInner = document.createElement('p'); tileInner.setAttribute('class', 'tile-inner'); tileInner.innerHTML = tile.value; // 创建一个tile const tileDom = document.createElement('p'); let classList = [ 'tile', `tile-${tile.value}`, `tile-position-${tile.row + 1}-${tile.column + 1}` ]; tileDom.setAttribute('class', classList.join(' ')); tileDom.appendChild(tileInner); this.tileContainer.appendChild(tileDom);};效果如下:
2048随机初始化
所有可用方格
// grid.js// 获取所有可用方格的位置Grid.prototype.availableCells = function() { const availableCells = []; for (let row = 0; row随机某个可用方格
// grid.js// 随机获取某个可用方格的位置Grid.prototype.randomAvailableCell = function() { // 获取到所有的空闲方格 const cells = this.availableCells(); if (cells.length > 0) { // 利用Math.random()随机获取其中的某一个 return cells[Math.floor(Math.random() * cells.length)]; }};index.js利用随机空闲位置创建节点
let grid = new Grid();let render = new Render();for (let i = 0; i2048移动处理
重构-Manager
//manager.jsfunction Manager(size = 4) { this.size = size; this.grid = new Grid(size); this.render = new Render(); this.start();}Manager.prototype.start = function () { for (let i = 0; i键盘监听
window.addEventListener('keyup', function (e) { console.log(e.code);});监听回调设置(监听器Listener)
function Listener() { window.addEventListener('keyup', function (e) { switch (e.code) { case 'ArrowUp': console.log('向上'); break; case 'ArrowLeft': console.log('向左'); break; case 'ArrowRight': console.log('向右'); break; case 'ArrowDown': console.log('向下'); break; } });}事件回调
//listener.jsfunction Listener({ move: moveFn }) { window.addEventListener('keyup', function (e) { switch (e.code) { case 'ArrowLeft': moveFn('向左'); break; case 'ArrowUp': moveFn('向上'); break; case 'ArrowDown': moveFn('向下'); break; case 'ArrowRight': moveFn('向右'); break; } });}Manager修改
//manager.jsthis.listener = new Listener({ move: function (direction) { console.log(direction); },});方向向量化(方向上下左右)
左 => row: 1, column: 0 // 列数索引 -1, 也就是column -1右 => row: 1, column: 2 // 列数索引 +1, 也就是column +1上 => row: 0, column: 1 // 行数索引 -1, 也就是row -1下 => row: 2, column: 1 // 行数索引 +1, 也就是row +1//listener.jsfunction Listener({ move: moveFn }) { window.addEventListener('keyup', function (e) { switch (e.code) { case 'ArrowUp': moveFn({ row: -1, column: 0 }); break; case 'ArrowLeft': moveFn({ row: 0, column: -1 }); break; case 'ArrowRight': moveFn({ row: 0, column: 1 }); break; case 'ArrowDown': moveFn({ row: 1, column: 0 }); break; } });}移动位置计算
Tile移动
代码实现-遍历顺序
//manager.jsManager.prototype.getPaths = function (direction) { let rowPath = []; let columnPath = []; return { rowPath, columnPath };};加入正常的坐下到右下的遍历顺序
Manager.prototype.getPaths = function (direction) { let rowPath = []; let columnPath = []; for (let i = 0; iManager.prototype.getPaths = function (direction) { let rowPath = []; let columnPath = []; for (let i = 0; i代码实现-找寻目标位置
// 寻找移动方向目标位置Manager.prototype.getNearestAvaibleAim = function (aim, direction) { // 位置 + 方向向量的计算公式 function addVector(position, direction) { return { row: position.row + direction.row, column: position.column + direction.column, }; } aim = addVector(aim, direction); // 获取grid中某个位置的元素 let next = this.grid.get(aim); // 如果next元素存在(也就是此目标位置已经有Tile),或者是超出游戏边界,则跳出循环。目的:就是找到最后一个空白且不超过边界的方格 while (!this.grid.outOfRange(aim) && !next) { aim = addVector(aim, direction); next = this.grid.get(aim); } // 这时候的aim总是多计算了一步,因此我们还原一下 aim = { row: aim.row - direction.row, column: aim.column - direction.column, }; return { aim, next, };};通过循环,找寻最后一个空白位置
// 如果next元素存在(也就是此目标位置已经有Tile,// 或者是超出游戏边界,则跳出循环。// 目的:就是找到最后一个空白且不超过边界的方格while (!this.grid.outOfRange(aim) && !next) { aim = addVector(aim, direction); next = this.grid.get(aim);}Grid新增两个方法
// grid.js// 获取某个位置的TileGrid.prototype.get = function (position) { if (this.outOfRange(position)) { return null; } return this.cells[position.row][position.column];};// 判断某个位置是否超出边界Grid.prototype.outOfRange = function (position) { return ( position.row = this.size || position.column = this.size );};Tile移动
Tile移动处理
1.根据方向获取遍历顺序,跟随顺序进行遍历
2.遍历时候,如果此位置上有Tie,则进行移动
3.根据当前Tle的位置和方向,获取目标移动位置
4.进行Tile移动
5.只要有一个节点产生移动,则重新调用渲染器渲染grid
// manager.jsManager.prototype.listenerFn = function(direction) { // 定义一个变量,判断是否引起移动 let moved = false; const { rowPath, columnPath } = this.getPaths(direction); for (let i = 0; iMoveFile处理
1.Tile对应的Grid原始位置设置为nul
2.更新Tile的position
3.将更新后的Tile设置到Grid新的位置
// manager.js// 移动Tile,先将grid中老位置删除,在添加新位置Manager.prototype.moveTile = function(tile, aim) { this.grid.cells[tile.row][tile.column] = null; tile.updatePosition(aim); this.grid.cells[aim.row][aim.column] = tile;};// tile.js// 更新Tile的位置Tile.prototype.updatePosition = function(position) { this.row = position.row; this.column = position.column;};监听联调
Listener监听回调中调用listenerFn方法,代码如下:
let self = this;this.listener = new Listener({ move: function(direction) { self.listenerFn(direction); }});这涉及到JS作用域,因为回调函数function(direction)是由Listener调用的,因此this会指向Listener,并不是Manager。在这种情况下,如果使用this.listenerFn将无法找到listenerFn方法,因此我们需要在方法调用之前(this还未改变之前)将this先保存到self
Render
// render.js// 渲染整个grid, 在之前先清空所有的TileRender.prototype.render = function(grid) { this.empty(); ...};Render.prototype.empty = function() { this.tileContainer.innerHTML = '';};总结:最终类图
2048合并处理
Tile合并规则
规则只有一个:方格移动到不能移动为止,并且下一个位置的Vaue
值和该方格value值一样。
// 寻找移动方向目标位置Manager.prototype.getNearestAvaibleAim = function(aim, direction) { //... return { aim, next };};Tile合并代码
// 移动核心逻辑Manager.prototype.listenerFn = function(direction) { // 定义一个变量,判断是否引起移动 let moved = false; const { rowPath, columnPath } = this.getPaths(direction); for (let i = 0; i新增Grid的remove方法,删除某个TIle
// grid.jsGrid.prototype.remove = function(tile) { this.cells[tile.row][tile.column] = null;};完善游戏步骤
TIle合并后置逻辑
当T11e合并或移动之后,游戏还得继续,因此每次移动之后,我们让游戏随机再次生成一个Ti1e。
//manager.js// 随机添加一个节点Manager.prototype.addRandomTile = function() { const position = this.grid.randomAvailableCell(); if (position) { // 90%概率为2,10%为4 const value = Math.random()修改调用区域代码,如下:
// manager.jsManager.prototype.start = function() { for (let i = 0; i2048动画效果
移动动画(UML图)
方块移动动画
1.使用CSS特性transition:transform 100ms ease-in-out给transform加入动画效果。
2.因为我们的每个T11e节点是临时创建的,并不会出现class切换的效果,当然也不会出现transform值变化过程,无法使用动画。我们可以使用一个猥琐逻辑,首先将Tile class设置为原始位置,然后延迟设置为当前位置。
const p = document.createElement('p');p.setAttribute('class', 'tile-position-1-1');setTimeout(() => { p.setAttribute('class', 'tile-posiiton-1-4');}, 16);// tile.jsfunction Tile(position, value) { this.row = position.row; this.column = position.column; this.value = value; // 新增prePosition属性 this.prePosition = null;}Tile.prototype.updatePosition = function(position) { // 更新的时候,先将当前位置,保存为prePosition this.prePosition = { row: this.row, column: this.column }; this.row = position.row; this.column = position.column;};// render.js// 渲染单个tileRender.prototype.renderTile = function(tile) { // 创建一个tile-inner const tileInner = document.createElement('p'); tileInner.setAttribute('class', 'tile-inner'); tileInner.innerHTML = tile.value; // 创建一个tile const tileDom = document.createElement('p'); let classList = [ 'tile', `tile-${tile.value}`, `tile-position-${tile.row + 1}-${tile.column + 1}` ]; if (tile.prePosition) { // 先设置之前的位置 classList[2] = `tile-position-${tile.prePosition.row + 1}-${tile.prePosition .column + 1}`; // 延迟设置当前的位置 setTimeout(function() { classList[2] = `tile-position-${tile.row + 1}-${tile.column + 1}`; tileDom.setAttribute('class', classList.join(' ')); }, 16); } tileDom.setAttribute('class', classList.join(' ')); tileDom.appendChild(tileInner); this.tileContainer.appendChild(tileDom);};.tile { position: absolute; width: $tile-size; height: $tile-size; border-radius: 4px; /*transition 移动动画*/ transition: transform 100ms ease-in-out;}移动动画(二)
// tile.jsfunction Tile(position, value) { this.row = position.row; this.column = position.column; this.value = value; // 新增prePosition属性 this.prePosition = null; // 存储merged两个Tile this.mergedTiles = null;}// 移动核心逻辑Manager.prototype.listenerFn = function(direction) { //... if (next && next.value === tile.value) { // 合并位置是next的位置,合并的value是tile.value * 2 const merged = new Tile( { row: next.row, column: next.column }, tile.value * 2 ); this.score += merged.value; //... if (merged.value === this.aim) { this.status = 'WIN'; } // 特别注意下面两句话 merged.mergedTiles = [tile, next]; tile.updatePosition({ row: next.row, column: next.column }); moved = true; } // ...};// 特别注意下面两句话merged.mergedTiles = [tile, next];tile.updatePosition({ row: next.row, column: next.column });我们提前存储merged行为发生的两个原始节点,并更新原始TiLe的位置,让其产生移动效果,最后继续修改渲染代码:
@keyframes appear { 0% { opacity: 0; transform: scale(0); } 100% { opacity: 1; transform: scale(1); }}2048本地存储
保存进度
1.想想有哪些信息需要被保存?
1.当前分数
2.历史最高分
3.当前的方格面板Grid,每一个方格里面的数字
2.想想在什么时候进行保存呢?
分数变化,方格里面数字变化都应该保存,所以应该在每一次移动之
后,渲染之前进行保存,也就是如下代码区域:
//manager.jsManager.prototype._render = function() { // 添加在此处进行处理 this.render.render(this.grid, { score: this.score, status: this.status });};3.想想使用什么技术进行保存呢?
毫无疑问,当然选择window.localStorage。
4.应该在什么时候恢复进度呢?
当页面重新加载的时候,初始化的时候,如果有历史进度,则加载历
史进度。
// 历史最高分const BestScoreKey = '2048BestScore';// 方格状态 和 分数const CellStateKey = '2048CellState';function Storage() {}Storage.prototype.setCellState = function({ score, grid }) { // 存储方格状态 和 分数};Storage.prototype.getCellState = function() { // 获取方格状态};本地存储(二)
序列化和反序列化
将对象信息变成字符串信息,我们通常叫做序列化。在这里我们分两步进行:
1.将grid变成通用的json格式
2.利用Json.stringify()将json序列化为字符串。
//tile.jsTile.prototype.serialize = function() { return { position: { row: this.row, column: this.column }, value: this.value };};继续序列化grid
//grid.jsGrid.prototype.serialize = function() { const cellState = []; // cellState 是一个二维数组,分别存储整个Grid信息。 // 如果该位置有Tile, 则返回 Tile序列化结果 // 如果该位置没有Tile,则存储null for (let row = 0; row反序列化
function Grid(size = 4, state) { this.size = size; this.cells = this.init(size); // 如果有之前的进度,则恢复 if (state) { this.recover(state); }}Grid.prototype.recover = function({ size, cells }) { this.size = size; // 遍历这个二维数组,如果某个cell存在,则新建一个Tile节点。 for (let row = 0; row历史进度流程(
1. 获取storage.js)存储/获取逻辑
const CellStateKey = '2048CellState';//...// 存储方格状态和分数Storage.prototype.setCellState = function({ score, grid }) { window.localStorage.setItem( CellStateKey, JSON.stringify({ score, grid: grid.serialize() }) );};// 获取方格信息Storage.prototype.getCellState = function() { const cellState = window.localStorage.getItem(CellStateKey); return cellState ? JSON.parse(cellState) : null;};2. 完善listenerFn方法,每一步移动后,都进行存储。
function Manager(size = 4, aim = 2048) { //... // 新增storage属性 this.storage = new Storage(); //...}Manager.prototype._render = function() { // 渲染之前调用存储 this.storage.setCellState({ score: this.score, grid: this.grid }); this.render.render(this.grid, { score: this.score, status: this.status });};3. 完善恢复方格状态的逻辑
3.1. manager.js
// manager.jsManager.prototype.defaultStart = function() { const state = this.storage.getCellState(); // 如果存在缓存则恢复 if (state) { this.score = state.score; this.status = 'DOING'; this.grid = new Grid(this.size, state.grid); this._render(); } else { this.start(); }};3.2. grid.js
//grid.jsfunction Grid(size = 4, state) { this.size = size; this.cells = []; this.init(size); //调用init方法进行初始化 // 如果有之前的进度,则恢复 if (state) { this.recover(state); }} //Grid对象表示一个游戏棋盘,大小为size*size的方格,参数state恢复游戏进度Grid.prototype.recover = function({ size, cells }) { this.size = size; //通过遍历cells二维数组,如果某个单元格存在内容,则新建一个Tile对象并放在Grid对象的位置上 for (let row = 0; row 0) { //如果存在空闲方格,利用Math.random()随机获取其中的某一个 return cells[Math.floor(Math.random() * cells.length)]; }};// 获取某个位置的TileGrid.prototype.get = function(position) { if (this.outOfRange(position)) { return null; } return this.cells[position.row][position.column];};// 判断某个位置是否超出边界Grid.prototype.outOfRange = function(position) { return ( position.row = this.size || position.column = this.size );};//满足以下任何条件,则判断trueGrid.prototype.serialize = function() { const cellState = []; // cellState 是一个二维数组,分别存储整个Grid信息。 // 如果该位置有Tile, 则返回 Tile序列化结果 // 如果该位置没有Tile,则存储null for (let row = 0; row3.3. listener.js
function Listener({ move: moveFn, start: startFn }) {//接受对象中的两个属性作为参数 window.addEventListener('keyup', function(e) { switch (e.code) {//注册键盘监听器,按键后会触发指定的事件 case 'ArrowUp': moveFn({ row: -1, column: 0 }); break; case 'ArrowLeft': moveFn({ row: 0, column: -1 }); break; case 'ArrowRight': moveFn({ row: 0, column: 1 }); break; case 'ArrowDown': moveFn({ row: 1, column: 0 }); break; }//条件分支结构,用于根据不同的表达式值执行不同的代码块 }); const buttons = document.querySelectorAll('button'); for (let i = 0; i3.4. manage.js
function Manager(size = 4, aim = 256) { this.size = size; this.aim = aim; this.render = new Render(); //处理游戏界面的渲染 this.storage = new Storage(); //处理游戏状态的存储 let self = this; //便于在后续的回调函数内部访问到正确的this对象 this.listener = new Listener({ move: function (direction) { self.listenerFn(direction); }, //用户执行移动操作时调用方法 start: function () { self.start(); //游戏开始时触发方法 }, }); this.defaultStart(); //默认的初始化设置} //以上为创建一个游戏管理器对象,方法绑定事件响应用户操作和游戏开始Manager.prototype.defaultStart = function () { const state = this.storage.getCellState(); //获取储存对象的游戏状态 let bestScore = this.storage.getBestScore(); //获取最佳得分 if (!bestScore) { bestScore = 0; } this.bestScore = bestScore; //获取游戏管理器对象最佳得分 // 如果存在缓存则恢复 if (state) { this.score = state.score; //恢复游戏当前得分 this.status = "DOING"; //游戏状态进行中 this.grid = new Grid(this.size, state.grid); //传入游戏大小和从 state.grid 中获取的游戏棋盘格子状态 //以恢复游戏棋盘 this._render(); //进行页面渲染,将恢复的游戏状态显示在界面上 } else { this.start(); }}; //根据存储的游戏状态或者新开始一个游戏,来进行默认的游戏初始化设置Manager.prototype.start = function () { this.score = 0; //计分板清0 this.status = "DOING"; //游戏状态设置为进行中 this.grid = new Grid(this.size); //创建游戏盘面 for (let i = 0; i this.bestScore) { this.bestScore = this.score; this.storage.setBestScore(this.bestScore); } this.render.render(this.grid, { //调用render属性的render方法 score: this.score, status: this.status, bestScore: this.bestScore, });}; //将游戏盘面(grid)的状态渲染到前端界面上// 随机添加一个节点Manager.prototype.addRandomTile = function () { const position = this.grid.randomAvailableCell(); //获取可用的随机空白格子 if (position) { // 90%概率为2,10%为4 const value = Math.random() 0) { return false; } for (let row = 0; row3.5. render.js
//render.jsfunction Render() { this.tileContainer = document.querySelector(".tile-container"); this.scoreContainer = document.querySelector(".now .value"); this.statusContainer = document.querySelector(".status"); this.bestScoreContainer = document.querySelector(".best .value");} //构造函数 Render(),用于初始化游戏界面的相关元素// 渲染整个游戏界面Render.prototype.render = function (grid, { score, status, bestScore }) { this.empty(); //用于清空游戏界面,即移除之前渲染的方块 this.renderScore(score); this.renderBestScore(bestScore); this.renderStatus(status); //分别用于渲染得分、历史最高分和游戏状态 for (let row = 0; row3.6. storage.js
// 历史最高分const BestScoreKey = "2048BestScore";// 方格状态和分数const CellStateKey = "2048CellState";function Storage() {}//为 Storage 对象提供构造函数,并为该构造函数创建的对象实例提供方法Storage.prototype.setBestScore = function (bestScore) { window.localStorage.setItem(BestScoreKey, bestScore);}; //将指定键名和键值作为参数来设置本地存储Storage.prototype.getBestScore = function () { return window.localStorage.getItem(BestScoreKey);}; //返回本地存储中键名为BestScoresKey的值,即最高分// 存储方格状态和分数Storage.prototype.setCellState = function ({ score, grid }) { window.localStorage.setItem( CellStateKey, JSON.stringify({ //转换为JSON字符串 score, grid: grid.serialize(), }) ); //以便下一次打开游戏页面时可以恢复之前的状态}; //将方格状态和得分序列化成 JSON 字符串并将其保存到浏览器的本地存储中// 获取方格信息Storage.prototype.getCellState = function () { const cellState = window.localStorage.getItem(CellStateKey); return cellState ? JSON.parse(cellState) : null;};//从本地存储中获取之前保存的方格状态和得分,并将其解析为对象后返回//如果之前没有保存过数据,则返回 null3.7. tile.js
function Tile(position, value) { this.row = position.row; this.column = position.column; this.value = value; // 新增prePosition属性 this.prePosition = null; // 存储merged两个Tile this.mergedTiles = null;}Tile.prototype.updatePosition = function(position) { // 更新的时候,先将当前位置保存为prePosition this.prePosition = { row: this.row, column: this.column }; this.row = position.row; this.column = position.column;};Tile.prototype.serialize = function() { return { position: { row: this.row, column: this.column }, value: this.value };};new Manager();
上一篇:租赁海外云服务器怎么收费?