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

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

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

목차

     

    지난 강의 리뷰

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

     

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

    목차 지난 강의 리뷰 테트리스 게임 개발 #3 - 블럭 이동 기능 구현 테트리스 게임 개발 #3 - 블럭 이동 기능 구현 목차 지난 강의 리뷰 테트리스 게임 개발 #2 - 블럭 모양 및 랜덤 선택 구현 테트리

    sangminem.tistory.com

     

    지금부터는 테트리스의 필수적인 기능을 마무리 하는 시간을 가져 보겠습니다.

     

    1. 하드 드랍

    2. 라인 제거

    3. 게임 종료

     

    위와 같은 순서로 작성을 한 번 해볼게요.

     

    하드 드랍 기능 구현

    먼저 하드 드랍 기능입니다.

    그러기 위해 새로운 키를 등록해야 겠죠.

    function keyHandler(event) {
        const inputKey = event.keyCode;
    
        const KEY = {
            //... 생략
            SPACE: 32
        }
    
        switch(inputKey) {
            //... 생략
            case KEY.SPACE :
                while(validMove(mainBlock, matrixMainBoard, 0, 1));
                break;
        }
    }

    keyHandler 함수에 스페이스 바를 하드 드랍 키로 등록을 하였습니다.

    유효한 범위 내에서 계속 아래로 한 칸씩 이동하도록 반복문으로 기능을 만들었습니다.

     

    간단하게 만들어졌죠.

     

    라인 제거 및 게임 오버 구현

    다음으로 블럭이 한 줄이 가득 채워지면 제거하는 기능을 만들어 볼게요.

    블럭을 쌓는 기능과 마찬가지로 배열을 이용하여 로직을 구현하는 것이 바람직해 보입니다.

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

    function checkFilledLines(matrix) {
        result = [];
        for(let y=0; y < matrix.length; y++) {
            if(matrix[y].every(value => value > 0)) {
                result.push(y);
            }
        }
        return result;
    }
    

    먼저 라인이 가득 채워졌는 지 검사하여 행 위치를 리턴하는 함수를 만들었습니다.

     

    그 다음에 가득 채워진 라인을 배열에서 제거하고

    제거된 라인 수만큼 다시 채우는 로직을 만들어야 합니다.

    function removeLines(matrix, lineIndexes) {
        lineIndexes.forEach((y, i) => {
            matrix.splice(y, 1);
            matrix.unshift(new Array(matrix[0].length).fill(0));
        });
    }

    배열의 splice 메서드를 이용하여 해당 위치의 라인 1줄을 제거했구요.

    새로운 0으로 채워진 행으로 맨 위에 한 줄을 다시 만드는 로직을 unshift 메서드를 이용하여 구현했습니다.

    라인을 제거하는 로직을 함께 만들 수도 있지만 애니메이션 효과를 고려하여 실제 처리를 뒤로 미룰 예정입니다.

     

    만든 기능을 적용시키기 위해 일단 global.js에 전역 변수를 몇 가지 선언해 보겠습니다.

    //... 기존 선언 변수 생략
    
    let timeForRemovingLines = 0;
    let filledLines = [];
    
    let playing = false;

    라인 제거 시 애니메이션을 주기 위한 시간 변수를 하나 선언 했구요.

    제거시킬 라인을 담기 위한 배열도 하나 선언 했습니다.

    또한 현재 게임이 진행 중인 지를 판단할 변수도 하나 만들어서 다각적으로 활용할 예정입니다.

     

     

     

     

    그러면 실제 사용을 위해 main.js 파일도 수정해 보겠습니다.

    //... 기존 로직 생략
    
    function main() {
        window.addEventListener('resize', rebuild);
        start();
    }
    
    function start() {
        playing = true;
        window.addEventListener('keydown', keyHandler);
        setNextBlock();
        repeatMotion(0);
    }
    
    function keyHandler(event) {
        //... 생략
        switch(inputKey) {
            //... 생략
            case KEY.SPACE :
                while(validMove(mainBlock, matrixMainBoard, 0, 1));
                nextStep(); // 추가
                time = 0; // 추가
                break;
        }
    }
    
    function setNextBlock() {
        mainBlock = nextBlock?nextBlock:createNextBlock();
        mainBlock.y = 0;
        mainBlock.x = 3;
        nextBlock = createNextBlock();
        nextBlock.y = (nextBlock.shape[1][0]===7)?0:1;
        nextBlock.x = (nextBlock.shape[0][0]===1)?1:0;
    }
    
    function initRemoveLines() {
        filledLines = [];
        timeForRemovingLines = 0;
        time = 0;
    }
    
    function nextStep() {
        stack(mainBlock, matrixMainBoard);
        filledLines = checkFilledLines(matrixMainBoard);
    
        if(filledLines.length === 0) {
            matrixMainBoard[0].some((value, x) => {
                if(value > 0) {
                    playing = false;
                    return true;
                }
            });
    
            const cloneNextBlock = clone(nextBlock);
            cloneNextBlock.y = 0;
            cloneNextBlock.x = 3;
            if(validate(cloneNextBlock, matrixMainBoard)) {
                setNextBlock();
            } else {
                playing = false;
            }
        }
    }
    
    function quit() {
        window.cancelAnimationFrame(requestAnimationId);
        requestAnimationId = null;
        window.removeEventListener('keydown', keyHandler);
    }
    
    function repeatMotion(timeStamp) {
        // 추가
        if(time === 0) {
            time = timeStamp;
        }
    
        if(timeStamp - time > 500) {
            if(!validMove(mainBlock, matrixMainBoard, 0, 1)) {
                nextStep(); // 기존 로직 nextStep 함수로 이동
            }
            time = timeStamp;
        }
    
        // 라인 제거 추가
        if(filledLines.length > 0) {
            if(timeForRemovingLines === 0) {
                timeForRemovingLines = timeStamp;
            }
    
            if(timeStamp - timeForRemovingLines > 300) {
                removeLines(matrixMainBoard, filledLines);
                initRemoveLines();
                setNextBlock();
            }
        }
    
        rebuild();
    
        // 게임 중일 때만 애니메이션 반복
        if(playing) {
            requestAnimationId = window.requestAnimationFrame(repeatMotion);
        } else {
            quit();
        }
    }

    변경한 부분이 좀 많습니다.

     

    게임 시작에 필요한 로직을 모두 start 함수로 옮기고 main 함수에서 start 함수를 호출하였습니다.

    playing 변수에 true를 할당함으로써 게임 중임을 표현하였고

    최초 블럭 셋팅과 함께 애니메이션 함수를 실행하였습니다.

     

    먼저 repeatMotion 함수에 블럭에 쌓는 부분을 nextStep 함수로 이동을 시켰습니다.

    하드 드랍 시에도 같은 동작이 필요하기 때문에 로직 중복을 피하기 위함입니다.

    setNextBlock 함수도 비슷한 이유로 분리를 했고 블럭의 초기 위치도 적당히 고려를 하였습니다.

     

    하드 드랍 시에 애니메이션 시간을 초기화 하는 것이 더 깔끔하므로

    time을 0으로 만들어서 다시 timeStamp 값을 받을 수 있도록 하였습니다.

     

    nextStep 함수에서는 filledLines 전역 변수를 이용하여 제거할 라인이 있는 지 체크를 합니다.

    제거할 라인이 0일 때만 영향을 받도록 하여 라인 제거 시에 사용자의 임의 동작을 방지하였습니다.

    그리고 main-board 배열을 검사하여 블럭이 맨 위에 다다른 경우나 다음 블럭이 더 이상 움직일 수 없는 경우

    게임을 중단 시키기 위해 playing 변수를 false로 바꾸어 줍니다.

    nextBlock의 초기 위치와 mainBlock의 초기 위치가 다르므로 복제&변경 후 유효성 검사를 진행 했습니다.

     

    repeatMotion 함수에서는 timeForRemovingLines 시간 변수를 이용하여

    0.3초 간 시간을 딜레이 시키고 removeLines 함수를 호출하여 라인 제거를 하였으며

    그 후 initRemoveLines 함수를 호출하여 라인 제거에 사용된 변수들을 초기화 시키고

    setNextBlock 함수로 다음 블럭을 셋팅하도록 하였습니다.

    앞서 nextStep 함수에서 playing 변수가 false가 되었다면 quit 함수를 호출하여

    애니메이션을 중단하고 keydown 이벤트리스너를 제거합니다.

    rebuild 함수는 마지막까지 호출이 되어야 모든 행위가 정상적으로 main-board에 그려지므로

    이러한 방식을 사용하였습니다.

     

    라인 제거 애니메이션

    마지막으로 제거되는 라인에 간단한 애니메이션 효과를 넣기 위해

    draw.js 파일에 작성을 해 보겠습니다.

    function drawRemovingLines(ctx, cols, lineIndexes) {
        lineIndexes.forEach((y, i) => {
            for(let x=0; x < cols; x++) {
                ctx.fillStyle = 'red';
                ctx.fillRect(x, y, 1, 1);
            }
        });
    }

    제거할 라인을 잠시 빨간색으로 보여주는 정도로 효과를 확인해 보겠습니다.

     

    drawing 작업을 적용시키기 위해 main.js 파일의 rebuild 함수 부분에서 호출을 하겠습니다.

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

    빨간 부분이 맨 겉에 보여져야 하므로 마지막 부분에 추가를 해 주었습니다.

     

     

     

     

    결과 확인

    이제 저장을 하고 실행해 보겠습니다.

    블럭이 끝까지 쌓이니 게임이 종료되었습니다.

     

    다시 제대로 한 번 쌓아 보겠습니다.

    블럭 위치도 보드별로 잘 표시되고 블럭도 잘 쌓입니다.

     

    그럼 라인을 한 번 꽉 채워 보겠습니다.

    정확하게 잘릴 부분이 빨간색으로 0.3초 간 표시된 후에 정상적으로 제거가 되고 있습니다.

    제거가 되기까지 걸리는 시간 동안에는 다음 블럭이 내려오지 않고 잠시 대기 상태가 됩니다.

     

    이로써 게임의 연속성까지 만족이 되었습니다.

     

    다음 강의부터는 필수 요소는 아니지만

    게임의 재미를 더해주기 위한 점수나 레벨, 라인 부분의 구현과

    게임 시작, 중단, 일시정지와 같은 기능을 만들어 보는 시간을 가져 보겠습니다.

     

    기대해 주세요. :)

     

    #다음강의

    테트리스 게임 개발 #6 - 시작, 종료, 일시 정지 및 레벨, 라인, 점수 계산 기능 구현

     

    테트리스 게임 개발 #6 - 시작, 종료, 일시 정지 및 레벨, 라인, 점수 계산 기능 구현

    목차 지난 강의 리뷰 테트리스 게임 개발 #5 - 하드 드랍 및 라인 제거 기능 구현 테트리스 게임 개발 #5 - 하드 드랍 및 라인 제거 기능 구현 목차 지난 강의 리뷰 테트리스 게임 개발 #4 - 블럭 쌓

    sangminem.tistory.com

     

    반응형

    댓글

    💲 추천 글