자바스크립트로 만든 테트리스
- https://medium.com/@michael.karen/learning-modern-javascript-with-tetris-92d532bcd057
- 위의 블로그 내용 보고 했지만 빼먹은거 엄청 많고 잘못 타이핑한 부분도 있으니 참고만 할 것
- 위의 블로그 내용을 각 단계별로 직접 분석하면서 작동 확인하면서 만들면서 커밋했으니 아래 내용들은 실행 잘됨!
- 단계별로 만들고 커밋한 내용은 아래 깃헙에 올렸으니 참고
- https://github.com/ds2lvg/JSwithTetris
사용하는 자바스크립트 ES6 문법
- Classes, Arrow functions, Spread operator, Let and const, Default parameters, Array.from(), Proxy
- 각 파트에서 해당 문법을 사용하면 제목옆에 표기 해놓았으니 참고
파일 구조
- board.js: 보드 로직
- constants.js: 게임 설정과 규칙을 정의
- index.html: 기본 html
- main.js: 게임 초기화와 종료 로직
- piece.js: 테트리스 조각 로직
- styles.css: 모든 스타일링
레이아웃 만들기
- html, css는 걍 복붙할 것(index.html, styles.css)
- 상수를 통해 보드 행렬, 블록 사이즈 정의
- canvas로 그래픽 표현
//*** constants.js
'use strict';
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
//*** main.js
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
// 캔버스 크기 계산
ctx.canvas.width = COLS * BLOCK_SIZE;
ctx.canvas.height = ROWS * BLOCK_SIZE;
// 블록 크기 변경: 매번 BLOCK_SIZE로 계산할 필요가 없이 블록의 크기를 1로 취급
ctx.scale(BLOCK_SIZE, BLOCK_SIZE);
테트리스 보드: Classes, Array.from()
- 테트리스 보드는 셀들로 구성되어 있고, 각 셀은 채워져 있거나 그렇지 않을 수 있다.
- 비어있는 셀은 0으로 표시하고 색상은 1-7을 사용해 표시하며 초기 보드의 모든 셀은 0 이다.
- 게임 보드는 행렬로 이뤄져있고 행을 나타내기 위해 숫자형의 배열을 사용한다.
//*** board.js
class Board {
grid;
// 보드 초기화
reset() {
this.grid = this.getEmptyBoard();
}
// 행(20)만큼 length를 가진 객체를 배열로 만들껀데 열(10)을 0으로 배열로 만들어라
getEmptyBoard() {
return Array.from(
{length: ROWS}, () => Array(COLS).fill(0)
);
}
}
//*** main.js
let board = new Board();
function play() {
board.reset(); // 보드판 초기화
console.table(board.grid);
}
테트로미노
- 테트리스 한조각은 4개 블록으로 구성되어 있다.
- 하나의 테트로미노는 I, J, L, O, S, T, Z 모양을 띄고 있다.
- J, L, T는 평평한 쪽을 먼저 수평으로 생성한다.
- 가령 J 모양은 아래의 행렬구조를 띄고 있다.
let j = [
[2, 0, 0],
[2, 2, 2],
[0, 0, 0];
];
- 보드에 각 테트로미노를 그릴 수 있도록 캔버스 컨텍스트를 참조하는 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]
];
// 시작 위치
this.x = 3;
this.y = 0;
}
draw() {
this.ctx.fillStyle=this.color;
// shape의 셀을 순회하면서 0보다 크면 색을 칠한다.
this.shape.forEach((row, y) => {
row.forEach((value, x) => {
if(value > 0) this.ctx.fillRect(this.x + x, this.y + y, 1, 1);
});
});
}
}
- 버튼을 클릭하면 테트로미노 그리는 기능 추가한다.
//*** main.js
let board = new Board();
function play() {
board.reset(); // 보드판 초기화
console.table(board.grid);
let piece = new Piece(ctx);
piece.draw(); // 테트로미노 그리기
board.piece=piece;
}
키보드 입력 : Spread operator, Let and const
- 보드 위에서 위치를 변경하기 위해 현재 조각의 x 또는 y 속성값을 변경하는 메서드 추가
//*** piece.js
// 키보드로 움직이기
move(p) {
this.x = p.x;
this.y = p.y;
}
- 키들을 키 코드 값으로 매핑한다. 열거형(enum)처럼 사용하기 위해 객체를 프리징한다.
//*** constants.js
// 키코드로 키매핑
const KEY = {
LEFT: 37,
RIGHT: 39,
DOWN: 40
}
// 불변으로 만드는 값은 1레벨에서만 동작한다 -> 객체 안에 하위의 객체는 불변하게 만들 수 없다.
Object.freeze(KEY);
- 키보드 이벤트를 감지해서, 왼쪽, 오른쪽, 아래 방향키를 누르면 조각이 움직이게 만든다.
//*** main.js
moves = {
[KEY.LEFT]: p => ({...p, x: p.x - 1}),
[KEY.RIGHT]: p => ({...p, x: p.x + 1}),
[KEY.DOWN]: p => ({...p, y: p.y + 1}),
}
document.addEventListener('keydown', event => {
if(moves[event.keyCode]) {
event.preventDefault();
// 조각의 새 상태를 얻음
let p = moves[event.keyCode](board.piece);
if(board.valid(p)) {
// 이동 가능한 조각을 이동
board.piece.move(p);
// 그리기 전에 이전 좌표를 삭제
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// 테트로미노 그림
board.piece.draw();
}
}
});
충돌 감지
-
먼저 아래와 같은 충돌을 확인 후 안전한 경우에만 테트로미노를 움직인다.
- 바닥에 닿는다.
- 왼쪽 또는 오른쪽 벽으로 이동한다.
- 보드 안에 다른 블록과 부딪친다.
- 회전하는 중에 벽 또는 다른 블록과 부딪친다.
-
테트로미노를 움직이기 전에 이동한 위치가 유효한지 확인하는 로직을 추가한다. 충돌을 감지하기 위해, 테트로미노가 새롭게 차지할 그리드의 모든 공간을 순회한다.
//*** board.js
insideWalls(x) {
return x >= 0 && x < COLS;
}
aboveFloor(y) {
return y <= ROWS;
}
notOccupied(x, y) {
return this.grid[y] && this.grid[y][x] === 0;
}
valid(p) {
// 조각의 모든 블록 좌표를 계산하고 유효한 위치인지 확인한다
return p.shape.every((row, dy) => {
return row.every((value, dx) => {
let x = p.x + dx;
let y = p.y + dy;
return (
value === 0 ||
(this.insideWalls(x) && this.aboveFloor(y) && this.notOccupied(x, y))
);
});
});
}
하드 드롭(hard drop)을 추가
- 스페이스 바를 누르면 테트로미노가 무언가와 충돌할 때까지 떨어진다.
//*** constants.js
// 키코드로 키매핑
const KEY = {
// ... 기존 코드
SPACE: 32,
}
//*** main.js
moves = {
// ... 기존 코드
[KEY.SPACE]: p => ({ ...p, y: p.y + 1 }),
}
document.addEventListener('keydown', event => {
if(moves[event.keyCode]) {
event.preventDefault();
// 조각의 새 상태를 얻음
let p = moves[event.keyCode](board.piece);
// 스페이스 누를 경우 하드 드롭
if (event.keyCode === KEY.SPACE) {
while (board.valid(p)) {
board.piece.move(p);
p = moves[KEY.DOWN](board.piece);
}
}
if(board.valid(p)) {
// 기존 코드
}
}
});
회전
- 시계 방향으로 회전시키는 방법
- 두 개의 반사 행렬은 45도에서 90도로 회전을 가능하게 하므로 행렬을 변환할 수 있다.
- 그런 다음 열의 순서를 바꾸는 치환 행렬을 곱한다.
- 한마디로 \ 을 기준으로 서로 바꿔주고 reverse하면 90도 회전
//*** board.js
rotate(p){
// 불변성을 위해 JSON으로 복사
let clone = JSON.parse(JSON.stringify(p));
// 행과 열을 서로 바꾸는 반사행렬 처리
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]];
}
}
// 열 순서대로 뒤집는다.
p.shape.forEach(row => row.reverse());
return clone;
}
- 회전하는 키코드 추가하고 회전메서드 연결
//*** constants.js
// 한번씩 등록하기 귀찮아서 미리 키코드 다 만듬
const KEY = {
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
P: 80,
Q: 81
}
//*** main.js
moves = {
//
[KEY.UP]: p => board.rotate(p, ROTATION.RIGHT),
}
테트로미노 랜덤화
- Super Rotation System에 따르면, 조각의 초기 위치를 지정하고 색상과 함께 상수에 추가할 수 있다. (https://tetris.fandom.com/wiki/SRS)
//*** constants.js
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]],
[[0, 0, 3], // 0,0 -> 2,0 ; 0,1 -> 1,0 ; 0,2 -> 0,0
[3, 3, 3], // 1,0 -> 2,1 ; 1,1 -> 1,1 ; 1,2 -> 0,1
[0, 0, 0]],// 2,0 -> 2,2 ; 2,1 -> 1,2 ; 2,2 -> 0,2
[[4, 4], [4, 4]],
[[0, 5, 5], [5, 5, 0], [0, 0, 0]],
[[0, 6, 0], [6, 6, 6], [0, 0, 0]],
[[7, 7, 0], [0, 7, 7], [0, 0, 0]]
];
Object.freeze(SHAPES);
//*** piece.js
class Piece {
// 한 조각을 선택하기 위해 조각들의 인덱스를 랜덤화
randomizeTetrominoType(noOfTypes) {
return Math.floor(Math.random() * noOfTypes + 1);
}
spawn() {
// this.color = 'blue';
// this.shape = [
// [2, 0, 0],
// [2, 2, 2],
// [0, 0, 0]
// ];
// Play 버튼을 누를 때마다 다른 모양과 색상의 조각들 생성
const typeId = this.randomizeTetrominoType(COLORS.length);
this.shape = SHAPES[typeId];
this.color = COLORS[typeId];
// 시작 위치
this.x = 0;
this.y = 0;
}
}
게임 루프 : Default parameters
- 같은 코어 함수를 실행하고 또 실행하는 사이클을 일컬어 게임 루프(game loop)라고 부른다.
- 테트로미노가 1초마다 스크린 아래로 움직이는 게임 루프를 만들어야 한다.
//*** board.js
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
}
return true;
}
- 1초마다 아래로 한칸씩 움직이는 drop()메서드를 반복해서 호출한다.
//*** main.js
time = { start: 0, elapsed: 0, level: 1000 };
function animate(now=0){
time.elapsed = now - time.start;
// 1초마다 아래로 한칸씩 움직이는 drop()메서드를 호출
if(time.elapsed > time.level) {
time.start = now;
board.drop();
}
// 새로운 상태로 그리기 전에 보드를 지운다.
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
board.piece.draw();
// drop()메서드를 호출을 애니메이션으로 반복 처리
requestId = requestAnimationFrame(animate);
}
'Ecmascript > Canvas' 카테고리의 다른 글
[테트리스 만들기] 점수와 난이도, 게임 종료/중지 (0) | 2020.01.26 |
---|---|
[테트리스 만들기] 테트리스 기능 구현(다음 블록 생성, 한줄 채우면 사라지고 점수 증가) (0) | 2020.01.19 |
캔버스와 비디오 (0) | 2019.12.18 |
캔버스 #01 애니메이션 중지, 이미지 (0) | 2019.12.15 |
캔버스 #01 그리기와 애니메이션 원리 (0) | 2019.12.15 |