본문 바로가기
개발 프로젝트/JavaScript

자바스크립트로 게임 만들기(3)

by BuyAndPray 2020. 11. 8.
반응형

지난번까지 블록을 만들고 수평 바를 만들어서 움직이는 것 까지 해보았습니다. 이번에는 실제로 블록을 없앨 수 있는 공을 만들고 움직임을 제어해서 실제 게임을 한 번 플레이해보겠습니다.

 

공 만들기

ball.js를 만들고 다음과 같이 입력해줍니다.

 

ball.js

export class Ball{
    constructor(r, canvasWidth, canvasHeight, bar){
        this.x = 0;
        this.y = 0;
        this.r = r;

        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;

        this.vx = 0;
        this.vy = 0;

        this.bar = bar;

        this.isGameStart = false;

        this.color = "#cf2f23";
    }

    draw(ctx){
        if(!this.isGameStart){
            this.x = this.bar.x + this.bar.width/2;
            this.y = this.bar.y - this.r;
        }

        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
        ctx.fill();
    }
}

공은 원이기 때문에 width와 height 대신 r을 가지고 draw()에서도 arc 함수를 이용해서 그립니다.

 

isGameStart는 게임이 시작되었는지 확인하는 변수인데 벽돌깨기 게임에서는 스페이스바를 눌러 게임이 시작하기 전까지 수평 바에 공이 붙어서 움직입니다. 이 움직임을 만들어 주기 위해서 만들었고 게임 시작 전 공은 항상 수평 바의 중앙에 위치하도록 하였습니다.

 

마찬가지로 app.js도 아래와 같이 수정합니다.

 

app.js

import { Block } from "./block.js";
import { Bar } from "./bar.js";
import { Ball } from "./ball.js";

class App{
    constructor(){
        this.canvas = document.getElementById("gameCanvas");
        this.ctx = this.canvas.getContext("2d");

        const blockWidth = 50;
        const blockHeight = 20;
        
        this.blocks = [];
        for(let i = 0; i <= this.canvas.width - blockWidth; i += blockWidth){
            for(let j = 50; j <= 200; j += blockHeight){
                this.blocks.push(new Block(i, j));
            }
        }

        this.bar = new Bar(100, this.canvas.width, this.canvas.height);

        this.ball = new Ball(10, this.canvas.width, this.canvas.height, this.bar);

        const moveSpeed = 10;

        window.addEventListener('keydown', (e) => {
            // 오른쪽
            if(e.key === "ArrowRight"){ this.bar.vx = moveSpeed; }
            // 왼쪽
            if(e.key === "ArrowLeft"){ this.bar.vx = -moveSpeed; }
            // 스페이스바
            if(e.key == " "){ this.ball.isGameStart = true; }
        });

        window.addEventListener('keyup', (e) => {
            if(e.key === "ArrowRight" || e.key == "ArrowLeft"){ this.bar.vx = 0; }
        });

        window.requestAnimationFrame(this.animate.bind(this));
    }

    draw(){
        this.ctx.fillStyle = "#102330";
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

        this.blocks.forEach((block) => {
            block.draw(this.ctx);
        });

        this.bar.draw(this.ctx);

        this.ball.draw(this.ctx);
    }

    animate(){
        this.draw();

        window.requestAnimationFrame(this.animate.bind(this));
    }
}

window.onload = () => {
    new App();
}

공은 아래와 같이 수평 바를 따라 이동합니다.

 

수평 바와 같이 움직이는 공

 

공 움직임과 충돌 제어

이제 공의 움직임을 구현하고 벽돌, 수평 바, canvas 테두리에 충돌하였을 때 공의 움직임을 변화시켜 보겠습니다.

 

ball.js

export class Ball{
    constructor(r, canvasWidth, canvasHeight, bar){
        this.x = 0;
        this.y = 0;
        this.r = r;

        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;

        this.vx = Math.random() * 5 + 3;
        this.vy = -5;

        this.bar = bar;

        this.isGameStart = false;

        this.color = "#cf2f23";
    }

    // 수평 바와 충돌한 경우
    collisionBar(ctx){
        const minX = this.bar.x - this.r;
        const maxX = this.bar.x + this.bar.width + this.r;
        const minY = this.bar.y - this.r;

        if(this.x >= minX && this.x <= maxX && this.y >= minY){
            this.y = this.bar.y - this.r;
            this.vy *= -1;
        }
    }

    // canvas의 외벽과 충돌한 경우
    collisionCanvas(ctx){
        if(this.x <= this.r) { 
            this.x = this.r; 
            this.vx *= -1;
        } else if(this.x + this.r >= this.canvasWidth){
            this.x = this.canvasWidth - this.r;
            this.vx *= -1;
        }

        if(this.y <= this.r) { 
            this.y = this.r; 
            this.vy *= -1;
        } 

        // 바닥에 충돌한 경우는 게임을 다시 시작
        if(this.y + this.r >= this.canvasHeight){
            this.y = this.bar.y - this.r;
            this.isGameStart = false;
        }
    }

    draw(ctx){
        if(!this.isGameStart){
            this.x = this.bar.x + this.bar.width/2;
            this.y = this.bar.y - this.r;
        } else{
            this.x += this.vx;
            this.y += this.vy;
        }

        this.collisionBar(ctx);
        this.collisionCanvas(ctx);

        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
        ctx.fill();
    }
}

이런 충돌 문제를 생각할 때는 항상 충돌하는 공의 반지름과 충돌되는 수평 바나 블록의 너비와 높이를 고려해주어야 합니다.

 

블록 충돌은 아직 구현하지 않았다

이제 공이 블록과 충돌하는 경우를 구현해보겠습니다. 이 경우 공의 반사 움직임과 함께 충돌한 블록이 사라져야 합니다. 이 부분은 reduce() 함수를 사용해서 구현하겠습니다.

 

먼저 app.js를 다음과 같이 바꿔줍니다. Ball 객체를 생성할 때 blocks도 함께 넘겨주는 것이 편합니다. 

 

app.js

import { Block } from "./block.js";
import { Bar } from "./bar.js";
import { Ball } from "./ball.js";

class App{
    constructor(){
        this.canvas = document.getElementById("gameCanvas");
        this.ctx = this.canvas.getContext("2d");

        const blockWidth = 50;
        const blockHeight = 20;
        
        this.blocks = [];
        for(let i = 0; i <= this.canvas.width - blockWidth; i += blockWidth){
            for(let j = 50; j <= 200; j += blockHeight){
                this.blocks.push(new Block(i, j));
            }
        }

        this.bar = new Bar(100, this.canvas.width, this.canvas.height);

        this.ball = new Ball(10, this.canvas.width, this.canvas.height, this.bar, this.blocks);

        const moveSpeed = 10;

        window.addEventListener('keydown', (e) => {
            // 오른쪽
            if(e.key === "ArrowRight"){ this.bar.vx = moveSpeed; }
            // 왼쪽
            if(e.key === "ArrowLeft"){ this.bar.vx = -moveSpeed; }
            // 스페이스바
            if(e.key == " "){ this.ball.isGameStart = true; }
        });

        window.addEventListener('keyup', (e) => {
            if(e.key === "ArrowRight" || e.key == "ArrowLeft"){ this.bar.vx = 0; }
        });

        window.requestAnimationFrame(this.animate.bind(this));
    }

    draw(){
        this.ctx.fillStyle = "#102330";
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

        this.bar.draw(this.ctx);

        this.ball.draw(this.ctx);
    }

    animate(){
        this.draw();

        window.requestAnimationFrame(this.animate.bind(this));
    }
}

window.onload = () => {
    new App();
}

 

그런 뒤 ball.js도 다음과 같이 바꿔주면 됩니다.

 

ball.js

export class Ball{
    constructor(r, canvasWidth, canvasHeight, bar, blocks){
        this.x = 0;
        this.y = 0;
        this.r = r;

        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;

        this.vx = Math.random() * 5 + 3;
        this.vy = -5;

        this.bar = bar;
        this.blocks = blocks;

        this.isGameStart = false;

        this.color = "#cf2f23";
    }

    // 수평 바와 충돌한 경우
    collisionBar(){
        const minX = this.bar.x - this.r;
        const maxX = this.bar.x + this.bar.width + this.r;
        const minY = this.bar.y - this.r;

        if(this.x >= minX && this.x <= maxX && this.y >= minY){
            this.y = this.bar.y - this.r;
            this.vy *= -1;
        }
    }

    // canvas의 외벽과 충돌한 경우
    collisionCanvas(){
        if(this.x <= this.r) { 
            this.x = this.r; 
            this.vx *= -1;
        } else if(this.x + this.r >= this.canvasWidth){
            this.x = this.canvasWidth - this.r;
            this.vx *= -1;
        }

        if(this.y <= this.r) { 
            this.y = this.r; 
            this.vy *= -1;
        } 

        // 바닥에 충돌한 경우는 게임을 다시 시작
        if(this.y + this.r >= this.canvasHeight){
            this.y = this.bar.y - this.r;
            this.isGameStart = false;
        }
    }

    // 벽돌과 충돌한 경우
    collisionBlock(){
        this.blocks = this.blocks.reduce((prev, block) => {
            const minX = block.x - this.r;
            const maxX = block.x + block.width + this.r;
            const minY = block.y - this.r;
            const maxY = block.y + block.height + this.r;

            if(this.x >= minX && this.x <= maxX && this.y >= minY && this.y <= maxY){
                // 위 아래/ 양 옆 중 어디에 충돌 했는지 확인한다.
                const distX = Math.min(Math.abs(this.x - minX), Math.abs(this.x - maxX));
                const distY = Math.min(Math.abs(this.y - minY), Math.abs(this.y - maxY));

                // 위 아래 충돌
                if (distX >= distY){
                    this.vy *= -1;
                    this.y += this.vy;
                } else {
                    this.vx *= -1;
                    this.x += this.vy;
                }

            } else{
                // 충돌하지 않을 때만 다시 그려준다.
                prev.push(block);
            }

            return prev;
        }, []);
    }

    draw(ctx, blocks){
        if(!this.isGameStart){
            this.x = this.bar.x + this.bar.width/2;
            this.y = this.bar.y - this.r;
        } else{
            this.x += this.vx;
            this.y += this.vy;
        }

        this.collisionBar();
        this.collisionCanvas();
        this.collisionBlock();

        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
        ctx.fill();

        this.blocks.forEach((block) => {
            block.draw(ctx);
        });
    }
}

this.blocks에 벽돌들의 정보가 저장되어 있습니다. 그리고 벽돌과 충돌을 확인하는 collisionBlock() 메서드가 추가되었는데, 자바스크립트 배열의 reduce() 함수를 사용하여 공과 충돌하지 않은 벽돌만 this.blocks에 새로 저장하도록 하였습니다.

 

코드를 실행하면 아래와 같은 최종 결과물이 나오게 됩니다.

 

최종 결과물

 

자바스크립트로 게임 만들기 시리즈

반응형

댓글