자바스크립트로 게임 만들기 - 포트리스(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가 화면을 벗어나지 않도록 하기 위함입니다.
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;
}
};
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) - 미사일
아래는 여기까지 구현한 기능들을 실행한 영상과 전체 코드입니다.
<!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>
'프론트엔드 HTML CSS JAVASCRIPT' 카테고리의 다른 글
[HTML, CSS] input창 클릭 시 CSS적용하는 방법(focus, animation (4) | 2019.12.29 |
---|---|
[HTML, JAVASCRIPT] 웹 게임 만들기 포트리스 (2) - 미사일 (0) | 2019.10.29 |
[HTML, JAVASCRIPT] 뒤로가기(이전 페이지), 앞으로가기(다음 페이지) 구현 - window.history.back(), forward(), go() (0) | 2019.08.29 |
[HTML, CSS] input, textarea의 placeholder에 스타일 작업하는 방법(색상 등) (0) | 2019.08.23 |
[HTML, CSS] 아이콘 추가하는 방법 - fontawesome 사용법 (1) | 2019.08.22 |