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

테트리스 게임 개발 #1 - HTML, 자바스크립트, CSS를 활용하여 기본 화면 구성

변태 개발자 아미넴 2020. 9. 10.

게임 개발 하면 제일 먼저 떠 오르는 종류가 보드 게임,

그 중에서도 테트리스가 아닌가 싶어요.

 

게다가 최근 웹 언어가 활용도가 높고 그 만큼 관심이 커지고 있기 때문에

지금부터 순수 HTML, JavaScript, CSS 만을 이용하여 테트리스 게임을 만들어 보기로 할건데요.

 

목차

     

    데모(Demo)

     

    완성되면 위처럼 동작을 합니다. (실제 플레이 가능합니다)

    기대되지 않나요? ㅎㅎ

     

    테트리스 - 민이게임

     

    테트리스

    민이게임

    sangminem-game.web.app

     

    깃허브(GitHub) 소스 공유

    GitHub Sangminem Tetris

     

    sangminem/tetris

    made with javascript, html, css. Contribute to sangminem/tetris development by creating an account on GitHub.

    github.com

    깃허브에 완성 소스를 공유하오니

    학습 목적으로만 활용해 주시기 바랄게요 :)

     

    화면 구성

    이번 포스팅에서는 간단하게 화면 구성만 해보겠습니다.

     

    1. 실제 블럭을 쌓기 위한 공간

    2. 다음 블럭을 표시하기 위한 공간

    3. 레벨, 남은 라인, 점수를 표현하기 위한 공간

     

    HTML 작성

    위의 3가지 사항을 추후 모바일 지원까지 고려하기 위해 반응형 웹으로 만들어 볼거예요.

    <!DOCTYPE html>
    <html>
        <head>
            <title>테트리스</title>
            <link href="common.css" rel="stylesheet"/>
        </head>
        <body>
            <div class="wrap">
                <div class="main-contents">
                    <canvas id="main-board" class="main-board"></canvas>
                </div>
                <div id="side-contents" class="side-contents">
                    <canvas id="next-board" class="next-board"></canvas>
                    <p>레벨: <span id="level">1</span></p>
                    <p>라인: <span id="lines">0</span></p>
                    <p>점수: <span id="score">0</span></p>
                </div>
            </div>
            <script src='main.js'></script>
        </body>
    </html>

    먼저 body 부분을 구성했습니다.

    wrap 클래스를 이용하여 전체적으로 한 번 감싸고

    그 안에서 main-contents와 side-contents로 구분을 하였습니다.

     

    main-contents 안에는 실제 게임이 진행될 보드를 구현하기 위해

    canvas 태그(엘리먼트)를 사용하였습니다.

     

    side-contents 안에는 다음 블럭을 보여주기 위한  next-board 부분을 canvas 태그로 작성을 했고

    레벨, 라인, 점수 부분도 p 태그를 이용하여 간단히 텍스트로 자리를 잡아 두었습니다.

     

    그리고 간단하게 css 파일과 js 파일 하나씩 포함을 시켰습니다.

    여기에 전체적인 스타일과 로직을 작성할 예정입니다.

     

    CSS 작성

    이제 css를 작성해 볼건데 그 전에 사용할 폰트도 간단히 소개할게요.

     

    폰트 소개 및 적용

    github.com/Dalgona/neodgm/releases/tag/v1.50

     

    Release v1.50 · Dalgona/neodgm

    업데이트 내역 Neo둥근모 및 Neo둥근모 Code 공통 다음 문자들의 자형이 변경되었습니다. U+004A "LATIN CAPITAL LETTER J" U+00B2 "SUPERSCRIPT TWO" U+00B3 "SUPERSCRIPT THREE" U+00B9 "SUPERSCRIPT ONE" U+00BC "VULGAR FRACTION ONE QUAR

    github.com

    Dalgona라는 분이 둥근모 폰트를 좀 더 완성도 있게 수정하고 계신거 같은데

    사용도 자유롭고 글씨체가 예뻐서 가져왔습니다. :)

    위에 사이트에서 neodgm.woff를 다운 받으시면 됩니다.

     

    @font-face {
        font-family: 'NeoDungGeunMo';
        src: url('neodgm.woff') format('woff');
        font-weight: normal;
        font-style: normal;
    }

    common.css에 다음과 같이 작성하면 웹에서 폰트 사용할 준비가 된겁니다.

     

     

    이어서 다음과 같이 작성해 보겠습니다.

    * {
        font-family: 'NeoDungGeunMo';
        background-color: darkblue;
        color: white;
    }
    
    html {
        font-size: 10px;
    }

    먼저 모든 태그에 사용할 폰트 및 기본 배경 및 글자 색상을 지정하였습니다.

    그리고 글자 크기도 반응형으로 구현하기 위해

    가장 상위 태그인 html 태그에 고정 크기를 10px로 잡아 두었습니다.

    폰트 상대 크기 단위인 rem은 html 폰트 크기 기준으로 움직입니다.

     

    화면 구성

    이제 각각의 클래스 스타일을 지정해 볼게요.

    .wrap {
        display: grid;
        grid-template-columns: 2fr 1fr;
        width: fit-content;
    }
    
    .main-contents {
        padding: 1vw;
    }
    
    .side-contents {
        padding: 1vw;
        font-size: 1.6rem;
    }
    
    .main-board {
        border: 2px solid white;
    }
    
    .next-board {
        border: 2px solid white;
    }

    먼저 grid 레이아웃을 사용하기 위해

    가장 바깥쪽에서 감싸는 태그 클래스인 wrap에서

    display 속성을 grid로 지정해 줍니다.

    이어서 grid-template-columns 속성을 이용하여

    main-contents와 side-contents의 비율을 2:1으로 나누었습니다.

    그리고 브라우저 너비가 일정 크기 이상 커질 경우 간격이 벌어지지 않도록

    width 속성으로 fit-content를 사용하였습니다.

    fit-content에 대해 더 자세히 알고 싶으시면 아래 사이트를 참고해보세요.

    https://o7planning.org/en/12557/the-keywords-min-content-max-content-fit-content-stretch-in-css

     

    main-contents 클래스는 안쪽 여백만 1vw만큼 주었습니다.

    1vw는 window 전체 너비의 1%를 의미합니다.

     

    side-contents 클래스도 마찬가지로 안쪽 여백을 1vw만큼 주고

    폰트 크기를 html에 지정한 10px의 1.6배를 의미하는 1.6rem로 지정했습니다.

     

    main-board와 next-board는 2px 두께의 흰색 테두리를 주었습니다.

     

     

    여기까지 작성하고 실행하면 어떤가요.

    뭔가 불완전하죠.

    아직 canvas 크기를 지정을 하지 않아서 그렇습니다.

    폰트는 역시 예쁘네요. ㅎ

     

    자바스크립트 작성

    그럼 크기를 지정하기 위해 main.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;

    먼저 html에서 작성한 2가지 canvas 엘리먼트를 가져와 전역 변수에 담고

    2d로 그리겠다는 의미로 getContext 메서드를 호출합니다.

     

    그리고 main-board와 next-board에 가로 세로 크기를 지정하기 위해

    필요한 행/열 갯수를 선언하였습니다.

     

    다음은 앞서 말씀드린대로 반응형 웹으로 만들기 위해

    브라우저 크기가 달라질 때마다 크기를 다시 계산하는 함수를 작성해 볼거예요.

    function resize() {
        const WINDOW_INNERWIDTH = (window.innerWidth > 660)?660:window.innerWidth;
        const MAIN_CONTENTS_WIDTH = Math.floor(WINDOW_INNERWIDTH*0.6);
        const BLOCK_SIZE = Math.floor(MAIN_CONTENTS_WIDTH/COLS_MAIN_BOARD);
    
        ctxMainBoard.canvas.width = BLOCK_SIZE*COLS_MAIN_BOARD;
        ctxMainBoard.canvas.height = BLOCK_SIZE*ROWS_MAIN_BOARD;
        ctxMainBoard.scale(BLOCK_SIZE, BLOCK_SIZE);
    
        ctxNextBoard.canvas.width = BLOCK_SIZE*COLS_NEXT_BOARD;
        ctxNextBoard.canvas.height = BLOCK_SIZE*ROWS_NEXT_BOARD;
        ctxNextBoard.scale(BLOCK_SIZE, BLOCK_SIZE);
    
        const FONT_RATIO = WINDOW_INNERWIDTH/350;
        document.querySelector('#side-contents').style.fontSize = FONT_RATIO+'rem';
    }

     

    먼저 board가 너무 커지는걸 방지하기 위해 너비 최대 크기를 지정했습니다.

    저는 적당히 660px로 잡았는데 상황에 맞게 적당히 잡으시면 됩니다.

    css에서도 지정할 수 있지만 canvas 엘리먼트의 scale 메서드와 함께 사용하면

    비율이 제대로 반영되지 않아 부득이 하게 자바스크립트로 구현하였습니다.

     

    다음으로 2:1비율로 나눈 main-contents와 side-contents 영역 중

    main-contents 영역에 main-board 크기를 계산하기 위해

    main-contents 영역을 window 너비의 60%로 결정하였고

    그에 따라 하나의 블럭 크기를 구했습니다.

    계산 값이 정수가 아니면 블럭이 예쁘게 표현이 안 되어 소숫점 미만은 버림을 하였습니다.

     

    그 블럭 크기를 기준으로 다시 main-board canvas의 width, height를 계산했고

    scale 메서드를 이용하여 하나의 픽셀을 BLOCK_SIZE 만큼 확대하였습니다.

    따라서 BLOCK_SIZE에 행/열 갯수를 곱하면 canvas 크기가 나옵니다.

     

    next-board canvas도 마찬가지로 계산을 하였습니다.

    main-board 기준으로 계산한 BLOCK_SIZE를 이용하여

    다음 블럭을 표시할 4x4 canvas 크기를 구했습니다.

     

    그 다음이 폰트 크기 부분인데 자칫하면 난해질 수 있어서

    처음 html에 고정 크기인 10px를 잡아 놓고 시작한 겁니다.

    그리고 rem 단위를 사용하면 쉽게 계산이 됩니다.

    예를 들어 10px와 같은 크기는 1rem이고

    15px 크기를 원한다면 1.5rem으로 선언하면 됩니다.

     

    여기서는 window 너비가 350일 때 10px 크기에 적당한거 같아서

    그 기준으로 비율을 산정하였구요.

    그 비율로 화면 사이즈가 바뀔 때마다 폰트 크기를 다시 계산합니다.

     

    resize 함수를 만들었으면 사이즈가 변할 때 마다 호출을 해야겠죠.

    (function (){
        main();
    })();
    
    function main() {
        resize();
        window.addEventListener('resize', resize);
    }

    main 함수를 하나 선언하여 최초 한번 resize 함수를 호출했구요.

    이벤트 리스너를 활용하여 resize 이벤트가 일어날 때마다 resize 함수가 호출되도록 작성했습니다.

    그리고 그 main 함수를 페이지 로드되는 시점에 호출하도록 하였습니다.

     

     

    결과 보기

    그럼 다시 한 번 실행해 보도록 할게요.

    아까보다 모양이 예뻐졌죠?

    canvas 크기도 원하는 사이즈로 초기화가 되었습니다.

     

    화면 크기를 줄여도 모양은 변하지 않고 정상적으로 비율만 작아졌습니다.

     

    이제 기본적인 틀은 만들어졌으니

    다음 포스팅부터는 기능을 하나씩 만들어 보도록 하겠습니다.

    기대해 주세요. :)

     

    #다음강의

    테트리스 게임 개발 #2 - 블럭 구현

     

    테트리스 게임 개발 #2 - 블럭 구현

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

    sangminem.tistory.com

     

    728x90
    반응형
    BIG

    댓글4

    • 신지원 2021.06.11 14:22

      코린이 테트리스 만들어보려고 시작했는데 쉽지 않은 것 같습니다.. const COLS_MAIN_BOARD 이부분부터 행과 열을 지정해주는 부분이라고 하셨는데 MAIN_BOARD하고 대문자로 써주는 것과 _이 문자를 쓰는 것이 이해가 안되고 실제로 저대로 써봤는데 캔버스의 크기가 변하지 않습니다.. 무슨 문제가 있는 지 모르겠습니다.. 혹시 resize함수를 다시 써줘서 함수를 선언해줘야 발동하는 건가요?
      답글

      • 진짜 코린이라면 처음부터 너무 어려운 것을 도전하시는게 아닌가 싶습니다 ㅎㅎ

        const는 상수 선언을 위한 키워드이고 관습적으로 상수는 all 대문자 및 언더바를 이용하여 표현해 주고 있습니다. 다른 값으로 하셔도 전혀 문제는 없습니다.

        최초 resize 함수를 한번 실행해 주셔야 크기가 적용이 됩니다.

    • 신지원 2021.06.12 13:27

      답변 감사합니다!
      그런데 또 실행도중 이상한 점이 있습니다.. 선생님의 완성소스를 보면서 공부중인데, 최적화를 위해js파일들을 나눠놓으신대오 따라해본 후 실행해보았더니 실행이 안되어서 function resize() {
      const WINDOW_INNERWIDTH (..이하 생략)부분을 global.js의 변수선언항 밑으로 내리고 resize();를 해주었더니 캔버스가 그려집니다. 근데 그 이후에 호출한 함수 밑으로 let... const... 변수선언항들을 global.js에 추가하고 새로고침해보니 테트리스canvas가 다시 원래 모양으로 되돌아와있습니다!! 원인이 무엇인 지 모르겠습니다.

      1. 왜 파일들을 나누면 실행이 안되고 합쳤을 때 캔버스가 그려지는 지 모르겠습니다.
      2. 함수선언항 밑으로 변수들을 추가적으로 선언하면 왜 잘 실행되었던 캔버스가 다시 원래모양으로 돌아오는 지 모르겠습니다.
      답글

      • 1. js 파일 위치가 중요합니다. html 내 js 파일을 임포트한 부분 확인해 주세요.
        테트리스 게임 개발 #7 - 최고 점수 표시, 블럭 색깔 추가, 배경음악 및 효과음 적용
        https://sangminem.tistory.com/43
        위 포스팅 참고 바랍니다.

        2. js는 스크립트 언어이므로 작성 순서가 중요합니다. 함수 호출 시점 다음에 선언한 변수를 인식하지 못합니다. 컴파일 언어와 인터프리터 언어의 차이점을 찾아보시기 바랍니다.

    💲 추천 글