๋ ํธ๋ก ๊ฒ์ ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋ - ํด๋์ ๊ฒ์์ ๋ง๋๋ ๋ฐฉ๋ฒ
๋ ํธ๋ก ๊ฒ์ ๊ฐ๋ฐ์ ํ๋ ๊ฒ์ ๊ฐ๋ฐ์ ๊ธฐ์ด๋ฅผ ๋ฐฐ์ฐ๋ ์ต๊ณ ์ ๋ฐฉ๋ฒ์ ๋๋ค. ๋ณต์กํ 3D ์์ง์ด๋ ์๋ฐฑ ๊ฐ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด๋ ์์ํ ํ๋ก๊ทธ๋๋ฐ ๋ก์ง๊ณผ ๊ฒ์ ๋์์ธ ์์น์ ๋ฐฐ์ธ ์ ์์ต๋๋ค. ์ด ๊ฐ์ด๋์์๋ ์ค๋ค์ดํฌ, ํ ํธ๋ฆฌ์ค, ํ๊ณผ ๊ฐ์ ํด๋์ ๊ฒ์์ ์ฒ์๋ถํฐ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋จ๊ณ๋ณ๋ก ์ค๋ช ํฉ๋๋ค.
1. ๊ฒ์ ๊ฐ๋ฐ ๊ธฐ์ด - ๊ฒ์ ๋ฃจํ๋?
๋ชจ๋ ๊ฒ์์ ํต์ฌ์ ๊ฒ์ ๋ฃจํ(Game Loop)์ ๋๋ค. ์ด๊ฒ์ ๊ฒ์์ด ์คํ๋๋ ๋์ ๋ฐ๋ณต์ ์ผ๋ก ์คํ๋๋ ์ฝ๋ ๋ธ๋ก์ผ๋ก, ์ผ๋ฐ์ ์ผ๋ก ์ด๋น 60ํ(60 FPS) ์คํ๋ฉ๋๋ค.
๊ฒ์ ๋ฃจํ๋ ์ธ ๊ฐ์ง ์ฃผ์ ๋จ๊ณ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค:
- ์ ๋ ฅ ์ฒ๋ฆฌ (Input): ํ๋ ์ด์ด์ ํค๋ณด๋, ๋ง์ฐ์ค, ํฐ์น ์ ๋ ฅ์ ๊ฐ์งํฉ๋๋ค.
- ๊ฒ์ ๋ก์ง ์ ๋ฐ์ดํธ (Update): ๊ฒ์ ์ํ๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค. ์บ๋ฆญํฐ ์ด๋, ์ถฉ๋ ๊ฐ์ง, ์ ์ ๊ณ์ฐ ๋ฑ์ด ์ฌ๊ธฐ์ ์ด๋ฃจ์ด์ง๋๋ค.
- ๋ ๋๋ง (Render): ์ ๋ฐ์ดํธ๋ ๊ฒ์ ์ํ๋ฅผ ํ๋ฉด์ ๊ทธ๋ฆฝ๋๋ค.
// JavaScript ๊ฒ์ ๋ฃจํ ์์
function gameLoop() {
processInput(); // 1. ์
๋ ฅ ์ฒ๋ฆฌ
updateGame(); // 2. ๊ฒ์ ๋ก์ง
renderGame(); // 3. ํ๋ฉด ๋ ๋๋ง
requestAnimationFrame(gameLoop); // ๋ค์ ํ๋ ์ ์์ฒญ
}
gameLoop(); // ๋ฃจํ ์์requestAnimationFrame์ ๋ธ๋ผ์ฐ์ ์๊ฒ ๋ค์ ํ๋ ์์ ๊ทธ๋ฆด ์ค๋น๊ฐ ๋๋ฉด ํจ์๋ฅผ ํธ์ถํ๋๋ก ์์ฒญํฉ๋๋ค. ์ด๋ ์ผ๋ฐ์ ์ผ๋ก ์ด๋น 60ํ ์คํ๋์ด ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
์ ๋ง๋ญ๋๋ค.
2. HTML5 Canvas - ๊ฒ์ ๊ทธ๋ํฝ์ ๊ธฐ์ด
๋ ํธ๋ก ๊ฒ์์ ๋ง๋ค ๋ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ ๊ฒ์ด HTML5 Canvas์ ๋๋ค. Canvas๋ JavaScript๋ก ๊ทธ๋ํฝ์ ๊ทธ๋ฆด ์ ์๋ HTML ์์์ ๋๋ค.
<!-- HTML -->
<canvas id="gameCanvas" width="800" height="600"></canvas>
// JavaScript
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ์ฌ๊ฐํ ๊ทธ๋ฆฌ๊ธฐ (์ค๋ค์ดํฌ ๋ฑ์ ๋ชธํต)
ctx.fillStyle = '#00ff00';
ctx.fillRect(100, 100, 20, 20);
// ์ ๊ทธ๋ฆฌ๊ธฐ (ํ์ ๊ณต)
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(400, 300, 10, 0, Math.PI * 2);
ctx.fill();Canvas์ ์ฃผ์ ๊ทธ๋ฆฌ๊ธฐ ํจ์๋ค:
fillRect(x, y, width, height)- ์ฌ๊ฐํ ์ฑ์ฐ๊ธฐstrokeRect(x, y, width, height)- ์ฌ๊ฐํ ํ ๋๋ฆฌarc(x, y, radius, startAngle, endAngle)- ์/ํธ ๊ทธ๋ฆฌ๊ธฐdrawImage(image, x, y)- ์ด๋ฏธ์ง/์คํ๋ผ์ดํธ ๊ทธ๋ฆฌ๊ธฐclearRect(x, y, width, height)- ์์ญ ์ง์ฐ๊ธฐ
3. ์ถฉ๋ ๊ฐ์ง (Collision Detection)
๊ฒ์์์ ๊ฐ์ฅ ์ค์ํ ๋ก์ง ์ค ํ๋๋ ์ถฉ๋ ๊ฐ์ง์ ๋๋ค. ์ค๋ค์ดํฌ๊ฐ ๋ฒฝ์ ๋ถ๋ชํ๋์ง, ํ์ ๊ณต์ด ํจ๋ค์ ๋ง์๋์ง, ํ ํธ๋ฆฌ์ค ๋ธ๋ก์ด ๋ฐ๋ฅ์ ๋ฟ์๋์ง ํ๋จํด์ผ ํฉ๋๋ค.
AABB (Axis-Aligned Bounding Box) ์ถฉ๋:
๊ฐ์ฅ ๊ฐ๋จํ๊ณ ๋ง์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. ๋ ์ฌ๊ฐํ์ด ๊ฒน์น๋์ง ํ์ธํฉ๋๋ค.
function checkCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
// ์ฌ์ฉ ์์ - ์ค๋ค์ดํฌ๊ฐ ๋จน์ด๋ฅผ ๋จน์๋์ง ํ์ธ
const snake = { x: 100, y: 100, width: 20, height: 20 };
const food = { x: 200, y: 200, width: 20, height: 20 };
if (checkCollision(snake, food)) {
console.log('๋จน์ด๋ฅผ ๋จน์์ต๋๋ค!');
score += 10;
growSnake();
}์ํ ์ถฉ๋ ๊ฐ์ง:
ํ์ด๋ ๋ธ๋ ์ดํฌ์์ ๊ฐ์ ๊ฒ์์์๋ ์ํ ์ถฉ๋์ด ํ์ํฉ๋๋ค.
function checkCircleCollision(circle1, circle2) {
const dx = circle1.x - circle2.x;
const dy = circle1.y - circle2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < circle1.radius + circle2.radius;
}4. ์คํ๋ผ์ดํธ์ ์ ๋๋ฉ์ด์
๋ ํธ๋ก ๊ฒ์์ ๋งค๋ ฅ์ ํฝ์ ์ํธ์ ๋๋ค. ์คํ๋ผ์ดํธ(sprite)๋ ๊ฒ์์์ ์ฌ์ฉํ๋ 2D ์ด๋ฏธ์ง๋ก, ์บ๋ฆญํฐ, ์ , ์์ดํ ๋ฑ์ ํํํฉ๋๋ค.
์คํ๋ผ์ดํธ ์ํธ (Sprite Sheet):
๋ชจ๋ ์ ๋๋ฉ์ด์ ํ๋ ์์ ํ๋์ ์ด๋ฏธ์ง์ ๋ด์๋๊ณ , ํ์ํ ๋ถ๋ถ๋ง ์๋ผ์ ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ๊ธฐ๋ฒ์ ๋๋ค. ์ด๋ ์ฌ๋ฌ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ํจ์จ์ ์ ๋๋ค.
// ์คํ๋ผ์ดํธ ์ํธ์์ ํน์ ํ๋ ์ ๊ทธ๋ฆฌ๊ธฐ
const spriteSheet = new Image();
spriteSheet.src = 'character.png';
function drawFrame(frameX, frameY, canvasX, canvasY) {
ctx.drawImage(
spriteSheet,
frameX * 32, frameY * 32, // ์์ค ์์น
32, 32, // ์์ค ํฌ๊ธฐ
canvasX, canvasY, // ์บ๋ฒ์ค ์์น
32, 32 // ์บ๋ฒ์ค ํฌ๊ธฐ
);
}
// ๊ฑท๊ธฐ ์ ๋๋ฉ์ด์
(ํ๋ ์ ์ํ)
let currentFrame = 0;
setInterval(() => {
currentFrame = (currentFrame + 1) % 4; // 4๊ฐ ํ๋ ์ ์ํ
drawFrame(currentFrame, 0, playerX, playerY);
}, 100); // 100ms๋ง๋ค ํ๋ ์ ๋ณ๊ฒฝ5. ์ฌ์ด๋ ๋์์ธ - 8๋นํธ ์์ ๊ณผ ํจ๊ณผ์
๋ ํธ๋ก ๊ฒ์์ ๋ ๋ค๋ฅธ ์ค์ํ ์์๋ 8๋นํธ ์ฌ์ด๋์ ๋๋ค. Web Audio API๋ฅผ ์ฌ์ฉํ๋ฉด ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์ฌ์ด๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
// Web Audio API๋ก 8๋นํธ ๋นํ์ ๋ง๋ค๊ธฐ
const audioCtx = new AudioContext();
function playBeep(frequency = 440, duration = 200) {
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.frequency.value = frequency;
oscillator.type = 'square'; // 8๋นํธ ์ฌ๊ฐํ
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01, audioCtx.currentTime + duration / 1000
);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + duration / 1000);
}
// ์ ์ ํ๋ ์ฌ์ด๋
playBeep(523.25, 100); // C5 ์๊ณ
// ๊ฒ์ ์ค๋ฒ ์ฌ์ด๋ (ํ๊ฐ ์๊ณ)
setTimeout(() => playBeep(392, 100), 0); // G4
setTimeout(() => playBeep(349.23, 100), 100); // F4
setTimeout(() => playBeep(293.66, 200), 200); // D46. ๊ฒ์ ์ํ ๊ด๋ฆฌ
๊ฒ์์ ์ฌ๋ฌ ์ํ๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค: ๋ฉ๋ด, ํ๋ ์ด ์ค, ์ผ์์ ์ง, ๊ฒ์ ์ค๋ฒ ๋ฑ. ์ํ ๊ด๋ฆฌ๋ฅผ ๊น๋ํ๊ฒ ํ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ์ฒด๊ณ์ ์ด ๋ฉ๋๋ค.
const GameState = {
MENU: 'menu',
PLAYING: 'playing',
PAUSED: 'paused',
GAME_OVER: 'game_over'
};
let currentState = GameState.MENU;
function gameLoop() {
switch(currentState) {
case GameState.MENU:
renderMenu();
break;
case GameState.PLAYING:
processInput();
updateGame();
renderGame();
break;
case GameState.PAUSED:
renderPauseScreen();
break;
case GameState.GAME_OVER:
renderGameOver();
break;
}
requestAnimationFrame(gameLoop);
}
// ํค ์
๋ ฅ์ผ๋ก ์ํ ์ ํ
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && currentState === GameState.MENU) {
currentState = GameState.PLAYING;
}
if (e.key === 'Escape' && currentState === GameState.PLAYING) {
currentState = GameState.PAUSED;
}
});7. ์ค์ ์์ : ๊ฐ๋จํ ์ค๋ค์ดํฌ ๊ฒ์ ๋ง๋ค๊ธฐ
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ ๊ฐ๋จํ ์ค๋ค์ดํฌ ๊ฒ์์ ๋ง๋ค์ด๋ด ์๋ค.
// ๊ฒ์ ์ค์
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gridSize = 20;
const tileCount = 20;
// ๋ฑ ์ด๊ธฐํ
let snake = [{ x: 10, y: 10 }];
let dx = 0, dy = 0;
// ๋จน์ด ์ด๊ธฐํ
let food = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
};
// ์
๋ ฅ ์ฒ๋ฆฌ
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' && dy === 0) { dx = 0; dy = -1; }
if (e.key === 'ArrowDown' && dy === 0) { dx = 0; dy = 1; }
if (e.key === 'ArrowLeft' && dx === 0) { dx = -1; dy = 0; }
if (e.key === 'ArrowRight' && dx === 0) { dx = 1; dy = 0; }
});
// ๊ฒ์ ์
๋ฐ์ดํธ
function update() {
// ๋จธ๋ฆฌ ์ด๋
const head = { x: snake[0].x + dx, y: snake[0].y + dy };
// ๋ฒฝ ์ถฉ๋ ์ฒดํฌ
if (head.x < 0 || head.x >= tileCount ||
head.y < 0 || head.y >= tileCount) {
alert('๊ฒ์ ์ค๋ฒ!');
snake = [{ x: 10, y: 10 }];
dx = 0; dy = 0;
return;
}
// ์๊ธฐ ๋ชธ ์ถฉ๋ ์ฒดํฌ
if (snake.some(s => s.x === head.x && s.y === head.y)) {
alert('๊ฒ์ ์ค๋ฒ!');
snake = [{ x: 10, y: 10 }];
dx = 0; dy = 0;
return;
}
snake.unshift(head); // ๋จธ๋ฆฌ ์ถ๊ฐ
// ๋จน์ด ๋จน๊ธฐ ์ฒดํฌ
if (head.x === food.x && head.y === food.y) {
food = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
};
} else {
snake.pop(); // ๊ผฌ๋ฆฌ ์ ๊ฑฐ (์ฑ์ฅํ์ง ์์)
}
}
// ๋ ๋๋ง
function render() {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// ๋ฑ ๊ทธ๋ฆฌ๊ธฐ
ctx.fillStyle = '#0f0';
snake.forEach(s => {
ctx.fillRect(s.x * gridSize, s.y * gridSize, gridSize - 2, gridSize - 2);
});
// ๋จน์ด ๊ทธ๋ฆฌ๊ธฐ
ctx.fillStyle = '#f00';
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 2, gridSize - 2);
}
// ๊ฒ์ ๋ฃจํ
function gameLoop() {
update();
render();
setTimeout(gameLoop, 100); // 100ms๋ง๋ค ์
๋ฐ์ดํธ
}
gameLoop();์ด ์ฝ๋๋ ๊ธฐ๋ณธ์ ์ธ ์ค๋ค์ดํฌ ๊ฒ์์ ํต์ฌ ๋ก์ง์ ๋ด๊ณ ์์ต๋๋ค. ์ ์ ์์คํ , ๋ ๋ฒจ ์ฆ๊ฐ, ์๋ ๋ณํ, ์ฌ์ด๋ ํจ๊ณผ ๋ฑ์ ์ถ๊ฐํ๋ฉด ์์ ํ ๊ฒ์์ด ๋ฉ๋๋ค.
8. ์ฑ๋ฅ ์ต์ ํ ํ
- ๋๋ธ ๋ฒํผ๋ง: ํ๋ฉด ๊น๋นก์์ ๋ฐฉ์งํ๊ธฐ ์ํด ์คํ์คํฌ๋ฆฐ ์บ๋ฒ์ค์ ๋จผ์ ๊ทธ๋ฆฐ ํ ํ ๋ฒ์ ๋ฉ์ธ ์บ๋ฒ์ค๋ก ๋ณต์ฌํฉ๋๋ค.
- ๊ฐ์ฒด ํ๋ง: ๊ฒ์ ์ค๋ธ์ ํธ๋ฅผ ๋งค๋ฒ ์์ฑ/์ญ์ ํ์ง ๋ง๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํ(pool)์ ๋ง๋ค์ด ์ฑ๋ฅ์ ํฅ์์ํต๋๋ค.
- ํ์ํ ๋ถ๋ถ๋ง ๋ค์ ๊ทธ๋ฆฌ๊ธฐ: ์ ์ฒด ํ๋ฉด์ ๋งค๋ฒ ์ง์ฐ๊ณ ๋ค์ ๊ทธ๋ฆฌ๋ ๋์ , ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ์ ๋ฐ์ดํธํฉ๋๋ค.
- requestAnimationFrame ์ฌ์ฉ: setTimeout/setInterval ๋์ requestAnimationFrame์ ์ฌ์ฉํ๋ฉด ๋ธ๋ผ์ฐ์ ๊ฐ ์ต์ ์ ํ์ด๋ฐ์ ๋ ๋๋งํฉ๋๋ค.
9. ์ถ์ฒ ํ์ต ์๋ฃ ๋ฐ ๋๊ตฌ
๋ฌด๋ฃ ํฝ์ ์ํธ ํด:
- Piskel - ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ํฝ์ ์ํธ ์๋ํฐ
- Aseprite - ์ ๋ฌธ๊ฐ์ฉ ํฝ์ ์ํธ & ์ ๋๋ฉ์ด์ ํด
- GIMP - ๋ฌด๋ฃ ์ด๋ฏธ์ง ํธ์ง๊ธฐ
8๋นํธ ์ฌ์ด๋ ์ ์:
- BeepBox - ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ 8๋นํธ ์์ ์๊ณก ํด
- BFXR - ๋ ํธ๋ก ๊ฒ์ ํจ๊ณผ์ ์์ฑ๊ธฐ
- ChipTone - 8๋นํธ ์ฌ์ด๋ ๋์์ธ ํด
ํ์ต ๋ฆฌ์์ค:
- MDN Web Docs - HTML5 Canvas ๋ฐ Web Audio API ๊ณต์ ๋ฌธ์
- Game Programming Patterns (์ฑ ) - ๊ฒ์ ๋์์ธ ํจํด
- Coding Math (์ ํ๋ธ) - ๊ฒ์ ์ํ ๋ฐ ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์
10. ๋ค์ ๋จ๊ณ: ๋ ๋ณต์กํ ๊ฒ์ ๋์ ํ๊ธฐ
์ค๋ค์ดํฌ๋ฅผ ๋ง์คํฐํ๋ค๋ฉด ๋ค์ ๋จ๊ณ๋ก ๋์ ํด๋ณด์ธ์:
- ํ ํธ๋ฆฌ์ค: ํ์ ๋ก์ง, SRS(Super Rotation System), ๋ผ์ธ ํด๋ฆฌ์ด ์ ๋๋ฉ์ด์ , ๋ ๋ฒจ ์์คํ ์ ๋ฐฐ์๋๋ค.
- ๋ธ๋ ์ดํฌ์์: ๋ฌผ๋ฆฌ ๊ธฐ๋ฐ ๊ณต ๋ฐ์ฌ, ํ์์ ์์คํ , ๋ ๋ฒจ ๋์์ธ์ ์ตํ๋๋ค.
- ์คํ์ด์ค ์ธ๋ฒ ์ด๋: ์ AI ํจํด, ๋ฐ์ฌ์ฒด ๊ด๋ฆฌ, ๋ฐฉ์ด๋ง ์์คํ ์ ๊ตฌํํฉ๋๋ค.
- ํฉ๋งจ: ๊ทธ๋ฆฌ๋ ๊ธฐ๋ฐ ์ด๋, ๊ฒฝ๋ก ํ์ AI, ์ ๋ น๋ค์ ๊ฐ๊ธฐ ๋ค๋ฅธ ํ๋ ํจํด์ ๋ง๋ญ๋๋ค.
๊ฒฐ๋ก
๋ ํธ๋ก ๊ฒ์ ๊ฐ๋ฐ์ ๋จ์ํด ๋ณด์ด์ง๋ง ๊ฒ์ ํ๋ก๊ทธ๋๋ฐ์ ๋ชจ๋ ํต์ฌ ๊ฐ๋ ์ ๋ด๊ณ ์์ต๋๋ค. ๊ฒ์ ๋ฃจํ, ์ํ ๊ด๋ฆฌ, ์ถฉ๋ ๊ฐ์ง, ์ ๋๋ฉ์ด์ , ์ฌ์ด๋ ๋์์ธ ๋ฑ ํ๋ ๊ฒ์ ๊ฐ๋ฐ์์๋ ๊ทธ๋๋ก ์ฌ์ฉ๋๋ ์๋ฆฌ๋ค์ ๋๋ค.
๊ฐ์ฅ ์ค์ํ ๊ฒ์ ์ง์ ๋ง๋ค์ด๋ณด๋ ๊ฒ์ ๋๋ค. ์ด ๊ฐ์ด๋์ ์ฝ๋๋ฅผ ๋ณต์ฌ-๋ถ์ฌ๋ฃ๊ธฐ๋ง ํ์ง ๋ง๊ณ , ์ง์ ํ์ดํํ๋ฉด์ ๊ฐ ์ค์ด ๋ฌด์จ ์ญํ ์ ํ๋์ง ์ดํดํ์ธ์. ๊ทธ๋ฆฌ๊ณ ์์ ๋ง์ ์์ด๋์ด๋ฅผ ์ถ๊ฐํด ๊ฒ์์ ํ์ฅํด๋ณด์ธ์.