목차

1. 개요
2. HTML 파일을 만드는 법
3. 이것을 이용한 작품
3.1. 대항해 시대(손현준)

1. 개요 ✎ ⊖

HTML은 세계 최고의 프로그래밍 언어이다! 개발자 도구로 각종 웹사이트를 들여다보면 그 코드가 HTML로 되어 있는 것을 볼 수 있을 것이다. 프로그래밍을 배우고 싶은 사람은 한번 HTML을 배워보도록 하자.

2. HTML 파일을 만드는 법 ✎ ⊖

Visual Studio Code 같은 전문 프로그래밍 도구도 필요 없을 정도로 쉽다. 'gpt야 만들어줘'를 통해 3분이면 HTML 코드를 얻을 수 있다!

3. 이것을 이용한 작품 ✎ ⊖

3.1. 대항해 시대(손현준) ✎ ⊖

손현준(교사)이 자신의 블로그 '번데기의 최후'에 올려 놓은 항해 게임이다. 코드는 아래와 같다.

전체 코드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌋 화산 피하기 게임 🚗</title>
<style>
/* 기본 스타일 및 레이아웃 */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
flex-direction: column;
}

#game-container {
position: relative;
width: 300px;
height: 500px;
background-color: #556b2f;
border: 5px solid #000;
overflow: hidden;
touch-action: none;
}

/* 플레이어 스타일 */
#player {
position: absolute;
width: 40px;
height: 40px;
font-size: 35px;
text-align: center;
line-height: 40px;
bottom: 10px;
transform: rotate(90deg);
transition: transform 0.1s linear;
z-index: 10;
}

/* 장애물 공통 스타일 */
.obstacle {
position: absolute;
box-sizing: border-box;
}

/* 화산 스타일 */
.volcano-spot {
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s linear;
}
.warning {
background-color: sienna;
opacity: 0.6;
box-shadow: 0 0 8px rgba(160, 82, 45, 0.7);
}
.erupted {
background-color: red;
box-shadow: 0 0 15px 8px #ff4500;
transform: scale(1.1);
opacity: 1;
}

/* 검은 벽 장애물 스타일 */
.black-wall {
background-color: #2c2c2c;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
z-index: 5;
}

/* UI 요소 스타일 */
.ui-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.8);
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
z-index: 20;
}

#game-title {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
color: white;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px #000;
z-index: 15;
}

#info-board {
position: absolute;
top: 5px; right: 5px;
color: white; font-size: 14px;
font-weight: bold;
z-index: 15;
}

#game-over { display: none; }
#start-screen { display: flex; }

.game-button {
padding: 10px 20px;
font-size: 20px;
cursor: pointer;
margin-top: 30px;
border: none;
border-radius: 5px;
background-color: #4CAF50;
color: white;
}
</style>
</head>
<body>

<div id="game-container">
<div id="game-title">🌋 화산 피하기 🚗</div>
<div id="info-board">
거리: <span id="distance-display">0</span>m
</div>

<div id="player">🚗</div>

<div id="start-screen" class="ui-overlay">
<h1>화산 피하기</h1>
<p>화산 폭발과 벽을 피해<br>최대한 멀리 도망가세요!</p>
<button id="start-button" class="game-button">게임 시작</button>
</div>
<div id="game-over" class="ui-overlay">
<h1>💥 게임 오버! 💥</h1>
최종 점수: <span id="final-score">0</span><br>
이동 거리: <span id="final-distance">0</span>m<br>
<button id="restart-button" class="game-button">다시 시작</button>
</div>
</div>

<script>
// DOM 요소
const gameContainer = document.getElementById('game-container');
const player = document.getElementById('player');
const distanceDisplay = document.getElementById('distance-display');
const gameOverScreen = document.getElementById('game-over');
const finalScoreSpan = document.getElementById('final-score');
const finalDistanceSpan = document.getElementById('final-distance');
const restartButton = document.getElementById('restart-button');
const startScreen = document.getElementById('start-screen');
const startButton = document.getElementById('start-button');

// 게임 설정
const CONTAINER_WIDTH = gameContainer.clientWidth;
const CONTAINER_HEIGHT = gameContainer.clientHeight;
const PLAYER_SIZE = 40;
const PLAYER_SPEED = 4;
const BASE_SCROLL_SPEED = 3.0;

// 게임 상태 변수
let isGameRunning = false;
let score = 0, distance = 0;
let playerX, playerY, playerAngle, playerScaleX;
let keysPressed = {};
let lastVolcanoTime = 0, lastWallTime = 0;
let animationFrameId;

function initGame() {
isGameRunning = true;
score = 0; distance = 0;
playerX = CONTAINER_WIDTH / 2 - PLAYER_SIZE / 2;
playerY = CONTAINER_HEIGHT - PLAYER_SIZE - 10;
playerAngle = 90; playerScaleX = 1;
distanceDisplay.textContent = '0';
startScreen.style.display = 'none';
gameOverScreen.style.display = 'none';

document.querySelectorAll('.obstacle').forEach(el => el.remove());

player.style.left = `${playerX}px`;
player.style.top = `${playerY}px`;
player.style.transform = `rotate(90deg) scaleX(1)`;
player.style.visibility = 'visible';

lastVolcanoTime = performance.now();
lastWallTime = performance.now();

if(animationFrameId) cancelAnimationFrame(animationFrameId);
gameLoop();
}

function updatePlayer() {
let dx = 0, dy = 0;
if (keysPressed['ArrowLeft'] || keysPressed['a']) dx -= PLAYER_SPEED;
if (keysPressed['ArrowRight'] || keysPressed['d']) dx += PLAYER_SPEED;
if (keysPressed['ArrowUp'] || keysPressed['w']) dy -= PLAYER_SPEED;
if (keysPressed['ArrowDown'] || keysPressed['s']) dy += PLAYER_SPEED;

let nextX = playerX + dx;
let nextY = playerY + dy;

document.querySelectorAll('.black-wall').forEach(wall => {
const wallRect = wall.getBoundingClientRect();
const containerRect = gameContainer.getBoundingClientRect();
const wallGameRect = {
left: wallRect.left - containerRect.left, top: wallRect.top - containerRect.top,
right: wallRect.right - containerRect.left, bottom: wallRect.bottom - containerRect.top
};

const hPlayerRect = { left: nextX, top: playerY, right: nextX + PLAYER_SIZE, bottom: playerY + PLAYER_SIZE };
if (hPlayerRect.right > wallGameRect.left && hPlayerRect.left < wallGameRect.right &&
hPlayerRect.bottom > wallGameRect.top && hPlayerRect.top < wallGameRect.bottom) {
nextX = playerX;
}

const vPlayerRect = { left: playerX, top: nextY, right: playerX + PLAYER_SIZE, bottom: nextY + PLAYER_SIZE };
if (vPlayerRect.right > wallGameRect.left && vPlayerRect.left < wallGameRect.right &&
vPlayerRect.bottom > wallGameRect.top && vPlayerRect.top < wallGameRect.bottom) {
nextY = playerY;
}
});

playerX = Math.max(0, Math.min(CONTAINER_WIDTH - PLAYER_SIZE, nextX));
playerY = Math.max(0, Math.min(CONTAINER_HEIGHT - PLAYER_SIZE, nextY));

if (dx !== 0 || dy !== 0) {
if (dy < 0) playerAngle = dx === 0 ? 90 : (dx < 0 ? 45 : 135);
else if (dy > 0) playerAngle = dx === 0 ? -90 : (dx < 0 ? -45 : -135);
else playerAngle = 0;
if (dx < 0) playerScaleX = 1; else if (dx > 0) playerScaleX = -1;
}

player.style.left = `${playerX}px`;
player.style.top = `${playerY}px`;
player.style.transform = `rotate(${playerAngle}deg) scaleX(${playerScaleX})`;
}

function isOverlapping(newRect) {
let overlapping = false;
document.querySelectorAll('.obstacle').forEach(obstacle => {
const existingRect = obstacle.getBoundingClientRect();
const containerRect = gameContainer.getBoundingClientRect();
const existingGameRect = {
left: existingRect.left - containerRect.left, top: existingRect.top - containerRect.top,
right: existingRect.right - containerRect.left, bottom: existingRect.bottom - containerRect.top
};
const padding = 10;
if (newRect.right + padding > existingGameRect.left && newRect.left - padding < existingGameRect.right &&
newRect.bottom + padding > existingGameRect.top && newRect.top - padding < existingGameRect.bottom) {
overlapping = true;
}
});
return overlapping;
}

function createVolcano(timestamp, difficultyLevel) {
const interval = Math.max(500, 1600 - difficultyLevel * 150);
if (timestamp - lastVolcanoTime < interval) return;
lastVolcanoTime = timestamp;

let newRect, w, h, x, y;
for (let i = 0; i < 10; i++) {
const minSize = Math.min(50, 30 + difficultyLevel * 4);
const maxSize = Math.min(90, 60 + difficultyLevel * 6);
const size = Math.random() * (maxSize - minSize) + minSize;

w = size; h = size;
if (Math.random() < 0.5) h = Math.random() * (maxSize - minSize) + minSize;
x = Math.random() * (CONTAINER_WIDTH - w);
y = -h;
newRect = { left: x, top: y, right: x + w, bottom: y + h };
if (!isOverlapping(newRect)) break;
if (i === 9) return;
}

const volcano = document.createElement('div');
volcano.className = 'obstacle volcano-spot';
volcano.style.width = `${w}px`; volcano.style.height = `${h}px`;
volcano.style.left = `${x}px`; volcano.style.top = `${y}px`;
gameContainer.appendChild(volcano);

setTimeout(() => volcano.classList.add('warning'), 600);
setTimeout(() => {
volcano.classList.remove('warning');
volcano.classList.add('erupted');
}, 800);
setTimeout(() => volcano.remove(), 3000);
}

function createBlackWall(timestamp, difficultyLevel) {
const interval = Math.max(1200, 2800 - difficultyLevel * 200);
if (timestamp - lastWallTime < interval) return;
lastWallTime = timestamp;

let newRect, width, angle, x, y;
for (let i = 0; i < 10; i++) {
const minWidth = Math.min(PLAYER_SIZE * 1.5, PLAYER_SIZE + difficultyLevel * 5);
const maxWidth = Math.min(PLAYER_SIZE * 2.5, PLAYER_SIZE * 2 + difficultyLevel * 8);
const WALL_HEIGHT = 20;
width = Math.random() * (maxWidth - minWidth) + minWidth;

angle = Math.random() * 180 - 90;
x = Math.random() * (CONTAINER_WIDTH - width);
y = -WALL_HEIGHT;
newRect = { left: x, top: y, right: x + width, bottom: y + WALL_HEIGHT };
if (!isOverlapping(newRect)) break;
if (i === 9) return;
}

const wall = document.createElement('div');
wall.className = 'obstacle black-wall';
wall.style.width = `${width}px`; wall.style.height = `20px`;
wall.style.left = `${x}px`; wall.style.top = `${y}px`;
wall.style.transform = `rotate(${angle}deg)`;
gameContainer.appendChild(wall);
setTimeout(() => wall.remove(), 5000);
}

function updateObstacles(currentScrollSpeed) {
document.querySelectorAll('.obstacle').forEach(obstacle => {
let top = parseFloat(obstacle.style.top) || 0;
obstacle.style.top = `${top + currentScrollSpeed}px`;

if (obstacle.classList.contains('erupted')) {
checkVolcanoCollision(obstacle);
}

if (obstacle.classList.contains('black-wall')) {
const playerRect = { left: playerX, top: playerY, right: playerX + PLAYER_SIZE, bottom: playerY + PLAYER_SIZE };
const wallRect = obstacle.getBoundingClientRect();
const containerRect = gameContainer.getBoundingClientRect();
const wallGameRect = {
left: wallRect.left - containerRect.left, top: wallRect.top - containerRect.top,
right: wallRect.right - containerRect.left, bottom: wallRect.bottom - containerRect.top
};

if (playerRect.right > wallGameRect.left && playerRect.left < wallGameRect.right &&
playerRect.bottom > wallGameRect.top && playerRect.top < wallGameRect.bottom) {

// ⭐ [수정됨] 벽이 플레이어를 밀어낼 때, 즉시 화면에 반영
playerY = wallGameRect.bottom;
player.style.top = `${playerY}px`;
}
}
});
}

function checkVolcanoCollision(volcano) {
if (!isGameRunning) return;
const playerRect = player.getBoundingClientRect();
const volcanoRect = volcano.getBoundingClientRect();
if (playerRect.right > volcanoRect.left && playerRect.left < volcanoRect.right &&
playerRect.bottom > volcanoRect.top && playerRect.top < volcanoRect.bottom) {
endGame();
}
}

function endGame() {
if (!isGameRunning) return;
isGameRunning = false;
cancelAnimationFrame(animationFrameId);
player.style.visibility = 'hidden';
finalScoreSpan.textContent = Math.floor(score);
finalDistanceSpan.textContent = Math.floor(distance);
gameOverScreen.style.display = 'flex';
}

function gameLoop(timestamp) {
if (!isGameRunning) return;

const difficultyLevel = Math.floor(distance / 200);
const currentScrollSpeed = BASE_SCROLL_SPEED + difficultyLevel * 0.25;

updatePlayer();
createVolcano(timestamp, difficultyLevel);
createBlackWall(timestamp, difficultyLevel);
updateObstacles(currentScrollSpeed);

score += currentScrollSpeed * 0.5;
distance += currentScrollSpeed * 0.1;
distanceDisplay.textContent = `${Math.floor(distance)}`;

if (playerY >= CONTAINER_HEIGHT - PLAYER_SIZE) {
endGame();
}

animationFrameId = requestAnimationFrame(gameLoop);
}

// 이벤트 리스너
document.addEventListener('keydown', (e) => { keysPressed[e.key] = true; });
document.addEventListener('keyup', (e) => { keysPressed[e.key] = false; });
startButton.addEventListener('click', initGame);
restartButton.addEventListener('click', initGame);
</script>
</body>
</html>
최근 수정 시각 : 2026-07-13 20:20:09  | 조회수 : 120