小工具知识网

当前位置: 首页 > 行业知识 > 2048小游戏(前端三板斧详细笔记,JavaScript初步详细教程)

行业知识

2048小游戏(前端三板斧详细笔记,JavaScript初步详细教程)

2026-11-14 06:32:17 来源:互联网转载

目录

项目源码: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; i  

2048移动处理

重构-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; i  
Manager.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; i  

MoveFile处理

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; i  

2048动画效果

移动动画(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; row  
3.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; i  
3.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; row  
3.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; row  
3.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;};//从本地存储中获取之前保存的方格状态和得分,并将其解析为对象后返回//如果之前没有保存过数据,则返回 null
3.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();

 

上一篇:租赁海外云服务器怎么收费?

下一篇:FTSP下载指南,轻松获取,极速上手操作!