首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >俄罗斯方块游戏Javascript

俄罗斯方块游戏Javascript
EN

Code Review用户
提问于 2019-05-23 16:11:39
回答 1查看 310关注 0票数 6

我正在用javascript创建俄罗斯方块。我已经完成了基本的机械操作。我希望能得到一些关于我如何改进它的反馈。

向下,左,右箭头键移动该部分,而向上箭头键旋转它。它在全页模式下工作得最好。

代码语言:javascript
复制
"use strict";

const pieceData = [
  { // O
    builds: [
      [
        [0, 0],
        [1, 0],
        [0, 1],
        [1, 1],
      ],
    ],
    type: "other",
  },

  { // T
    builds: [
      [
        [0, 1],
        [1, 1],
        [1, 2],
        [2, 1],
      ],
      [
        [1, 0],
        [1, 1],
        [2, 1],
        [1, 2],
      ],
      [
        [1, 0],
        [0, 1],
        [1, 1],
        [2, 1],
      ],
      [
        [1, 0],
        [1, 1],
        [1, 2],
        [0, 1],
      ],
    ],
    type: "other",
  },
  { // long bar
    builds: [
      [
        [0, 0],
        [1, 0],
        [2, 0],
        [3, 0],
      ],      
      [
        [1, -1],
        [1, 0],
        [1, 1],
        [1, 2],
      ], 
    ],
    type: "other",
  },
  { // Z
    builds: [
      [
        [0, 0],
        [1, 0],
        [1, 1],
        [2, 1],
      ],
      [
        [1, -1],
        [1, 0],
        [0, 0],
        [0, 1],
      ],
    ],
    type: "left",
  },
  { // S
    builds: [
      [
        [1, 0],
        [0, 0],
        [0, 1],
        [-1, 1],
      ],
      [
        [0, -1],
        [0, 0],
        [1, 0],
        [1, 1],
      ],
    ],
    type: "right",
  },
  { // L
    builds: [
      [ 
        [0, 0],
        [0, 1],
        [0, 2],
        [1, 2],
      ],
      [
        [-1, 1],
        [0, 1],
        [1, 1],
        [1, 0],
      ],
      [
        [-1, 0],
        [0, 0],
        [0, 1],
        [0, 2],
      ],
      [
        [-1, 2],
        [-1, 1],
        [0, 1],
        [1, 1],
      ],
    ],
    type: "right",
  },
  { // J
    builds: [
      [
        [1, 0],
        [1, 1],
        [1, 2],
        [0, 2],
      ],
      [
        [0, 0],
        [0, 1],
        [1, 1],
        [2, 1],
      ],
      [
        [1, 0],
        [0, 0],
        [0, 1],
        [0, 2],
      ],
      [
        [-1, 1],
        [0, 1],
        [1, 1],
        [1, 2],
      ]
    ],
    type: "left",
  },
];


const canvas = document.getElementById("display");
const nextPieceDisplay = document.getElementById("next");
const npctx = nextPieceDisplay.getContext("2d");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const npw = nextPieceDisplay.width;
const nph = nextPieceDisplay.height;
const colors = {
  "left": "#118c8b",
  "right": "#f14d49",
  "other": "#bca18d",
}

class Block {
  constructor(x, y, size, type) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.type = type;
  }

  draw(loc) {
    loc.fillStyle = colors[this.type];
    loc.fillRect(this.x, this.y, this.size, this.size);
    loc.strokeRect(this.x, this.y, this.size, this.size);
  }
}

class Piece {
  constructor(x, y, size, type, builds) {
    this.x = x;
    this.y = y;
    this.size = size;
    this.type = type;
    this.builds = builds;
    this.blocks = [];
    this.current = randInt(0, this.builds.length);
    this.swap(0);
  }

  draw(loc) {
    this.blocks.forEach(block => {
      block.draw(loc);
    });
  }

  swap(dir) {
    this.current += dir;
    this.current = mod(this.current, this.builds.length);
    let newBuild = [];
    for (let i = 0; i < this.builds[this.current].length; ++i) {
      let pos = this.builds[this.current][i];
      newBuild.push(new Block(this.x + pos[0] * this.size, this.y + pos[1] * this.size, this.size, this.type));
    }
    this.blocks = newBuild;
  }

  move(x, y) {
    this.x += x * this.size;
    this.y += y * this.size;
    this.blocks.forEach(block => {
      block.x += x * this.size;
      block.y += y * this.size;
    });
  }
}

class Board {
  constructor(w, h, gridSize) {
    this.active = undefined;
    this.blocks = [];
    this.grid = gridSize;
    this.w = w;
    this.h = h;
    this.shouldLock = false;
    this.score = 0;
    this.level = 0;
    this.nextPieceId = randInt(0, pieceData.length);
  }

  draw() {
    ctx.clearRect(0, 0, this.w, this.h);
    this.active.draw(ctx);
    this.blocks.forEach(block => {
      block.draw(ctx);
    });
  }

  newPiece() {
    let data = pieceData[this.nextPieceId];
    this.active = new Piece(Math.round(this.w / 2) - this.grid, 0, this.grid, data.type, data.builds);
    if (this.pieceBlocked(0, 0)) {
      clearInterval(gameLoop);
      alert(`Final score: ${this.score}`);
      location.reload();
    }
    this.nextPieceId = randInt(0, pieceData.length);
    let tempData = pieceData[this.nextPieceId];
    let tempPiece = new Piece(50, 50, 25, tempData.type, tempData.builds);
    npctx.clearRect(0, 0, npw, nph);
    tempPiece.draw(npctx);
  }

  findXs(y) {
    let xs = [];
    for (let i = 0; i < this.blocks.length; ++i) {
      if (this.blocks[i].y === y) {
        xs.push(this.blocks[i].x);
      }
    }
    return xs;
  }

  next() {
    this.active.swap(1);
    if (this.pieceBlocked(0, 0)) {
      this.active.swap(-1);
    }
    else {
      this.shouldLock = false;
    }
  }

  findFulls() {
    let fulls = [];
    for (let y = 0; y < height; y += this.grid) {
      let xs = this.findXs(y);
      if (xs.length >= this.w / this.grid) {
        fulls.push(y);
      }
    }
    return fulls;
  }

  shift(ys) {
    if (ys.length) {
      let grid = this.grid;
      this.blocks.forEach(block => {
        let below = ys.filter(a => a > block.y);
        block.y += below.length * grid;
      });
    }
  }

  update() {
    let noFall = this.pieceBlocked(0, 1);
    if (this.shouldLock) {
      if (noFall) {
        this.lock();
        this.newPiece();
        return;
      }
      this.shouldLock = false;
    }
    if (noFall) {
      this.shouldLock = true;
    }
    else {
      this.active.move(0, 1);
    }
    let fulls = this.findFulls();
    for (let i = 0; i < this.blocks.length; ++i) {
      if (fulls.includes(this.blocks[i].y)) {
        this.blocks[i] = undefined;
      }
    }
    let len = fulls.length;
    this.blocks = this.blocks.filter(a => a);
    this.shift(fulls);
    if (len) {
      this.score += len ** 2 * 100;
    }
    this.level = Math.floor(Math.sqrt(this.score / 1000));
    document.getElementById("score").innerText = "Score: " + this.score.toString();
    document.getElementById("level").innerText = "Level: " + this.level.toString();
    this.draw();
  }

  handleMovement(x, y) {
    if (!this.pieceBlocked(x, y)) {
      this.active.move(x, y);
      if (x === 0 && y === 1) {
        this.score++;
        this.shouldLock = true;
      }
    }
  }

  blocked(block, xVel, yVel) {
    let x = block.x + xVel * this.grid;
    let y = block.y + yVel * this.grid;
    if (x < 0 || x >= this.w || y >= this.h) {
      return true;
    }
    for (let i = 0; i < this.blocks.length; ++i) {
      if (x == this.blocks[i].x && y == this.blocks[i].y) {
        return true;
      }
    }
  }

  pieceBlocked(xVel, yVel) {
    for (let i = 0; i < this.active.blocks.length; ++i) {
      if (this.blocked(this.active.blocks[i], xVel, yVel)) {
        return true;
      }
    }
    return false;
  }

  lock() {
    this.blocks = this.blocks.concat(this.active.blocks);
    this.active = undefined;
    this.score += 10;
  }

  move(x, y) {
    if (!this.pieceBlocked(x, y)) {
      this.active.move(x, y);
    }
  }
}

function mod(n, m) {
  return ((n % m) + m) % m;
}

function randChoice(arr) {
  let rand = randInt(0, arr.length);
  return arr[rand];
}

function randInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}



const board = new Board(width, height, 25);
let lastLevel = 0;
board.newPiece();

function game() {
  board.update();
  if (board.level != lastLevel) {
    clearInterval(gameLoop);
    lastLevel = board.level;
    gameLoop = setInterval(game, (1 / lastLevel) * 500)
  }
}

let gameLoop = setInterval(game, 500);

addEventListener("keydown", e => {
  switch (e.code) {
    case "ArrowUp":
      board.next();
      break;
    case "ArrowDown":
      board.handleMovement(0, 1);
      break;
    case "ArrowLeft":
      board.handleMovement(-1, 0);
      break;
    case "ArrowRight":
      board.handleMovement(1, 0);
      break;
  }
  board.draw();
});
代码语言:javascript
复制
html {
  height: 100%;
  display: grid;
}

body {
  margin: auto;
}

#display {
  border: 3px solid black;
  float: left;
}

#info {
  float: right;
  margin: 10px;
}
代码语言:javascript
复制
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>Tetris</title>
  </head>
  <body>
    <canvas id="display" width="250" height="450"></canvas>
    <div id="info">
      <p id="score">Score: 0</p>
      <p id="level">Level: 0</p>
      <p>Next piece: </p>
      <canvas id="next" width="200" height="200"></canvas>
    </div>
  </body>
</html>
EN

回答 1

Code Review用户

回答已采纳

发布于 2019-05-25 13:31:01

通用点

  • 将常量和变量声明移到代码的顶部。将所有这些保持在一起使管理开发变得更容易。
  • 游戏常量应该是UPPER_CASE_SNAKE,不要将属性名称放在引号中

例如

代码语言:javascript
复制
const COLORS = {
  left: "#118c8b",
  right: "#f14d49",
  other: "#bca18d",
}
  • 类语法看起来很方便,但是您可以构建一些对象来封装它们的工作属性,以避免对混乱的、语义上不明确的this令牌的需求(代码中有大约120个this令牌)。

示例几乎与Block类相同,但看不到this

代码语言:javascript
复制
function Block(x, y, size, type = "other") {
    const FILL_STYLE = COLORS[type];
    return {
        get x() { return x },
        get y() { return y },
        draw(ctx) {
            ctx.fillStyle = FILL_STYLE;
            ctx.fillRect(x, y, size, size);
            ctx.strokeRect(x, y, size, size);
        },
        move(addX, addY) {
            x += addX * size;
            y += addY * size;
        },
    };
}

const b = new Block(x, y, size, type);  // instantiate
// or you no longer need the new token
const b = Block(x, y, size, type);
b.move(1, 0);          // move right
filter(a => a < b.y);  // access getter
  • 定义形状的方式有点过熟,可以通过一些额外的代码来简化。制作一个好的游戏的一部分是能够调整游戏,所以确保内容容易创建和修改对于创建一个吸引人的游戏有很大的帮助。

例如,下面的代码片段从字符串构建片段

代码语言:javascript
复制
const O_PIECE = "####";
const T_PIECE = "### # ";
const BAR_PIECE = "####";            
const Z_PIECE = " # ###";
const S_PIECE = " #### ";

const pieceData = [
  createPiece(O_PIECE,2),
  createPiece(T_PIECE,3),
  createPiece(S_PIECE,3),
  createPiece(Z_PIECE,2),
  createPiece(BAR_PIECE,4),      
];

function createPiece(layout, width, type = "other") {
    const height = layout.length / width;
    var i = layout.length;
    const build = []
    while(i--) { 
        if(layout[i] === "#") {
            const x = i % width;
            const y = i / width | 0;
            build.push([x, y]);
        }
    }
    return {
        builds: [build],
        type,
    };
}

构建资源

将您的实用程序代码放在一起,这样您就可以在开发其他应用程序时构建一个库。

代码语言:javascript
复制
const gMath = {};  // g for game maths
gMath.mod = (n, m) => (n % m + m) % m;
gMath.randInt = (min, max = min + (min = 0)) => Math.floor(Math.random() * (max - min) + min);
gMath.randItem = arr => arr[gMath.randInt(arr.length)];

渲染与定时

虽然这个游戏不需要一个高的帧速率,但最好还是让你的渲染与显示同步。使用requestAnimationFrame,您可以创建一个以60 per (每秒帧)运行的呈现循环。

若要控制游戏渲染速率,请使用帧计数器和渲染速率,以便游戏呈现任意n个帧。

代码语言:javascript
复制
requestAnimationFrame(mainLoop); // will start the animation loop when execution is idle
var frameCounter = 0;
var gameSpeed = 30; // Render every 30 frame or 1/2 second
var updateRender = true; // when true will re-render the game
function mainLoop(time) { // time is a high resolution time supplied by the DOM

    // call IO code here

    if (frameCounter % gameSpeed === 0) {
        updateGame(); // call the game update
        updateRender = true;
    }

    if (updateRender) {
        updateRender = false;
        draw(); // draw the game
    }


    frameCounter ++;
    requestAnimationFrame(mainLoop);
}

解耦IO

尝试将IO代码与游戏逻辑分开。使用键盘和鼠标/触摸等IO事件来设置IO对象的状态。使用IO对象接口控制游戏

举例说明。键状态IO.keys.为true,而键已关闭,如果不是,则为false。他们移除自动重复。

代码语言:javascript
复制
const IO = {  
    keys: { // list only the keys you need
        ArrowLeft: false, 
        ArrowRight: false,
        ArrowDown: false,
        Escape: false,  // reset or end game
    },
    clear() {
        for(const key of Object.keys(IO.keys) { IO.keys[key] = false }
    },
    keyboard(event) {
        if (IO.keys[event.keyCode] !== undefined) {
            IO.keys[event.keyCode] = event.type === "keydown";
            event.preventDefault();
        }
    }
}
addEventListener("keydown", IO.keyboard);
addEventListener("keyup", IO.keyboard);

将绑定IO状态用于游戏操作,以便无需进入游戏代码就可以重新配置IO。还允许您将其他IO类型(如点击或点击)绑定到游戏操作中。

代码语言:javascript
复制
const GAME_DOWN = "ArrowDown"; // bind key to action
const GAME_LEFT = "ArrowLeft"; // bind key to action
const GAME_RIGHT = "ArrowRight"; // bind key to action
const KEY_REPEAT_RATE = 20; // every 20 frames 5 times a second
var rightRepeat = 0;
function doInput() {
    // Action while key is down
    if (IO.keys[GAME_LEFT]) {
        /* do stuff */
    }

    // Action only once per key down
    if (IO.keys[GAME_DOWN]) {
        IO.keys[GAME_DOWN] = false; // turn off key. 
        /* do stuff */
    }

    // Action repeats on key down
    if (IO.keys[GAME_RIGHT]) {
        if (rightRepeat === 0) {
            rightRepeat = keyRepeatRate;                
            /* do stuff */
        }
        rightRepeat--; // count down to next repeat
    } else {
        rightRepeat = 0; // reset repeat
    }

    // when changing levels clear the input state so if the user
    // is holding a key down it does not effect the new level or game
    IO.clear();
}

不要忘记,很多设备都是触控的,而且没有键盘。

封装

当它的时间来炫耀你的游戏,你将需要它很容易被插入到第三方网页。

为了确保没有问题,封装整个游戏,使其在全球范围内没有内容。这可以作为一个调用其自身的函数来完成。IIF (立即调用函数)

代码语言:javascript
复制
;((container)=> {
    "use strict";
    /* 
       All game code in here

    */
})(document.body);  // the container for your game

封装的一部分也是DOM内容,您不能依赖托管游戏的站点来提供内容,所以请尝试使游戏独立运行(无需链接到您宿主的站点以提供内容)。

代码语言:javascript
复制
const gDOM = {}; // g for game DOM 
gDOM.create = (tag, props = {}) => Object.assign(document.createElement(tag), props);
gDOM.append = (parent, ...children) => children.reduce((p, c) => (p.appendChild(c), p), parent);

用法

代码语言:javascript
复制
const styles = `
    tetris--score-display {
        /* add style stuff
    }
    /* and so on */
`;
gDOM.append(container, gDOM.create("style", {innerHTML: styles}));
const canvas = gDOM.create("canvas", {width: gameWidth, height: gameHeight});
const block = gDOM.create("canvas", {width: blockWidth, height: blockHeight}); 
const score = gDOM.create("div", {textContent: "Score:", className: "tetris--score-display"}); 
const level = gDOM.create("div", {textContent: "Level:", className: "tetris--level-display"}); 

const ctx = canvas.getContext("2d");
const bctx = block.getContext("2d");

gDOM.append(container, canvas, block, score, level);

// To set score or level
score.innerText = "Score: " + this.score;  // Note you had this.score.toString() the toString is autonomic
level.innerText = "Level: " + this.level;

响应

使您的游戏响应空间可用。尽可能多地填充空间,你不希望你的游戏成为4K屏幕上的一个小盒子,更糟糕的是,你不希望你的游戏超出可观看的区域。

不要担心高分辨率设备具有高端GPU的性能,但是始终将画布分辨率设置为与显示的大小相匹配,这样就不会对GPU进行过多的煮熟。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/220850

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档