코딩 강의/컨텐츠를 만들어 볼까요

테트리스 게임 개발 #4 - 블럭 쌓는 로직 작성

아미넴 2020. 9. 18.
반응형

목차

     

    지난 강의 리뷰

    테트리스 게임 개발 #3 - 블럭 이동 기능 구현

     

    테트리스 게임 개발 #3 - 블럭 이동 기능 구현

    목차 지난 강의 리뷰 테트리스 게임 개발 #2 - 블럭 모양 및 랜덤 선택 구현 테트리스 게임 개발 #2 - 블럭 모양 및 랜덤 선택 구현 목차 지난 강의 리뷰 테트리스 게임 개발 #1 - 소개 및 기본 화면

    sangminem.tistory.com

     

    이번에는 다음과 같은 기능을 구현해 보겠습니다.

     

    1. 블럭이 위에서 아래로 자동으로 내려오고

    2. 블럭이 바닥에 도달하면 새로운 블럭을 다시 내려오도록 해 볼게요.

    3. 그런 다음 내려온 블럭을 쌓아 보겠습니다.

     

    블럭 애니메이션 효과

    애니메이션 효과를 주기 위해서는 window.requestAnimationFrame() 메서드를 이용하면 됩니다.

    자세한 사항을 알고 싶으시면 아래 문서를 참고하시면 되구요.

    developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame

     

    window.requestAnimationFrame()

    화면에 새로운 애니메이션을 업데이트할 준비가 될때마다 이 메소드를 호출하는것이 좋습니다. 이는 브라우저가 다음 리페인트를 수행하기전에 호출된 애니메이션 함수를 요청합니다. 콜백의

    developer.mozilla.org

    블럭이 1초마다 내려오는 함수를 작성해 보도록 할게요.

    먼저 global.js에 전역변수 선언이 필요합니다.

    //...기존 선언 변수 생략
    
    let time = 0;
    let requestAnimationId = null;

    일단 처음 시작 시간은 0으로 초기화 합니다.

    그리고 애니메이션 요청 ID 관리를 위해서 변수를 하나 선언해 주었습니다.

     

    다음으로 main.js에 실제 동작 함수를 작성합니다.

    function main() {
        //... 기존 로직 생략
        //rebuild 함수 호출 제거
        repeatMotion(0);
    }
    
    function repeatMotion(timeStamp) {
        if(timeStamp - time > 1000) {
            if(!validMove(mainBlock,0,1)) {
                mainBlock = nextBlock;
                nextBlock = createNextBlock();
            }
            time = timeStamp;
        }
    
        rebuild();
        requestAnimationId = window.requestAnimationFrame(repeatMotion);
    }

    repeatMotion의 맨 마지막 부분을 보면 window.requestAnimationFrame에서

    콜백 함수로 자기 자신을 다시 호출하면서 무한 반복을 하는 구조입니다.

    문서에 초당 60번을 콜 한다고 되어 있습니다.

    그리고 콜백 함수에서 인자로 0부터 시작하는 ms 단위의 시간을 지속적으로 전달해 줍니다.

    리턴 값으로는 요청 id가 전달 되고, 리턴 받은 id 값으로 애니메이션을 중단할 수도 있습니다.

     

    main 함수에서 최초 timeStamp 값을 0으로 전달하였고

    전역 변수 time의 초기 값을 0으로 주었기 때문에 초기 시간 차(timeStamp - time)는 0이 됩니다.

    이는 if문을 만족 시키지 못하므로 건너 뛰게 되고 나머지를 지속적으로 실행하게 됩니다.

    timeStamp 값은 계속 늘어나므로 결국 시간 차가 1000ms를 넘게 될 것이며

    if문을 실행하게 되고 time은 다시 그 시점의 timeStamp 값을 저장합니다.

    다시 timeStamp 값은 증가하여 시간 차가 1000ms가 넘을 때 if문을 실행하겠죠.

    이런 방식으로 1초 단위의 애니메이션을 구현하였습니다.

     

    if문 안을 좀 더 자세히 들여다 보면, validMove 함수를 y축 방향으로 1만큼 증가 시키고 있습니다.

    1초마다 아래로 한 칸씩 이동하도록 한 것이죠.

    리턴 값이 false가 된다면 nextBlock을 mainBlock에 할당하고 다시 nextBlock에 새로운 블럭을 담습니다.

     

    그리고 1초에 60번 rebuild가 호출되면서 지속적으로 resizing과 redrawing을 하게 됩니다.

    main 함수에서 rebuild 함수 호출 부분은 제거해 줍니다.

     

     

     

     

    중간 확인

    지금까지 작성 내용을 저장하고 실행을 해 보겠습니다.

    1초에 아래로 한 칸씩 잘 이동합니다.

    블럭이 바닥에 닿으면 위에서 새로운 다음 블럭도 잘 내려 오구요.

    그런데 블럭이 쌓이지 않는군요.

     

    지금부터는 블럭을 쌓는 로직을 생각해 볼게요.

     

    블럭 쌓는 로직 작성

    canvas 엘리먼트만으로는 데이터를 컨트롤하기 어렵기 때문에

    main-board에 대응하는 2차원 배열을 만들어 관리하도록 하겠습니다.

    matrix.js 파일에 함수를 하나 만들어 볼게요.

    function initMatrix(rows, cols) {
        let matrix = [];
        for(let y=0; y < rows; y++) {
            matrix.push(new Array(cols).fill(0));
        }
        return matrix;
    }

    행, 열 갯수를 인자로 받아서 2차원 배열을 생성해 주는 함수입니다.

     

    그리고 다음과 같이 global.js 파일에 전역변수를 선언해 주겠습니다.

    const matrixMainBoard = initMatrix(ROWS_MAIN_BOARD, COLS_MAIN_BOARD);

     

    initMatrix 함수에 의해 아래 표와 같이 0으로 채워진 2차원 배열이 생성되었습니다.

     

    그 다음으로 블럭이 바닥에 닿으면

    그 부분을 블럭이 가지는 숫자로 채워주는 로직을 구현해 보겠습니다.

    마찬가지로 matrix.js 파일에 구현을 해 줍니다.

    function stack(block, matrix) {
        block.shape.forEach((row, y) => {
            row.forEach((value, x) => {
                if(value > 0) {
                    matrix[y+block.y][x+block.x] = block.shape[y][x];
                }
            });
        });
    }

    블럭을 쌓는 원리는 간단합니다.

    블럭이 가지는 위치와 블럭의 모양(value > 0인 부분)을 계산하여 앞서 생성한 2차원 배열에 그 값을 대입합니다.

     

     

     

     

    블럭 이동 검증 수정

    여기서 한 가지 더 고려할 것이 생겼습니다.

    블럭이 main-board에 쌓이면 바닥만 고려해서는 안 되겠죠.

    쌓여 있는 블럭도 고려를 해서 블럭의 이동을 검증해야 합니다.

     

    validate 함수를 수정해 보겠습니다.

    function validate(block, matrix) {
        let isValid = true;
    
        block.shape.some((row, dy) => {
            row.some((value, dx) => {
                if(value > 0) {
                    if(block.x+dx < 0 || block.x+dx >= matrix[0].length ||
                       block.y+dy < 0 || block.y+dy >= matrix.length ||
                       matrix[block.y+dy][block.x+dx] > 0) { //추가된 조건
                        isValid = false;
                        return true;
                    }
                }
            });
            if(!isValid) {
                return true;
            }
        });
    
        return isValid;
    }

    matrix 인자를 추가하여 블럭의 위치와 보드의 위치를 비교하여 값이 0보다 크다면 이동을 제한해야 합니다.

     

    이와 연관되어 validMove와 validRotate 함수도 약간의 수정이 이루어졌습니다.

    function validMove(block, matrix, x, y) {
        const cloneBlock = clone(block);
        move(cloneBlock, x, y);
        if(validate(cloneBlock, matrix)) {
            move(block, x, y);
            return true;
        } else {
            return false;
        }
    }
    
    function validRotate(block, matrix) {
        const cloneBlock = clone(block);
        rotate(cloneBlock);
        if(validate(cloneBlock, matrix)) {
            rotate(block);
            return true;
        } else {
            return false;
        }
    }

    조금 번거롭지만 matrix 인자도 받아 처리를 하였습니다.

    전역변수여서 그냥 사용할 수도 있지만 좋지 못한 습관입니다.

    main 함수를 제외하고는 모두 인자를 받아 처리하는 구조로 작성할 예정입니다.

     

    실제 호출하는 main.js 파일도 수정을 해야겠죠.

    function keyHandler(event) {
        //... 생략
        switch(inputKey) {
            case KEY.UP :
                validRotate(mainBlock, matrixMainBoard);
                break;
            case KEY.DOWN :
                validMove(mainBlock, matrixMainBoard, 0, 1);
                break;
            case KEY.LEFT :
                validMove(mainBlock, matrixMainBoard, -1, 0);
                break;
            case KEY.RIGHT :
                validMove(mainBlock, matrixMainBoard, 1, 0);
                break;
        }
    
        drawBlock(mainBlock, ctxMainBoard);
    }
    
    function repeatMotion(timeStamp) {
        const duration = timeStamp - time;
    
        if(duration > 1000) {
            if(!validMove(mainBlock, matrixMainBoard, 0, 1)) {
                stack(mainBlock, matrixMainBoard); //추가 부분
                mainBlock = nextBlock;
                nextBlock = createNextBlock();
                
                /* 추가 부분 시작 */
                matrixMainBoard[0].some((value, x) => {
                    if(value > 0) {
                        window.cancelAnimationFrame(requestAnimationId);
                        requestAnimationId = null;
                        return true;
                    }
                });
                
                if(requestAnimationId == null) {
                    return;
                }
                /* 추가 부분 끝 */
            }
            time = timeStamp;
        }
    
        rebuild();
        requestAnimationId = window.requestAnimationFrame(repeatMotion);
    }

    validMove 함수와 validRotate 함수 호출 부에 matrix 인자를 추가해 주었구요.

    repeatMotion 함수 내에서 stack 함수를 호출하였습니다.

    그리고 블럭이 끝까지 쌓인 경우(y축이 0인 x값이 0보다 큰 경우) 애니메이션을 중단하는 로직도 추가했습니다.

     

    화면에 그리기

    블럭을 쌓는 로직은 어느 정도 완료된 것으로 보이고

    이제 canvas에 그리기만 하면 될거 같습니다.

    draw.js 파일에 함수를 하나 만들어 보겠습니다.

    function drawBoard(matrix, ctx) {
        matrix.forEach((row, y) => {
            row.forEach((value, x) => {
                if(value > 0) {
                    ctx.fillStyle = 'white';
                    ctx.fillRect(x, y, 1, 1);
                }
            });
        });
    }

    단순하게 값이 있는 부분을 흰색 사각형으로 채우는 로직입니다.

    이 함수를 rebuild 함수에서 반복적으로 호출만 해주면 되겠습니다.

     

    function rebuild() {
        resize();
        drawBlock(mainBlock, ctxMainBoard);
        drawBlock(nextBlock, ctxNextBoard);
        drawBoard(matrixMainBoard, ctxMainBoard); //추가된 부분
    }

    main.js 파일의 rebuild 함수에 drawBoard 함수 호출을 추가하였습니다.

    블럭을 먼저 그리고 추가로 보드에 쌓인 블럭을 그리게 됩니다.

     

     

     

     

    결과 확인

    거의 된 거 같은데 한 번 실행해 보겠습니다.

    블럭이 기가 막히게 잘 쌓이기만 하네요.

    한 줄이 다 채워졌으면 제거가 되어야 게임이 되겠죠. :)

     

    다음 시간에는 블럭을 클리어 하는 기능과 하드 드랍, 게임 종료 등

    실제 게임에 필요한 요소들을 구성해 보도록 할게요.

     

    드디어 끝이 보입니다!

     

    #다음강의

    테트리스 게임 개발 #5 - 하드 드랍 및 라인 제거 기능 구현

     

    테트리스 게임 개발 #5 - 하드 드랍 및 라인 제거 기능 구현

    목차 지난 강의 리뷰 테트리스 게임 개발 #4 - 블럭 쌓는 로직 작성 테트리스 게임 개발 #4 - 블럭 쌓는 로직 작성 목차 지난 강의 리뷰 테트리스 게임 개발 #3 - 블럭 이동 기능 구현 테트리스 게임

    sangminem.tistory.com

     

    반응형

    댓글

    💲 추천 글