Space Invaders game [Javascript toy project]

SeBin·2022년 12월 31일
0

Space Invaders game

Javascript Study 5주차 toy project
Space Invaders tutorial

JS 문법 정리

Event

addEventListener를 사용해야 한다.

  • DOMContentLoaded
    - 브라우저가 HTML을 전부 읽고 DOM 트리를 완성하는 즉시 발생한다.
    - 이미지 파일이나 스타일시트 등의 기타 자원은 기다리지 않는다.

  • keydown
    - 키가 눌렸을 때 이벤트 발생
    - 어떤 키가 눌렸는지 나타내는 코드를 제공

    • KeyboardEvent.key
      이벤트가 나타내는 키의 키 값을 나타내는 문자열을 반환

element.classList

  • contains("class")
    - class 존재 여부에 따라 true/false를 반환
  • add("class")
    - class 추가
  • remove("class")
    - class 삭제

forEach문

  alienInvaders.forEach( invader => squares[currentInvaderIndex + invader].classList.add('invader'))

switch문

  // shooter 움직이기
  function moveShooter(e) {
    // 예전 위치는 지우기
    squares[currentShooterIndex].classList.remove('shooter')
    // 이벤트 요소의 키값 비교하기
    switch(e.key) {
      case 'ArrowLeft':
        // 인덱스 범위 안에서만 움직이게 하기
        if (currentShooterIndex % width !== 0) currentShooterIndex -= 1
        break
      case 'ArrowRight':
        // 인덱스 범위 안에서만 움직이게 하기
        if (currentShooterIndex % width < width - 1) currentShooterIndex += 1
        break
    }
    squares[currentShooterIndex].classList.add('shooter')
  }
  document.addEventListener('keydown', moveShooter)

setInterval() / clearInterval()

  • setInterval(함수, 시간) : "시간(ms)"을 간격으로 "함수"를 반복 호출 하는 함수
  • clearInterval(변수) : 반복을 중단하는 함수
// setInterval()함수의 반환값을 변수에 할당하여 반복 시작
let 변수 = setInterval(함수, 시간);

// 변수에 담은 것을 호출하여 반복 중단
clearInterval(변수);

// 반복을 재시작 하려면 재할당하면 된다.
변수 = setInterval(함수, 시간);

setInterval()함수의 return값으로 고유한 interval ID를 갖는다
이를 통해 clearInterval() 로 해당 interval ID를 제거해서 반복 호출을 중단시킬 수 있다.

전체 코드

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Space Invaders</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h2>Score: <span id="result"></span></h2>
  <div class="grid">
    <!-- <div class="laser"></div> -->
  </div>
  <script src="app.js" charset="utf-8"></script>
</body>
</html>

CSS

body {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  margin: 0;
}

.grid {
  display: flex;
  flex-wrap: wrap;
  border: solid 1px;
  width: 600px;
  height: 600px;
}

.grid div {
  width: 40px;
  height: 40px;
  box-sizing: border-box;
  position: relative;
}

.shooter {
  background-image: url(images/shooter.png);
  background-size: cover;
}

.invader {
  background-image: url(images/invader.png);
  background-size: cover;
}

.boom {
  background-color: red;
  border-radius: 20px;
}

.laser {
  border-left: solid 5px orange;
  position: absolute;
  left: 15px;
}

Javascript

// 15 x 15칸 만들기
for (let i = 0; i < 255; i++) {
  // div 요소 생성
  const divElement = document.createElement("div");
  // grid에 div 요소 추가
  const grid = document.querySelector(".grid");
  grid.appendChild(divElement);
}

// 브라우저가 HTML을 전부 읽고 DOM 트리를 완성하자마자 발생하는 이벤트
document.addEventListener("DOMContentLoaded", () => {
  const squares = document.querySelectorAll(".grid div");
  const resultDisplay = document.querySelector("#result");
  let width = 15;
  let currentShooterIndex = 202;
  let currentInvaderIndex = 0;
  let alienInvadersTakenDown = [];
  let result = 0;
  let direction = 1;
  let invaderId;

  // invaders 정의하기, 15 x 15칸에 0부터 시작하는 30개 뭉치
  const alienInvaders = [
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 30,
    31, 32, 33, 34, 35, 36, 37, 38, 39,
  ];

  // invaders 그리기, 모든 칸이 들어있는 배열 squares에서 invader에 해당하는 칸만 style 적용
  alienInvaders.forEach((invader) =>
    squares[currentInvaderIndex + invader].classList.add("invader")
  );

  // shooter 그리기
  squares[currentShooterIndex].classList.add("shooter");

  // shooter 움직이기
  function moveShooter(e) {
    // 예전 위치 지우기
    squares[currentShooterIndex].classList.remove("shooter");
    // 이벤트 요소의 키값 비교하기
    switch (e.key) {
      case "ArrowLeft":
        // 인덱스 범위 안에서만 움직이게 하기
        if (currentShooterIndex % width !== 0) currentShooterIndex -= 1;
        break;
      case "ArrowRight":
        // 인덱스 범위 안에서만 움직이게 하기
        if (currentShooterIndex % width < width - 1) currentShooterIndex += 1;
        break;
    }
    squares[currentShooterIndex].classList.add("shooter");
  }
  document.addEventListener("keydown", moveShooter);

  // invaders 움직이기
  function moveInvaders() {
    const leftEdge = alienInvaders[0] % width === 0;
    const rightEdge = alienInvaders[alienInvaders.length - 1] % width === width - 1;
    
    // 왼쪽 벽이면서 왼쪽으로 가려할 때, 혹은 오른쪽 벽이면서 오른쪽으로 가려할 때
    if ((leftEdge && direction === -1) || (rightEdge && direction === 1)) {
      // invaders 전체를 밑으로 내려주기
      direction = width
    } else if (direction === width) {
      if (leftEdge) direction = 1
      else direction = -1
    }
    for (let i = 0; i <= alienInvaders.length - 1; i++) {
      squares[alienInvaders[i]].classList.remove('invader')
    }
    for (let i = 0; i <= alienInvaders.length - 1; i++) {
      alienInvaders[i] += direction
    }
    for (let i = 0; i <= alienInvaders.length - 1; i++) {
      // 레이저에 맞지 않았다면 
      if (!alienInvadersTakenDown.includes(i)) {
        squares[alienInvaders[i]].classList.add('invader')
      }
    }

    // game over 정의하기
    // shooter가 있는 인덱스에 클래스가 invader, shooter 둘다 있다면 game over
    if (squares[currentShooterIndex].classList.contains('invader', 'shooter')) {
      resultDisplay.textContent = 'Game Over'
      squares[currentShooterIndex].classList.add('boom')
      clearInterval(invaderId)
    }
    for (let i = 0; i <= alienInvaders.length -1; i++) {
      // invaders의 인덱스가 grid의 마지막 줄보다 크다면 game over
      if (alienInvaders[i] > (squares.length - (width - 1))) {
        resultDisplay.textContent = 'Game Over'
        clearInterval(invaderId)
      }
    }

    // 이겼을 때
    if (alienInvadersTakenDown.length === alienInvaders.length) {
      resultDisplay.textContent = 'You Win'
      clearInterval(invaderId)
    }

  }
  invaderId = setInterval(moveInvaders, 500)

  // invader에게 레이저 쏘기
  function shoot(e) {
    let laserId
    let currentLaserIndex = currentShooterIndex

    // shooter에서부터 레이저 나가기
    function moveLaser() {
      squares[currentLaserIndex].classList.remove('laser')
      // 레이저의 행 이동
      currentLaserIndex -= width
      squares[currentLaserIndex].classList.add('laser')
      // 만약 invader가 레이저에 맞았다면
      if (squares[currentLaserIndex].classList.contains('invader')) {
        squares[currentLaserIndex].classList.remove('laser')
        squares[currentLaserIndex].classList.remove('invader')
        squares[currentLaserIndex].classList.add('boom')

        setTimeout(() => squares[currentLaserIndex].classList.remove('boom'), 250)
        clearInterval(laserId)

        // invaders 배열에 레이저 인덱스가 있다면 alienInvadersTakenDown에 추가하기
        const alienTakenDown = alienInvaders.indexOf(currentLaserIndex)
        alienInvadersTakenDown.push(alienTakenDown)
        result++
        resultDisplay.textContent = result
      }

      // 레이저는 빠르게 사라지기
      if (currentLaserIndex < width) {
        clearInterval(laserId)
        setTimeout(() => squares[currentLaserIndex].classList.remove('laser'), 100)
      }
    }

    switch(e.code) {
      case 'Space':
        laserId = setInterval(moveLaser, 100)
        break
    }
  }
document.addEventListener('keydown', shoot)
});

0개의 댓글