안녕하세요.
오늘은 벽돌깨기 게임만들기 두번째 시간입니다.
이전 편에서 우리는 HTML5 <canvas>와 자바스크립트를 사용하여 벽돌 깨기 게임의 기본 구조를 만들고, 캔버스에 공, 패들, 벽돌을 그렸습니다. 이번 편에서는 공이 벽이나 패들, 벽돌에 닿았는지 여부를 확인해서 공이 튕겨나갈 수 있도록 동작을 추가해 보겠습니다.
이전 시간에 작성한 코드에서 이어서 작성합니다. 이전 코드는 아래 링크를 참고해주세요.
2024.08.12 - [JavaScript] - [JavaScript] 벽돌깨기 게임 만들기 (1)
1. 공이 캔버스 밖으로 벗어나지 않게 하기
이 작업은 draw() 함수에서 다음 공의 위치를 업데이트할 때 이루어집니다.
공이 캔버스 밖으로 빠져나가지 않게 하려면
(1) 좌,우 벽에 닿았을때 공을 튕겨내기
(2) 위쪽 벽에 닿았을때 공을 튕겨내기
두가지가 진행되어야 합니다.
먼저 (1) 좌, 우 벽에 닿았았을 때 공을 튕겨내기 부분 부터 살펴보겠습니다.
우선은 공이 좌,우 벽에 닿았는지 여부를 먼저 판단해야 합니다.
위 사진에서 보이는 것처럼 공의 x 좌표가 ballRadius(공의 반지름) 보다 작으면 벽에 좌측 벽에 닿은 것으로 볼 수 있습니다. 반대로 x 좌표가 canvas.width(캔버스 가로길이) - ballRadius(공의 반지름) 보다 크면 우측 벽에 닿은 것으로 볼 수 있습니다.
draw() 함수에서는 공의 x 좌표와 y 좌표를 업데이트 할 때, 현재 좌표에서 각각 dx와 dy 만큼을 더해서 계산하고 있는데요.
이때 현재 그려야 할 프레임의 x 좌표는 (x + dx) 이므로, 위 조건을 수식으로 쓰게 되면 다음처럼 쓸 수 있습니다.
// 현재 프레임에 그려질 x 좌표가 우측 벽에 닿거나, 좌측 벽에 닿을 때
if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
}
이제 다음으로 공이 화면 밖으로 나가지 않도록 이동 방향을 조절해주어야 합니다.
위 그림처럼 공이 좌, 우측 벽에 닿았을때 공은 기존의 이동방향과 비교해서 x축 이동방향이 반전됩니다. 즉, 원래라면 dx 만큼 이동했어야할 x 좌표가 -dx 만큼 이동하게 됩니다. 이를 수식으로 작성하면 다음처럼 쓸 수 있습니다.
// 현재 프레임에 그려질 x 좌표가 우측 벽에 닿는 경우보다 크거나, 좌측 벽에 닿는 경우보다 작을ㄷ 때
if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -x; // x축 방향 반전
}
(2) 위쪽 벽에 닿았을때 공을 튕겨내기의 경우도 y 좌표에 대해서 위 내용과 동일합니다. 해당 수식을 쓰면 아래와 같습니다.
// 현재 프레임에 그려질 공의 y 좌표가 위쪽 벽에 닿는 경우보다 작으면
if(y + dy < ballRadius) {
dy = -dy; // y축 방향 반전
}
이 코드를 draw()에 반영하면 다음과 같습니다.
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
// 공이 좌,우측 벽에 닿을 경우, x축 방향 반전
if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// 공이 위쪽 벽에 닿으면, y축 방향 반전
if(y + dy < ballRadius) {
dy = -dy;
}
// 공의 위치 업데이트
x += dx;
y += dy;
requestAnimationFrame(draw);
}
여기까지 실행해보면 공이 벽에 부딪혔을때 튕겨나오는 것을 확인할 수 있습니다.
2. 패들로 공 튀기기
다음으로 패들이 공과 충돌 했을때, 공이 튕겨나가도록 설정합니다.
여기서는 이전에 위쪽 벽에 공이 닿았을 때 튕겨냈던 것과 비슷합니다.
(1) 패들이 공과 닿았는지 여부 확인하기
(2) 닿았다면 공의 y축 이동 방향을 반전시키기
먼저 패들에 공이 닿았다는 것을 판단해봅시다.
y 좌표를 기준으로 봤을 때, 공이 패들에 닿은 순간에는 y 좌표가 cnavas.height(캔버스 세로 길이) - paddleHeight(패들 세로 길이) - ballRadius(공의 반지름) 가 됩니다. 이걸 현재 프레임에 그려져야할 y 좌표에 대한 수식으로 작성하면 다음과 같습니다.
if (y + dy > canvas.height - paddleHeight - ballRadius) {
}
그런데 이때 x 좌표 또한 아래 그림처럼 패들 안쪽에 위치해야 합니다. 즉, 공의 x 좌표는 패들의 가장 왼쪽을 나타내는 paddleX보다는 크고, 패들의 가장 오른쪽 좌표인 paddleX + paddleWidth 보다는 작아야 공이 패들에 닿았다고 할 수 있습니다.
이 두가지 조건을 한번에 만족시킬 때 공을 y축 방향 반전하여 튕긴다면 다음과 같이 수식이 작성됩니다.
// 공이 패들에 닿았을 때
if ((y + dy > canvas.height - paddleHeight - ballRadius)
&& (x > paddleX && x < paddleX + paddleWidth)) {
dy = -dy; // y축 방향 반전
}
그런데 y축 방향 전환을 한다는 점에서 공이 위쪽 벽에 닿았을 때와 실행문이 일치합니다. 그래서 다음과 같이 정리해서 draw() 코드에 적용해주겠습니다.
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// 공이 위쪽 벽에 닿거나, 패들에 닿으면, y축 방향 반전
if (y + dy < ballRadius ||
((y + dy > canvas.height - paddleHeight - ballRadius) &&
(x > paddleX && x < paddleX + paddleWidth))) {
dy = -dy;
}
// 공의 위치 업데이트
x += dx;
y += dy;
requestAnimationFrame(draw);
}
여기까지 진행된 코드를 실행해보면, 다음처럼 패들에 닿았을 때 공이 잘 튕겨지는 것을 볼 수 있습니다.
3. 공으로 벽돌 부수기
이제 공과 벽돌이 닿았을 때 했을 때, 공은 튕겨나가고 부딧힌 벽돌은 사라지게 만들어보겠습니다.
기본적인 개념은 이전과 동일합니다. 공이 특정 벽돌에 닿았다고 판단하는 조건을 위 그림을 통해서 확인하면, 다음과 같이 조건문을 작성할 수 있습니다.
if(x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
}
그런데 벽돌은 하나가 아니고, 동적으로 부수어지기도 합니다. 따라서 매 프레임마다 벽돌 2차원 배열을 순회하면서 공이 닿았는지 여부를 확인하고, 벽돌의 상태를 업데이트 해주어야 합니다. 이를 메서드로 다음과 같이 작성하여 game.js 에 추가하겠습니다.
// 벽돌과 공의 충돌 감지
function collisionDetection() {
for(let c = 0; c < brickColumnCount; c++) {
for(let r = 0; r < brickRowCount; r++) {
let b = bricks[c][r];
if(b.status == 1) { // 벽돌이 남아있을 때만 충돌 확인
if(x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
dy = -dy; // 공의 방향 반전
b.status = 0; // 벽돌이 깨짐
}
}
}
}
}
그리고 매 프레임마다 해당 메서드가 실행될 수 있도록 draw() 를 다음과 같이 수정해주었습니다.
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
collisionDetection(); // 벽돌과 공 충돌 감지 추가
if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy < ballRadius ||
((y + dy > canvas.height - paddleHeight - ballRadius) &&
(x > paddleX && x < paddleX + paddleWidth))) {
dy = -dy;
}
x += dx;
y += dy;
requestAnimationFrame(draw);
}
여기까지 실행하게 되면 다음 동영상처럼 벽돌이 잘 깨지는 것을 확인 할 수 있습니다.
4. 현재까지 전체 코드
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>벽돌 깨기 게임</title>
<style>
canvas {
background: #eee;
display: block;
margin: 0 auto;
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="480" height="320"></canvas>
<script src="game.js"></script>
</body>
</html>
game.js
// 캔버스와 2D 컨텍스트 가져오기
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// 공의 위치와 반지름 설정
let x = canvas.width / 2; // 공의 최초 위치 x 좌표
let y = canvas.height - 30; // 공의 최초 위치 y 좌표
let dx = 2; // 공의 x축 이동 속도
let dy = -2; // 공의 y축 이동 속도
let ballRadius = 10; // 공의 반지름 크기
// 패들 설정
let paddleHeight = 10; // 패들 세로 길이
let paddleWidth = 75; // 패들 가로 길이
let paddleX = (canvas.width - paddleWidth) / 2; // 패들의 초기 위치
// 벽돌 설정
let brickRowCount = 3; // 벽돌 열 수
let brickColumnCount = 5; // 벽돌 행 수
let brickWidth = 75; // 벽돌 가로 길이
let brickHeight = 20; // 벽돌 세로 길이
let brickPadding = 10; // 벽돌 간 간격
let brickOffsetTop = 30; // 벽돌과 캔버스 위쪽 가장자리 사이의 간격
let brickOffsetLeft = 30; // 벽돌과 캔버스 왼쪽 가장자리 사이의 간격
/** 현재 화면에 존재하는 벽돌의 상태를 추적하고 관리하기 위해 2차원 배열 사용
* bricks[c][r] 형태
* x, y : 벽돌의 위치를 지정하기 위한 속성, 초기에 0으로 설정하고, 이후 실제 위치를 계산할 때 업데이트.
* status : 벽돌이 화면에 표시되어 있는지(1), 깨져서 사라졌는지(0)을 나타냄
*/
let bricks = [];
for(let c = 0; c < brickColumnCount; c++) {
bricks[c] = []; // 열을 위한 빈 배열 생성
for(let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 }; // 벽돌 상태 초기화
}
}
// 공 그리기 함수
function drawBall() {
ctx.beginPath(); // 새로운 경로 시작
ctx.arc(x, y, ballRadius, 0, Math.PI * 2); // 원 그리기
ctx.fillStyle = "#0095DD"; // 공의 색상
ctx.fill(); // 원 채우기
ctx.closePath(); // 경로 닫기
}
// 패들 그리기 함수
function drawPaddle() {
ctx.beginPath(); // 경로 그리기 시작
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); // 사각형 그리기
ctx.fillStyle = "#0095DD"; // 패들 색상
ctx.fill(); // 내부 채우기
ctx.closePath(); // 경로 닫기
}
// 벽돌 그리기 함수 - 2차원 배열을 순회하면서 status가 1인 벽돌만 화면에 그린다.
function drawBricks() {
for(let c = 0; c < brickColumnCount; c++) {
for(let r = 0; r < brickRowCount; r++) {
if(bricks[c][r].status == 1) {
let brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
let brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}
// 벽돌과 공의 충돌 감지
function collisionDetection() {
for(let c = 0; c < brickColumnCount; c++) {
for(let r = 0; r < brickRowCount; r++) {
let b = bricks[c][r];
if(b.status == 1) { // 벽돌이 남아있을 때만 충돌 확인
if(x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
dy = -dy; // 공의 방향 반전
b.status = 0; // 벽돌이 깨짐
}
}
}
}
}
// 마우스 이동에 대한 이벤트 리스너 설정
document.addEventListener("mousemove", mouseMoveHandler, false);
// 마우스 이동시 패들이 마우스 위치를 따라가도록 구현
function mouseMoveHandler(e) {
let relativeX = e.clientX - canvas.offsetLeft;
if(relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth / 2;
}
}
// 애니메이션 프레임마다 실행되는 함수
function draw() {
// 이전 프레임을 지우고, 새로운 프레임을 그리기 위해 준비
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks(); // 벽돌 그리기
drawBall(); // 공 그리기
drawPaddle(); // 패들 그리기
collisionDetection(); // 공과 벽돌 충돌 감지
// 공이 좌,우측 벽에 닿을 경우, x축 방향 반전
if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// 공이 위쪽 벽에 닿거나, 패들에 닿으면, y축 방향 반전
if (y + dy < ballRadius ||
((y + dy > canvas.height - paddleHeight - ballRadius) &&
(x > paddleX && x < paddleX + paddleWidth))) {
dy = -dy;
}
// 공의 위치 업데이트
x += dx;
y += dy;
requestAnimationFrame(draw); // 다음 프레임 요청
}
draw(); // 첫 번째 프레임 실행
'JavaScript' 카테고리의 다른 글
[JavaScript] 간단한 테트리스 게임 만들기 (0) | 2024.08.24 |
---|---|
[JavaScript] 벽돌깨기 게임 만들기 (3) 게임 시작, 게임 오버, 게임 클리어, 다시 시작 구현하기 (0) | 2024.08.22 |
[JavaScript] 사칙연산 계산기 만들기 (0) | 2024.08.16 |
[JavaScript] 벽돌깨기 게임 만들기 (1) <canvas> 에 도형 그리기 (0) | 2024.08.12 |
[JavaScript] 드래그 앤 드랍으로 이미지 순서 변경하기 (0) | 2024.08.08 |