相信大家都玩过 2048 吧!什么,你还没玩过??那就快来跟我一起玩一下吧,顺便了解一下如何使用 canvas + js 开发一款我们自己的 2048 吧!话不多说,let’s go~

2048 试玩

首先我们还是先来玩一下 2048,并且了解一下到底 2048 的相关规则。可以看一下下面的动态图片,应该就能了解 2048 的大致玩法了,如图:

上图中,我们可以通过控制键盘的 上下左右 键来移动对于的数字,当两个数字相同时,它们就会合成更大的数字,例如 22 可以合成 4,同理越大的数字合成的数字也越大,当整个区域没有可以合成的数字后,游戏就结束了,这就是 2048 的大致规则了,下面我们就一起来看一下该如何实现一个自定义的 2048 吧!

基础架子

因为我们整个游戏是基于 canvas 来编写的,但是还需要有一个基础的架子,我们使用 html + css 来完成,大致的 html 代码如下:

<div class='container'><div class='fluid'><h1>2048</h1><div class='edits'><div class='size-block'><div class='set-size'><p id='size-title'>Size: </p><input id='size' type='number' value='4'></div><div class='btns'><div class='btn start'>Start</div><div class='btn reset'>Reset</div></div></div><div class='scores'><span id='score'>Score: 0</span><span id='bestScore'>Best Score: 0</span></div></div><div id='canvas-block'><div class='start2'>&gt; Start &lt;</div><div class='lose'>Game Over!</div><canvas id='canvas' width='500' height='500'></canvas></div></div>
</div>

在上面的代码中,我们通过在页面添加 input 输入框,让玩家可以自定义 2048 的游戏区域,通过输入不同的数字,可以生成不同的游戏区域,当然有个限制,最小是 3 块,最大是 10 块,并且还添加了相关的游戏数据,例如当前的游戏分数,以及当游戏结束时,我们会通过 localstorage 将最高分数记录在本地,这样当下次再玩时,如果获得的分数没有之前的分数高,则不会进行分数的更新,反之就会将最新的分数存储在本地并展示在页面上。

有了基本的 html 架子还不够,还需要添加相关的 css 代码才能让我们的游戏有一个基本的样子,这里只截取部分 css 代码,完整的代码会在最后放出,css 相关的代码如下:

...
#canvas{background: rgba(var(--blue),1);margin-top: 30px;box-sizing: border-box;
}
h1{font-size: 58px;color: white;margin-top: 30px;
}
.scores{width: 50%;display: flex;flex-direction: column;justify-content: center;align-items: flex-end;
}
.edits{min-width: 500px;width: 100%;display: flex;flex-direction: row;justify-content: space-between;align-content: center;margin-top: 30px;font-size: 24px;
}
.set-size{width: 100%;display: flex;justify-content: flex-start;align-items: center;flex-direction: row;
}
.size-block{display: flex;flex-direction: column;width: 50%;justify-content: center;align-items: flex-start;
}
.btns, #bestScore{margin-top: 15px;
}
.size-block input{margin-left: 10px;max-width: 50px;
}
#size{background: transparent;border: 2px solid white;color: white;font-size: 18px;padding: 5px 8px 5px 5px;width: 60px;outline: none;box-sizing: border-box;text-align: center;
}
.lose, .start2{z-index: 999;position: absolute;margin-top: 15px;font-size: 35px;display: none;
}
.start2{display: block;min-width: 150px;margin-top: 200px;cursor: pointer;
}
#canvas-block{position: relative;display: flex;justify-content: center;align-items: center;
}
.btns{display: flex;flex-direction: row;justify-content: flex-start;align-items: center;cursor: pointer;
}
.reset{margin-left: 15px;
}

通过 htmlcss 的加成,最终的展示如下图所示:

因为我们还没有添加相关的 js 代码,因此整个页面上除了一些静态的文字以外,别的基本都看不到了。

基础的架子已经搭建好了,那么我们就来实现这个游戏吧!

js + canvas 实现

我们使用 canvas 的时候本身就是需要配合 js 来进行开发的,因为 canvas 只是一个标签元素,就跟普通的 div 元素一样,只是它上面提供了很多方法给我们使用。这里我们使用 ES6 相关的知识点来进行开发,如果对 ES6 还不熟悉的童鞋,可以点击这里进行学习。

因为使用的是 ES6 ,因此我们直接使用 面向对象 的写法来开发这个游戏。在 ES6 中,它给我们提供了一个 class 语法糖,让我们可以不需要在构造函数的原型上去添加方法,不过本质上也还是一样的,这里不做深究。

首先我们有一个 ,这个 的名字你可以随意定,这里就叫做 Game,然后需要在这个 构造函数 中初始化相关的内容,让我们可以在后续使用,代码如下:

class Game {constructor() {this.canvas = document.getElementById('canvas');this.ctx = this.canvas.getContext('2d');this.sizeInput = document.getElementById('size');this.startBtn = document.querySelector('.start');this.startBtn2 = document.querySelector('.start2');this.scoreLabel = document.getElementById('score');this.resetBtn = document.querySelector('.reset');this.lessHtml = document.querySelector('.lose');this.scoreValue = 0;this.bestScore = document.getElementById('bestScore');this.bestScoreValue = localStorage.getItem('score2048');this.size = 4;this.width = this.canvas.width / this.size - 6;this.cells = [];this.fontSize = 0;this.loss = false;}
}

在这个 构造函数 中,我们将页面中需要用到的元素通过 document.getElementById 或者 document.querySelector 选取到,这样后续就可以直接使用。

基本的信息有了后,我们就该初始化游戏的相关信息了,代码如下:

...init() {this.startBtn.addEventListener('click', () => {this.publicEvent();});this.resetBtn.addEventListener('click', () => {this.scoreValue = 0;this.canvas.style.opacity = '1';this.loss = false;this.lessHtml.style.display = 'none';this.bestScoreValue = localStorage.getItem('score2048');this.scoreLabel.innerHTML = `Score: ${+this.scoreValue}`;this.startGame();this.initScore();});
}initState() {this.initStart();this.initScore();this.initEvent();
}initStart() {this.canvas.style.display = 'none';this.startBtn2.addEventListener('click', () => {this.publicEvent();});
}publicEvent() {if (this.sizeInput.value >= 3 && this.sizeInput.value <= 10) {this.size = this.sizeInput.value;this.width = this.canvas.width / this.size - 6;this.canvasClear();this.startGame();this.canvas.style.display = 'block';this.startBtn2.style.display = 'none';} else {alert('不在生成的区间内,无法开始游戏');return;}
}...

在初始化的过程中,我们需要给页面上的相关元素添加点击事件,包括开始游戏、重置游戏等内容,然后我们需要在 Game 的 构造函数,也就是上述的 constructor 中执行 init 方法,这样当整个 实例化 的时候,就会自动将各种初始化信息及绑定事件完成。

当初始信息和绑定事件都完成后,页面中的 canvas 上目前还是没有任何内容显示的,因此我们就需要通过 canvas 相关的 api 来生成对应的方块和数字了。

这里我们还需要一个新的 ,用来帮我们生成每一个小方块里面的初始值和它的位置信息,这样我们才能在 canvas 上画出来,小方块的生成代码如下:

class Cell {constructor(row, col, width) {this.value = 0;this.x = col * width + 5 * (col + 1);this.y = row * width + 5 * (row + 1);}
}

这个 很简单,只有三个属性 ,其中 value 默认为 0,这么做是为了后面初始化的时候根据不同的 value 生成不同颜色的方块。其次是 xy,这两个值主要是这个小方块在 canvas 中的位置信息,通过从外部传入的行、列以及宽度来生成。

接下来我们只需要根据前面设置的 初始值 或者 input 中输入的值来生成对应的游戏区域即可,代码如下:

async createCells() {for (let i = 0; i < this.size; i++) {this.cells[i] = [];for (let j = 0; j < this.size; j++) {this.cells[i][j] = new Cell(i, j, this.width);}}
}

上述代码中,通过两个循环来创建小方块的位置信息,最终的数据如下图所示:

通过上图可以看到我们已经生成了一个 4 * 4 的格子,并且里面的每一个小方块的 x轴y轴 信息都已经有了,接下来我们就可以通过上面的信息来生成一个基本的游戏区域了,生成的游戏区域如下图所示:

那么我们是如何生成上面这样的游戏区域的呢?让我们一起来看代码,如下:

...other codeasync drawAllCells() {for (let i = 0; i < this.size; i++) {for (let j = 0; j < this.size; j++) {this.drawCell(this.cells[i][j]);}}
}...other code

通过调用 drawAllCells 方法,在内部循环执行 drawCell 方法,然后得到上图。我们一起来看一下 drawCell 内部是如何实现的,代码如下:

drawCell(cell) {this.ctx.beginPath();this.ctx.rect(cell.x, cell.y, this.width, this.width);this.ctx.fillStyle = "#384081";this.ctx.fill();
}

通过上述代码可以看到,我们直到现在才真正使用到了 canvas 上面提供的相关方法。首先我们要调用画布上面的 beginPath 方法,这个 API 主要用于 “作画” 的开始,因此它是必须的,只要使用 canvas 绘制,就一定需要这个方法;然后我们使用了 rect 方法,它主要用于绘制一个方块,其中有四个参数,前两个参数分别是需要绘制方块的 x轴坐标y轴坐标,后两个参数就是这个方块的 宽度高度,这些信息在前面我们都准备好了,因此这里就可以直接拿来用;最后我们需要给方块添加颜色,也就是 fillStyle,并完成收尾,也就是填充这个方块 fill,这样我们就得到了如上图所示的游戏区域。

当我们将这些准备工作完成后,接下来就要实现当玩家点击开始后,游戏区域生成对应的小方块和数字了,那么该如何做呢?在前面我们初始化的时候,已经给相关的按钮添加了点击事件,并且绑定了一个 startGame 方法,让我们看一下这个方法内部的实现,如下:

 async startGame() {await this.createCells();await this.drawAllCells();await this.pasteNewCell();await this.pasteNewCell();
}

这个方法很简单,只是调用了 createCellsdrawAllCells 方法用于生成基本的游戏区域,然后后面连续调用了两次 pasteNewCell 方法,那 pasteNewCell 内部实现了什么呢?让我们一起来看一下代码,如下:

async pasteNewCell() {let countFree = 0;for (let i = 0; i < this.size; i++) {for (let j = 0; j < this.size; j++) {if (!this.cells[i][j].value) {countFree++;}}}if (!countFree) {this.finishGame();return;}while (true) {let row = Math.floor(Math.random() * this.size);let col = Math.floor(Math.random() * this.size);if (!this.cells[row][col].value) {this.cells[row][col].value = 2 * Math.ceil(Math.random() * 2);this.drawAllCells();return;}}
}

pasteNewCell 内部,我们通过不断的循环来生成新的游戏方法,其中最主要的就是判断当前小方块的值是否为 0,如果不为 0,就会画一个新的方块展示在游戏区域内,反之则不执行。

通过两次执行 pasteNewCell 方法,是为了在初始化的时候生成两个数字,这样才能开始游戏。而通过生成的不同方法内的值来生成不同颜色的小方法,这是如何实现的呢?还记得前面的 drawCell 方法吗?这里其实还有一部分代码之前没有放出来,我们一起来看看,如下:

drawCell(cell) {this.ctx.beginPath();this.ctx.rect(cell.x, cell.y, this.width, this.width);this.ctx.fillStyle = "#384081";this.ctx.fill();if (cell.value) {this.ctx.fillStyle = `${this.cellColor(cell.value)}`;this.ctx.fill();this.fontSize = this.width / 2;this.ctx.font = this.fontSize + 'px Viga';this.ctx.fillStyle = 'white';this.ctx.textAlign = "center";this.ctx.fillText(cell.value, cell.x + this.width / 2, cell.y + this.width / 1.5);}
}cellColor(value) {const colorList = new Map([[0, 'rgb(135,200,116)'],[2, 'rgb(135,200,116)'],[4, 'rgb(95,149,212)'],[8, 'rgb(139,89,177)'],[16, 'rgb(229,195,81)'],[32, 'rgb(202,77,64)'],[64, 'rgb(108,129,112)'],[128, 'rgb(207,126,63)'],[256, 'rgb(82,125,124)'],[512, 'rgb(191,76,134)'],[1024, 'rgb(119,41,92)'],[2048, 'rgb(118,179,194)'],[4096, 'rgb(52,63,79)'],]);return colorList.get(value) || 'rgba(70,80,161,0.8)';
}

上述代码与前面的代码相比,通过判断当前的小方块的值是否为真,也就是当前小方块的值是否不为 0,从而重新生成一个新的小方块。在 drawCell 内部,通过新生成的小方块的 value 值来调用 cellColor 方法,在 cellColor 方法内,我们使用 ES6 中的 Map 方法来匹配到当前不同 value 所对应的颜色值,这样比直接使用 if…elseswitch…case… 要更加清晰明了。

最后我们还需要添加相关的键盘绑定的事件,在最开始初始化的时候我们已经给 document 绑定了相关的键盘事件,代码如下:

initEvent() {document.addEventListener('keydown', (event) => {if (!this.loss && this.canvas.style.display === 'block') {switch (event.key) {case 'ArrowUp':this.moveUp();break;case 'ArrowDown':this.moveDown();break;case 'ArrowLeft':this.moveLeft();break;case 'ArrowRight':this.moveRight();break;}this.scoreLabel.innerHTML = `Score: ${+this.scoreValue}`;}});
}

在上述的键盘事件中,通过监听玩家按下的键盘方法键,从而让我们的小方块进行相关的合并,这里截取部分代码,如下:

...other codemoveUp() {let row;for (let j = 0; j < this.size; j++) {for (let i = 1; i < this.size; i++) {if (this.cells[i][j].value) {row = i;while (row > 1) {if (!this.cells[row - 1][j].value) {this.cells[row - 1][j].value = this.cells[row][j].value;this.cells[row][j].value = 0;row--;} else if (this.cells[row][j].value === this.cells[row - 1][j].value) {this.cells[row - 1][j].value *= 2;this.scoreValue += this.cells[row - 1][j].value;this.cells[row][j].value = 0;break;} else {break;}}}}}this.pasteNewCell();
}moveDown() {let row;for (let j = 0; j < this.size; j++) {for (let i = this.size - 2; i >= 0; i--) {if (this.cells[i][j].value) {row = i;while (row + 1 < this.size) {if (!this.cells[row + 1][j].value) {this.cells[row + 1][j].value = this.cells[row][j].value;this.cells[row][j].value = 0;row++;} else if (this.cells[row][j].value === this.cells[row + 1][j].value) {this.cells[row + 1][j].value *= 2;this.scoreValue += this.cells[row + 1][j].value;this.cells[row][j].value = 0;break;} else {break;}}}}}this.pasteNewCell();
}...other code

在上面的代码中,通过获取当前的方块移动的方法,找到它左、右、上或者下方的方块,从而判断它们的值是否一致,如果一致就可以进行合并,反之则不能进行合并和移动。当玩家的游戏区域内已经没有任何可移动的小方块时,整个游戏就结束了,最终游戏结束的代码如下:

finishGame() {const currentScore = localStorage.getItem('score2048');if (currentScore < this.scoreValue) {localStorage.setItem('score2048', this.scoreValue);}this.canvas.style.opacity = '0.3';this.loss = true;this.lessHtml.style.display = 'block';
}

当游戏结束时,就像一开始说的一样,我们会获取当前的值以及存储在 localStorage 中的值进行对比,如果当前的值比之前的值要大,则会将新的值更新到 localStorage 中,最后在页面中展示一个 Game Over 告诉玩家游戏结束了。

最终整个游戏的实现在这里可以查看,也可以直接通过键盘玩耍,如下:

最后

通过上面对 ES6 的基本使用以及 canvas 中基础 API 的介绍,相信大家已经学会了如何开发一个 2048 小游戏了,那么就赶快动手实现一个你自己的 2048 吧!

还记得2048怎么玩吗?快来玩会儿(摸鱼)吧相关推荐

  1. 命令行玩斗地主,摸鱼再也不怕老板了!

    文章目录 背景 安装 连接服务器 游戏规则 游戏 背景 想必大家都在手机玩过斗地主吧?如果你想在上班时玩斗地主,又不想让老板看到你摸鱼,该怎么办呢? 今天教大家在命令行界面玩斗地主,不仅可以上班摸鱼, ...

  2. 摸鱼神器:在命令行中玩斗地主

    斗地主我想大家都会玩吧,但是不知道大家有没有在命令行内玩过斗地主.这个项目是基于 Netty 实现的一款命令行斗地主游戏,在下班后或者工作闲暇之余,你都可以肆无忌惮的在命令行中玩斗地主. 逛逛常用的摸 ...

  3. 带你一起来摸鱼(快看摸鱼方法了)

    摸鱼源自成语"浑水摸鱼"现多用在上班族之间,指上班时间偷懒,不认真工作.具体表现在上班时间做与工作无关的事.或为凑齐八小时工作时间,早早做好准备等着下班.俗话说,"只工作 ...

  4. 中信涂猪android抓包,玩卡计划 篇二:中信《鼠来宝》规则攻略,珠光宝气之后的活动!还记得去年的涂猪吗?...

    玩卡计划 篇二:中信<鼠来宝>规则攻略,珠光宝气之后的活动!还记得去年的涂猪吗? 2020-01-06 17:37:56 2点赞 9收藏 0评论 创作立场声明:1月6日活动即将开启,提前做 ...

  5. 第一款计算机游戏,还记得你玩过的第一款电脑游戏吗?那些青春已经回不去了...

    记得有一次,饭局上,大家热火朝天的在讨论着<英雄联盟>.忽然,有位颇有诗意的哥们,沉默了--然后忽然抬起头,双眼闪着泪光,问我们:还记得咱们在电脑上,玩过的第一款游戏是什么吗? 我们这几个 ...

  6. 那些年的java游戏_那些年我们曾经玩过的游戏,你还记得几个

    标题:那些年我们曾经玩过的游戏,你还记得几个 随着时间长河的推进,我们已经长大了.你还记得我们那些年一起玩过的游戏么? 弹弓 一般用树枝做弓架,也可以用旱伞的伞骨做弹弓架.要买弹力很大的像皮筋,就和那 ...

  7. java最早的手机网游_还记得你们玩过的最早的手机网游是什么吗?

    不知你们是否记得一个叫做<冒泡社区>的手游平台,在那个传统的功能机时代,冒泡社区可以算是国内第一的手游社区平台了,还记得初一的时候,我爸买了个杂牌子手机,当前用里面的"JAVA& ...

  8. 还记得小时候玩的坦克大战么,用Python就可以轻松实现

    <坦克大战>是由Namco游戏公司开发的一款平面射击游戏,于1985年发售.游戏以坦克战斗及保卫基地为主题,属于策略型联机类. 同时也是FC平台上少有的内建关卡编辑器的几个游戏之一,玩家可 ...

  9. sbc8600_还记得我复制的价值8600万美元的车牌扫描仪吗? 我抓住了它。

    sbc8600 by Tait Brown 泰特·布朗(Tait Brown) 还记得我复制的价值8600万美元的车牌扫描仪吗? 我抓住了它. (Remember the $86 million li ...

  10. 还记得年少时的梦吗?(文字版)[强烈推荐]

    童年的十大经典...之一 1.烤红薯 香喷喷,甜丝丝,好吃看得见.2毛钱一斤.只是吃多了会打屁.蠢人俗称"勺货" 2.爆米花 一个老头扛着一口葫芦一样的黑锅,走街串巷.将米变成白白 ...

最新文章

  1. Android 面试题目之 线程池
  2. XAMPP的MYSQL无法启动 -This may be due to a blocked port, missing dependencies,
  3. Android布局动态化,一种基于堆积木思想的卡片式Android动态布局方法与流程
  4. swagger 修改dto注解_Swagger介绍及使用
  5. android 直播 h5,H5移动端直播的要点
  6. TBSchedule调度平台疑难解答
  7. Mongo 创建数据库
  8. 计算机的kb和m之间的换算,g和兆的换算(G和M之间的换算)
  9. 松翰单片机--SN8F5702学习笔记(二)HelloWorld
  10. 【STM32】关于DMA控制器的介绍和使用
  11. 图神经网络(CNN)一
  12. 计算机桌面为什么没有语言栏了,语言栏不见了怎么办,教您语言栏不见了怎么办...
  13. 欧美html游戏安卓,HTML5 Games - Rated M or for 18+ only
  14. python爬取链家_通过Python爬取链家所有房源和小区信息
  15. 一文看完计算机基础知识总结
  16. wxpython后台如何更新界面信息_wxpython后台线程更新界面控件方法
  17. python用matplotlib画玫瑰_用Python matplotlib 怎么画风向玫瑰图 ,能给出程序的?
  18. C语言一维数组的定义与常见用法
  19. 基于JAVA高校社区生鲜配送系统计算机毕业设计源码+数据库+lw文档+系统+部署
  20. 随机森林的特征重要性排序

热门文章

  1. 现代支付系统的资金流向
  2. 开发一个微信小程序实例教程
  3. 单片机 STM32 HAL GSM通讯 SIM800L
  4. ppt —— 矢量图标库
  5. 基于Python+Django的微博评论热点舆情分析可视化系统的设计与实现-计算机毕业设计选题题目推荐
  6. 小米手环 / 运动手环 记步功能原理
  7. 威纶通触摸屏与仪表通讯_威纶通触摸屏与英威腾变频器通信详细说明
  8. 历年被3.15晚会曝光的科技企业现状
  9. APP上架市场隐私政策被拒(关于未经用户同意收集用户信息)
  10. 用微课学计算机应用基础--PPT制作