분류 javascript

테트리스로 현대 JavaScript 배우기

컨텐츠 정보

  • 조회 439 (작성일 )

본문

클래스 및 화살표 함수와 같은 최신 JS 기능에 중점을 두고 테트리스 게임을 처음부터 새로 만드는 훌륭한 자습서입니다.


https://medium.com/@michael.karen/learning-modern-javascript-with-tetris-92d532bcd057 


1*hekydXN81H_tb8GT_OSQqw.png 

오늘 저는 고전적인 테트리스 게임과 함께 게임 개발의 여정을 함께하겠습니다. 

그래픽, 게임 루프 및 충돌 감지와 같은 개념을 다루겠습니다. 

결국, 우리는 포인트와 레벨로 완벽하게 작동하는 게임을 가지고 있습니다. 

여정의 일부는 ECMAScript 2015 (ES6)에 도입 된 기능을 의미하는 최신 JavaScript 개념을 사용하는 것입니다.


JavaScript 트릭을 가져다 줄 새로운 것을 선택하십시오!


프로젝트를 생성하고 코드 스니펫에서 오류가 발생하면 GitHub의 리포지토리에서 코드를 확인하십시오.

작동하지 않는 것을 찾으면 메시지를 보내주십시오.

향후 게임 개발은 마스터 브랜치에 들어가며 블로그 브랜치는 그대로 유지됩니다.


완성 된 게임은 다음과 같습니다.


1*AujqYqvKXWEMezT2QdkwVw.gif 


Tetris 


테트리스는 1984 년 Alexey Pajitnov에 의해 만들어졌습니다. 

이 게임에서는 플레이어가 떨어지는 테트리스 조각을 회전하고 움직여야 합니다. 

플레이어는 빈 셀 없이 블록의 가로 행을 완성하여 선을 지웁니다. 그러나 조각이 정상에 도달하면 게임은 끝납니다!


테트리스는 게임 개발의 여정을 시작하기에 좋은 게임입니다. 

게임의 필수 요소를 포함하고 있으며 프로그래밍 하기가 비교적 쉽습니다. 

테트로 미노는 대부분의 게임보다 그래픽을 조금 더 쉽게 만들어주는 4 개의 블록 모음입니다.


프로젝트 구조 


크지 않더라도 프로젝트에서 코드를 분할하는 것이 좋습니다. JavaScript는 네 가지 파일로 되어 있습니다.


1*amI06KlchjQvIuk1wxxgMA.png 


  • constants.js 게임의 구성과 규칙을 넣는 곳입니다.
  • board.js 보드 로직용.
  • piece.js 조각 논리를 위해
  • main.js 게임 및 전체 게임 로직을 초기화하는 코드.
  • index.html 끝에 추가 한 스크립트의 순서는 필수적입니다.
  • styles.css 모든 아름다운 스타일이 여기에 있습니다.
  • README.md 저장소의 첫 번째 페이지인 markdown info 파일.

크기와 스타일 


게임 보드는 10 개의 열과 20 개의 열로 구성됩니다. 우리는 이러한 값을 자주 사용하여 보드를 반복하므로 블록의 크기와 함께 constants.js에 추가 할 수 있습니다.


constants.js : 

const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;

그래픽에 캔버스 요소를 사용하는 것을 선호합니다.


index.html : 

<div class="grid">
  <canvas id="board" class="game-board"></canvas>
  <div class="right-column">
    <div>
      <h1>TETRIS</h1>
      <p>Score: <span id="score">0</span></p>
      <p>Lines: <span id="lines">0</span></p>
      <p>Level: <span id="level">0</span></p>
      <canvas id="next" class="next"></canvas>
    </div>
    <button onclick="play()" class="play-button">Play</button>
  </div>
</div>

main.js에서 canvas 요소와 2d 컨텍스트를 가져 와서 상수를 사용하여 크기를 설정할 수 있습니다.


main.js : 

const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');

// Calculate size of canvas from constants.
ctx.canvas.width = COLS * BLOCK_SIZE;
ctx.canvas.height = ROWS * BLOCK_SIZE;

// Scale blocks
ctx.scale(BLOCK_SIZE, BLOCK_SIZE);

scale을 사용하면 언제 어디서나 BLOCK_SIZE로 계산하지 않고 블록 크기를 항상 1로 지정할 수 있으므로 코드가 간단 해집니다.


Styling 


게임에 80 년대 느낌을 주는 것이 좋습니다. Press Start 2P는 1980 년대 Namco 아케이드 게임의 글꼴 디자인을 기반으로 하는 비트 맵 글꼴입니다. <head>에서 링크하여 스타일에 추가 할 수 있습니다.


index.html : 

<link 
  href="https://fonts.googleapis.com/css?family=Press+Start+2P" 
  rel="stylesheet"
/>


styles.css의 첫 번째 섹션은 아케이드 스타일의 글꼴입니다. 레이아웃에 CSS Grid와 Flexbox를 사용하는 것에 주목하십시오 :


styles.css : 


* {
  font-family: 'Press Start 2P', cursive;
}

.grid {
  display: grid;
  grid-template-columns: 320px 200px;
}

.right-column {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.game-board {
  border: solid 2px;
}

.play-button {
  background-color: #4caf50;
  font-size: 16px;
  padding: 15px 30px;
  cursor: pointer;
}

이를 통해 게임 컨테이너의 스타일이 준비되고 코드를 기다리고 있습니다.


1*LE4-gyBpLp5RzJnQUXwddQ.png 

The Board


테트리스의 보드는 점유 여부에 관계없이 셀로 구성됩니다. 

내 첫 번째 생각은 부울 값을 가진 셀을 나타내는 것이었습니다. 

그러나 숫자를 사용하면 더 잘할 수 있습니다. 

빈 셀은 0으로, 색상은 1-7로 표현할 수 있습니다.


다음 개념은 게임 보드의 행과 열을 나타냅니다. 숫자 배열을 사용하여 행을 나타낼 수 있습니다. 그리고 보드는 행의 배열입니다. 다시 말해, 2 차원 (2D) 배열 또는 행렬이라고 하는 것입니다.


board.js에서 모든 셀이 0으로 설정된 빈 보드를 반환하는 함수를 만들어 봅시다. fill() 메소드는 다음과 같이 편리합니다.


board.js : 

class Board {
  grid;
  
  // Reset the board when we start a new game.
  reset() {
    this.grid = this.getEmptyBoard();
  }
  
  // Get matrix filled with zeros.
  getEmptyBoard() {
    return Array.from(
      {length: ROWS}, () => Array(COLS).fill(0)
    );
  }
}

play를 누르면 main.js에서 이 함수를 호출 할 수 있습니다.


main.js :

let board = new Board();

function play() {
  board.reset();
  console.table(board.grid);
}

console.table을 사용하면 보드의 숫자를 숫자로 볼 수 있습니다.

1*CcbOlMIzhLOEzo1is7sLmQ.png 


X 및 Y 좌표는 보드의 셀을 나타냅니다. 이제 보드가 완성되었으므로 움직이는 부분을 살펴 보겠습니다.


Tetrominos 


테트리스의 한 조각은 하나의 블록으로 움직이는 네 개의 블록으로 구성된 모양입니다. 

그들은 종종 tetrominos라고 하며 7 가지 패턴과 색상으로 나옵니다. 이름 I, J, L, O, S, T 및 Z는 모양이 비슷합니다.


1*SFEmFOmgrQhxULNIY6O_qw.png 


우리는 J tetromino를 숫자 2가 유색 세포를 나타내는 행렬로 나타냅니다. 중심을 회전시키기 위해 0의 행을 추가합니다.


[2, 0, 0],
[2, 2, 2],
[0, 0, 0]; 


테트로 미노는 J, L 및 T가 평평한 면을 먼저 생성하여 수평으로 생성됩니다. 


우리는 Piece 클래스가 보드에서의 위치, 색상 및 모양을 알기를 원합니다. 

따라서 보드에 자신을 그리려면 캔버스 컨텍스트에 대한 참조가 필요합니다.


우선, 우리는 조각의 가치를 하드 코딩 할 수 있습니다 :


piece.js : 

class Piece {
  x;
  y;
  color;
  shape;
  ctx;
  
  constructor(ctx) {
    this.ctx = ctx;
    this.spawn();
  }
  
  spawn() {
    this.color = 'blue';
    this.shape = [
      [2, 0, 0], 
      [2, 2, 2], 
      [0, 0, 0]
    ];
    
    // Starting position.
    this.x = 3;
    this.y = 0;
  }
}

보드에 테트로 미노를 그리려면 모양의 모든 셀을 반복합니다. 셀의 값이 0보다 크면 해당 블록의 색상을 지정합니다.


piece.js : 

draw() {
  this.ctx.fillStyle = this.color;
  this.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      // this.x, this.y gives the left upper position of the shape
      // x, y gives the position of the block in the shape
      // this.x + x is then the position of the block on the board
      if (value > 0) {
        this.ctx.fillRect(this.x + x, this.y + y, 1, 1);
      }
    });
  });
}

보드는 보드에서 tetromino를 추적하여 재생 버튼을 누를 때 만들고 칠할 수 있습니다.


main.js : 


function play() {
  board = getEmptyBoard();
  let piece = new Piece(ctx);
  piece.draw();
  
  board.piece = piece;
}

파란색 J 테트로 미노가 나타납니다!


1*dRg72KWT13LSUicEIQjrFg.png 


다음으로 키보드를 통해 마술을 만들어 봅시다.


Keyboard input 


키보드 이벤트를 연결하여 보드에서 조각을 이동해야 합니다. 

이동 기능은 현재 조각의 x 또는 y 변수를 변경하여 보드에서의 위치를 ​​변경합니다.


move(p) {
this.x = p.x;
this.y = p.y;
} 


Enums 


다음으로 constants.js의 키 코드에 키를 매핑합니다. 이를 위해 열거형을 갖는 것이 좋습니다.


열거형 (enumeration)은 상수 컬렉션을 정의하는 데 사용되는 특수한 유형입니다. 


JavaScript에는 내장 열거형이 없으므로 값을 가진 객체를 만들어서 열거형을 만들어 보겠습니다.


constants.js : 


const KEY = {
  LEFT: 37,
  RIGHT: 39,
  DOWN: 40
}
Object.freeze(KEY);

const는 객체와 배열로 작업 할 때 약간 오도 될 수 있으며 실제로 변경할 수는 없습니다. 

이를 위해 Object.freeze()를 사용할 수 있습니다. 

여기 몇 가지 문제가 있습니다.

  • 이것이 제대로 작동하려면 엄격 모드를 사용해야 합니다.
  • 이것은 한 수준 아래로만 작동합니다. 다시 말해서, 객체 안에 배열이나 객체가 있다면 그것들은 고정되지 않습니다.

Object literals 


주요 이벤트를 액션과 일치 시키기 위해 객체 리터럴 조회를 사용할 수 있습니다.


ES6을 사용하면 객체 리터럴의 속성 키에서 표현식을 사용하여 계산 된 속성 키를 만들 수 있습니다. 


상수를 사용할 수 있도록 계산 된 속성 이름을 얻으려면 대괄호가 필요합니다. 다음은 작동 방식에 대한 간단한 예입니다.


const X = 'x';
const a = { [X]: 5 };
console.log(a.x); // 5 


우리는 현재 tetromino를 보내고 좌표 변경과 함께 사본을 반환하려고 합니다. 

이를 위해 스프레드 연산자를 사용하여 얕은 복사본을 얻은 다음 좌표를 원하는 위치로 변경할 수 있습니다.


JavaScript에서는 얕은 복사를 사용하여 숫자 및 문자열과 같은 기본 데이터 유형을 복사 할 수 있습니다. 

우리의 경우 좌표는 숫자입니다. 

ES6는 두 가지 얕은 복사 메커니즘을 제공합니다 : Object.assign()스프레드 연산자. 


다시 말해,이 코드 스니펫에서 많은 작업이 진행되고 있습니다.


main.js : 

moves = {
  [KEY.LEFT]:  p => ({ ...p, x: p.x - 1 }),
  [KEY.RIGHT]: p => ({ ...p, x: p.x + 1 }),
  [KEY.UP]:    p => ({ ...p, y: p.y + 1 })
};

원본 코드를 변경하지 않고 새 상태를 얻기 위해 아래 코드와 함께 사용할 수 있습니다. 

항상 새로운 위치로 이동하고 싶지는 않기 때문에 중요합니다.


const p = this.moves[event.key](this.piece); 


다음으로 키 다운 이벤트를 수신하는 이벤트 리스너를 추가합니다.


main.js : 

document.addEventListener('keydown', event => {
  if (moves[event.keyCode]) {  
    // Stop the event from bubbling.
    event.preventDefault();
    
    // Get new state of piece
    let p = moves[event.keyCode](board.piece);
    
    if (board.valid(p)) {    
      // If the move is valid, move the piece.
      board.piece.move(p);
      
      // Clear old position before drawing.
      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 
      
      board.piece.draw();
    }
  }
});

이제 키보드 이벤트를 듣고 왼쪽, 오른쪽 또는 아래쪽 화살표를 누르면 조각이 움직이는 것을 볼 수 있습니다.


1*Y2AO__23zaWQWjpxSvSjJQ.gif 


운동이 있습니다! 그러나 벽을 통과하는 유령 조각은 우리가 원하는 것이 아닙니다.


충돌 감지 


테트리스는 모든 블록이 서로를 통과 할 수 있거나 벽과 바닥이 막히지 않았다면 특히 흥미 진진한 게임이 아닙니다. 

따라서 테트로 미노를 옮기는 대신 잠재적 충돌을 먼저 확인한 다음 안전한 경우에만 테트로 미노를 움직입니다. 

고려해야 할 몇 가지 다른 충돌이 있습니다.


우리는 tetromino가 충돌 할 때

  • 바닥에 부딪힐 때
  • 왼쪽 또는 오른쪽 벽으로 이동할 때
  • 보드위 블록과 부딪힐 때
  • 회전할 때 벽 또는 블록에 부딪힐 때

우리는 이미 모양에 대한 잠재적인 새로운 위치를 정의했습니다. 

이제 이 위치로 이동하기 전에 이 위치가 유효한지 점검 할 수 있습니다. 

충돌을 확인하기 위해 tetromino가 잠재적인 새로운 위치에서 차지할 그리드의 모든 공간을 반복합니다.


이것에 가장 적합한 배열 메소드는 every()입니다. 

이를 통해 배열의 모든 요소가 제공하는 테스트를 통과하는지 확인할 수 있습니다. 

조각의 모든 블록의 좌표를 계산하고 유효한 위치인지 확인합니다.


board.js : 

valid(p) {
  return p.shape.every((row, dy) => {
    return row.every((value, dx) => {
      let x = p.x + dx;
      let y = p.y + dy;
      return (
        this.isEmpty(value) ||
       (this.insideWalls(x) &&
        this.aboveFloor(y)
      );
    });
  });
}


이동하기 전에 이 방법을 사용하면 다음과 같은 곳으로 이동하지 않아야 합니다.


if (this.valid(p)) {
this.piece.move(p);
} 


다시 그리드 밖으로 나가 봅시다.


1*0mSROBceNAAiyDZ49q1EPQ.gif 

더 이상 유령이 없습니다!


바닥이 테트로 미노를 멈추게 되었으므로 하드 드롭이라는 또 다른 움직임을 추가 할 수 있습니다. 

공간을 누르면 무언가와 충돌 할 때까지 테트로 미노가 떨어집니다. 

이것을 하드 드롭이라고 합니다. 

또한 새로운 키 매핑을 추가하고 이동해야 합니다.


main.js : 


const KEY = {  
  SPACE: 32,
  // ...
}

moves = {  
  [KEY.SPACE]: p => ({ ...p, y: p.y + 1 })
  // ...
};

// In EventListener
if (event.keyCode === KEY.SPACE) {
  // Hard drop
  while (board.valid(p)) {
    board.piece.move(p);   
    p = moves[KEY.DOWN](board.piece);
  }
}

향후 계획?


Rotation 


이제 움직일 수는 있지만 조각을 회전 시킬 수 없다면 재미가 없습니다. 테트로 미노를 중심 주위로 회전시켜야 합니다.


1*G4Sx0AmzkokyhZW8E61zyw.png 


학교에서 선형 대수학을 공부한 지 오래되었습니다. 그러나 시계 방향으로 회전하는 것은 다음과 같습니다.


두 번의 반사로 45도 각도에서 90도 회전 할 수 있으므로 행렬의 조 옮김과 열 순서를 반대로 하는 순열 행렬을 곱할 수 있습니다.


1*1wuZSv9HXx2-g-r8uYTL7Q.png 


그리고 JavaScript에서 :


board.js : 


// Transpose matrix, p is the Piece.
for (let y = 0; y < p.shape.length; ++y) {
  for (let x = 0; x < y; ++x) {
    [p.shape[x][y], p.shape[y][x]] = 
    [p.shape[y][x], p.shape[x][y]];
  }
}

// Reverse the order of the columns.
p.shape.forEach(row => row.reverse());

도형을 회전 시키는 함수를 추가 할 수 있습니다. 이전에는 스프레드 연산자를 사용하여 좌표를 복제했습니다. 이 경우 다중 레벨 배열을 사용하지만 스프레드 연산자는 한 레벨 깊이만 복사합니다. 나머지는 참조로 복사됩니다.


대신 JSON.parseJSON.stringify를 사용하고 있습니다. stringify() 메서드는 행렬을 JSON 문자열로 변환합니다. parse() 메소드는 JSON 문자열을 구문 분석하여 행렬을 다시 복제본으로 구성합니다.


board.js : 


rotate(p){
  // Clone with JSON for immutability
  let clone = JSON.parse(JSON.stringify(p));
  
  // Do algorithm
  
  return clone;
}

그런 다음 board.js에서 ArrowUp의 새로운 상태를 추가합니다.


[KEY.UP]: (p) => this.rotate(p) 


이제 우리는 회전합니다!


1*e31IyM9nSYqjMzZfJaHfyg.gif 


Randomize Tetromino 


다른 종류의 조각을 얻을 수 있으려면 코드에 임의의 비트를 추가해야 합니다.


수퍼 회전 시스템에 따라 조각의 첫 번째 위치를 취하여 색상과 함께 상수에 추가 할 수 있습니다.


constants.js : 


const COLORS = [  
  'cyan',
  'blue',
  'orange',
  'yellow',
  'green',
  'purple',
  'red'
];

const SHAPES = [  
  [
    [0, 0, 0, 0], 
    [1, 1, 1, 1],
    [0, 0, 0, 0], 
    [0, 0, 0, 0]
  ], 
  [
    [2, 0, 0],
    [2, 2, 2],
    [0, 0, 0]
  ],
  // And so on
];

하나를 선택하려면 이들 중 하나의 색인을 무작위화해야 합니다. 난수를 얻기 위해 배열의 길이를 사용하는 함수를 만듭니다.


randomizeTetrominoType(noOfTypes) {
return Math.floor(Math.random() * noOfTypes);
} 


이 방법을 사용하면 산란 할 때 무작위 테트로미노 유형을 얻을 수 있습니다.


const typeId = this.randomizeTetrominoType(COLORS.length);
this.shape = SHAPES[typeId];
this.color = COLORS[typeId]; 


재생을 누르면 페이지에 모양과 색상이 다른 조각이 표시됩니다.


1*umHlSitIS-uRdzdo7g1OCg.gif 


게임 루프 


거의 모든 게임에는 사용자가 아무 것도 하지 않아도 게임이 계속 실행되도록 하는 하나의 주요 기능이 있습니다. 

동일한 핵심 기능을 반복해서 실행하는 이 주기를 게임 루프라고 합니다. 

우리 게임에는 테트로 미노를 화면 아래로 이동 시키는 게임 루프가 필요합니다.


RequestAnimationFrame 


게임 루프를 만들려면 requestAnimationFrame을 사용할 수 있습니다. 

브라우저에 애니메이션을 적용 할 것을 알리고 다음 다시 그리기 전에 애니메이션을 업데이트하는 함수를 호출해야 합니다. 

다시 말해, 우리는 브라우저에“다음에 화면에 페인트를 칠할 때 무언가 페인트 하기를 원하기 때문에 이 기능을 실행합니다”라고 말합니다.


"애니메이션은 움직이는 그림의 예술이 아니라 그려진 움직임의 예술입니다."— Norman McLaren 


window.requestAnimationFrame()을 사용하여 애니메이션을 적용하는 방법은 프레임을 페인트 한 다음 다시 예약하는 함수를 만드는 것입니다. 

클래스 내에서 사용하는 경우 (이 경우에는 필요하지 않음) 호출을 바인딩 해야 하거나 창 객체를 컨텍스트로 사용합니다. 

애니메이션 함수가 포함되어 있지 않으므로 오류가 발생합니다.


animate() {
this.piece.draw();
requestAnimationFrame(this.animate.bind(this));
} 


이전의 draw() 호출을 모두 제거하고 대신 play() 함수에서 animate()를 호출하여 애니메이션을 시작할 수 있습니다. 

게임을 시도해도 여전히 이전과 같이 실행됩니다.


Timer 


다음으로 타이머가 필요합니다. 매 시간마다, 우리는 tetromino를 떨어뜨립니다. 

MDN 페이지에는 필요에 따라 수정할 수 있는 가 있습니다.


필요한 정보로 객체를 생성하는 것으로 시작합니다.


time = { start: 0, elapsed: 0, level: 1000 }; 


게임 루프에서 시간 간격을 기준으로 게임 상태를 업데이트 한 다음 결과를 그립니다.


main.js : 


function animate(now = 0) {
  // Update elapsed time.  
  time.elapsed = now - time.start;
  
  // If elapsed time has passed time for current level  
  if (time.elapsed > time.level) {
  
    // Restart counting from now
    time.start = now;   
    
    this.drop();  
  }
  
  // Clear board before drawing new state.
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 
  
  board.draw();  
  requestId = requestAnimationFrame(animate);
}

우리는 애니메이션이 있습니다!


1*RJ53Jyn7U4O81gtup3C5WA.gif 


다음으로 바닥에 도달했을 때 어떤 일이 발생하는지 살펴 보겠습니다.


Freeze 


더 이상 아래로 이동할 수 없으면 조각을 고정하고 새 조각을 생성합니다. 

freeze()를 정의하여 시작하겠습니다. 이 기능은 tetromino 블록을 보드에 병합합니다.


board.js : 


freeze() {
  this.piece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value > 0) {
        this.grid[y + this.piece.y][x + this.piece.x] = value;
      }
    });
  });
}

아직 아무것도 볼 수 없지만 보드의 표현을 기록하면 모양이 보드에 있음을 알 수 있습니다.


1*-tYxQYvNoMjf9VLXsldi6w.png 


보드를 그리는 기능을 추가합시다 :


board.js : 


drawBoard() {
  this.grid.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value > 0) {
        this.ctx.fillStyle = COLORS[value];
        this.ctx.fillRect(x, y, 1, 1);
      }
    });
  });
}

이제 그리기 기능은 다음과 같습니다.


draw() {
this.piece.draw();
this.drawBoard();
} 


게임을 실행하면 조각이 나타나는 것을 볼 수 있습니다.


1*bud1tYYCukEy7Cg_Fhc7cw.png 


이제 조각을 고정시키고 있으므로 새로운 충돌 감지를 추가해야 합니다. 

이번에는 보드에 얼어 붙은 테트로 미노와 충돌하지 않도록 해야 합니다. 

셀이 0인지 확인하면 됩니다. 이것을 유효한 메소드에 추가하고 인수로 보드에 보내십시오.


board[p.y + y][p.x + x] === 0; 


이제 보드에 조각을 추가하면 빠르게 혼잡 해집니다. 우리는 그것에 대해 뭔가를 해야 합니다.


Line clear 


더 오래 지속하려면 전체 행에 걸쳐있는 블록의 행으로 테트로 미노를 조립해야 합니다. 

그렇게 하면 행이 사라지고 그 위의 행이 사라집니다.


1*fS655OUnLAhb8Zqd0QD6Gg.png 


형성된 라인을 감지하는 것은 0이 있는지 확인하는 것만 큼 쉽습니다.


board.js : 


this.grid.forEach((row, y) => {

  // If every value is greater than 0.
  if (row.every(value => value > 0)) {
  
    // Remove the row.
    this.grid.splice(y, 1);
    
    // Add zero filled row at the top. 
    this.grid.unshift(Array(COLS).fill(0));
  } 
});

freeze() 호출 후 이 clearLines() 함수에 호출을 추가 할 수 있습니다. 

우리는 게임을 시도하고 행이 지워지는 것을 볼 수 있습니다.


1*2x_sUioF1TdVS-EgL8U9RQ.gif 


Score 


좀 더 흥분하려면 점수를 유지해야 합니다. 테트리스 지침에서 다음 값을 얻습니다.


constants.js : 


const POINTS = {
  SINGLE: 100,
  DOUBLE: 300,
  TRIPLE: 500,
  TETRIS: 800,
  SOFT_DROP: 1,
  HARD_DROP: 2
}
Object.freeze(POINTS);


게임 진행 상황을 추적하기 위해 점수와 선이 있는 accountValues ​​객체를 추가합니다. 

이러한 값 중 하나라도 변경되면 화면에서 값을 변경하려고 합니다. 

HTML에서 요소를 가져오고 textContext를 제공된 값으로 변경하는 일반 함수를 추가합니다.


계정 객체의 변경 사항을 처리하기 위해 Proxy 객체를 만들고 코드를 실행하여 set 메서드에서 화면을 업데이트 할 수 있습니다. 

우리는 accountValues ​​객체를 프록시로 보냅니다. 

왜냐하면 이것은 우리가 커스텀 비헤이비어를 갖고 자하는 객체이기 때문이다.


main.js : 


let accountValues = {
  score: 0,
  lines: 0
}

function updateAccount(key, value) {
  let element = document.getElementById(key);
  if (element) {
    element.textContent = value;
  }
}

let account = new Proxy(accountValues, {
  set: (target, key, value) => {
    target[key] = value;
    updateAccount(key, value);
    return true;
  }
}

이제 프록시 계정에서 속성을 호출 할 때마다 updateAccount()를 호출하고 DOM을 업데이트합니다. 

이벤트 핸들러에서 소프트 드롭 및 하드 드롭 포인트를 추가해 보겠습니다.


main.js : 


if (event.keyCode === KEY.SPACE) {
  while (board.valid(p)) {
    account.score += POINTS.HARD_DROP;
    board.piece.move(p);
    p = moves[KEY.DOWN](board.piece);
  }
} else if (board.valid(p)) {
  board.piece.move(p);
  if (event.keyCode === KEY.DOWN) {
    account.score += POINTS.SOFT_DROP;
  }
}

이제 명확한 점을 찾으십시오. 라인 수에 따라 정의 된 포인트를 얻습니다.


board.js : 


getLineClearPoints(lines) {  
  return lines === 1 ? Points.SINGLE :
         lines === 2 ? Points.DOUBLE :  
         lines === 3 ? Points.TRIPLE :     
         lines === 4 ? Points.TETRIS : 
         0;
}

이 작업을 수행하려면 몇 줄의 논리를 추가하여 몇 줄을 지우는 지 계산해야 합니다.


board.js : 


clearLines() {
  let lines = 0;
  this.board.forEach((row, y) => {    
    if (row.every(value => value !== 0)) {      
      lines++; // Increase for cleared line
      this.board.splice(y, 1); 
      this.board.unshift(Array(COLS).fill(0));
    }  
  });  
  if (lines > 0) {    
    // Add points if we cleared some lines
    account.score += this.getLineClearPoints(lines);  
  }
}

지금 게임을 시도하면 점수가 높아지고 있음을 알 수 있습니다. 

우리가 명심해야 할 것은 화면에 무언가가 나타나기를 원할 때마다 계정 개체로 직접 이동하는 대신 프록시를 거쳐야 한다는 것입니다.


1*fT8ivB6gf76HM4eI_5a4aw.gif 


Levels 


테트리스에서 더 나아지면 시작하는 속도가 너무 쉬워집니다. 

그리고 너무 쉬운 것은 지루하다는 의미입니다. 

따라서 난이도를 높여야 합니다. 

게임 루프에서 인터벌 속도를 줄이면 됩니다.


constants.js : 


const LINES_PER_LEVEL = 10;

const LEVEL = {
  0: 800,
  1: 720,
  2: 630,
  3: 550,
  // ...
}

Object.freeze(LEVEL);

플레이어가 현재 어느 레벨에 있는지 보여줄 수도 있습니다. 

추적하고 레벨과 라인을 표시하는 논리는 포인트와 동일합니다. 

우리는 그것들을 위한 가치를 초기화하고, 새로운 게임을 시작할 때 그것들을 재설정 해야 합니다.


계정 개체에 추가 할 수 있습니다.


let accountValues = {
score: 0,
lines: 0,
level: 0
} 


게임의 초기화는 play()에서 호출하는 함수로 갈 수 있습니다.


function resetGame() {
account.score = 0;
account.lines = 0;
account.level = 0;
board = this.getEmptyBoard();
} 


레벨이 높아지면 라인 클리어에 더 많은 포인트가 온다. 우리는 포인트를 현재 레벨과 곱하고 레벨 0에서 시작하므로 하나를 추가합니다.


(account.level + 1) * lineClearPoints; 


라인이 구성된 대로 지워지면 다음 레벨에 도달합니다. 레벨의 속도도 업데이트해야 합니다.


board.js : 


if (lines > 0) {
  // Calculate points from cleared lines and level.
  
  account.score += this.getLinesClearedPoints(lines, this.level);
  account.lines += lines;
  
  // If we have reached the lines for next level
  if (account.lines >= LINES_PER_LEVEL) {
    
    // Goto next level
    account.level++;
    
    // Remove lines so we start working for the next level
    account.lines -= LINES_PER_LEVEL;
    
    // Increase speed of game.
    time.level = Level[account.level];
  }
}

이제 10 개의 라인을 플레이하고 지우면 레벨이 증가하고 포인트가 두 배가 됩니다. 

물론 게임은 조금 더 빨리 움직이기 시작합니다.


1*Xj3k4-E23PSQ3yJgaQNgGg.gif 


Game Over 


한동안 놀아도 테트로 미노가 떨어지지 않는 것을 알 수 있습니다. 게임을 언제 끝내야 하는지 알아야 합니다.


드롭 후 우리는 여전히 행 0에 있는지 확인할 수 있으며 이 경우 게임 루프 기능을 종료하여 게임을 중지합니다.


if (this.piece.y === 0) {
this.gameOver();
return;
} 


종료하기 전에 cancelAnimationFrame을 사용하여 이전에 예약 된 애니메이션 프레임 요청을 취소합니다. 

그리고 우리는 사용자에게 메시지를 보여줍니다.


main.js : 


function gameOver() {
  cancelAnimationFrame(requestId);
  this.ctx.fillStyle = 'black';
  this.ctx.fillRect(1, 3, 8, 1.2);
  this.ctx.font = '1px Arial';
  this.ctx.fillStyle = 'red';
  this.ctx.fillText('GAME OVER', 1.8, 4);
}


1*eO8rjmhiBI2EqGeLO78afw.png 


Next tetromino 


마지막으로 다음 테트로 미노를 추가하겠습니다. 이를 위해 다른 캔버스를 추가 할 수 있습니다.


<canvas id="next" class=”next”></canvas> 


다음으로, 우리는 첫 번째 캔버스에서 했던 것처럼 :


const canvasNext = document.getElementById('next');
const ctxNext = canvasNext.getContext('2d');
// Size canvas for four blocks.
ctxNext.canvas.width = 4 * BLOCK_SIZE;
ctxNext.canvas.height = 4 * BLOCK_SIZE;
ctxNext.scale(BLOCK_SIZE, BLOCK_SIZE);


드롭 함수에서 로직을 약간 변경해야 합니다. 새 조각을 만드는 대신 다음 조각으로 설정하고 대신 새 다음 조각을 만듭니다.


this.piece = this.next;
this.next = new Piece(this.ctx);
this.next.drawNext(this.ctxNext); 


다음에 어떤 조각이 올지 알면 좀 더 전략적 일 수 있습니다.


1*bhOeldoCgtRnJx6mclQlVg.png 



결론 


오늘은 게임 개발의 기본 사항과 그래픽에 캔버스를 사용하는 방법에 대해 배웠습니다. 

또한 이 프로젝트가 현대 JavaScript를 배우는 재미있는 방법이 되기를 원했습니다. 

기사를 즐기고 JavaScript 도구 상자에 대한 새로운 내용을 배우기를 바랍니다.


이제 게임 개발의 첫 단계를 밟았으니 다음에는 어떤 게임을 합니까?


Resources