이전글
2019.10.28 - [HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (1) - 탱크, 표적
자바스크립트로 게임 만들기 - 포트리스(2) 미사일
탱크와 표적에 이어서 이번 글에서는 미사일을 구현해 보겠습니다.
다음 순서로 진행합니다.
- 파워게이지 충전
- 미사일 발사
- 판정
먼저 필요한 변수들을 선언합니다. <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;
}
};
파워게이지가 충전됩니다. 현재는 스페이스바에서 손을 떼면 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;
}
};
스페이스바에서 손을 뗄 때의 흐름은 다음과 같습니다.
- isCharging이 false로 되면서 파워게이지의 충전이 끝납니다.
- isFired가 true로 되면서 미사일의 발사가 감지됩니다.
- 충전된 gauge에 의해서 미사일의 파워와 x방향 속도, y방향 초기속도가 결정됩니다.
- gauge는 다시 처음값으로 초기화됩니다.
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>
'프론트엔드 HTML CSS JAVASCRIPT' 카테고리의 다른 글
[HTML, CSS] 기본적인 페이지 레이아웃(layout) 잡기 네이버 클론코딩 (1) (6) | 2020.03.21 |
---|---|
[HTML, CSS] input창 클릭 시 CSS적용하는 방법(focus, animation (4) | 2019.12.29 |
[HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (1) - 탱크, 표적 (0) | 2019.10.28 |
[HTML, JAVASCRIPT] 뒤로가기(이전 페이지), 앞으로가기(다음 페이지) 구현 - window.history.back(), forward(), go() (0) | 2019.08.29 |
[HTML, CSS] input, textarea의 placeholder에 스타일 작업하는 방법(색상 등) (0) | 2019.08.23 |