| r1 ➤ r2 |
| 4 | 웹사이트를 만드는데 유용한 언어이다. '<어쩌구>' 같은 태그를 사용한다. |
| 5 | |
| 6 | ==HTML 파일을 만드는 법== |
| 7 | Visual Studio Code 같은 전문 프로그래밍 도구도 필요 없을 정도로 쉽다. '''메모장'''을 열어서 코드를 적은 다음에 파일 형식을 .html로 해 놓으면 끝이다. |
| 8 | |
| 9 | ==이것을 이용한 작품== |
| 10 | ===대항해 시대([[손현준(교사)|손현준]])=== |
| 11 | [[손현준(교사)]]이 자신의 블로그 '번데기의 최후'에 올려 놓은 항해 게임이다. 코드는 아래와 같다. |
| 12 | |
| 13 | {{{#!folding 전체 코드 |
| 14 | <!DOCTYPE html> |
| 15 | <html lang="ko"> |
| 16 | <head> |
| 17 | <meta charset="UTF-8"> |
| 18 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 19 | <title>🌋 화산 피하기 게임 🚗</title> |
| 20 | <style> |
| 21 | /* 기본 스타일 및 레이아웃 */ |
| 22 | body { |
| 23 | display: flex; |
| 24 | justify-content: center; |
| 25 | align-items: center; |
| 26 | height: 100vh; |
| 27 | margin: 0; |
| 28 | background-color: #f0f0f0; |
| 29 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| 30 | flex-direction: column; |
| 31 | } |
| 32 | |
| 33 | #game-container { |
| 34 | position: relative; |
| 35 | width: 300px; |
| 36 | height: 500px; |
| 37 | background-color: #556b2f; |
| 38 | border: 5px solid #000; |
| 39 | overflow: hidden; |
| 40 | touch-action: none; |
| 41 | } |
| 42 | |
| 43 | /* 플레이어 스타일 */ |
| 44 | #player { |
| 45 | position: absolute; |
| 46 | width: 40px; |
| 47 | height: 40px; |
| 48 | font-size: 35px; |
| 49 | text-align: center; |
| 50 | line-height: 40px; |
| 51 | bottom: 10px; |
| 52 | transform: rotate(90deg); |
| 53 | transition: transform 0.1s linear; |
| 54 | z-index: 10; |
| 55 | } |
| 56 | |
| 57 | /* 장애물 공통 스타일 */ |
| 58 | .obstacle { |
| 59 | position: absolute; |
| 60 | box-sizing: border-box; |
| 61 | } |
| 62 | |
| 63 | /* 화산 스타일 */ |
| 64 | .volcano-spot { |
| 65 | border-radius: 50%; |
| 66 | opacity: 0; |
| 67 | transition: opacity 0.2s linear; |
| 68 | } |
| 69 | .warning { |
| 70 | background-color: sienna; |
| 71 | opacity: 0.6; |
| 72 | box-shadow: 0 0 8px rgba(160, 82, 45, 0.7); |
| 73 | } |
| 74 | .erupted { |
| 75 | background-color: red; |
| 76 | box-shadow: 0 0 15px 8px #ff4500; |
| 77 | transform: scale(1.1); |
| 78 | opacity: 1; |
| 79 | } |
| 80 | |
| 81 | /* 검은 벽 장애물 스타일 */ |
| 82 | .black-wall { |
| 83 | background-color: #2c2c2c; |
| 84 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.7); |
| 85 | z-index: 5; |
| 86 | } |
| 87 | |
| 88 | /* UI 요소 스타일 */ |
| 89 | .ui-overlay { |
| 90 | position: absolute; |
| 91 | top: 0; left: 0; |
| 92 | width: 100%; height: 100%; |
| 93 | background-color: rgba(0, 0, 0, 0.8); |
| 94 | color: white; |
| 95 | display: flex; |
| 96 | flex-direction: column; |
| 97 | justify-content: center; |
| 98 | align-items: center; |
| 99 | text-align: center; |
| 100 | z-index: 20; |
| 101 | } |
| 102 | |
| 103 | #game-title { |
| 104 | position: absolute; |
| 105 | top: 10px; |
| 106 | width: 100%; |
| 107 | text-align: center; |
| 108 | color: white; |
| 109 | font-size: 24px; |
| 110 | font-weight: bold; |
| 111 | text-shadow: 2px 2px 4px #000; |
| 112 | z-index: 15; |
| 113 | } |
| 114 | |
| 115 | #info-board { |
| 116 | position: absolute; |
| 117 | top: 5px; right: 5px; |
| 118 | color: white; font-size: 14px; |
| 119 | font-weight: bold; |
| 120 | z-index: 15; |
| 121 | } |
| 122 | |
| 123 | #game-over { display: none; } |
| 124 | #start-screen { display: flex; } |
| 125 | |
| 126 | .game-button { |
| 127 | padding: 10px 20px; |
| 128 | font-size: 20px; |
| 129 | cursor: pointer; |
| 130 | margin-top: 30px; |
| 131 | border: none; |
| 132 | border-radius: 5px; |
| 133 | background-color: #4CAF50; |
| 134 | color: white; |
| 135 | } |
| 136 | </style> |
| 137 | </head> |
| 138 | <body> |
| 139 | |
| 140 | <div id="game-container"> |
| 141 | <div id="game-title">🌋 화산 피하기 🚗</div> |
| 142 | <div id="info-board"> |
| 143 | 거리: <span id="distance-display">0</span>m |
| 144 | </div> |
| 145 | |
| 146 | <div id="player">🚗</div> |
| 147 | |
| 148 | <div id="start-screen" class="ui-overlay"> |
| 149 | <h1>화산 피하기</h1> |
| 150 | <p>화산 폭발과 벽을 피해<br>최대한 멀리 도망가세요!</p> |
| 151 | <button id="start-button" class="game-button">게임 시작</button> |
| 152 | </div> |
| 153 | <div id="game-over" class="ui-overlay"> |
| 154 | <h1>💥 게임 오버! 💥</h1> |
| 155 | 최종 점수: <span id="final-score">0</span><br> |
| 156 | 이동 거리: <span id="final-distance">0</span>m<br> |
| 157 | <button id="restart-button" class="game-button">다시 시작</button> |
| 158 | </div> |
| 159 | </div> |
| 160 | |
| 161 | <script> |
| 162 | // DOM 요소 |
| 163 | const gameContainer = document.getElementById('game-container'); |
| 164 | const player = document.getElementById('player'); |
| 165 | const distanceDisplay = document.getElementById('distance-display'); |
| 166 | const gameOverScreen = document.getElementById('game-over'); |
| 167 | const finalScoreSpan = document.getElementById('final-score'); |
| 168 | const finalDistanceSpan = document.getElementById('final-distance'); |
| 169 | const restartButton = document.getElementById('restart-button'); |
| 170 | const startScreen = document.getElementById('start-screen'); |
| 171 | const startButton = document.getElementById('start-button'); |
| 172 | |
| 173 | // 게임 설정 |
| 174 | const CONTAINER_WIDTH = gameContainer.clientWidth; |
| 175 | const CONTAINER_HEIGHT = gameContainer.clientHeight; |
| 176 | const PLAYER_SIZE = 40; |
| 177 | const PLAYER_SPEED = 4; |
| 178 | const BASE_SCROLL_SPEED = 3.0; |
| 179 | |
| 180 | // 게임 상태 변수 |
| 181 | let isGameRunning = false; |
| 182 | let score = 0, distance = 0; |
| 183 | let playerX, playerY, playerAngle, playerScaleX; |
| 184 | let keysPressed = {}; |
| 185 | let lastVolcanoTime = 0, lastWallTime = 0; |
| 186 | let animationFrameId; |
| 187 | |
| 188 | function initGame() { |
| 189 | isGameRunning = true; |
| 190 | score = 0; distance = 0; |
| 191 | playerX = CONTAINER_WIDTH / 2 - PLAYER_SIZE / 2; |
| 192 | playerY = CONTAINER_HEIGHT - PLAYER_SIZE - 10; |
| 193 | playerAngle = 90; playerScaleX = 1; |
| 194 | distanceDisplay.textContent = '0'; |
| 195 | startScreen.style.display = 'none'; |
| 196 | gameOverScreen.style.display = 'none'; |
| 197 | |
| 198 | document.querySelectorAll('.obstacle').forEach(el => el.remove()); |
| 199 | |
| 200 | player.style.left = `${playerX}px`; |
| 201 | player.style.top = `${playerY}px`; |
| 202 | player.style.transform = `rotate(90deg) scaleX(1)`; |
| 203 | player.style.visibility = 'visible'; |
| 204 | |
| 205 | lastVolcanoTime = performance.now(); |
| 206 | lastWallTime = performance.now(); |
| 207 | |
| 208 | if(animationFrameId) cancelAnimationFrame(animationFrameId); |
| 209 | gameLoop(); |
| 210 | } |
| 211 | |
| 212 | function updatePlayer() { |
| 213 | let dx = 0, dy = 0; |
| 214 | if (keysPressed['ArrowLeft'] || keysPressed['a']) dx -= PLAYER_SPEED; |
| 215 | if (keysPressed['ArrowRight'] || keysPressed['d']) dx += PLAYER_SPEED; |
| 216 | if (keysPressed['ArrowUp'] || keysPressed['w']) dy -= PLAYER_SPEED; |
| 217 | if (keysPressed['ArrowDown'] || keysPressed['s']) dy += PLAYER_SPEED; |
| 218 | |
| 219 | let nextX = playerX + dx; |
| 220 | let nextY = playerY + dy; |
| 221 | |
| 222 | document.querySelectorAll('.black-wall').forEach(wall => { |
| 223 | const wallRect = wall.getBoundingClientRect(); |
| 224 | const containerRect = gameContainer.getBoundingClientRect(); |
| 225 | const wallGameRect = { |
| 226 | left: wallRect.left - containerRect.left, top: wallRect.top - containerRect.top, |
| 227 | right: wallRect.right - containerRect.left, bottom: wallRect.bottom - containerRect.top |
| 228 | }; |
| 229 | |
| 230 | const hPlayerRect = { left: nextX, top: playerY, right: nextX + PLAYER_SIZE, bottom: playerY + PLAYER_SIZE }; |
| 231 | if (hPlayerRect.right > wallGameRect.left && hPlayerRect.left < wallGameRect.right && |
| 232 | hPlayerRect.bottom > wallGameRect.top && hPlayerRect.top < wallGameRect.bottom) { |
| 233 | nextX = playerX; |
| 234 | } |
| 235 | |
| 236 | const vPlayerRect = { left: playerX, top: nextY, right: playerX + PLAYER_SIZE, bottom: nextY + PLAYER_SIZE }; |
| 237 | if (vPlayerRect.right > wallGameRect.left && vPlayerRect.left < wallGameRect.right && |
| 238 | vPlayerRect.bottom > wallGameRect.top && vPlayerRect.top < wallGameRect.bottom) { |
| 239 | nextY = playerY; |
| 240 | } |
| 241 | }); |
| 242 | |
| 243 | playerX = Math.max(0, Math.min(CONTAINER_WIDTH - PLAYER_SIZE, nextX)); |
| 244 | playerY = Math.max(0, Math.min(CONTAINER_HEIGHT - PLAYER_SIZE, nextY)); |
| 245 | |
| 246 | if (dx !== 0 || dy !== 0) { |
| 247 | if (dy < 0) playerAngle = dx === 0 ? 90 : (dx < 0 ? 45 : 135); |
| 248 | else if (dy > 0) playerAngle = dx === 0 ? -90 : (dx < 0 ? -45 : -135); |
| 249 | else playerAngle = 0; |
| 250 | if (dx < 0) playerScaleX = 1; else if (dx > 0) playerScaleX = -1; |
| 251 | } |
| 252 | |
| 253 | player.style.left = `${playerX}px`; |
| 254 | player.style.top = `${playerY}px`; |
| 255 | player.style.transform = `rotate(${playerAngle}deg) scaleX(${playerScaleX})`; |
| 256 | } |
| 257 | |
| 258 | function isOverlapping(newRect) { |
| 259 | let overlapping = false; |
| 260 | document.querySelectorAll('.obstacle').forEach(obstacle => { |
| 261 | const existingRect = obstacle.getBoundingClientRect(); |
| 262 | const containerRect = gameContainer.getBoundingClientRect(); |
| 263 | const existingGameRect = { |
| 264 | left: existingRect.left - containerRect.left, top: existingRect.top - containerRect.top, |
| 265 | right: existingRect.right - containerRect.left, bottom: existingRect.bottom - containerRect.top |
| 266 | }; |
| 267 | const padding = 10; |
| 268 | if (newRect.right + padding > existingGameRect.left && newRect.left - padding < existingGameRect.right && |
| 269 | newRect.bottom + padding > existingGameRect.top && newRect.top - padding < existingGameRect.bottom) { |
| 270 | overlapping = true; |
| 271 | } |
| 272 | }); |
| 273 | return overlapping; |
| 274 | } |
| 275 | |
| 276 | function createVolcano(timestamp, difficultyLevel) { |
| 277 | const interval = Math.max(500, 1600 - difficultyLevel * 150); |
| 278 | if (timestamp - lastVolcanoTime < interval) return; |
| 279 | lastVolcanoTime = timestamp; |
| 280 | |
| 281 | let newRect, w, h, x, y; |
| 282 | for (let i = 0; i < 10; i++) { |
| 283 | const minSize = Math.min(50, 30 + difficultyLevel * 4); |
| 284 | const maxSize = Math.min(90, 60 + difficultyLevel * 6); |
| 285 | const size = Math.random() * (maxSize - minSize) + minSize; |
| 286 | |
| 287 | w = size; h = size; |
| 288 | if (Math.random() < 0.5) h = Math.random() * (maxSize - minSize) + minSize; |
| 289 | x = Math.random() * (CONTAINER_WIDTH - w); |
| 290 | y = -h; |
| 291 | newRect = { left: x, top: y, right: x + w, bottom: y + h }; |
| 292 | if (!isOverlapping(newRect)) break; |
| 293 | if (i === 9) return; |
| 294 | } |
| 295 | |
| 296 | const volcano = document.createElement('div'); |
| 297 | volcano.className = 'obstacle volcano-spot'; |
| 298 | volcano.style.width = `${w}px`; volcano.style.height = `${h}px`; |
| 299 | volcano.style.left = `${x}px`; volcano.style.top = `${y}px`; |
| 300 | gameContainer.appendChild(volcano); |
| 301 | |
| 302 | setTimeout(() => volcano.classList.add('warning'), 600); |
| 303 | setTimeout(() => { |
| 304 | volcano.classList.remove('warning'); |
| 305 | volcano.classList.add('erupted'); |
| 306 | }, 800); |
| 307 | setTimeout(() => volcano.remove(), 3000); |
| 308 | } |
| 309 | |
| 310 | function createBlackWall(timestamp, difficultyLevel) { |
| 311 | const interval = Math.max(1200, 2800 - difficultyLevel * 200); |
| 312 | if (timestamp - lastWallTime < interval) return; |
| 313 | lastWallTime = timestamp; |
| 314 | |
| 315 | let newRect, width, angle, x, y; |
| 316 | for (let i = 0; i < 10; i++) { |
| 317 | const minWidth = Math.min(PLAYER_SIZE * 1.5, PLAYER_SIZE + difficultyLevel * 5); |
| 318 | const maxWidth = Math.min(PLAYER_SIZE * 2.5, PLAYER_SIZE * 2 + difficultyLevel * 8); |
| 319 | const WALL_HEIGHT = 20; |
| 320 | width = Math.random() * (maxWidth - minWidth) + minWidth; |
| 321 | |
| 322 | angle = Math.random() * 180 - 90; |
| 323 | x = Math.random() * (CONTAINER_WIDTH - width); |
| 324 | y = -WALL_HEIGHT; |
| 325 | newRect = { left: x, top: y, right: x + width, bottom: y + WALL_HEIGHT }; |
| 326 | if (!isOverlapping(newRect)) break; |
| 327 | if (i === 9) return; |
| 328 | } |
| 329 | |
| 330 | const wall = document.createElement('div'); |
| 331 | wall.className = 'obstacle black-wall'; |
| 332 | wall.style.width = `${width}px`; wall.style.height = `20px`; |
| 333 | wall.style.left = `${x}px`; wall.style.top = `${y}px`; |
| 334 | wall.style.transform = `rotate(${angle}deg)`; |
| 335 | gameContainer.appendChild(wall); |
| 336 | setTimeout(() => wall.remove(), 5000); |
| 337 | } |
| 338 | |
| 339 | function updateObstacles(currentScrollSpeed) { |
| 340 | document.querySelectorAll('.obstacle').forEach(obstacle => { |
| 341 | let top = parseFloat(obstacle.style.top) || 0; |
| 342 | obstacle.style.top = `${top + currentScrollSpeed}px`; |
| 343 | |
| 344 | if (obstacle.classList.contains('erupted')) { |
| 345 | checkVolcanoCollision(obstacle); |
| 346 | } |
| 347 | |
| 348 | if (obstacle.classList.contains('black-wall')) { |
| 349 | const playerRect = { left: playerX, top: playerY, right: playerX + PLAYER_SIZE, bottom: playerY + PLAYER_SIZE }; |
| 350 | const wallRect = obstacle.getBoundingClientRect(); |
| 351 | const containerRect = gameContainer.getBoundingClientRect(); |
| 352 | const wallGameRect = { |
| 353 | left: wallRect.left - containerRect.left, top: wallRect.top - containerRect.top, |
| 354 | right: wallRect.right - containerRect.left, bottom: wallRect.bottom - containerRect.top |
| 355 | }; |
| 356 | |
| 357 | if (playerRect.right > wallGameRect.left && playerRect.left < wallGameRect.right && |
| 358 | playerRect.bottom > wallGameRect.top && playerRect.top < wallGameRect.bottom) { |
| 359 | |
| 360 | // ⭐ [수정됨] 벽이 플레이어를 밀어낼 때, 즉시 화면에 반영 |
| 361 | playerY = wallGameRect.bottom; |
| 362 | player.style.top = `${playerY}px`; |
| 363 | } |
| 364 | } |
| 365 | }); |
| 366 | } |
| 367 | |
| 368 | function checkVolcanoCollision(volcano) { |
| 369 | if (!isGameRunning) return; |
| 370 | const playerRect = player.getBoundingClientRect(); |
| 371 | const volcanoRect = volcano.getBoundingClientRect(); |
| 372 | if (playerRect.right > volcanoRect.left && playerRect.left < volcanoRect.right && |
| 373 | playerRect.bottom > volcanoRect.top && playerRect.top < volcanoRect.bottom) { |
| 374 | endGame(); |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | function endGame() { |
| 379 | if (!isGameRunning) return; |
| 380 | isGameRunning = false; |
| 381 | cancelAnimationFrame(animationFrameId); |
| 382 | player.style.visibility = 'hidden'; |
| 383 | finalScoreSpan.textContent = Math.floor(score); |
| 384 | finalDistanceSpan.textContent = Math.floor(distance); |
| 385 | gameOverScreen.style.display = 'flex'; |
| 386 | } |
| 387 | |
| 388 | function gameLoop(timestamp) { |
| 389 | if (!isGameRunning) return; |
| 390 | |
| 391 | const difficultyLevel = Math.floor(distance / 200); |
| 392 | const currentScrollSpeed = BASE_SCROLL_SPEED + difficultyLevel * 0.25; |
| 393 | |
| 394 | updatePlayer(); |
| 395 | createVolcano(timestamp, difficultyLevel); |
| 396 | createBlackWall(timestamp, difficultyLevel); |
| 397 | updateObstacles(currentScrollSpeed); |
| 398 | |
| 399 | score += currentScrollSpeed * 0.5; |
| 400 | distance += currentScrollSpeed * 0.1; |
| 401 | distanceDisplay.textContent = `${Math.floor(distance)}`; |
| 402 | |
| 403 | if (playerY >= CONTAINER_HEIGHT - PLAYER_SIZE) { |
| 404 | endGame(); |
| 405 | } |
| 406 | |
| 407 | animationFrameId = requestAnimationFrame(gameLoop); |
| 408 | } |
| 409 | |
| 410 | // 이벤트 리스너 |
| 411 | document.addEventListener('keydown', (e) => { keysPressed[e.key] = true; }); |
| 412 | document.addEventListener('keyup', (e) => { keysPressed[e.key] = false; }); |
| 413 | startButton.addEventListener('click', initGame); |
| 414 | restartButton.addEventListener('click', initGame); |
| 415 | </script> |
| 416 | </body> |
| 417 | </html> |
| 418 | }}} |