[테트리스 만들기] 테트리스 기능 구현(다음 블록 생성, 한줄 채우면 사라지고 점수 증가)
undeadloper2020. 1. 19. 15:29
고정하기
테트리미노 화면 바닥에 닿으면 조각을 고정하고 새로운 조각을 생성해야 한다.
먼저 조각을 고정하는 freeze() 함수 생성해서 테스트
//*** board.js
class Board {
grid; // 보드 그리드
piece; // 현재 조각 객체
// ... 기존 코드
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
} else {
// 유효한 공간이 아니면 못움직이게 고정
this.freeze();
if (this.piece.y === 0) {
// Game over
return false;
}
}
return true;
}
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;
}
});
});
}
}
블록이 바닥에 닿으면 움직일수 없고, board.grid를 콘솔 찍어보면 해당 그리드에 블록이 차지
추가 블록(테트리미노) 생성하기
블록이 바닥에 닿으면 추가로 블록을 생성
블록을 추가된다는건 각 조각의 인스턴스마다 속성값을 가지고 있어야 된다는 것
piece의 typeID의 인스턴스화 this.typeId으로 접근하게 변경
각 조각 시작위치를 구하는 부분을 추가
//*** piece.js
class Piece {
x;
y;
color;
shape;
ctx;
typeId; // 조각타입도 인스턴스마다 할당(this.typeId으로 접근)
constructor(ctx) {
this.ctx = ctx;
this.spawn();
}
// Play 버튼을 누를 때마다 다른 모양과 색상의 조각들 생성
spawn() {
this.typeId = this.randomizeTetrominoType(COLORS.length);
this.shape = SHAPES[this.typeId];
this.color = COLORS[this.typeId];
// 시작 위치
this.x = 0;
this.y = 0;
}
// ... 기존 코드
// 키보드로 움직이기
move(p) {
this.x = p.x;
this.y = p.y;
this.shape = p.shape;
}
// ... 기존 코드
// 시작 위치
setStartingPosition() {
this.x = this.typeId === 4 ? 4 : 3;
}
}
블록을 추가하기 위해 다음 조각의 객체와 캔버스 객체를 추가한다
main.js에 있던 캔버스와 블록 크기 관련 로직을 가져와서 board의 init() 메서드에서 처리
다음 조각 생성하는 getNewPiece() 메서드 생성
끝까지 내려오면(drop) getNewPiece() 호출해서 새로운 조각 생성
각 인스턴스가 회전할수 있게 boardd의 rotate() 메서드를 수정 필요
로직이 변경되는건 아니고 원본 블로거가 반사행렬할때 언급안하고 변수명들을 바꿔놨다다. (clone -> p, p -> piece)
//*** board.js
class Board {
grid; // 보드 그리드
piece; // 현재 조각 객체
next; // 다음 조각 객체
ctx; // 현재 조각의 캔버스
ctxNext; // 다음 조각의 캔버스
constructor(ctx, ctxNext) {
this.ctx = ctx;
this.ctxNext = ctxNext;
this.init();
}
init() {
// 캔버스 크기 계산
this.ctx.canvas.width = COLS * BLOCK_SIZE;
this.ctx.canvas.height = ROWS * BLOCK_SIZE;
// 블록 크기 변경: 매번 BLOCK_SIZE로 계산할 필요가 없이 블록의 크기를 1로 취급
this.ctx.scale(BLOCK_SIZE, BLOCK_SIZE);
// 다음 조각 생성
this.getNewPiece();
}
// 게임 초기화
reset() {
// 보드 초기화
this.grid = this.getEmptyBoard();
// 새로 조각 생성
this.piece = new Piece(this.ctx);
this.piece.setStartingPosition();
this.getNewPiece();
}
// ... 기존 코드
rotate(piece){
// 불변성을 위해 JSON으로 복사
let p = JSON.parse(JSON.stringify(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]];
}
}
// 열 순서대로 뒤집는다.
p.shape.forEach(row => row.reverse());
return p;
}
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
} else {
// 유효한 공간이 아니면 못움직이게 고정
this.freeze();
if (this.piece.y === 0) {
// Game over
return false;
}
// 끝까지 내려오면 새로운 조각 생성
this.piece = this.next;
this.piece.ctx = this.ctx;
this.piece.setStartingPosition();
this.getNewPiece();
}
return true;
}
// ... 기존 코드
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(); // 테트로미노 색으로 칠해서 화면을 그림
}
// 다음 조각 생성
getNewPiece() {
this.next = new Piece(this.ctxNext);
this.ctxNext.clearRect(
0,
0,
this.ctxNext.canvas.width,
this.ctxNext.canvas.height
);
this.next.draw();
}
블록을 추가할수 있게 메인에서 다음 블록의 캔버스 객체를 추가
board의 init() 메서드로 옮긴 캔버스와 블록 크기 관련 로직 삭제
각 인스턴스마다 애니메이션을 반복하게 requestId 변수 추가해서 play() 함수의 로직을 변경한다.
다음 블록 크기를 만드는 initNext() 함수 선언 및 호출
//*** main.js
// 현재 블록
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
// 다음 블록
const canvasNext = document.getElementById('next');
const ctxNext = canvasNext.getContext('2d');
// 캔버스 크기 계산
// ctx.canvas.width = COLS * BLOCK_SIZE;
// ctx.canvas.height = ROWS * BLOCK_SIZE;
// 블록 크기 변경: 매번 BLOCK_SIZE로 계산할 필요가 없이 블록의 크기를 1로 취급
// ctx.scale(BLOCK_SIZE, BLOCK_SIZE);
let requestId;
let board = new Board(ctx, ctxNext);
initNext();
function initNext() {
// Calculate size of canvas from constants.
ctxNext.canvas.width = 4 * BLOCK_SIZE;
ctxNext.canvas.height = 4 * BLOCK_SIZE;
ctxNext.scale(BLOCK_SIZE, BLOCK_SIZE);
}
function play() {
board.reset(); // 보드판 초기화
// console.table(board.grid);
// let piece = new Piece(ctx);
// board.piece=piece;
if (requestId) {
cancelAnimationFrame(requestId);
}
// piece.draw(); // 테트로미노 그리기
animate();
}
// ... 기존 코드
function animate(now=0){
// ... 기존 코드
// board에 piece 추가했으므로 piece 삭제
board.draw();
// drop()메서드를 호출을 애니메이션으로 반복 처리
requestId = requestAnimationFrame(animate);
}
// ... 기존 코드
꽉찬 라인 지우기
꽉찬 라인은 사라지고, 그 위에 나머지 조각들이 남아야 함
라인은 지우는 clearLines() 메서드 생성
한 라인의 모든 값이 0보다 크면 행을 삭제하고 맨 위에 0으로 채워진 행을 추가하는 원리
//*** board.js
// ... 기존 코드
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
} else {
// 유효한 공간이 아니면 못움직이게 고정
this.freeze();
// 한 라인이 꽉차면 블록 삭제
this.clearLines();
if (this.piece.y === 0) {
// Game over
return false;
}
// 끝까지 내려오면 새로운 조각 생성
this.piece = this.next;
this.piece.ctx = this.ctx;
this.piece.setStartingPosition();
this.getNewPiece();
}
return true;
}
// ... 기존 코드
clearLines() {
this.grid.forEach((row, y) => {
// 모든 값이 0보다 큰지 비교한다.
if (row.every(value => value > 0)) {
// 행을 삭제한다.
this.grid.splice(y, 1);
// 맨 위에 0으로 채워진 행을 추가한다.
this.grid.unshift(Array(COLS).fill(0));
}
});
}
board에 지운 라인수 체크하는 로직 추가하고 라인수당 점수를 반환하는 getLineClearPoints() 메서드 추가
//*** board.js
// ... 기존 코드
clearLines() {
let lines = 0; // 지운 라인 수
this.grid.forEach((row, y) => {
// 모든 값이 0보다 큰지 비교한다.
if (row.every(value => value > 0)) {
lines++;
// 행을 삭제한다.
this.grid.splice(y, 1);
// 맨 위에 0으로 채워진 행을 추가한다.
this.grid.unshift(Array(COLS).fill(0));
}
});
if (lines > 0) {
// 지워진 라인들이 있다면 점수를 추가
account.score += this.getLineClearPoints(lines);
}
}
// 지운 라인 갯수당 점수 제공
getLineClearPoints(lines) {
let score =
lines === 1 ? POINTS.SINGLE :
lines === 2 ? POINTS.DOUBLE :
lines === 3 ? POINTS.TRIPLE :
lines === 4 ? POINTS.TETRIS :
0;
return score;
}
}
점수와 블록 라인을 계산하기 위한 accountValues 객체를 추가
accountValues 객체는 이벤트에 따라 다르게 값이 할당되는 객체이며 이 객체를 Proxy 객체에 전달
Proxy 객체를 생성하고 set 메서드에서 화면의 점수 텍스트를 변경하는 updateAccount() 메서드 실행
//*** main.js
// ... 기존 코드
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)) {
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; // 아래 방향키 눌러서 빨리 내리면 점수 증가
}
}
if(board.valid(p)) {
// 이동 가능한 조각을 이동
board.piece.move(p);
// 그리기 전에 이전 좌표를 삭제
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// 테트로미노 그림
board.piece.draw();
}
}
});
// 점수 계산
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;
}
});