寫一個html+js+css的「紙飛機」遊戲,要求是控制一個svg的紙飛機,從上往下經過隨機出現的矩形方塊。紙飛機只受左右方向箭控制,當其垂直向下時,速度最快,當其向左或向右時,角度越大,下落越慢。矩形方塊的縱向長度較短,横向長度較長,紙飛機只要碰到它就立即結束遊戲。合理安排矩形出現的密度,在程式中給出調節的參數。紙飛機下落時,要實時計算它下落的像素值/100,以此作為它的分數。紙飛機一開始時是在最左邊最上方以最傾斜向右的角度滑翔,當它下落到屏幕的垂直中點時,將是矩形方塊往上移動,而紙飛機只保持在中線左右移動。當輸掉遊戲時紙飛機將變色,撞到的方塊亦變色,最終的分數以及計時將停止,並出現「重新開始」按鈕。起初是「開始遊戲」按鈕。當開始遊戲或結束遊戲時會出現「排行榜」,列出所有參加者的用戶名、分數、用時以及結束時間(yyyy-mm-dd hh:ii:ss)。用戶名是guest1開始遞進,在結束遊戲時給出默認用戶名並可修改。svg紙飛機在不同角度應重繪而不只是單純角度轉向,以展現出偽三維效果。畫面應為卡通風格,由淺藍色、淺綠色作為背景色,紙飛機是白色有描邊,矩形方塊是粉紅色有描邊。
代码: 全选
這是一個完整的單文件(HTML + JS + CSS)紙飛機遊戲程式碼。嚴格按照您的需求開發,包含完整的遊戲機制、偽三維的動態 SVG 實時渲染、無限滾動、碰撞檢測以及排行榜系統。
您可以將以下代碼複製並保存為 `index.html`,然後在瀏覽器中打開即可遊玩。
```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>紙飛機極速下落</title>
<style>
/* 基礎與卡通風格設定 */
:root {
--bg-top: #A1C4FD;
--bg-bottom: #C2E9FB;
--primary: #4CAF50;
--text-color: #333;
}
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f8ff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#game-wrapper {
position: relative;
width: 400px;
height: 600px;
background: linear-gradient(to bottom, var(--bg-top), var(--bg-bottom));
border: 4px solid #333;
border-radius: 16px;
box-shadow: 8px 8px 0px rgba(0,0,0,0.2);
overflow: hidden;
}
/* 畫布與SVG */
canvas {
display: block;
width: 100%;
height: 100%;
}
#plane-svg {
position: absolute;
width: 60px;
height: 60px;
transform: translate(-50%, -50%); /* 中心定位 */
pointer-events: none;
z-index: 10;
}
/* UI 層 */
.ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.85);
z-index: 20;
backdrop-filter: blur(4px);
}
.hidden {
display: none !important;
}
.hud {
position: absolute;
top: 10px;
left: 15px;
right: 15px;
display: flex;
justify-content: space-between;
font-size: 18px;
font-weight: bold;
color: #333;
text-shadow: 1px 1px 0px #fff;
z-index: 15;
pointer-events: none;
}
/* 按鈕與輸入框 */
button {
padding: 12px 24px;
font-size: 18px;
font-weight: bold;
color: #fff;
background-color: var(--primary);
border: 3px solid #333;
border-radius: 8px;
cursor: pointer;
box-shadow: 4px 4px 0px #333;
transition: all 0.1s;
margin-bottom: 20px;
}
button:active {
transform: translate(4px, 4px);
box-shadow: 0px 0px 0px #333;
}
input[type="text"] {
padding: 10px;
font-size: 16px;
border: 3px solid #333;
border-radius: 8px;
margin-bottom: 15px;
text-align: center;
width: 200px;
}
/* 排行榜 */
.leaderboard {
width: 90%;
max-height: 250px;
overflow-y: auto;
background: #fff;
border: 3px solid #333;
border-radius: 8px;
padding: 10px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
text-align: center;
}
th {
background-color: #eee;
padding: 5px;
border-bottom: 2px solid #333;
}
td {
padding: 5px;
border-bottom: 1px dashed #ccc;
}
h2 {
margin-top: 0;
color: #333;
text-shadow: 2px 2px 0px #fff;
}
</style>
</head>
<body>
<div id="game-wrapper">
<!-- HUD 資訊 -->
<div class="hud" id="hud">
<div id="scoreDisplay">分數: 0</div>
<div id="timeDisplay">時間: 0.0s</div>
</div>
<!-- 障礙物渲染畫布 -->
<canvas id="gameCanvas" width="400" height="600"></canvas>
<!-- 偽三維動態 SVG 紙飛機 -->
<svg id="plane-svg" viewBox="-30 -30 60 60">
<!-- 左機身 -->
<path id="planeLeft" d="" fill="#FFF" stroke="#333" stroke-width="2" stroke-linejoin="round"/>
<!-- 右機身 -->
<path id="planeRight" d="" fill="#F0F0F0" stroke="#333" stroke-width="2" stroke-linejoin="round"/>
</svg>
<!-- 主選單 UI -->
<div class="ui-layer" id="menuUI">
<h2 style="font-size: 32px; color: var(--primary);">紙飛機極速下落</h2>
<button id="startBtn">開始遊戲</button>
<div class="leaderboard" id="menuLeaderboard"></div>
</div>
<!-- 遊戲結束 UI -->
<div class="ui-layer hidden" id="gameOverUI">
<h2>遊戲結束!</h2>
<p style="font-size: 18px; font-weight: bold;">最終分數: <span id="finalScore">0</span></p>
<input type="text" id="usernameInput" placeholder="輸入玩家名稱" maxlength="10">
<button id="saveRestartBtn">保存並重新開始</button>
<div class="leaderboard" id="gameOverLeaderboard"></div>
</div>
</div>
<script>
/**
* 遊戲參數配置 (可在此調節遊戲難度與密度)
*/
const CONFIG = {
blockDensity: 180, // 矩形方塊出現的縱向密度 (像素間距,越小越密)
blockMinW: 100, // 方塊最小寬度
blockMaxW: 220, // 方塊最大寬度
blockHeight: 25, // 方塊高度 (較短)
baseFallSpeed: 8, // 飛機垂直向下時的最大速度 (每幀像素)
turnSpeed: 0.06, // 左右方向鍵轉向速度
maxAngle: Math.PI / 2.2, // 最大傾斜角度 (約81度)
worldWidth: 400, // 世界寬度 (與畫布相同)
worldHeight: 600 // 視窗高度
};
// DOM 元素
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const planeSvg = document.getElementById('plane-svg');
const pathLeft = document.getElementById('planeLeft');
const pathRight = document.getElementById('planeRight');
const uiMenu = document.getElementById('menuUI');
const uiGameOver = document.getElementById('gameOverUI');
const scoreDisp = document.getElementById('scoreDisplay');
const timeDisp = document.getElementById('timeDisplay');
// 遊戲狀態變數
let state = {
isPlaying: false,
score: 0,
time: 0,
startTime: 0,
totalDistance: 0, // 總下落距離 (用於計算世界坐標和分數)
cameraY: 0, // 攝像機 Y 坐標 (達到中線後開始跟隨)
lastSpawnY: 0, // 上次生成方塊的世界 Y 坐標
hitBlock: null // 撞擊到的方塊
};
// 飛機物件
let plane = {
x: 0,
worldY: 0, // 在世界中的絕對高度
screenY: 0, // 在屏幕上的顯示高度
angle: 0, // 當前角度 (0為筆直向下,正數為偏右)
isDead: false
};
let blocks = [];
let keys = {};
let animationFrameId;
// 排行榜資料
let leaderboards = JSON.parse(localStorage.getItem('paperPlaneLB')) || [];
// 鍵盤監聽
window.addEventListener('keydown', (e) => keys[e.code] = true);
window.addEventListener('keyup', (e) => keys[e.code] = false);
// 格式化日期函數 (yyyy-mm-dd hh:ii:ss)
function getFormattedDate() {
const d = new Date();
const pad = n => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
/**
* 更新並繪製偽三維 SVG 紙飛機
*/
function updatePlaneSVG() {
// 更新位置
planeSvg.style.left = plane.x + 'px';
planeSvg.style.top = plane.screenY + 'px';
// 根據角度計算傾斜 (Bank)
let bank = plane.angle / CONFIG.maxAngle; // 範圍 -1 到 1
// 飛機始終機頭朝下(基底角度),根據控制角度旋轉並發生偽 3D 形變
let angle = plane.angle;
// 頂點計算 (機頭, 機尾, 左翼, 右翼)
let noseX = Math.sin(angle) * 25;
let noseY = Math.cos(angle) * 25;
let tailX = -Math.sin(angle) * 15;
let tailY = -Math.cos(angle) * 15;
// 當向右傾斜(bank > 0)時,左翼變寬展現出上面,右翼變窄收起;反之亦然
let lwX = -18 * (1 - bank * 0.4) - Math.sin(angle) * 8;
let lwY = -18 * (1 + bank * 0.2) + Math.cos(angle) * 8;
let rwX = 18 * (1 + bank * 0.4) - Math.sin(angle) * 8;
let rwY = -18 * (1 - bank * 0.2) + Math.cos(angle) * 8;
// 設置 SVG Path
pathLeft.setAttribute('d', `M ${noseX} ${noseY} L ${lwX} ${lwY} L ${tailX} ${tailY} Z`);
pathRight.setAttribute('d', `M ${noseX} ${noseY} L ${rwX} ${rwY} L ${tailX} ${tailY} Z`);
// 死亡時變色
if (plane.isDead) {
pathLeft.setAttribute('fill', '#999');
pathRight.setAttribute('fill', '#666');
pathLeft.setAttribute('stroke', '#000');
pathRight.setAttribute('stroke', '#000');
} else {
pathLeft.setAttribute('fill', '#FFF');
pathRight.setAttribute('fill', '#F0F0F0');
pathLeft.setAttribute('stroke', '#333');
pathRight.setAttribute('stroke', '#333');
}
}
/**
* 生成矩形方塊
*/
function spawnBlock(worldY) {
let w = Math.random() * (CONFIG.blockMaxW - CONFIG.blockMinW) + CONFIG.blockMinW;
let h = CONFIG.blockHeight;
// 保證方塊在屏幕內
let x = Math.random() * (CONFIG.worldWidth - w);
blocks.push({ x, y: worldY, w, h, isHit: false });
}
/**
* 初始化/重置遊戲
*/
function initGame() {
// 初始狀態:最左上方,最大角度向右傾斜
plane = {
x: 20, // 最左邊 (留點邊距)
worldY: 20,
screenY: 20,
angle: CONFIG.maxAngle, // 最傾斜向右
isDead: false
};
state = {
isPlaying: true,
score: 0,
time: 0,
startTime: performance.now(),
totalDistance: plane.worldY,
cameraY: 0,
lastSpawnY: CONFIG.worldHeight, // 從屏幕底部開始生成
hitBlock: null
};
blocks = [];
// 初始生成幾個方塊
for (let i = 250; i < CONFIG.worldHeight + 200; i += CONFIG.blockDensity) {
spawnBlock(i);
}
uiMenu.classList.add('hidden');
uiGameOver.classList.add('hidden');
updatePlaneSVG();
requestAnimationFrame(gameLoop);
}
/**
* 碰撞檢測 (使用飛機中心附近的小圓形與矩形進行檢測,增加手感)
*/
function checkCollision() {
const hitRadius = 8; // 飛機判定半徑
for (let b of blocks) {
// 計算方塊在屏幕上的實際位置
let screenBlockY = b.y - state.cameraY;
// 尋找矩形上最接近圓心的點
let testX = plane.x;
let testY = plane.screenY;
if (plane.x < b.x) testX = b.x;
else if (plane.x > b.x + b.w) testX = b.x + b.w;
if (plane.screenY < screenBlockY) testY = screenBlockY;
else if (plane.screenY > screenBlockY + b.h) testY = screenBlockY + b.h;
let distX = plane.x - testX;
let distY = plane.screenY - testY;
let distance = Math.sqrt((distX * distX) + (distY * distY));
if (distance <= hitRadius) {
return b; // 返回撞到的方塊
}
}
return null;
}
/**
* 遊戲主循環
*/
function gameLoop(currentTime) {
if (!state.isPlaying) return;
// 計算時間
let dt = (currentTime - state.startTime) / 1000;
state.time = dt;
// 1. 處理輸入與飛機物理
if (keys['ArrowLeft']) plane.angle -= CONFIG.turnSpeed;
if (keys['ArrowRight']) plane.angle += CONFIG.turnSpeed;
// 限制角度
plane.angle = Math.max(-CONFIG.maxAngle, Math.min(CONFIG.maxAngle, plane.angle));
// 速度計算:垂直時(angle=0) cos(0)=1 速度最快;傾斜時速度變慢
let speedFactor = Math.cos(plane.angle);
let fallSpeed = CONFIG.baseFallSpeed * speedFactor;
// 橫向移動:基於傾斜角度的 sin 值
let sideSpeed = CONFIG.baseFallSpeed * Math.sin(plane.angle);
plane.x += sideSpeed;
plane.worldY += fallSpeed;
state.totalDistance = plane.worldY;
// 邊界限制 (防止飛出左右螢幕)
plane.x = Math.max(15, Math.min(CONFIG.worldWidth - 15, plane.x));
// 2. 攝像機(滾動)邏輯
let midPoint = CONFIG.worldHeight / 2;
if (plane.worldY < midPoint) {
// 飛機未達中點,自由下落
plane.screenY = plane.worldY;
state.cameraY = 0;
} else {
// 飛機到達中點,鎖定在屏幕中央,世界(方塊)向上滾動
plane.screenY = midPoint;
state.cameraY = plane.worldY - midPoint;
}
// 3. 障礙物生成與清理
let bottomOfScreenWorldY = state.cameraY + CONFIG.worldHeight;
if (bottomOfScreenWorldY - state.lastSpawnY >= CONFIG.blockDensity) {
state.lastSpawnY += CONFIG.blockDensity;
spawnBlock(state.lastSpawnY + 100); // 提早一點在螢幕外生成
}
// 移除已經滾動到螢幕上方的方塊
blocks = blocks.filter(b => b.y - state.cameraY + b.h > -50);
// 4. 分數計算 (實時下落像素/100)
state.score = Math.floor(state.totalDistance / 100);
// 5. 碰撞檢測
let hit = checkCollision();
if (hit) {
gameOver(hit);
return; // 終止當前幀
}
// 6. 渲染
render();
// 7. 更新 HUD
scoreDisp.innerText = `分數: ${state.score}`;
timeDisp.innerText = `時間: ${state.time.toFixed(1)}s`;
animationFrameId = requestAnimationFrame(gameLoop);
}
/**
* 畫布渲染
*/
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 繪製方塊
blocks.forEach(b => {
let screenY = b.y - state.cameraY;
ctx.lineWidth = 3;
ctx.strokeStyle = '#333'; // 描邊
// 撞到時變色
if (b.isHit) {
ctx.fillStyle = '#ff4d4d'; // 紅色
} else {
ctx.fillStyle = '#FFB6C1'; // 粉紅色
}
// 圓角矩形
ctx.beginPath();
ctx.roundRect(b.x, screenY, b.w, b.h, 6);
ctx.fill();
ctx.stroke();
});
// 觸發 SVG 更新
updatePlaneSVG();
}
/**
* 遊戲結束處理
*/
function gameOver(hitBlock) {
state.isPlaying = false;
plane.isDead = true;
if (hitBlock) {
hitBlock.isHit = true;
}
// 渲染最後一幀 (顯示變色效果)
render();
// 獲取並遞增默認 Guest 名稱
let guestNum = parseInt(localStorage.getItem('paperPlaneGuestNum') || '1');
document.getElementById('usernameInput').value = `guest${guestNum}`;
localStorage.setItem('paperPlaneGuestNum', guestNum + 1);
document.getElementById('finalScore').innerText = state.score;
updateLeaderboardHTML('gameOverLeaderboard');
setTimeout(() => {
uiGameOver.classList.remove('hidden');
}, 500); // 延遲半秒顯示結束畫面
}
/**
* 保存記錄並更新排行榜
*/
function saveAndRestart() {
let name = document.getElementById('usernameInput').value.trim() || 'Anonymous';
let record = {
name: name,
score: state.score,
time: state.time.toFixed(1),
date: getFormattedDate()
};
leaderboards.push(record);
// 排序:分數高優先,時間短優先
leaderboards.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return parseFloat(a.time) - parseFloat(b.time);
});
// 只保留前10名
leaderboards = leaderboards.slice(0, 10);
localStorage.setItem('paperPlaneLB', JSON.stringify(leaderboards));
initGame();
}
/**
* 生成排行榜 HTML
*/
function updateLeaderboardHTML(containerId) {
const container = document.getElementById(containerId);
if (leaderboards.length === 0) {
container.innerHTML = '<p style="text-align:center; color:#666;">暫無記錄</p>';
return;
}
let html = `
<table>
<thead>
<tr>
<th>排名</th>
<th>玩家</th>
<th>分數</th>
<th>用時(s)</th>
<th>結束時間</th>
</tr>
</thead>
<tbody>
`;
leaderboards.forEach((entry, index) => {
let rankColor = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? '#cd7f32' : 'inherit';
html += `
<tr style="color: ${rankColor}; font-weight: ${index < 3 ? 'bold' : 'normal'}">
<td>${index + 1}</td>
<td>${entry.name}</td>
<td>${entry.score}</td>
<td>${entry.time}</td>
<td style="font-size:11px;">${entry.date}</td>
</tr>
`;
});
html += `</tbody></table>`;
container.innerHTML = html;
}
// 事件綁定
document.getElementById('startBtn').addEventListener('click', initGame);
document.getElementById('saveRestartBtn').addEventListener('click', saveAndRestart);
// 初始載入時顯示選單排行榜
updateLeaderboardHTML('menuLeaderboard');
</script>
</body>
</html>
```
### 功能亮點說明:
1. **核心玩法與物理引擎**:
- 使用左/右方向鍵操控。紙飛機垂直時(角度為 0)向下墜落最快;向左右傾斜時,水平位移增加,下墜速度會依據三角函數 `Math.cos()` 平滑減緩。
- 包含平滑鏡頭滾動設計:當飛機下落至螢幕中央前為自由下落,一旦碰到螢幕中點,飛機在垂直方向將保持固定,改為使背景矩形方塊往上「無限滾動」。
2. **偽三維 SVG 即時渲染**:
- 嚴格採用 `<svg>` 標籤覆蓋於 `<canvas>` 之上。
- 遊戲每幀不僅僅對飛機進行簡單的 CSS 旋轉,而是動態計算左翼、右翼與機首、機尾的 2D 坐標。
- 當向右偏移時,左翼形變拉寬(展現頂部平面),右翼變窄收縮,結合深淺相異的顏色填充,實現極具立體感的動態「紙摺效果」。
3. **碰撞與遊戲判定**:
- 使用畫布 (Canvas) 的 API 即時渲染生成的粉紅色障礙物。
- 配置高頻碰撞檢測機制。觸碰障礙物時觸發遊戲結束,觸碰的方塊變為紅色,紙飛機變為暗灰色以符合需求。
4. **排行榜與資料存儲**:
- 利用 `localStorage` 本地存儲技術保留歷次遊玩紀錄。
- 當遊戲結束時預設載入不斷自動遞增的 `guestX` 玩家名稱(玩家可任意修改)。
- 排行榜嚴格依據分數排序並顯示要求的所有資料維度(玩家名稱、分數、秒數用時與 `yyyy-mm-dd hh:ii:ss` 格式的精準時間)。
5. **參數與美觀調整**:
- 包含完整的卡通化 UI 配置。淡藍漸變背景、描黑邊物件效果完美呈現。
- 在 JS 代碼開頭有 `CONFIG` 物件,您可以直接修改 `blockDensity`(密度)或 `baseFallSpeed`(重力速度)來輕鬆調節難度。