一、背景

在网上能查找到通过 Javascript 编写俄罗斯方块的资料,而且更牛的是只需要不到百行的代码就实现了,笔者非常佩服这些牛人。佩服之余,笔者也想尝试通过 Javascript 编写俄罗斯方块。在翻阅网上的资料和代码中,发现这些代码的可读性不高,因此不能让读者很好地去理解和学习代码。因此,笔者通过本文介绍自己如何通过面向对象的思想实现该游戏。

二、项目介绍

# 2.1 效果展示

# 2.2 实现思路

  1. 地图:大小已经通过 css 样式确定(300px x 600px)。

  2. 堆积方块:创建 200 个小方块(30px x 30px 的div),填充(通过二维数组存放)到地图中。通过 css 样式区分堆积的方块(done)和可活动的区域(none)。

  3. 下落方块:方块有7个种类,都是通过4个小方块(30px x 30px 的div)构成。其横向活动区域为[0,9],纵向活动区域为[0,19],通过 css 样式设置颜色,如下表示:

  1. [
  2. { x: 4, y: 0, className: "current_0" },
  3. { x: 3, y: 0, className: "current_0" },
  4. { x: 5, y: 0, className: "current_0" },
  5. { x: 6, y: 0, className: "current_0" }
  6. ]

坐标图表示如下:

  1. 移动方块:通过设置 position: absolute ,再动态设置 top 和 left 即可。

  2. 旋转方块:通过公式旋转,以上文的坐标案例演示:

  1. this.current = [
  2. { x: 4, y: 0, className: "current_0" },
  3. { x: 3, y: 0, className: "current_0" },
  4. { x: 5, y: 0, className: "current_0" },
  5. { x: 6, y: 0, className: "current_0" }
  6. ]
  7. for (var j = 1; j < this.current.length; j++) {
  8. var newX = this.current[0].y + this.current[0].x - this.current[j].y;
  9. var newY = this.current[0].y - this.current[0].x + this.current[j].x;
  10. this.current[j].x = newX;
  11. this.current[j].y = newY;
  12. }
  1. 消行:通过切换样式实现,具体内容下文介绍。

  2. 动态效果:通过 setInterval 不断刷新页面(调用 map 对象的 _refreshMap 方法改变方块位置)。

# 2.3 涉及技术

DOM操作、面向对象、事件操作和间隔函数 setInterval

# 2.4 项目结构

三、实现步骤

由于逻辑较为复杂,代码编写较长,因此只演示关键代码。

# 3.1 css 样式介绍

none 表示地图中的活动区域样式

current 开头的表示当前活动的方块样式

done 表示堆积的方块样式

游戏开始时,地图(300px x 600px)被 200 个小方块(30px x 30px 的div)填充,其 class 为 none。

当前方块在地图中下落时,设置 class 为 current 开头的样式。

当方块不能下落要堆积时,将其在地图当前区域的 div 样式由 none 改成 done。同时,将当前下落方块坐标设置为下一个方块坐标。

当方块消行时,遍历所有行,设置当前行的样式为上一行的样式,即修改坐标,让堆积的方块下落到消行的位置。

# 3.2 初始化地图

map.js 文件

  1. var Map = function(square) {
  2. // 边界
  3. this.minX = this.minY = 0;
  4. this.maxX = 9;
  5. this.maxY = 19;
  6. // 当前移动的方块
  7. this.square = square;
  8. // 用于记录完成下落动作的方块
  9. this.mapDivs = [];
  10. // 定时器id
  11. this.timeId = null;
  12. // 暂停标记
  13. this.pauseFlag = false;
  14. // 关闭标记
  15. this.closeFlag = false;
  16. // 阴影
  17. this.shadow = null;
  18. this.shadowFlag = false;
  19. }
  20. // 初始化
  21. Map.prototype.init = function(domObjs) {
  22. this.shadow = domObjs.shadow;
  23. // 绘制地图,即填充小方块
  24. for (var i = 0; i <= this.maxY; i++) {
  25. var arr = [];
  26. for (var s = 0; s <= this.maxX; s++) {
  27. var mapDiv = document.createElement("div");
  28. mapDiv.className = "none";
  29. mapDiv.style.top = (i * this.square.size) + "px";
  30. mapDiv.style.left = (s * this.square.size) + "px";
  31. domObjs.map.appendChild(mapDiv);
  32. arr.push(mapDiv);
  33. }
  34. this.mapDivs.push(arr);
  35. }
  36. // 当前下落方块
  37. for (var j = 0; j < this.square.current.length; j++) {
  38. var cdiv = document.createElement("div");
  39. cdiv.className = this.square.current[j].className;
  40. cdiv.style.left = this.square.current[j].x * this.square.size + "px";
  41. cdiv.style.top = this.square.current[j].y * this.square.size + "px";
  42. domObjs.map.appendChild(cdiv);
  43. this.square.currentDivs.push(cdiv);
  44. }
  45. if (this.shadowFlag) {
  46. this._showShadow();
  47. }
  48. // 下一个方块
  49. for (var k = 0; k < this.square.next.length; k++) {
  50. var ndiv = document.createElement("div");
  51. ndiv.className = this.square.next[k].className;
  52. ndiv.style.left = (this.square.next[k].x - 2) * this.square.size + "px";
  53. ndiv.style.top = this.square.next[k].y * this.square.size + "px";
  54. domObjs.next.appendChild(ndiv);
  55. this.square.nextDivs.push(ndiv);
  56. }
  57. var that = this;
  58. // 启动定时器
  59. this.timeId = setInterval(function() {
  60. if (!that.pauseFlag) {
  61. that.square.moveDown(that);
  62. that._refreshMap();
  63. }
  64. }, 300);
  65. // 添加键盘监听器
  66. this.addEventListener();
  67. }
  68. // 刷新地图
  69. Map.prototype._refreshMap = function() {
  70. var squareDivs = this.square.currentDivs;
  71. for (var j = 0; j < squareDivs.length; j++) {
  72. squareDivs[j].className = this.square.current[j].className;
  73. squareDivs[j].style.top = this.square.current[j].y * this.square.size + "px";
  74. squareDivs[j].style.left = this.square.current[j].x * this.square.size + "px";
  75. }
  76. // 阴影
  77. this._showShadow();
  78. var nextDivs = this.square.nextDivs;
  79. for (var k = 0; k < nextDivs.length; k++) {
  80. nextDivs[k].className = this.square.next[k].className;
  81. nextDivs[k].style.left = (this.square.next[k].x - 2) * this.square.size + "px";
  82. nextDivs[k].style.top = this.square.next[k].y * this.square.size + "px";
  83. }
  84. }

# 3.3 创建方块

square.js 文件

  1. // 方块由4个小方块组成
  2. var Square = function(info) {
  3. this.info = info;
  4. // 小方块大小
  5. this.size = 30;
  6. // 当前下落方块div
  7. this.currentDivs = [];
  8. // 下一个方块div
  9. this.nextDivs = [];
  10. // 当前方块坐标对象
  11. this.current = null;
  12. // 下一个方块坐标对象
  13. this.next = null;
  14. this._init();
  15. }
  16. // 初始化
  17. Square.prototype._init = function() {
  18. if (this.next == null) {
  19. this.current = this._getSquareType();
  20. } else {
  21. this.current = this.next;
  22. }
  23. this.next = this._getSquareType();
  24. }
  25. // 随机获取方块,7种方块类型
  26. Square.prototype._getSquareType = function() {
  27. // 坐标顺序决定旋转点
  28. var data = [
  29. [
  30. { x: 4, y: 0, className: "current_0" },
  31. { x: 3, y: 0, className: "current_0" },
  32. { x: 5, y: 0, className: "current_0" },
  33. { x: 6, y: 0, className: "current_0" }
  34. ],
  35. [
  36. { x: 4, y: 0, className: "current_1" },
  37. { x: 3, y: 0, className: "current_1" },
  38. { x: 4, y: 1, className: "current_1" },
  39. { x: 5, y: 0, className: "current_1" }
  40. ],
  41. [
  42. { x: 4, y: 0, className: "current_2" },
  43. { x: 3, y: 0, className: "current_2" },
  44. { x: 3, y: 1, className: "current_2" },
  45. { x: 5, y: 0, className: "current_2" }
  46. ],
  47. [
  48. { x: 4, y: 0, className: "current_3" },
  49. { x: 3, y: 1, className: "current_3" },
  50. { x: 4, y: 1, className: "current_3" },
  51. { x: 5, y: 0, className: "current_3" }
  52. ],
  53. [
  54. { x: 5, y: 0, className: "current_4" },
  55. { x: 4, y: 0, className: "current_4" },
  56. { x: 4, y: 1, className: "current_4" },
  57. { x: 5, y: 1, className: "current_4" }
  58. ],
  59. [
  60. { x: 4, y: 0, className: "current_5" },
  61. { x: 3, y: 0, className: "current_5" },
  62. { x: 5, y: 0, className: "current_5" },
  63. { x: 5, y: 1, className: "current_5" }
  64. ],
  65. [
  66. { x: 4, y: 0, className: "current_6" },
  67. { x: 3, y: 0, className: "current_6" },
  68. { x: 4, y: 1, className: "current_6" },
  69. { x: 5, y: 1, className: "current_6" }
  70. ]
  71. ];
  72. return data[Math.floor(Math.random() * data.length)];
  73. }

# 3.4 移动方块

square.js 文件

  1. // 移动方块
  2. Square.prototype._move = function(moveX, moveY, map) {
  3. for (var i = 0; i < this.current.length; i++) {
  4. var newX = this.current[i].x + moveX;
  5. var newY = this.current[i].y + moveY;
  6. if (this._isOverZone(newX, newY, map)) {
  7. return false;
  8. }
  9. }
  10. for (var j = 0; j < this.current.length; j++) {
  11. this.current[j].x += moveX;
  12. this.current[j].y += moveY;
  13. }
  14. return true;
  15. }
  16. // 向左移动
  17. Square.prototype.moveLeft = function(map) {
  18. this._move(-1, 0, map);
  19. }
  20. // 向右移动
  21. Square.prototype.moveRight = function(map) {
  22. this._move(1, 0, map);
  23. }
  24. // 坠落
  25. Square.prototype.fastDown = function(map) {
  26. while (this._move(0, 1, map));
  27. }

# 3.5 旋转方块

square.js 文件

  1. // 旋转
  2. Square.prototype.round = function(map) {
  3. // 田字方块不用旋转
  4. if (this.current[0].className == "current_4") {
  5. return;
  6. }
  7. for (var i = 1; i < this.current.length; i++) {
  8. var newX = this.current[0].y + this.current[0].x - this.current[i].y;
  9. this._modify(newX, map);
  10. }
  11. for (var j = 1; j < this.current.length; j++) {
  12. var newX = this.current[0].y + this.current[0].x - this.current[j].y;
  13. var newY = this.current[0].y - this.current[0].x + this.current[j].x;
  14. this.current[j].x = newX;
  15. this.current[j].y = newY;
  16. }
  17. }

# 3.6 消行

square.js 文件

  1. // 向下移动
  2. Square.prototype.moveDown = function(map) {
  3. if (this._move(0, 1, map)) {
  4. return false;
  5. }
  6. // 堆积
  7. for (var i = 0; i < this.current.length; i++) {
  8. map.mapDivs[this.current[i].y][this.current[i].x].className = "done";
  9. }
  10. // 消行
  11. for (var j = 0; j <= map.maxY; j++) {
  12. if (this._isCanRemoveLine(j, map)) {
  13. this._removeLine(j, map);
  14. // 加分
  15. this.info.plusScore(10);
  16. }
  17. }
  18. // 重新初始化
  19. this._init();
  20. }
  21. // 消行
  22. Square.prototype._removeLine = function(row, map) {
  23. for (var col = 0; col <= map.maxX; col++) {
  24. for (var y = row; y > 0; y--) {
  25. // 当前行样式 = 上一行样式
  26. map.mapDivs[y][col].className = map.mapDivs[y - 1][col].className;
  27. }
  28. map.mapDivs[0][col].className = "none";
  29. }
  30. }
  31. // 是否可以消行
  32. Square.prototype._isCanRemoveLine = function(row, map) {
  33. var flag = [];
  34. for (var col = 0; col <= map.maxX; col++) {
  35. flag.push(map.mapDivs[row][col].className);
  36. }
  37. return !(flag.join(",").indexOf("none") > -1);
  38. }

# 3.7 启动游戏

game.js 文件

  1. var Game = function() {
  2. this.info = null;
  3. this.square = null;
  4. this.map = null;
  5. }
  6. // 开始游戏
  7. Game.prototype.start = function() {
  8. // 初始化数据
  9. this.info = new Info();
  10. this.square = new Square(this.info);
  11. this.map = new Map(this.square);
  12. this.map.init({
  13. "map":document.getElementById("map"),
  14. "shadow":document.getElementById("shadow"),
  15. "next":document.getElementById("next")
  16. });
  17. // 监听游戏状态
  18. var that = this;
  19. var timeId = setInterval(function() {
  20. if (that.map.closeFlag) {
  21. that.map.close();
  22. clearInterval(that.map.timeId);
  23. clearInterval(that.info.timeId);
  24. var startBtn = document.getElementById("startBtn");
  25. startBtn.removeAttribute("disabled");
  26. startBtn.innerHTML = "重新开始";
  27. document.getElementById("pauseBtn").setAttribute("disabled",true);
  28. clearInterval(timeId);
  29. }
  30. },10);
  31. }
  32. window.onload = function() {
  33. var game = new Game();
  34. var startBtn = document.getElementById("startBtn");
  35. var pauseBtn = document.getElementById("pauseBtn");
  36. startBtn.addEventListener("click", function() {
  37. if (this.innerHTML == "开始游戏") {
  38. this.setAttribute("disabled",true);
  39. pauseBtn.style.display = "inline-block";
  40. pauseBtn.removeAttribute("disabled");
  41. game.start();
  42. } else {
  43. this.setAttribute("disabled",true);
  44. pauseBtn.style.display = "inline-block";
  45. pauseBtn.removeAttribute("disabled");
  46. game.restart();
  47. }
  48. });
  49. pauseBtn.addEventListener("click",function() {
  50. if (this.innerHTML == "暂停游戏") {
  51. this.innerHTML = "恢复游戏";
  52. game.pause();
  53. } else {
  54. this.innerHTML = "暂停游戏";
  55. game.recover();
  56. }
  57. });
  58. }

四、源码

俄罗斯方块下载