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

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

아미넴 2020. 9. 16.

목차

     

    지난 강의 리뷰

    테트리스 게임 개발 #2 - 블럭 모양 및 랜덤 선택 구현

     

    테트리스 게임 개발 #2 - 블럭 모양 및 랜덤 선택 구현

    목차 지난 강의 리뷰 테트리스 게임 개발 #1 - 소개 및 기본 화면 구성 테트리스 게임 개발 #1 - 소개 및 기본 화면 구성 게임 개발 하면 제일 먼저 떠 오르는 종류가 보드 게임, 그 중에서도 테트리

    sangminem.tistory.com

     

    앞서 main-board와 next-board에 block을 표시하는 것까지 해 보았는데요.

    이번 포스팅에서는 직접 조작을 해 보겠습니다.

     

    1. board에 표시된 block을 방향키로 움직여 보도록 할게요. :)

    2. 그 전에 JavaScript 파일을 정비하고 가야 할 필요가 있어보입니다.

     

    소스 코드 정비

    하나의 파일에 체계없이 이것저것 기술하려고 하면

    아무리 짧은 코드라도 작업 효율이 떨어집니다.

    따라서 역할에 맞게 함수들을 각 파일에 나눠 담아 보겠습니다.

     

    나름대로 기준을 만드실 수도 있지만

    저는 일단 아래와 같은 구조로 갈 거구요.

    필요에 따라 더 추가될 수도 있을 것 같습니다.

     

    play.html에서 common.css와  js파일 6개를 import 합니다.

    <!DOCTYPE html>
    <html>
        <head>
            <title>테트리스</title>
            <link href="common.css" rel="stylesheet"/>
        </head>
        <body>
            <!-- 생략 -->
            <script src='common.js'></script>
            <script src='matrix.js'></script>
            <script src='move.js'></script>
            <script src='draw.js'></script>
            <script src='global.js'></script>
            <script src='main.js'></script>
        </body>
    </html>

    바로 이런 식으로요.

    Javascript는 함수 선언 위치에 따라 영향이 있어서

    import하는 순서도 중요하므로 잘 확인하고 따라 오셔야 합니다.

    JavaScript import 위치는 보통 body 끝쪽을 추천합니다.

    이에 대한 설명은 찾아 보시면 잘 정리한 글이 많이 있습니다.

     

    기존 전역 변수들은 모두 global.js에 넣었습니다.

    const canvasMainBoard = document.querySelector('#main-board');
    const ctxMainBoard = canvasMainBoard.getContext('2d');
    const canvasNextBoard = document.querySelector('#next-board');
    const ctxNextBoard = canvasNextBoard.getContext('2d');
    
    const COLS_MAIN_BOARD = 10;
    const ROWS_MAIN_BOARD = 20;
    const COLS_NEXT_BOARD = 4;
    const ROWS_NEXT_BOARD = 4;
    
    let mainBlock = null;
    let nextBlock = null;

     

    그리고 common.js에서는 테트리스와는 직접적으로 관련 없는 공통 처리함수를 담을 예정입니다.

    function getRandomIndex(length) {
        return Math.floor(Math.random()*length);
    }
    

    랜덤 인덱스를 가져오는 함수도 그 중 하나겠죠.

     

    matrix.js 파일에는 배열 처리를 위한 함수만을 담을 예정입니다.

    function randomNextBlockMatrix() {
        //... BLOCK_SET 생략
        
        return BLOCK_SET[getRandomIndex(BLOCK_SET.length)];
    }

    현재까지 작성된 함수 중에는 다음 블럭을 선택하는 함수가 해당될 수 있을 것 같습니다.

     

    draw.js 파일에는 단어 뜻 그대로 canvas에 그리는 함수를 구현하겠습니다.

    function drawBlock(block, ctx) {
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    
        block.shape.forEach((row, y) => {
            row.forEach((value, x) => {
                if(value > 0) {
                    ctx.fillStyle = 'white';
                    ctx.fillRect(x + block.x, y + block.y, 1, 1);
                }
            });
        });
    }
    

    현재까지는 블럭을 그리는 함수를 포함시킬 수 있습니다.

     

    마지막으로 main.js는 프로그램의 진입점이자

    다른 파일에서 선언한 변수와 함수들을 상황에 맞게 사용하고 컨트롤 하는 부분입니다.

    (function (){
        main();
    })();
    
    function main() {
        //... 내용 생략
    }
    
    function resize() {
        //... 내용 생략
    }
    
    function createNextBlock() {
        //... 내용 생략
    }

    앞서 구현한 함수 중에는 위와 같은 함수만 남겨 두었습니다.

     

    js 파일을 나눌 때 board, block처럼 각 요소별로 구분할 수도 있겠으나

    그렇게 하면 오히려 애매한 부분이 많아져서 저는 좀 다르게 행위 위주로 분리를 해 보았습니다.

     

    지금부터는 정리한 파일 기준으로 좀 더 체계적으로 소스를 구현해 보겠습니다.

     

     

    블럭 이동 기능 구현

    먼저 이동을 어떻게 하면 좋을지 생각해 볼게요.

    function move(block, x, y) {
        block.x += x;
        block.y += y;
    }

    함수 구현 자체는 어려울 것이 없습니다.

    block이 가지고 있는 좌표 x, y 값을 1칸씩 원하는 위치로 이동시켜주면 됩니다.

    이 함수는 move.js 파일에 작성하였습니다.

     

    블럭의 회전은 어떻게 구현해야 할까요.

    그림을 먼저 보겠습니다.

    선형대수학을 공부하신 분이라면 좀 더 이해가 빠를 수도 있는데

    저는 깊이 공부한 적이 없어서 다음에 필요하면 더 공부하기로 하고

    일단 방법만 가져왔습니다.

    x와 y 좌표를 역으로 바꿔 준 뒤에 행을 뒤집으면 시계방향 회전 효과가 나옵니다.

     

    이것을 코드로 표현하면 다음과 같습니다.

    function rotate(block) {
        block.shape.forEach((row, y) => {
            for(let x=0; x < y; x++) {
                const tempValue = block.shape[x][y];
                block.shape[x][y] = block.shape[y][x]
                block.shape[y][x] = tempValue;
            }
        });
    
        block.shape.forEach((row) => {
            row.reverse();
        });
    }

    블럭을 인자로 받아 위의 그림과 같은 원리로 구현하였습니다.

     

    키보드 키 입력 받기

    다음으로 위의 함수를 동작시키려면 키보드 키를 입력 받아 프로그램에서 입력 값을 구분해야겠죠.

    function main() {
        window.addEventListener('keydown', keyHandler);
        //... 생략
    }

    먼저 main 함수에 keydown 이벤트리스너를 추가해 주면

    키보드로 키를 누를 때마다 keyHandler 함수를 호출하게 됩니다.

     

    keydown 이벤트의 Event 객체를 인자로 받아서 처리하는 keyHandler 함수도 만들어 보겠습니다.

    function keyHandler(event) {
        const inputKey = event.keyCode;
    
        const KEY = {
            LEFT: 37,
            UP: 38,
            RIGHT: 39,
            DOWN: 40
        }
    
        switch(inputKey) {
            case KEY.UP :
                rotate(mainBlock);
                break;
            case KEY.DOWN :
                move(mainBlock, 0, 1);
                break;
            case KEY.LEFT :
                move(mainBlock, -1, 0);
                break;
            case KEY.RIGHT :
                move(mainBlock, 1, 0);
                break;
        }
    
        drawBlock(mainBlock, ctxMainBoard);
    }

    keydown 이벤트에 의해 생성된 event 객체는 keyCode라는 값을 갖는데요.

    각 키마다 매핑된 아스키 코드값을 가지게 됩니다.

    여기서는 이동에 필요한 방향키 4가지만 먼저 선언해 보았습니다.

    아래 사이트에서 필요한 값을 하나하나 입력해 보면서 값을 알아내실 수 있습니다.

    keycode.info

     

    JavaScript Event KeyCodes

    Press any key to get the JavaScript event keycode

    keycode.info

     

     

    중간 점검

    한 번 실행해 보겠습니다.

    아래로 옆으로 잘 이동하네요.

    회전도 잘 됩니다.

     

    그런데 문제가 한 가지 있습니다.

    오른쪽 방향키를 계속 누르니 벽을 뚫고 가버리는군요.

    당연히 이러면 안 되겠죠?

     

    블럭 이동 가능 위치 검증

    블럭이 이동 가능한 위치인지 판단하는 처리도 해 보겠습니다.

    function validate(block) {
        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) {
                        isValid = false;
                        return true;
                    }
                }
            });
            if(!isValid) {
                return true;
            }
        });
    
        return isValid;
    }

    블럭을 인자로 받아서 현재 블럭의 위치와 블럭 자체의 값이 있는 부분을 더합니다.

    계산 결과 블럭의 한 부분이라도 x, y 값이 0보다 작아지거나 행, 열 갯수 이상이 되면

    지정된 영역을 벗어난 것으로 판단하여 false를 리턴합니다.

    some 메서드는 return true일 때 빠져나오므로 원하는 결과를 얻으면 true를 리턴하도록 하였습니다.

    참고로 some 메서드는 return true일 때 break, return false일 때는 continue 효과를 갖습니다.

     

    블럭이 담긴 변수를 이용하여 유효성을 검사를 하게 되면 실제 블럭의 위치가 바뀌게 되므로

    똑같은 오브젝트를 새롭게 만들어 검사를 진행하여야 합니다.

    그렇게 하기 위해서는 오브젝트를 복제하는 함수를 만들어 사용해야겠죠?

    JSON 오브젝트는 참조형 변수이므로 그냥 대입을 하면 절대 안 됩니다.

    //공통 성격이므로 common.js에 작성
    function clone(obj) {
        return JSON.parse(JSON.stringify(obj));
    }

    저는 간단하게 JSON 오브젝트를 인자로 받아서

    스트링 변환 후 다시 파싱하는 방법으로 복제를 하였습니다.

    복제 방법은 다양하므로 아시는 방법을 이용해도 좋습니다.

    이해가 안 되시는 분은 좋은 포스팅이 하나 있으니 참고 바랍니다.

    wanna-b.tistory.com/18

     

    [JavaScript] 참조 복사와 값 복사 (얕은 복사와 깊은 복사)

    자바스크립트 참조 복사와 값 복사 자바스크립트에서 = 를 이용하여 객체를 복사하면 값을 복사하는게 아니라 그 값의 위치를 참조만 하게 된다. 한번 자세히 알아보도록 하자. 자료형의 값 복��

    wanna-b.tistory.com

    validate 함수와 clone 함수를 이용하여 다시 이동 및 회전 함수를 작성해 보겠습니다.

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

    먼저 인자로 받은 블럭을 clone 함수로 복제하여 validate 함수로 이동이 유효한 지 체크를 한 다음

    유효하다면 실제 블럭을 이동하는 방식으로 로직을 작성하였습니다.

    문제 없어 보이죠?

    keyHandler 함수에서 rotate 함수를 validRotate, move 함수를 validMove 함수로 바꿔주세요.

     

     

    결과 보기

    그럼 다시 실행해 보겠습니다.

    이제 벽 밖으로 이동하려고 시도하니 전혀 반응을 안하네요.

    회전 동작도 마찬가지로 회전 시 벽을 벗어나는 경우는 회전하지 않습니다.

     

    여기까지 따라 오느라 고생 많으셨습니다.

    이제 상당 부분 테트리스의 기본 베이스가 구축이 되었습니다.

     

    다음에는 위에서 아래로 내려오는 애니메이션 효과와

    블럭을 쌓는 로직을 한 번 구현해 보도록 할게요.

     

    궁금하신 점이나 잘못된 점은 댓글로 부탁드립니다. :)

     

    #다음강의

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

     

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

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

    sangminem.tistory.com

     

    반응형

    댓글6

    • minddi 2021.03.15 15:11

      안녕하세요! 질문이 있어서 댓글 남깁니다! validate 함수에서 isValid가 true라는 것은 블럭이 지정된 영역 내에 있다는 뜻이 맞나요??
      그리고 block.x + dx < 0 이 부분에서 dx는 전체 블럭을 구성하는 각각의 네모의 x좌표인것은 알겠습니다만, block.x가 나타내는 것은 무엇인가요??
      현재 블럭의 위치와 블럭 자체의 값이 있는 부분을 더한다고 설명해 주셨는데 블럭 자체의 값이 있는 부분이 무엇인지 잘 모르겠습니다.ㅠㅠ
      답글

      • 아미넴 2021.03.15 19:05 신고

        네 맞아요. 실제 이동을 하지않고 복제한 블럭으로 다음 위치를 계산한 다음 유효한 위치라면 그 후 실제 블럭을 이동시키는 로직입니다.
        block.x는 현재 블럭 위치의 기준이 되는 값이고 그 값에 블럭 자체 위치(shape 좌표) 값을 더하면 실제 블럭이 보드 내 어느 위치에 있는 지 구할 수 있습니다.

        글로 설명하기가 상당히 어렵네요 ㅎㅎ

      • minddi 2021.03.15 20:07

        계속 같은 것을 질문 드려서 번거롭게 한거 같아 죄송하네요 😂 친절한 설명 정말 감사드립니다.
        아직도 블럭 위치의 기준이라는 말이 가슴에 와닿지는 않지만, 달아주신 댓글을 보고 나니 조금은 속이 시원해 지는 것 같아요! 여러번 반복해서 보면 언젠가는 완벽하게 이해할 수 있겠죠?
        더 힘내서 꼭 마지막까지 완성시켜 보겠습니다!ㅎㅎㅎ

      • minddi 2021.03.15 20:27

        아!! 혹시 ㅗ 모양 블럭을 예로 들었을 때,
        ㅗ 블럭은
        [0,2,0]
        [2,2,2]
        [0,0,0]
        의 배열들로 이루어져 있으니까 블럭 위치의 기준은 0번째 배열의 0번째 요소인 0이 되는게 맞을까요?!?!!!

      • 아미넴 2021.03.15 21:15 신고

        아네 맞아요. 그 부분을 좀 더 상세히 얘기해 드렸어야 하는데 ㅎㅎ 제가 당연하다고 생각했던게 문제네요~
        아마 (block.x + dx, block.y + dy) 의 값을 쭉 찍어 보셔도 감 잡는데 도움이 될 수 있을 것 같아요!

      • minddi 2021.03.15 21:19

        네! 다시 한번 찬찬히 콘솔창에 찍어보면서 확인해 보겠습니다. 이렇게 답답했던걸 하나씩 풀어가는게 재미있네요! :)

    💲 추천 글