본문 바로가기
프론트엔드 HTML CSS JAVASCRIPT

[HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (1) - 탱크, 표적

by jaewooojung 2019. 10. 28.

HTML JAVASCRIPT


자바스크립트로 게임 만들기 - 포트리스(1) 탱크, 표적

HTML5의 canvas와 자바스크립트를 이용하여 간단한 포트리스 게임을 만들어보겠습니다.

 

1. 화면 틀 만들기

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>fortress</title>
    <style>
      body {
        margin: 0;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      #fortress {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <canvas id="fortress" width="1000px" height="700px"></canvas>
    <script>
      const canvas = document.getElementById("fortress");
    </script>
  </body>
</html>

 

적당한 크기의 게임판을 만들었습니다. canvas에는 자바스크립트로 로직을 제어하기 위해 id값을 설정했습니다. 이후부터는 HTML 부분은 건드리지 않고 <script> 내부에서 모든 제어가 이루어집니다.

 

코드에서처럼 canvas에도 다른 HTML 요소(div, img 등)처럼 css를 적용할 수 있습니다. 단, canvas 내부에 그려지는 요소들에는 해당 css가 적용되지 않고, 주로 canvas의 위치를 설정하거나 테두리를 그리기 위해서만 사용합니다. 위 코드에서는 canvas의 테두리를 검은색으로 설정해 주었습니다.

 

 

2. 탱크

2-1 탱크 본체

게임판에 탱크와 표적을 추가하겠습니다. canvas 내부에 그림을 그리기 위해서는 먼저 context를 불러와야 합니다. 2d로 그림을 그리기 위해 getContext 메서드를 사용하여 2d용 context를 불러오겠습니다.

const canvas = document.getElementById("fortress");
const ctx = canvas.getContext("2d");

 

 

다음으로 canvas 내부에 그림을 그리는 함수 draw를 만듭니다. 함수의 로직은 코드가 복잡해지지 않게 탱크, 목표물, 미사일로 나누어서 각각 drawTank, drawTarget, drawMissile이라는 이름으로 나누겠습니다.

const canvas = document.getElementById("fortress");
const ctx = canvas.getContext("2d");
const draw = () => {
  drawTank();
  drawTarget();
  drawMissile();
}
const drawTank = () => {}
const drawTarget = () => {}
const drawMissile = () => {}

draw();

 

 

 

탱크를 그립니다.

const canvas = document.getElementById("fortress");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const tankWidth = 50;
const tankHeight = 50;
let tankX = 0;
const tankDx = 3;
let tankLeftPressed = false;
let tankRightPressed = false;

// ...

const drawTank = () => {
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.beginPath();
  ctx.moveTo(tankX, height - tankHeight);
  ctx.lineTo(tankX + tankWidth, height - tankHeight);
  ctx.lineTo(tankX + tankWidth, height);
  ctx.lineTo(tankX, height);
  ctx.lineTo(tankX, height - tankHeight);
  ctx.stroke();
  ctx.closePath();
};

 

tankWidth 탱크의 가로길이
tankHeight 탱크의 세로길이
tankX  탱크의 위치(왼쪽 위 꼭짓점)
tankDx 탱크가 움직이는 속도
tankLeftPressed  왼쪽 방향키가 눌렸는지 여부
tankRightPressed  른쪽 방향키가 눌렸는지 여부

 

먼저 필요한 변수들을 선언한 후 context를 활용하여 drawTank에서 탱크를 그려줍니다. 이 글에서는 moveTo, lineTo 등 context 메서드들에 관한 설명은 생략하겠습니다. 자세한 설명은 하단의 MDN Canvas API 링크를 참조해 주세요.

 

간단하게 정사각형으로 표현했습니다. 참고로 canvas의 좌표는 왼쪽 상단이 원점(0, 0)입니다.

실행화면

 

 

2-2 탱크 움직임

다음으로 방향키를 눌렀을 때 탱크가 왼쪽, 오른쪽으로 움직이도록 구현해 보겠습니다. <script> 내부의 맨 밑에 다음 코드를 추가합니다.

const keydownHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = true;
  } else if (event.keyCode === 39) {
    tankRightPressed = true;
  }
};
const keyupHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = false;
  } else if (event.keyCode === 39) {
    tankRightPressed = false;
  }
};
const start = setInterval(draw, 10);
document.addEventListener("keydown", keydownHandler, false);
document.addEventListener("keyup", keyupHandler, false);

 

keydown과 keyup은 각각 키보드가 눌렸을 때와 눌린 키보드에서 손을 떼었을 때 실행되는 이벤트입니다. 각각 keydownHandler와 keyupHandler라는 콜백함수를 생성해서 전달하였습니다. 이렇게 하면 눌린 키보드가 왼쪽 방향키(keyCode 37)라면 tankLeftPressed를 true로, 오른쪽방향키(keyCode로 39)라면 tankRightPressed를 true로 변경하고 손을 뗄 때에는 다시 false로 되돌립니다.

 

canvas에 변화가 있을 때에는 이전에 그려진 그림들을 모두 지우고 업데이트된 그림을 새로 그려야 합니다. 영상의 각 프레임이 새로 렌더링 되는 것과 같은 원리입니다. 이를 위해 setInterval을 통해 draw함수가 10ms마다 실행되도록 구현하였습니다. 변수 start에 초기화한 이유는 이후에 setInterval를 해제하면서 게임을 종료시키기 위함입니다.

 

다음은 draw 함수입니다.

const draw = () => {
  ctx.clearRect(0, 0, width, height);
  if (tankLeftPressed && tankX > 0) {
    tankX -= tankDx;
  }
  if (tankRightPressed && tankX + tankWidth < width) {
    tankX += tankDx;
  }
  drawTank();
  drawTarget();
  drawMissile();
};

 

clearRect()를 통해 캔버스의 모든 그림을 지우고 tankLeftPressed와 tankRightPressed 값에 따라 탱크의 위치(tankX)를 변화시키며 움직이도록 구현했습니다. if의 && 뒤에 준 조건들은 tank가 화면을 벗어나지 않도록 하기 위함입니다.

 

왼쪽, 오른쪽 움직임 gif

 

2-1 탱크 캐논

다음은 미사일을 쏘기 위한 캐논을 추가해 보겠습니다. 먼저 아래 5개의 변수를 추가해 줍니다.

tankCenterX  탱크의 중심 x좌표
tankCenterY  탱크의 중심 y좌표
cannonAngle  캐논의 각도
cannonAngleDIF  각도의 변화량
cannonLength  캐논의 길이

 

const canvas = document.getElementById("fortress");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const tankWidth = 50;
const tankHeight = 50;
let tankX = 0;
const tankDx = 3;
let tankLeftPressed = false;
let tankRightPressed = false;
let tankCenterX;
let tankCenterY;
let cannonAngle = Math.PI / 4;
const cannonAngleDIF = Math.PI / 60;
const cannonLength = tankWidth * Math.sqrt(2);

 

 

선언한 변수를 활용하여 drawTank에서 캐논을 그려줍니다.

const drawTank = () => {
  tankCenterX = tankX + 0.5 * tankWidth;
  tankCenterY = height - 0.5 * tankHeight;
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.beginPath();
  ctx.moveTo(tankX, height - tankHeight);
  ctx.lineTo(tankX + tankWidth, height - tankHeight);
  ctx.lineTo(tankX + tankWidth, height);
  ctx.lineTo(tankX, height);
  ctx.lineTo(tankX, height - tankHeight);
  ctx.moveTo(tankCenterX, tankCenterY);
  ctx.lineTo(
    tankCenterX + cannonLength * Math.cos(cannonAngle),
    tankCenterY - cannonLength * Math.sin(cannonAngle)
  );
  ctx.stroke();
  ctx.closePath();
};

 

탱크의 위치 변화에 따른 중심을 각각 tankCenterX와 tankCenterY에 저장한 뒤 중심을 기준으로 캐논을 직선으로 표현했습니다.

 

여기서 잠깐, tankCenterX와 tankCenterY는 이후에 다른 그림에서도 사용되기 때문에 계산이 중복되지 않도록 drawTank밖으로 옮겨줍니다. draw함수로 블록으로 옮기겠습니다.

const draw = () => {
  ctx.clearRect(0, 0, width, height);
  tankCenterX = tankX + 0.5 * tankWidth;
  tankCenterY = height - 0.5 * tankHeight;
  if (tankLeftPressed && tankX > 0) {
    tankX -= tankDx;
  }
  if (tankRightPressed && tankX + tankWidth < width) {
    tankX += tankDx;
  }
  drawTank();
  drawTarget();
  drawMissile();
};

 

옮긴 후 기존의 drawTank에서는 지워주시면 됩니다.

 

 

캐논 추가

 

 

다음은 각도 조절을 위해 keydownHandler에 위쪽방향키와 아래쪽 방향키가 눌렸을 때 각도가 변화되도록 코드를 추가해 줍니다.

const keydownHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = true;
  } else if (event.keyCode === 39) {
    tankRightPressed = true;
  } else if (event.keyCode === 38 && cannonAngle <= Math.PI) {
    cannonAngle += cannonAngleDIF;
  } else if (event.keyCode === 40 && cannonAngle >= 0) {
    cannonAngle -= cannonAngleDIF;
  }
};

각도조절 gif

 

 

3. 표적

다음차례는 표적입니다. 탱크를 그릴때와 같은 방법으로 이번에는 drawTarget함수에서 그려주면 됩니다. 다만 표적이 항상 같은 자리에서 같은 크기로 나오면 재미가 없으니 위치와 크기가 무작위로 생성되도록 만들어보겠습니다. 다음 4가지 변수를 추가합니다.

targetWidth  표적의 가로길이
targetHeight  표적의 세로길이
targetX  표적의 위치 (x좌표)
targetY  표적의 위치 (y좌표)
const targetWidth = Math.floor(Math.random() * 100 + 30);
const targetHeight = Math.floor(Math.random() * 100 + 10);
const targetX = Math.floor(Math.random() * (500 - targetWidth) + 500);
const targetY = height - targetHeight;

 

 

위 변수를 사용하여 drawTarget에서 표적을 그립니다.

const drawTarget = () => {
  ctx.fillRect(targetX, targetY, targetWidth, targetHeight);
  ctx.fillStyle = "red";
};

 

위치와 크기가 무작위로 생성되므로 새로고침 할 때마다 위치, 크기가 변화합니다.

 

이상으로 게임판과 탱크, 표적을 만들었습니다.

이 글은 여기에서 마치고, 다음 글에서는 미사일 발사와 적중 기능을 구현해 보겠습니다.

2019.10.29 - [HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (2) - 미사일

 

[HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (2) - 미사일

이전글 2019.10.28 - [HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (1) - 탱크, 표적 [HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (1) - 탱크, 표적 자바스크립트로 게임 만들기 - 포트리스(1) 탱크, 표적 HTML5의 canv

codingbroker.tistory.com

 

 

아래는 여기까지 구현한 기능들을 실행한 영상과 전체 코드입니다.

실행 영상

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>fortress</title>
    <style>
      body {
        margin: 0;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      #fortress {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <canvas id="fortress" width="1000px" height="700px"></canvas>
    <script>
      const canvas = document.getElementById("fortress");
      const ctx = canvas.getContext("2d");
      const width = canvas.width;
      const height = canvas.height;
      const tankWidth = 50;
      const tankHeight = 50;
      let tankX = 0;
      const tankDx = 3;
      let tankLeftPressed = false;
      let tankRightPressed = false;
      let tankCenterX;
      let tankCenterY;
      let cannonAngle = Math.PI / 4;
      const cannonAngleDIF = Math.PI / 60;
      const cannonLength = tankWidth * Math.sqrt(2);
      const targetWidth = Math.floor(Math.random() * 100 + 30);
      const targetHeight = Math.floor(Math.random() * 100 + 10);
      const targetX = Math.floor(Math.random() * (500 - targetWidth) + 500);
      const targetY = height - targetHeight;
      const draw = () => {
        ctx.clearRect(0, 0, width, height);
        tankCenterX = tankX + 0.5 * tankWidth;
        tankCenterY = height - 0.5 * tankHeight;
        if (tankLeftPressed && tankX > 0) {
          tankX -= tankDx;
        }
        if (tankRightPressed && tankX + tankWidth < width) {
          tankX += tankDx;
        }
        drawTank();
        drawTarget();
        drawMissile();
      };
      const drawTank = () => {
        ctx.lineWidth = 5;
        ctx.lineCap = "round";
        ctx.beginPath();
        ctx.moveTo(tankX, height - tankHeight);
        ctx.lineTo(tankX + tankWidth, height - tankHeight);
        ctx.lineTo(tankX + tankWidth, height);
        ctx.lineTo(tankX, height);
        ctx.lineTo(tankX, height - tankHeight);
        ctx.moveTo(tankCenterX, tankCenterY);
        ctx.lineTo(
          tankCenterX + cannonLength * Math.cos(cannonAngle),
          tankCenterY - cannonLength * Math.sin(cannonAngle)
        );
        ctx.stroke();
        ctx.closePath();
      };
      const drawTarget = () => {
        ctx.fillRect(targetX, targetY, targetWidth, targetHeight);
        ctx.fillStyle = "red";
      };
      const drawMissile = () => {};

      draw();
      const keydownHandler = event => {
        if (event.keyCode === 37) {
          tankLeftPressed = true;
        } else if (event.keyCode === 39) {
          tankRightPressed = true;
        } else if (event.keyCode === 38 && cannonAngle <= Math.PI) {
          cannonAngle += cannonAngleDIF;
        } else if (event.keyCode === 40 && cannonAngle >= 0) {
          cannonAngle -= cannonAngleDIF;
        }
      };
      const keyupHandler = event => {
        if (event.keyCode === 37) {
          tankLeftPressed = false;
        } else if (event.keyCode === 39) {
          tankRightPressed = false;
        }
      };
      const start = setInterval(draw, 10);
      document.addEventListener("keydown", keydownHandler, false);
      document.addEventListener("keyup", keyupHandler, false);
    </script>
  </body>
</html>

 


MDN - Canvas API

 

Canvas API - Web API | MDN

Canvas API는 JavaScript와 HTML <canvas> 엘리먼트를 통해 그래픽을 그리기위한 수단을 제공합니다. 무엇보다도 애니메이션, 게임 그래픽, 데이터 시각화, 사진 조작 및 실시간 비디오 처리를 위해 사용

developer.mozilla.org

 



        
답변을 생성하고 있어요.