엘리스 AI트랙 1차 스터디 5주차, 6주차

Deong_gu·2022년 7월 28일
0
post-custom-banner

[엘리스 AI트랙 5기 1차 스터디 프로젝트]

<canvas 요소와 JavaScript를 활용해서 벽돌 부수기 게임 만들기>

1. 서론

4주차 공의 움직임 및 충돌을 구현했고, 각도와 움직임의 한계를 알지만 애써 무시하기로 하고, 목표로 그렸던 gif 파일대로 구현하기 위해서 벽돌생성, 벽돌 충돌 후 이벤트, 게임 오버 상황 구현을 하였다.
(5주차, 6주차 같이 작성하는 건 함정)

2. 본론

벽과 막대기에 충돌했을 때와 비슷하게 벽돌 충돌을 구현하기 때문에 간단할 줄 알았는데, 좌표의 개념을 활용해서 공간을 구현해서 자연스러운 충돌을 연출하고 싶었다.

공간을 구현하려고 했지만, 그냥 공간이라고 하기보단 접촉(?)정도의 수준으로 구현을 했다. 그래서인지 공이 지나가면서 공의 면적과 벽돌의 면적이 겹쳐보이는 문제가 발생한다. 완성을 위해서 또 못 본 척하고 넘어갔다. 스터디 기간이 끝난 이후에 리팩토링을 통해서 깔끔하게 구현해봐야겠다.

벽돌을 반복문의 형태로 구현, 2차원 배열로 상태 관리

시작버튼은 간단하게 클릭 이벤트로 구현

게임오버 시 alert()를 이용해서 화면에 표시하고 확인을 누르면 페이지 새로고침을 통해 다시 시작할 수 있게 만들었다.

//벽돌
const brick = {
  rowCount: 3,
  columnCount: 4,
  width: 75,
  height: 20,
  padding: 10,
  offSetTop: 30,
  offSetLeft: 30,
  color: "green",
};

const bricks = [];
for (let i = 0; i < brick.columnCount; i++) {
  bricks[i] = [];
  for (let j = 0; j < brick.rowCount; j++) {
    bricks[i][j] = { x: 0, y: 0, status: 1 };
  }
}

function drawBricks() {
  for (let i = 0; i < brick.columnCount; i++) {
    for (let j = 0; j < brick.rowCount; j++) {
      if (bricks[i][j].status == 1) {
        let brickX = i * (brick.width + brick.padding) + brick.offSetLeft;
        let brickY = j * (brick.height + brick.padding) + brick.offSetTop;
        bricks[i][j].x = brickX;
        bricks[i][j].y = brickY;
        ctx.beginPath();
        ctx.rect(brickX, brickY, brick.width, brick.height);
        ctx.fillStyle = "green";
        ctx.fill();
        ctx.closePath();
      }
    }
  }
}

게으른 자신을 반성하면서.. 마지막 스터디 6주차의 내용도 여기에 작성을 한다.

충돌 시에 테두리가 지워지는 것과 테두리와 공이 겹쳐지는 것을 좌표를 고려해서 범위를 수정했다. 다행이 이 부분 해결했지만, 아직 벽돌과 공이 겹쳐지는 것은 해결 못했다.

벽돌과 충돌 시, 벽돌이 없어짐과 동시에 점수를 올리는 점수보드 구현

기존에 간단하게 게임오버를 화면에 표시했던 것 alert()를 prompt()로 변경하면서 작성 값을 변수에 할당했다.

작성 값(이름)과 점수를 localStorage를 활용해서 랭킹 보드를 구현했다.(정렬까지 반영했다.) 처음 프로젝트를 시작할 때, 단순하게 순위와 이름, 점수를 데이터베이스에 저장하고 싶은 욕망(?)이 있었지만, DB와 백엔드, 프론트엔드의 흐름을 잘못 이해해서 시간을 날렸다.. 추후에라도 간단하게 DB에 저장을 해보겠다.

3.결론

처음 시작을 할 때, 어떻게 시작을 해야할까 부터 멘탈이 나갔다. 스터디원분들에게는 당당하게 타블로그를 참고하지 않을거다. mdn문서만을 참고하면서 해볼 것이다! 라고 포부를 비쳤지만... 머리가 회전이 안됐다.. 그래서 일단 핵심인 canvas태그의 mdn문서를 정독했다. 예시코드를 작성하고 결과물을 확인하면서 천천히 이해하는 방향으로 갔다. 확실히 애니메이션의 개념같이 화면을 프레임별로 촤라락 (지우고 쓰고 지우고 쓰고 ~ 반복)인데, 좌표 개념도 들어가서 힘들었다ㅠㅠ

충돌은 mdn문서의 힘을 많이 받았다.

함수 호출 순서에 따라서 화면에 표시되는 것들이 바뀌고, 컨트롤 하기도 쉽지 않았다.
시작과 끝을 명시적으로 표현은 했지만, 그게 맞게 표현한 지 확신을 하지 못했다.

이렇게 부족함이 많은 첫번째 스터디 프로젝트를 마무리했는데, 아쉬움이 많이 남지만, 깃허브 페이지로 다른 팀원분들께 보여드렸는데, 칭찬을 해주셔서 뿌듯했다. (팀원분들이 너무 착하심.ㅠㅠㅠ)

시작에 어려움을 느꼈는데, 이젠 시작을 어떻게 해야할 지 어느정도 감이 잡힌 느낌이다. 구현할 것을 생각하면서 하나씩 천천히!

소스코드

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");
const btn = document.querySelector("button");
const score = document.querySelector("#score");
const rankBoard = document.querySelector(".rank_board_content");

let scoreNumber = 0;

const user = {
  scoreNumber: 0,
  userName: "",
};

const rank = [];

//랭킹 보드
for (let i = 0; i < window.localStorage.length; i++) {
  rank.push([
    window.localStorage.key(i),
    Number(window.localStorage.getItem(window.localStorage.key(i))),
  ]);
}

rank.sort((a, b) => {
  return b[1] - a[1];
});

for (let i = 0; i < rank.length; i++) {
  const li = document.createElement("li");
  const liText = document.createTextNode(`${rank[i][0]} : ${rank[i][1]}`);
  rankBoard.appendChild(li);
  li.appendChild(liText);
}

//막대기
const bar = {
  x: 150,
  y: 350,
  vx: 15,
  width: 100,
  height: 20,
  color: "black",
};

//벽돌
const brick = {
  rowCount: 3,
  columnCount: 4,
  width: 75,
  height: 20,
  padding: 10,
  offSetTop: 30,
  offSetLeft: 30,
  color: "green",
};

// 공
const ball = {
  x: 200,
  y: 335,
  vx: 5,
  vy: 2,
  radius: 15,
  color: "black",
};

//이벤트
function rightMove() {
  bar.x += bar.vx;
}
function leftMove() {
  bar.x -= bar.vx;
}

document.addEventListener("keydown", (e) => {
  if (e.code === "ArrowRight") {
    window.requestAnimationFrame(rightMove);
  }
});
document.addEventListener("keydown", (e) => {
  if (e.code === "ArrowLeft") {
    window.requestAnimationFrame(leftMove);
  }
});
btn.addEventListener("click", function (e) {
  window.requestAnimationFrame(draw);
});

//함수
function draw() {
  drawBall();
  drawBar();
  drawBricks();
  collisionDetection();
}

function drawBall() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2, true);
  ctx.closePath();
  ctx.fillStyle = ball.color;
  ctx.fill();
}

function drawBar() {
  ctx.beginPath();
  ctx.strokeRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = bar.color;
  ctx.closePath();

  ctx.beginPath();
  ctx.fillStyle = bar.color;
  ctx.fillRect(bar.x, bar.y, bar.width, bar.height);
  ctx.closePath();

  if (bar.x + 100 > canvas.width) {
    bar.x = canvas.width - 100;
  } else if (bar.x < 0) {
    bar.x = 0;
  }
}

const bricks = [];
for (let i = 0; i < brick.columnCount; i++) {
  bricks[i] = [];
  for (let j = 0; j < brick.rowCount; j++) {
    bricks[i][j] = { x: 0, y: 0, status: 1 };
  }
}

function drawBricks() {
  for (let i = 0; i < brick.columnCount; i++) {
    for (let j = 0; j < brick.rowCount; j++) {
      if (bricks[i][j].status == 1) {
        let brickX = i * (brick.width + brick.padding) + brick.offSetLeft;
        let brickY = j * (brick.height + brick.padding) + brick.offSetTop;
        bricks[i][j].x = brickX;
        bricks[i][j].y = brickY;
        ctx.beginPath();
        ctx.rect(brickX, brickY, brick.width, brick.height);
        ctx.fillStyle = "green";
        ctx.fill();
        ctx.closePath();
      }
    }
  }
}

function collisionDetection() {
  if (
    ball.y + ball.vy + ball.radius > bar.y &&
    ball.y + ball.vy + ball.radius < bar.y + bar.height &&
    ball.x >= bar.x &&
    ball.x <= bar.x + bar.width
  ) {
    ctx.beginPath();
    ctx.arc(
      ball.x - ball.radius - ball.vx,
      ball.y - ball.radius - ball.vy,
      ball.radius,
      0,
      true
    );
    ctx.closePath();
    ctx.fillStyle = "black";
    ctx.fill();

    ball.vy = -ball.vy;
    ball.vx = -ball.vx;
    ball.x += ball.vx;
    ball.y += ball.vy;

    window.requestAnimationFrame(draw);
  } else {
    ball.x += ball.vx;
    ball.y += ball.vy;

    window.requestAnimationFrame(draw);
  }

  if (
    ball.x + ball.vx > canvas.width - ball.radius ||
    ball.x + ball.vx < ball.radius
  ) {
    ball.vx = -ball.vx;
  }

  //천장 충돌
  if (ball.y + ball.vy < ball.radius) {
    ball.vy = -ball.vy;
  }

  //바닥 충돌
  if (ball.y + ball.vy > canvas.height) {
    ball.vx = 0;
    ball.vy = 0;
    const name = prompt("이름?", "홍길동");
    user.userName = name;
    user.scoreNumber = scoreNumber;

    window.localStorage.setItem(user.userName, user.scoreNumber);
    location.reload(true);
  }

  for (let i = 0; i < brick.columnCount; i++) {
    for (let j = 0; j < brick.rowCount; j++) {
      let b = bricks[i][j];
      if (b.status === 1) {
        if (
          ball.x > b.x &&
          ball.x < b.x + brick.width &&
          ball.y > b.y &&
          ball.y < b.y + brick.height
        ) {
          b.status = 0;
          scoreNumber += 10;
          score.innerHTML = scoreNumber;
          ball.vy = -ball.vy;
        }
      }
    }
  }
}
drawBall();
drawBar();
drawBricks();

최종 결과물

profile
큰 것을 작게, 작은 것을 구체적이게, 개발자답게
post-custom-banner

0개의 댓글