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

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

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

목차

     

    지난 강의 리뷰

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

     

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

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

    sangminem.tistory.com

     

    지난 시간까지 기본적인 테트리스의 기능은 구현이 완료 되었습니다.

    지금부터는 부가적인 기능을 구현해 보는 시간을 갖도록 할 거예요.

     

    1. 게임 시작, 종료, 일시 정지

    2. 레벨, 라인, 점수 계산

     

    위와 같은 기능이 추가 되어야 더욱 게임 다운 게임이 되겠죠.

     

    게임 시작, 종료, 일시 정지

    일단 게임 시작, 일시 정지, 종료 부분을 생각해 보겠습니다.

    여태까지는 main 함수에서 바로 start 함수를 호출 하였는데요.

    이 기능을 버튼으로 구현해 보겠습니다.

    function main() {
        rebuild(); // 추가
        window.addEventListener('resize', rebuild);
        // start 함수 호출 삭제
    }

    먼저 페이지를 열자마자 실행되던 start 함수 호출을 main 함수에서 제거 했습니다.

     

    시작 기능

    그리고 start 함수를 좀 더 다듬어 보려고 합니다.

    function start() {
        reset();
        gameStatus = 'A';
        window.addEventListener('keydown', keyHandler);
        setNextBlock();
        repeatMotion(0);
    }
    
    function reset() {
        matrixMainBoard = initMatrix(ROWS_MAIN_BOARD, COLS_MAIN_BOARD);
        time = 0;
        timeForRemovingLines = 0;
        mainBlock = null;
        nextBlock = null;
    }

    일단 reset 함수를 새로 만들어서 변수 초기화 작업을 한 곳으로 몰았습니다.

    그리고 playing 변수를 gameStatus로 바꿔서 게임 상태를 A(Active), P(Pause), Q(Quit) 3가지로 나누었습니다.

    나머지는 동일합니다.

     

    종료 기능

    그리고 기존 quit 함수를 변경해 보겠습니다.

    function quit() {
        //... 기존 로직 생략
        
        ctxMainBoard.fillStyle = '#f0b71b';
        ctxMainBoard.fillRect(1, 3, 8, 1.8);
        ctxMainBoard.font = '1px NeoDungGeunMo';
        ctxMainBoard.fillStyle = '#ffffff';
        ctxMainBoard.fillText('게임 오버', 2.8, 4.2);
    
        gameStatus = 'Q';
    }

    상단에 적당히 위치를 잡아서 main-board에 '게임 오버'라는 문구를 띄우는 부분만 추가해 주었습니다.

    그리고 게임 상태를 Q로 변경하였습니다.

     

    일시 정지 기능

    그리고 pause 함수를 작성해 보도록 할게요.

    function pause() {
        if(requestAnimationId) {
            window.cancelAnimationFrame(requestAnimationId);
            requestAnimationId = null;
            gameStatus = 'P';
            
            ctxMainBoard.fillStyle = '#6f9cf0';
            ctxMainBoard.fillRect(1, 3, 8, 1.8);
            ctxMainBoard.font = '1px NeoDungGeunMo';
            ctxMainBoard.fillStyle = '#ffffff';
            ctxMainBoard.fillText('일시 정지', 2.8, 4.2);
        
        } else {
            gameStatus = 'A';
            repeatMotion(0);
        }
    }

    애니메이션이 진행 중이면 중단을 하고 진행 중이지 않으면 다시 시작을 하는 토글 형태로 구현을 했습니다.

    게임 중에 일시 정지를 하면 애니메이션을 중단 시키고 게임 상태를 P로 바꿔 준 다음

    main-board에 '일시 정지' 문구를 띄웁니다.

    그 상태에서 다시 pause 함수를 호출하면 게임 상태를 A로 바꾸고 애니메이션을 재시작 합니다.

     

     

     

     

    function nextStep() {
        //... 생략
        
        if(filledLines.length === 0) {
            //... 생략
            matrixMainBoard[0].some((value, x) => {
                if(value > 0) {
                    gameStatus = 'Q'; // 변경한 부분
                    return true;
                }
            });
            //... 생략
            if(validate(cloneNextBlock, matrixMainBoard)) {
                setNextBlock();
            } else {
                gameStatus = 'Q'; // 변경한 부분
            }
        }
    }
    
    function repeatMotion(timeStamp) {
        //... 생략
    
        rebuild();
    
        if(gameStatus === 'A') { // 변경한 부분
            requestAnimationId = window.requestAnimationFrame(repeatMotion);
        } else {
            quit();
        }
    }

    nextStep 및 repeatMotion 함수에서 playing 변수를 gameStatus 변수로 변경을 해 주었습니다.

     

    버튼 만들기

    그리고 side-contents 하위에 버튼을 만들어 보겠습니다.

    <!-- 생략 -->
    <div id="side-contents" class="side-contents">
        <!-- 생략 -->
        <p>
            <button id="start-button" onclick="start()" class="start-button">게임 시작</button>
        </p>
        <p>
            <button id="quit-button" onclick="quit()" class="quit-button">게임 종료</button>
        </p>
        <p>
            <button id="pause-button" onclick="pause()" class="pause-button">일시 정지</button>
        </p>
    </div>
    <!-- 생략 -->

    play.html 파일에 이렇게 3가지 버튼을 만들었습니다.

     

    일단 버튼 스타일도 적당히 잡아주겠습니다.

    common.css 파일에 작성합니다.

    .start-button {
        background-color: #227aec;
        border: 2px solid #1d42e6;
        color: #d7e5ff;
        text-align: center;
    }
    
    .quit-button {
        background-color: #e4a656;
        border: 2px solid #d36a15;
        color: #4e3513;
        text-align: center;
    }
    
    .pause-button {
        background-color: #33ce24;
        border: 2px solid #408d0d;
        color: #194d09;
        text-align: center;
    }

    버튼 색 및 글자 가운데 정렬만 작성했고 크기 조절은 반응형 웹에 맞게 만들어야 하므로 Javascript에서 진행하겠습니다.

     

    먼저 global.js에 전역 변수로 버튼 태그를 선언합니다.

    //... 기존 선언 변수 생략
    
    const startButton = document.querySelector('#start-button');
    const quitButton = document.querySelector('#quit-button');
    const pauseButton = document.querySelector('#pause-button');

    필요한 부분에 직접 기술해도 되지만 코드를 정리하는 차원에서 한 곳에 모아 작성하였습니다.

     

    다음은 main.js 파일의 resize 함수를 수정해 보겠습니다.

    function resize() {
        //... 기존 로직 생략
    
        setButtonStyle(startButton, WINDOW_INNERWIDTH);
        setButtonStyle(quitButton, WINDOW_INNERWIDTH);
        setButtonStyle(pauseButton, WINDOW_INNERWIDTH);
    }
    
    function setButtonStyle(elem, witdh) {
        elem.style.fontSize = witdh/350+'rem';
        elem.style.paddingTop = witdh/1400+'rem';
        elem.style.paddingBottom = witdh/1400+'rem';
        elem.style.paddingLeft = witdh/350+'rem';
        elem.style.paddingRight = witdh/350+'rem';
    }

    버튼 크기 및 안쪽 여백은 모두 동일하게 할 예정이므로 함수를 하나 만들어서 로직 중복을 피했습니다.

    각 버튼 엘리먼트와 브라우저 너비 값을 인자로 받아서 너비가 변할 때마다 크기가 조정되도록 하였습니다.

     

     

     

     

    중간 확인

    잘 적용되었는지 저장 후 실행을 해 보겠습니다.

    적당히 잘 반영된 것을 볼 수 있습니다.

    버튼 기능도 다 잘 작동이 되고 있습니다.

    스타일은 저보다 더 예쁘게 꾸미실 수 있을테니 여기서는 일단 넘어 가겠습니다.

     

    레벨, 라인, 점수 계산

    이제 레벨, 라인, 점수 계산을 구현해 볼 차례인데요.

    이 3가지는 모두 연관 관계가 있기 때문에 잘 생각을 해 봐야 할거 같아요.

    먼저 점수 계산을 생각해 보겠습니다.

    <p>점수: <span id="score">0</span> <span id="add-score" class="add-score"></span></p>

    play.html을 약간 수정했는데요.

    총 점수와 몇 점이 플러스 됐는지를 표현하기 위한 영역을 나눠 보았습니다.

     

    그리고 global.js 파일에 필요한 변수를 모아서 선언하겠습니다.

    let totalScore = 0;
    let scoreElem = document.querySelector('#score');
    let addScoreElem = document.querySelector('#add-score');
    let addScoreId = null;
    let globalAddScore = 0;
    
    let currentLevel = 1;

    각 엘리먼트 들과 총점, 그리고 setTimeout으로 플러스 된 점수를 잠깐 보여주기 위해 사용할 변수까지 선언하였습니다.

    그리고 드랍과 블럭 제거가 동시에 일어날 수 있으므로 점수가 합산되어 표시되어야 할 경우도 있습니다.

    그럴 때를 대비하여 전역 변수를 선언해서 계산할 예정입니다.

    점수와 레벨과 연관있게 하기 위해서 현재 레벨 변수도 선언하였습니다.

     

    이어서 실제 점수 계산 및 화면에 표시해 주기 위한 함수를 구현해 보겠습니다.

    function addScore(score) {
        totalScore += score;
        scoreElem.textContent = totalScore;
        if(score > 0) {
            addScoreElem.textContent = '+' + score;
        }
        if(addScoreId){
            clearTimeout(addScoreId);
        }
        addScoreId = setTimeout(() => {
            addScoreElem.textContent = "";
        }, 1000);
    }

    먼저 총점을 계산하여 score 엘리먼트에 대입을 했습니다.

    그리고 인자로 전달 받은 추가 점수를 1초간 보여줬다 사라지도록 setTimeout 함수를 사용하여 구현했습니다.

     

    다음은 실제 점수가 부여되어야 할 부분을 생각해 보겠습니다.

    function keyHandler(event) {
        //... 생략
    
        switch(inputKey) {
            //... 생략
            case KEY.DOWN :
                if(gameStatus === 'A') {
                    if(validMove(mainBlock, matrixMainBoard, 0, 1)) {
                        addScore(10*currentLevel); // 점수 계산
                    } else {
                        nextStep();
                        time = 0;
                    };
                }
                break;
            //... 생략
            case KEY.SPACE :
                if(gameStatus === 'A') {
                    while(validMove(mainBlock, matrixMainBoard, 0, 1)) {
                        globalAddScore += 20*currentLevel; // 점수 계산
                    };
                    nextStep();
                    time = 0;
                }
                break;
        }
    }

    일단 블럭을 드랍할 때 10점에 레벨을 곱하도록 하였고 하드 드랍 시에는 한 칸당 20점에 레벨을 곱하여 점수 계산을 하고 있습니다.

    하드 드랍으로 블럭을 제거할 때 점수 합산을 고려하기 위해 계산된 점수를 전역 변수에 임시로 저장만 해 둔 상태입니다.

     

    블럭이 제거되었을 때 점수 반영도 생각해 보겠습니다.

    function nextStep() {
        //... 생략
        
        if(filledLines.length === 0) {
            addScore(globalAddScore); // 추가
            globalAddScore = 0; // 추가
            //... 생략
        }
        //... 생략
    }
    
    function repeatMotion(timeStamp) {
        //... 생략
        
        if(filledLines.length > 0) {
            if(timeForRemovingLines === 0) {
                timeForRemovingLines = timeStamp;
            }
    
            if(timeStamp - timeForRemovingLines > 100) {
                removeLines(matrixMainBoard, filledLines);
                globalAddScore += 100*filledLines.length*currentLevel;
                addScore(globalAddScore); // 추가
                globalAddScore = 0; // 추가
                initRemoveLines();
                setNextBlock();
            }
        }
        //... 생략
    }

    일단 nextStep에서 제거된 라인이 0일 경우 추가할 점수가 없기 때문에 즉시 점수 반영을 해 줍니다.

    그런 다음 전역 변수에 저장된 임시 점수를 0으로 초기화 해줍니다.

    제거된 라인이 있을 경우에는 repeatMotion에서 전역 변수에 라인당 100점씩 레벨을 곱하여 점수를 합산해 줍니다.

    총점을 반영한 후에 마찬가지로 전역 변수를 0으로 초기화 합니다.

     

    그리고 라인과 레벨을 함께 생각해 보겠습니다.

    우선 global.js 파일에 필요한 변수를 선언해 볼게요.

    //... 기존 선언 변수 생략
    
    let levelElem = document.querySelector('#level');
    let levelUpElem = document.querySelector('#level-up');
    let levelUpId = null;
    
    let remaningLines = 0;
    let linesElem = document.querySelector('#lines');
    let removeLinesElem = document.querySelector('#remove-lines');
    let removeLinesId = null;

    기본적으로 라인과 레벨을 보여줄 부분의 엘리먼트들을 가져와서 변수에 할당하였고

    점수와 마찬가지로 잠깐 보여주고 사라지게 할 텍스트를 위한 변수들도 선언했습니다.

     

    그럼 계산에 사용할 함수를 작성해 보겠습니다.

    function addLines(lines) {
        remaningLines += lines;
        linesElem.textContent = remaningLines;
        if(lines < 0) {
            removeLinesElem.textContent = lines;
        }
        if(removeLinesId){
            clearTimeout(removeLinesId);
        }
        removeLinesId = setTimeout(() => {
            removeLinesElem.textContent = "";
        }, 1000);
    }
    
    function addLevel(level) {
        currentLevel += level;
        levelElem.textContent = currentLevel;
        if(level > 0) {
            levelUpElem.textContent = '레벨 업!';
        }
        if(levelUpId){
            clearTimeout(levelUpId);
        }
        levelUpId = setTimeout(() => {
            levelUpElem.textContent = "";
        }, 2000);
    
    }

    일단 라인은 레벨 업까지 남은 라인 수를 보여주는 방식을 생각을 했습니다.

    제거된 라인 수를 1초 간 보여주기 위해 setTimeout 함수를 사용했구요.

    레벨 업은 점수와 마찬가지로 계속 증가를 하며

    레벨 업을 했을 경우 '레벨 업!' 이라는 문구를 2초 간 띄우도록 작성하였습니다.

     

    이 계산은 라인을 제거하는 부분만 고려하면 되므로 생각보다 간단합니다.

    function repeatMotion(timeStamp) {
        //... 생략
    
        if(filledLines.length > 0) {
            //... 생략
    
            if(timeStamp - timeForRemovingLines > 100) {
                removeLines(matrixMainBoard, filledLines);
                //... 생략
                addLines(filledLines.length*-1);
    
                while(remaningLines <= 0) {
                    addLevel(1);
                    addLines(3*currentLevel);
                }
                initRemoveLines();
                setNextBlock();
            }
        }
        //... 생략
    }

    라인을 제거한 개수를 남은 라인 수에서 빼주면 되고 그 값이 0 이하가 됐을 경우

    레벨 업을 1 시켜주고 다음 레벨 업에 필요한 남은 라인 수(여기서는 레벨에 3을 곱한 수)를 더해주면 됩니다.

     

    그리고 게임 시작 및 재시작할 경우를 대비하여 점수판 초기화 함수도 작성해 보겠습니다.

    function start() {
        //...생략
        addLines(3);
    }
    
    function resetScoreBoard() {
        totalScore = 0;
        scoreElem.textContent = "0";
        addScoreElem.textContent = "";
    
        currentLevel = 1;
        levelElem.textContent = "1";
        levelUpElem.textContent = "";
    
        remaningLines = 0;
        linesElem.textContent = "0";
        removeLinesElem.textContent = "";
    }
    
    function reset() {
        //... 생략
        resetScoreBoard();
    }

    start 함수에서 초기 제거해야할 라인 수를 지정해 주었고

    resetScoreBoard 함수에서 모든 관련 변수를 초기 상태로 설정하도록 하였습니다.

    작성한 함수는 reset 함수에서 호출하였습니다.

     

    마지막으로 스타일을 조금 추가하고 마무리 해 볼게요.

    .remove-lines {
        color: #ffa4ff;
    }
    
    .level-up {
        color: #ffdb76;
    }
    
    .add-score {
        color: #a4c4ff;
    }

    별 건 아니고 표시되는 남은 라인 수, 레벨 업 텍스트, 추가되는 점수의 색깔을 지정했습니다.

     

     

     

     

    결과 확인

    다시 게임을 실행해 보도록 할게요.

    의도한대로 잘 표시가 되고 있습니다.

    이제야 좀 더 게임다운 게임이 된거 같네요. :)

     

    다음 포스팅에서는

     

    블럭에 색깔을 추가하고

    배경음, 효과음도 넣어 보고

    맨 밑에서 블럭이 올라오는 로직도 추가해 보도록 하겠습니다.

     

    다음 편도 많은 기대 부탁드릴게요!

     

    #다음강의

    테트리스 게임 개발 #7 - 최고 점수 표시, 블럭 색깔 추가, 배경음악 및 효과음 적용

     

    테트리스 게임 개발 #7 - 최고 점수 표시, 블럭 색깔 추가, 배경음악 및 효과음 적용

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

    sangminem.tistory.com

     

    반응형

    댓글

    💲 추천 글