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

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

by jaewooojung 2019. 10. 29.

HTML JAVASCRIPT


이전글

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

 

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

자바스크립트로 게임 만들기 - 포트리스(1) 탱크, 표적 HTML5의 canvas와 자바스크립트를 이용하여 간단한 포트리스 게임을 만들어보겠습니다. 1. 화면 틀 만들기 적당한 크기의 게임판을 만들었습

codingbroker.tistory.com


자바스크립트로 게임 만들기 - 포트리스(2) 미사일

탱크와 표적에 이어서 이번 글에서는 미사일을 구현해 보겠습니다.

 

다음 순서로 진행합니다.

  1. 파워게이지 충전
  2. 미사일 발사
  3. 판정

 

먼저 필요한 변수들을 선언합니다. <script> 내부에서 위쪽 변수가 선언된 부분에 추가해 주시면 됩니다.

let missileRadius = 5;
let missileX;
let missileY;
let isCharging = false;
let isFired = false;
let isHitted = false;
let gauge = Math.PI;
const gaugeDIF = Math.PI / 60;
const gaugeBarRadius = 30;
let missilePower;
let missileDx;
let missileDy;
const GRAVITY_ACCELERATION = 0.098;
missileRadius  미사일의 반지름
missileX  미사일의 x좌표
missileY  미사일의 y좌표
isCharging  파워게이지 채우는 중인지 여부
isFired  공이 발사되었는지 여부
isHitted  공이 목표물에 명중했는지 여부
gauge  파워게이지
gaugeDIF  파워게이지가 충전되는 속도
gaugeBarRadius  파워게이지바의 반지름
missilePower  미사일 파워
missileDx  미사일 x방향 속도
missileDy  미사일 y방향 속도
GRAVITY_ACCELERATION  공이 아래쪽으로 받는 힘(중력가속도 같은 개념입니다)

 

 

 

1. 파워게이지 충전

keydownHandler와 keyupHandler 아래쪽에 추가합니다.

const keydownHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = true;
  } else if (event.keyCode === 39) {
    tankRightPressed = true;
  } else if (event.keyCode === 38 && cannonAngle <= Math.PI / 2) {
    cannonAngle += cannonAngleDIF;
  } else if (event.keyCode === 40 && cannonAngle >= 0) {
    cannonAngle -= cannonAngleDIF;
  } else if (event.keyCode === 32 && !isFired) {
    isCharging = true;
  }
};
const keyupHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = false;
  } else if (event.keyCode === 39) {
    tankRightPressed = false;
  } else if (event.keyCode === 32 && !isFired) {
    isCharging = false;
    isFired = true;
  }
};

 

스페이스바(keyCode로 32)를 누르면 게이지 충전을 시작하고 => isCharging = true

손을 떼면 게이지 충전이 끝나면서 => isCharging = false

미사일이 발사됩니다. => isFired = true

 

 

draw함수 내부에서 isCharging에 따라서 게이지가 충전되도록 합니다.

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;
  }
  if (isCharging && !isFired) {
    if (gauge < Math.PI * 2) {
      gauge += gaugeDIF;
    }
  }
  drawTank();
  drawTarget();
  drawMissile();
};

 

isCharing이 true이고 아직 발사되지 않았을 때 gauge는 충전됩니다. 충전 시 gauge는 초기값인 Math.PI 에서 시작해서 Math.PI * 2까지 충전됩니다.

 

충전된 게이지를 그리는 drawGauge함수를 작성하고 충전될 때마다 canvas에 그려지도록 조건문 안에서 호출합니다.

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;
  }
  if (isCharging && !isFired) {
    if (gauge < Math.PI * 2) {
      gauge += gaugeDIF;
    }
    drawGausing();
  }
  drawTank();
  drawTarget();
  drawMissile();
};
const drawGausing = () => {
  ctx.beginPath();
  ctx.arc(
    tankCenterX,
    tankCenterY - cannonLength,
    gaugeBarRadius,
    Math.PI,
    gauge,
    false
  );
  ctx.stroke();
};

 

 

스페이스바에서 손을 떼면 gauge가 초기화되도록 keyupHandler에 아래 한 줄을 추가합니다.

const keyupHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = false;
  } else if (event.keyCode === 39) {
    tankRightPressed = false;
  } else if (event.keyCode === 32 && !isFired) {
    isCharging = false;
    isFired = true;
    gauge = Math.PI;
  }
};

파워게이지 충전 gif

 

파워게이지가 충전됩니다. 현재는 스페이스바에서 손을 떼면 isFired가 true로 되기 때문에 다시 스페이스바를 눌러도 게이지바가 나타나지 않습니다. 이 부분은 밑에 미사일 판정 부분에서 미사일이 명중되었거나 빗나갔을 때 isFired가 false로 돌아오면서 다시 게이지바가 나타날 수 있도록 구현됩니다.

 

 

2. 미사일 발사

충전된 파워로 미사일을 발사하는 기능을 구현하겠습니다. 우선 캐논의 끝부분에 미사일이 보이도록 미사일을 그려줍니다.

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;
  }
  if (isCharging && !isFired) {
    if (gauge < Math.PI * 2) {
      gauge += gaugeDIF;
    }
    drawGausing();
  }
  if (!isFired) {
    missileX = tankCenterX + cannonLength * Math.cos(cannonAngle);
    missileY = tankCenterY - cannonLength * Math.sin(cannonAngle);
  }
  drawTank();
  drawTarget();
  drawMissile();
};

const drawMissile = () => {
  ctx.beginPath();
  ctx.arc(missileX, missileY, missileRadius, 0, Math.PI * 2);
  ctx.fillStyle = "blue";
  ctx.fill();
  ctx.closePath();
};

 

if 조건으로 미사일이 발사 중이 아닐 때 캐논의 끝부분에 위치하도록 했습니다.

 

캐논 끝부분에 미사일 위치

 

 

다음으로 발사되었을 때의 움직이는 미사일을 그리기 위해 if 조건문에 아래 코드를 추가합니다.

if (!isFired) {
  missileX = tankCenterX + cannonLength * Math.cos(cannonAngle);
  missileY = tankCenterY - cannonLength * Math.sin(cannonAngle);
} else {
  missileDy -= GRAVITY_ACCELERATION;
  missileX = missileX + missileDx;
  missileY = missileY - missileDy;
}

 

미사일이 날아가는 동안에는 x방향으로는 missileDx라는 시간당 동일한 위치 변화(등속도)가 발생하고, y방향으로는 중력의 영향으로 아래쪽으로 점점 빨라지는(등가속도) 움직임이 발생합니다.

 

missileDx와 missileDy가 각도와 파워게이지에 따라서 정해지도록 keyupHandler에 아래 코드를 추가합니다.

const keyupHandler = event => {
  if (event.keyCode === 37) {
    tankLeftPressed = false;
  } else if (event.keyCode === 39) {
    tankRightPressed = false;
  } else if (event.keyCode === 32 && !isFired) {
    isCharging = false;
    isFired = true;
    missilePower = gauge * 1.6;
    missileDx = missilePower * Math.cos(cannonAngle);
    missileDy = missilePower * Math.sin(cannonAngle);
    gauge = Math.PI;
  }
};

 

스페이스바에서 손을 뗄 때의 흐름은 다음과 같습니다.

  1. isCharging이 false로 되면서 파워게이지의 충전이 끝납니다.
  2. isFired가 true로 되면서 미사일의 발사가 감지됩니다.
  3. 충전된 gauge에 의해서 미사일의 파워와 x방향 속도, y방향 초기속도가 결정됩니다.
  4. gauge는 다시 처음값으로 초기화됩니다.
  5.  

미사일 발사 gif

 

3. 판정

미사일이 명중되지 않거나 명중되었을 때의 상황을 구현해 보겠습니다.

 

checkMissile함수를 작성하고 draw함수에서 호출합니다.

const draw = () => {

  // ... 생략
  
  checkMissile();
  drawTank();
  drawTarget();
  drawMissile();
};
const checkMissile = () => {};

 

3-1 명중되지 않았을 때

미사일이 명중되지 않는 경우는 미사일이 왼쪽, 오른쪽, 아래쪽 canvas 테두리에 닿았을 때입니다. 아래와 같이 구현합니다.

const checkMissile = () => {
  // canvas 왼쪽, 오른쪽, 아래 벽에 닿으면
  if (missileX <= 0 || missileX >= width || missileY >= height) {
    isFired = false;
  }
};

 

3-2 명중되었을 때

미사일이 명중된 경우는 미사일의 x좌표와 y좌표가 목표물의 테두리에 닿았을 때입니다. 아래와 같이 구현합니다.

const checkMissile = () => {
  // canvas 왼쪽, 오른쪽, 아래 벽에 닿으면
  if (missileX <= 0 || missileX >= width || missileY >= height) {
    isFired = false;
  }
  // target 명중
  if (
    missileX >= targetX &&
    missileX <= targetX + targetWidth &&
    missileY >= targetY
  ) {
    isHitted = true;
    clearInterval(start);
    if (confirm("명중입니다. 다시 하시겠습니까?")) {
      location.reload();
    }
  }
};

 

미사일이 명중되면 isHitted가 true가 되면서 clearInterval로 인해 draw함수의 실행이 중지됩니다.

 

마지막으로 draw함수 내부에서 isHitted에 따라 미사일과 목표물이 그려지도록 수정합니다.

const draw = () => {
  
  // ...생략
  
  checkMissile();
  drawTank();
  if (!isHitted) {
    drawTarget();
    drawMissile();
  }
};

 

미사일이 목표물에 명중되어 isHitted가 true가 되면 목표물과 미사일이 더 이상 보이지 않게 됩니다.


👏완성. 수고하셨습니다.

이상으로 자바스크립트와 canvas를 이용하여 간단한 포트리스 게임을 만들어봤습니다.

 

게임 실행 영상과 전체코드를 첨부합니다.

실행영상

 

<!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() * 150 + 30);
      const targetHeight = Math.floor(Math.random() * 100 + 10);
      const targetX = Math.floor(Math.random() * (500 - targetWidth) + 500);
      const targetY = height - targetHeight;
      let missileRadius = 5;
      let missileX;
      let missileY;
      let isCharging = false;
      let isFired = false;
      let isHitted = false;
      let gauge = Math.PI;
      const gaugeDIF = Math.PI / 60;
      const gaugeBarRadius = 30;
      let missilePower;
      let missileDx;
      let missileDy;
      const GRAVITY_ACCELERATION = 0.098;
      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;
        }
        if (isCharging && !isFired) {
          if (gauge < Math.PI * 2) {
            gauge += gaugeDIF;
          }
          drawGausing();
        }
        if (!isFired) {
          missileX = tankCenterX + cannonLength * Math.cos(cannonAngle);
          missileY = tankCenterY - cannonLength * Math.sin(cannonAngle);
        } else {
          missileDy -= GRAVITY_ACCELERATION;
          missileX = missileX + missileDx;
          missileY = missileY - missileDy;
        }
        checkMissile();
        drawTank();
        if (!isHitted) {
          drawTarget();
          drawMissile();
        }
      };
      const checkMissile = () => {
        // canvas 왼쪽, 오른쪽, 아래 벽에 닿으면
        if (missileX <= 0 || missileX >= width || missileY >= height) {
          isFired = false;
        }
        // target 명중
        if (
          missileX >= targetX &&
          missileX <= targetX + targetWidth &&
          missileY >= targetY
        ) {
          isHitted = true;
          clearInterval(start);
          if (confirm("명중입니다. 다시 하시겠습니까?")) {
            location.reload();
          }
        }
      };
      const drawMissile = () => {
        ctx.beginPath();
        ctx.arc(missileX, missileY, missileRadius, 0, Math.PI * 2);
        ctx.fillStyle = "blue";
        ctx.fill();
        ctx.closePath();
      };
      const drawGausing = () => {
        ctx.beginPath();
        ctx.arc(
          tankCenterX,
          tankCenterY - cannonLength,
          gaugeBarRadius,
          Math.PI,
          gauge,
          false
        );
        ctx.stroke();
      };
      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";
      };
      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 / 2) {
          cannonAngle += cannonAngleDIF;
        } else if (event.keyCode === 40 && cannonAngle >= 0) {
          cannonAngle -= cannonAngleDIF;
        } else if (event.keyCode === 32 && !isFired) {
          isCharging = true;
        }
      };
      const keyupHandler = event => {
        if (event.keyCode === 37) {
          tankLeftPressed = false;
        } else if (event.keyCode === 39) {
          tankRightPressed = false;
        } else if (event.keyCode === 32 && !isFired) {
          isCharging = false;
          isFired = true;
          missilePower = gauge * 1.6;
          missileDx = missilePower * Math.cos(cannonAngle);
          missileDy = missilePower * Math.sin(cannonAngle);
          gauge = Math.PI;
        }
      };
      const start = setInterval(draw, 10);
      document.addEventListener("keydown", keydownHandler, false);
      document.addEventListener("keyup", keyupHandler, false);
    </script>
  </body>
</html>


        
답변을 생성하고 있어요.