분류 javascript

게임을 통해 자바 스크립트 배우기

컨텐츠 정보

  • 조회 362 (작성일 )

본문

Google에서 "Javascript"라는 용어를 사용하면 수십억 개의 검색 결과가 나타납니다. 그것이 얼마나 인기가 있습니다. 거의 모든 최신 웹 응용 프로그램은 Javascript를 사용합니다. 

JS 개발자는 프레임 워크와 관련하여 React, Node, Vue 등 다양한 옵션이 있습니다. 이 방대한 프레임 워크의 바다 속에서 우리는 종종 가장 순수한 형태의 자바 스크립트 인 우리의 오랜 친구 인 Vanilla JS를 잊는 경향이 있습니다.


https://dev.to/nitdgplug/learn-javascript-through-a-game-1beh


그래서 우리는 재미 있고 독특한 방식으로 Vanilla JS의 기본을 포함하는 프로젝트를 만들고 평범하고 단순한 JS를 사용하여 고전적인 Snake Game을 만드는 것보다 더 좋은 방법을 생각했습니다. 그러니 바로 들어가 봅시다.


전제 조건 


이동 중에 학습 할 의지가 있는 한이 프로젝트에는 전제 조건이 없습니다. 그러나 약간의 프로그래밍 지식은 아프지 않을 것입니다.


프로젝트 


이 기사는 프로젝트의 모든 측면을 다룰 것이기 때문에 긴 기사가 될 것입니다. 따라서 전체 프로젝트는 명확하고 이해하기 쉽도록 다음 섹션으로 나뉩니다.


우리가 만드는 것


Game Screenshot 


코드에 들어가기 전에 정확히 무엇을 만들지 공식화해야 합니다. 우리는 많은 부분으로 구성된 머리와 꼬리로 표현되는 뱀을 만들어야 합니다. 또한 뱀이 그것을 먹고 길이가 자라도록 화면의 임의의 위치에 음식을 스폰해야 합니다. 플레이어의 점수를 추적하고 게임을 일시 중지하는 기능도 추가합니다.


The Skeleton 


게임을 위한 별도의 폴더를 만듭니다. 폴더 안에 index.html과 game.js라는 두 개의 파일을 만듭니다. index.html 파일에는 게임에 생명을 불어 넣을 수 있는 매우 특별한 요소 인 캔버스와 함께 일반 HTML 상용구 코드가 포함됩니다.


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Snake Game</title>
</head>
<body>

    <canvas id="game-area"></canvas>
    <script type="text/javascript" src="game.js"></script>

</body>
</html>



HTML 캔버스 태그는 자바 스크립트를 사용하여 그래픽을 그리는 데 사용됩니다. 호, 직사각형, 선과 같은 단순한 모양을 그리는 기능이 내장되어 있습니다. 또한 텍스트와 이미지를 표시 할 수 있습니다. 스크립트 태그를 사용하여 game.js 파일에 대한 참조를 추가하면 게임의 논리가 결정됩니다.


계속하기 전에 다음과 같이 HTML 파일의 head 태그 내에 스타일 태그를 추가해야 합니다.


<style type="text/css">
        *{
            margin: 0;
            padding: 0;
            overflow: hidden;
            box-sizing: border-box;
        }
        canvas{
            background-color: #333;
        }
</style>


브라우저 요소의 기본 설정을 재정의 하기 위해 페이지에 대한 사용자 정의 CSS 스타일을 작성하고 여백과 패딩을 0으로 설정합니다. border-box 속성은 요소에 추가 된 테두리를 고려하여 요소의 범위 내에 맞 춥니 다. 오버플로 속성은 숨김으로 설정되어 브라우저에서 스크롤바를 비활성화 하거나 숨 깁니다. 마지막으로 게임 캔버스의 배경색을 설정했습니다.


초기화 


여기에 game.js 파일이 있습니다. 첫째, 게임 전체에서 참조 할 몇 가지 전역 변수를 선언해야 합니다. 이러한 변수는 게임의 동작을 제어하는 ​​특정 속성을 나타냅니다. init라는 함수를 통해 이러한 속성을 초기화합니다. 함수는 몇 개의 명령문을 실행하여 특정 작업을 수행하는 것과 동일하며 여기서 작업은 변수의 초기화입니다.


처음에는 game.js 파일에 다음 코드를 추가합니다.


let width;
let height;
let tileSize;
let canvas;
let ctx;

// Initialization of the game objects.
function init() {

    tileSize = 20;

    // Dynamically controlling the size of canvas.
    width = tileSize * Math.floor(window.innerWidth / tileSize);
    height = tileSize * Math.floor(window.innerHeight / tileSize);

    canvas = document.getElementById("game-area");
    canvas.width = width;
    canvas.height = height;
    ctx = canvas.getContext("2d");

}


너비와 높이 변수는 캔버스의 너비와 높이를 저장합니다. canvas 변수는 HTML canvas 요소에 대한 참조를 저장합니다. ctx는 캔버스 컨텍스트의 약어로 작업 할 좌표계를 지정합니다. 우리의 경우 2D 좌표를 사용합니다.


tileSize 변수는 게임의 필수 요소입니다. 화면에 표시되는 기본 단위의 차원입니다. 뱀과 음식을 완벽하게 정렬하기 위해 전체 화면을 격자로 나누고 각 격자는 크기가 tileSize에 해당합니다. 이것이 캔버스의 너비와 높이를 가장 가까운 tileSize 배수로 근사화 하는 이유이기도 합니다.


The Food 


뱀이 먹을 음식에 대한 언급이 필요합니다. 우리는 그것을 실제 개체와 매우 유사한 특정 속성과 동작을 가진 개체로 생각할 것입니다. 이를 위해 기본적인 OOP (Object Oriented Programming)에 대해 알아 보겠습니다.


다음과 같이 Food라는 클래스를 만들 것입니다.


// Treating the food as an object.
class Food {

    // Initialization of object properties.
    constructor(pos, color) {

        this.x = pos.x;
        this.y = pos.y;
        this.color = color;

    }

    // Drawing the food on the canvas.
    draw() {

        ctx.beginPath();
        ctx.rect(this.x, this.y, tileSize, tileSize);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.stroke();
        ctx.closePath();

    }

}


JS의 클래스는 생성자 메서드로 구성되며, 생성자 메서드는 이를 기반으로 객체의 속성을 초기화하고 동작을 정의하는 일부 멤버 함수를 담당합니다.


여기에서는 매개 변수화 된 생성자를 사용하여 음식 개체에 위치와 색상을 제공합니다. 위치 pos에는 캔버스에서 X 및 Y 좌표를 지정하는 속성 x 및 y가 있습니다. this 키워드는 클래스의 현재 인스턴스 (또는 객체)를 참조하는 데 사용됩니다. 즉, 현재 고려중인 객체의 속성을 참조하고 있습니다. 객체를 만들 때 더 명확해질 것입니다.


여기에서 사용되는 멤버 함수는 draw이며, 캔버스에 음식을 그리는 역할을 합니다. draw 함수는 캔버스에 음식을 그리는 모든 코드를 포함 할 수 있지만 간단하게 하기 위해 x와 y의 위치와 tileSize의 너비와 높이가 있는 빨간색 사각형으로 음식을 표현합니다. 함수 내부에 작성된 모든 코드는 캔버스에 빨간색 사각형을 그리는 작업을 수행합니다.


마지막으로 다음과 같이 전역 변수 목록에 음식 개체를 추가하고 init 함수 내에 음식 개체를 만들어야 합니다.


전역 변수 :


// Other global variables.

let food;


init function:


// Initialization of the game objects.
function init() {

    tileSize = 20;

    // Dynamically controlling the size of canvas.
    width = tileSize * Math.floor(window.innerWidth / tileSize);
    height = tileSize * Math.floor(window.innerHeight / tileSize);

    canvas = document.getElementById("game-area");
    canvas.width = width;
    canvas.height = height;
    ctx = canvas.getContext("2d");

    food = new Food(spawnLocation(), "red");
}


spawnLocation이 무엇인지 궁금 할 것입니다. 음식이 스폰 될 수 있도록 캔버스에서 임의의 위치를 ​​반환하는 함수입니다. 코드는 다음과 같습니다.


// Determining a random spawn location on the grid.
function spawnLocation() {

    // Breaking the entire canvas into a grid of tiles.
    let rows = width / tileSize;
    let cols = height / tileSize;

    let xPos, yPos;

    xPos = Math.floor(Math.random() * rows) * tileSize;
    yPos = Math.floor(Math.random() * cols) * tileSize;

    return { x: xPos, y: yPos };

}


The Snake 


뱀은 아마도 게임에서 가장 중요한 부분 일 것입니다. Food 클래스를 기반으로 하는 food 객체와 유사하게, 우리는 뱀의 속성과 행동을 구성하는 Snake라는 클래스를 만들 것입니다. Snake 클래스는 다음과 같이 진행됩니다.


class Snake {

    // Initialization of object properties.
    constructor(pos, color) {

        this.x = pos.x;
        this.y = pos.y;
        this.tail = [{ x: pos.x - tileSize, y: pos.y }, { x: pos.x - tileSize * 2, y: pos.y }];
        this.velX = 1;
        this.velY = 0;
        this.color = color;

    }

    // Drawing the snake on the canvas.
    draw() {

        // Drawing the head of the snake.
        ctx.beginPath();
        ctx.rect(this.x, this.y, tileSize, tileSize);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.strokeStyle = "black";
        ctx.lineWidth = 3;
        ctx.stroke();
        ctx.closePath();

        // Drawing the tail of the snake.
        for (var i = 0; i < this.tail.length; i++) {

            ctx.beginPath();
            ctx.rect(this.tail[i].x, this.tail[i].y, tileSize, tileSize);
            ctx.fillStyle = this.color;
            ctx.fill();
            ctx.strokeStyle = "black";
            ctx.lineWidth = 3;
            ctx.stroke();
            ctx.closePath();

        }


    }

    // Moving the snake by updating position.
    move() {

        // Movement of the tail.    
        for (var i = this.tail.length - 1; i > 0; i--) {

            this.tail[i] = this.tail[i - 1];

        }

        // Updating the start of the tail to acquire the position of the head.
        if (this.tail.length != 0)
            this.tail[0] = { x: this.x, y: this.y };

        // Movement of the head.   
        this.x += this.velX * tileSize;
        this.y += this.velY * tileSize;

    }

    // Changing the direction of movement of the snake.
    dir(dirX, dirY) {

        this.velX = dirX;
        this.velY = dirY;

    }

    // Determining whether the snake has eaten a piece of food.
    eat() {

        if (Math.abs(this.x - food.x) < tileSize && Math.abs(this.y - food.y) < tileSize) {

            // Adding to the tail.
            this.tail.push({});
            return true;
        }

        return false;

    }

    // Checking if the snake has died.
    die() {

        for (var i = 0; i < this.tail.length; i++) {

            if (Math.abs(this.x - this.tail[i].x) < tileSize && Math.abs(this.y - this.tail[i].y) < tileSize) {
                return true;
            }

        }

        return false;

    }

    border() {

        if (this.x + tileSize > width && this.velX != -1 || this.x < 0 && this.velX != 1)
            this.x = width - this.x;

        else if (this.y + tileSize > height && this.velY != -1 || this.velY != 1 && this.y < 0)
            this.y = height - this.y;

    }

}



이 클래스는 코드 측면에서 많은 부분을 포함하고 있으므로 방법을 하나씩 살펴 보도록 하겠습니다.


먼저, 변수 x와 y에서 뱀 머리의 X 및 Y 좌표, 색상에서 뱀의 색상, velX 및 velY로 지정된 X 및 Y 방향의 속도를 초기화하는 매개 변수화 된 생성자가 있습니다. . 꼬리 부분에 대한 참조를 저장하는 객체 목록 인 꼬리 변수도 있습니다. 꼬리는 처음에 두 개의 세그먼트를 갖도록 설정되며 X 및 Y 좌표는 자체 x 및 y 속성에 의해 지정됩니다.


이제 클래스의 다양한 멤버 메서드에 초점을 맞춥니 다.


  • draw 함수 : 그리기 기능은 Food의 기능과 유사합니다. 캔버스에 뱀을 그리는 역할을 합니다. 다시 말하지만, 우리는 뱀을 표현하기 위해 무엇이든 사용할 수 있었지만, 단순화를 위해 우리는 뱀의 머리와 꼬리의 각 부분에 대한 tileSize로 차원이 있는 녹색 사각형을 사용합니다. 함수 내부의 코드는 정확히 이를 수행하고 캔버스에 녹색 사각형을 그립니다.
  • move 함수 : 뱀의 움직임의 주된 도전은 꼬리의 적절한 운동에 있습니다. 뱀이 특정 경로를 따르도록 하려면 꼬리의 다른 부분의 위치를 ​​저장할 수 있어야 합니다. 꼬리 부분을 앞 부분과 같은 위치에 할당하면 됩니다. 이렇게 하면 뱀의 꼬리가 과거에 머리가 되돌아온 길을 따라갑니다. 뱀의 위치는 속도 velX 및 velY에 그리드의 기본 단위 인 tileSize를 곱한 값으로 증가합니다.
  • dir 함수 : dir 함수의 목적은 뱀 머리의 움직임 방향을 변경하는 것입니다. 우리는 이것에 대해 잠시 후에 올 것입니다.
  • eat 함수 : 먹기 기능은 뱀이 음식을 먹었는지 확인하는 역할을 합니다. 이것은 뱀의 머리와 음식이 겹치는 부분을 찾아서 얻을 수 있습니다. tileSize는 그리드의 크기에 해당하므로 머리와 음식의 위치 차이가 tileSize에 해당하는지 확인하고 이에 따라 true 또는 false를 반환 할 수 있습니다. 이를 바탕으로 뱀의 꼬리에 세그먼트를 추가하여 길이를 늘립니다.
  • die 함수 : 우리 뱀은 꼬리의 일부를 물면 죽을 것입니다. 이것이 우리가 이 함수에서 확인하는 것입니다. 즉, 머리와 꼬리의 일부가 겹치는 지 여부입니다. 따라서 우리는 응답으로 참 또는 거짓을 반환합니다.
  • border 함수 : 보더 기능은 뱀이 화면 경계 내에 있는지 확인합니다. 어떻게 든 뱀이 화면 옆에서 사라지면 이상 할 것입니다. 여기서 우리는 다음 두 가지 중 하나를 수행 할 수 있습니다. 우리는 거기서 게임을 끝낼 수도 있고, 고전적인 스네이크 게임과 비슷하게 화면 반대편에서 뱀이 마술처럼 보이도록 만들 수도 있습니다. 두 번째 옵션을 사용하여 함수 내부의 코드를 사용했습니다.

뱀을 위해 마지막으로 해야 할 일이 있습니다. 다음과 같이 전역 변수 목록 아래에 스네이크 객체를 선언합니다.


let snake;


다음과 같이 init 함수 내에서 초기화합니다.


snake = new Snake({ x: tileSize * Math.floor(width / (2 * tileSize)), y: tileSize * Math.floor(height / (2 * tileSize)) }, "#39ff14");


The Game Loop 


더 진행하기 전에 게임 실행을 담당 할 함수를 정의해야 합니다. 따라서 다음과 같이 정의하겠습니다.


// The actual game function.
function game() {

    init();

}


이 함수 내에서 전역 변수의 초기화 만 처리하는 init 함수를 호출합니다. 캔버스에 개체를 그리고 게임을 계속해서 실행하는 것은 어떻습니까? 이것은 게임 루프가 들어오는 곳입니다.


반복적으로 실행될 게임 루프 또는 로직은 함수, 즉 업데이트 내에 작성됩니다. 업데이트 기능은 다음과 같이 정의됩니다.


// Updating the position and redrawing of game objects.
function update() {

        if (snake.die()) {
            alert("GAME OVER!!!");
            window.location.reload();
        }

        snake.border();

        if (snake.eat()) {
            food = new Food(spawnLocation(), "red");
        }

        // Clearing the canvas for redrawing.
        ctx.clearRect(0, 0, width, height);

        food.draw();
        snake.draw();
        snake.move();

}



업데이트 기능은 매 프레임마다 게임 로직을 업데이트합니다 (예 : 뱀, 음식 그리기, 뱀 이동). 또한 뱀이 음식을 먹었는지 또는 죽었는지 확인합니다. 뱀이 죽으면 논리에 표시된 대로 게임을 다시 로드합니다.


이제 특정 시간 간격 후에 업데이트 함수를 반복적으로 호출하는 작업이 남았습니다. 무엇보다 먼저 FPS 또는 초당 프레임에 대해 이야기 해야 합니다. 느슨하게 정의되는 것은 게임 화면이 초당 렌더링 되는 횟수를 나타냅니다. 전통적인 스네이크 게임은 프레임 속도가 약 10FPS로 우리가 준수 할 것입니다.


전역 변수 목록 아래에 fps라는 변수를 정의하고 init 함수에서 10으로 초기화합니다.


그런 다음 게임 함수 내부의 코드를 다음과 같이 업데이트합니다.


// The actual game function.
function game() {

    init();

    // The game loop.
    interval = setInterval(update,1000/fps);

}


setInterval 함수는 지정된 밀리 초 후에 특정 함수를 주기적으로 호출합니다. 이 참조를 interval이라는 변수에 저장합니다.


마지막으로 뱀이 죽으면 다음과 같이 clearInterval 함수를 호출하여 이 간격을 제거해야 합니다.


if (snake.die()) {
     alert("GAME OVER!!!");
     clearInterval(interval);
     window.location.reload();
}


따라서 우리의 게임 루프는 준비가 되어 있고 갈 준비가 되었습니다.


The Logistics 


이제 게임 루프가 준비되었으므로 플레이어의 점수를 계산하고 게임 일시 중지 기능을 제공하는 시스템이 필요합니다.


두 개의 전역 변수 score 및 isPaused를 정의하고 다음과 같이 init 함수 내에서 초기화합니다.


score = 0;
isPaused = false;


그런 다음 캔버스에 게임의 점수와 상태를 표시하는 두 가지 함수를 다음과 같이 정의합니다.


// Showing the score of the player.
function showScore() {

    ctx.textAlign = "center";
    ctx.font = "25px Arial";
    ctx.fillStyle = "white";
    ctx.fillText("SCORE: " + score, width - 120, 30);

}

// Showing if the game is paused.
function showPaused() {

    ctx.textAlign = "center";
    ctx.font = "35px Arial";
    ctx.fillStyle = "white";
    ctx.fillText("PAUSED", width / 2, height / 2);

}



업데이트 함수의 시작 부분에 다음 코드를 추가합니다.


if(isPaused){
   return;
}


업데이트가 끝날 때 다음과 같이 showScore 함수를 호출합니다.


showScore();


snake.eat 아래의 업데이트 기능 내부에 다음을 추가하십시오.


score += 10;



Keyboard Controls 


플레이어는 게임과 상호 작용할 수 있어야 합니다. 이를 위해 코드에 이벤트 리스너를 추가해야 합니다. 이러한 리스너에는 다음과 같이 키 누르기를 찾고 코드를 실행하여 게임을 제어하는 ​​콜백 함수가 있습니다.


// Adding an event listener for key presses.
window.addEventListener("keydown", function (evt) {
    if (evt.key === " ") {
        evt.preventDefault();
        isPaused = !isPaused;
        showPaused();
    }
    else if (evt.key === "ArrowUp") {
        evt.preventDefault();
        if (snake.velY != 1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(0, -1);
    }
    else if (evt.key === "ArrowDown") {
        evt.preventDefault();
        if (snake.velY != -1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(0, 1);
    }
    else if (evt.key === "ArrowLeft") {
        evt.preventDefault();
        if (snake.velX != 1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(-1, 0);
    }
    else if (evt.key === "ArrowRight") {
        evt.preventDefault();
        if (snake.velX != -1 && snake.x >= 0 && snake.x <= width && snake.y >= 0 && snake.y <= height)
            snake.dir(1, 0);
    }

});


위 코드의 dir 함수는 뱀의 이동 방향을 지정합니다. 우리는 다음과 같은 컨벤션을 고안합니다. 위쪽 및 아래쪽 이동은 Y 속도에 대해 각각 -1 및 1에 해당하고 왼쪽 및 오른쪽 이동은 X 속도에 대해 각각 -1 및 1로 표시됩니다. evt.key 속성은 누르는 키의 이름을 리스너에게 전달합니다. 따라서 이제 화살표 키를 사용하여 뱀을 제어하고 스페이스 바 키를 사용하여 게임을 일시 중지 할 수 있습니다.


마무리 


이제 모든 것이 준비되었으므로 코드에 마지막 기능을 추가합니다. HTML 문서가 브라우저에 로드되는 즉시 게임을 로드합니다. 이를 위해 문서가로드 되었는지 여부를 확인하는 또 다른 이벤트 리스너를 추가합니다. 코드는 다음과 같습니다.


// Loading the browser window.
window.addEventListener("load",function(){

     game();

});


그리고 lo! 브라우저에서 index.html 파일을 실행하면 게임이 실행되고 있어야 합니다.


자원 


https://github.com/Soupaul/Snake-Game


리포지토리의 업데이트 된 브랜치는 게임을 더 아름답고, 견고하고, 부드럽게 만들기 위해 코드에 몇 가지 추가 사항을 더 포함합니다. 또한 예상치 못한 버그를 방지하기 위해 몇 가지 검사를 추가했습니다.


여기서 게임을 할 수 있습니다.