Files
2026-07-23 11:30:26 +08:00

556 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
<title>天空冲刺 · 手机躲避游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: linear-gradient(180deg, #0b1a3a 0%, #1b3b5c 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
padding: 10px;
margin: 0;
}
.game-container {
width: 100%;
max-width: 400px;
background: radial-gradient(circle at 50% 30%, #35637c, #0e2a3f);
border-radius: 48px;
box-shadow: 0 25px 50px rgba(0,0,0,0.6), inset 0 0 20px rgba(255,255,255,0.2);
padding: 16px 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 32px;
background: radial-gradient(ellipse at 50% 20%, #7fc9ff 0%, #3a6d9c 70%);
box-shadow: inset 0 -8px 20px rgba(0,20,40,0.7), 0 12px 25px rgba(0,0,0,0.5);
touch-action: none; /* 禁止默认手势,优化触摸响应 */
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 10px;
color: white;
font-weight: bold;
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
font-size: 1.3rem;
}
.score-board {
background: rgba(10, 25, 45, 0.7);
backdrop-filter: blur(12px);
border-radius: 50px;
padding: 8px 22px;
letter-spacing: 1px;
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.25);
}
.restart-btn {
background: #ffb84d;
border: none;
color: #0a1f32;
font-weight: bold;
font-size: 1.2rem;
padding: 10px 25px;
border-radius: 40px;
background: linear-gradient(180deg, #ffd966 0%, #f0a500 100%);
box-shadow: 0 8px 0 #b76e00, 0 12px 22px rgba(0,0,0,0.4);
transition: all 0.08s ease;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border: 1px solid rgba(255,255,200,0.7);
touch-action: manipulation;
}
.restart-btn:active {
transform: translateY(4px);
box-shadow: 0 4px 0 #b76e00, 0 8px 14px rgba(0,0,0,0.5);
background: linear-gradient(180deg, #f5c45e 0%, #d18d00 100%);
}
.hint {
color: rgba(255,255,255,0.8);
font-size: 0.8rem;
margin-top: 6px;
text-align: center;
font-weight: 500;
text-shadow: 0 2px 5px black;
}
@media (max-width: 400px) {
.game-container {
padding: 12px 8px 16px;
}
.info-panel {
font-size: 1.1rem;
}
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="360" height="600" aria-label="天空冲刺游戏画面"></canvas>
<div class="info-panel">
<div class="score-board" id="scoreDisplay">0</div>
<button class="restart-btn" id="restartButton">↺ 起飞</button>
</div>
<div class="hint">👆 手指滑动或拖动飞机 | 躲避石块</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
// 固定尺寸,适配高清显示
const CW = 360;
const CH = 600;
canvas.width = CW;
canvas.height = CH;
// ---------- 游戏状态 ----------
let gameActive = true;
let score = 0;
// 玩家飞机属性
const PLAYER_WIDTH = 38;
const PLAYER_HEIGHT = 44;
let playerX = CW / 2 - PLAYER_WIDTH/2;
const playerY = CH - 90; // 垂直位置固定(玩家只水平移动)
// 障碍物数组
let obstacles = [];
// 障碍物设置
const OBSTACLE_WIDTH = 36;
const OBSTACLE_HEIGHT = 48;
const OBSTACLE_SPEED = 4.2; // 每帧下落速度
const SPAWN_FRAME_INTERVAL = 38; // 大约每38帧生成一个
let frameCounter = 0;
// 移动控制 (触摸/鼠标)
let targetPlayerX = playerX; // 用于平滑跟随或直接设置
// 触摸状态
let activeTouchId = null; // 追踪当前触摸点
// ---------- 辅助函数:限制玩家位置 ----------
function clampPlayerX(value) {
return Math.max(5, Math.min(CW - PLAYER_WIDTH - 5, value));
}
// ---------- 生成新障碍物 ----------
function spawnObstacle() {
// 随机x位置,确保不超出边界
const maxX = CW - OBSTACLE_WIDTH;
const x = Math.random() * maxX;
obstacles.push({
x: x,
y: -OBSTACLE_HEIGHT - Math.random() * 20, // 从顶外出现
width: OBSTACLE_WIDTH,
height: OBSTACLE_HEIGHT,
});
}
// ---------- 重置游戏 ----------
function restartGame() {
gameActive = true;
score = 0;
obstacles = [];
frameCounter = 0;
playerX = CW / 2 - PLAYER_WIDTH/2;
targetPlayerX = playerX;
activeTouchId = null;
updateScoreDisplay();
}
// ---------- 游戏结束 ----------
function endGame() {
if (!gameActive) return; // 防止重复触发
gameActive = false;
// 可以添加震动反馈(如果支持)
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(30);
}
}
// ---------- 更新分数显示 ----------
function updateScoreDisplay() {
scoreSpan.textContent = score;
}
// ---------- 碰撞检测 (矩形重叠) ----------
function checkCollision(playerRect, obs) {
return (playerRect.x < obs.x + obs.width &&
playerRect.x + playerRect.width > obs.x &&
playerRect.y < obs.y + obs.height &&
playerRect.y + playerRect.height > obs.y);
}
// ---------- 更新逻辑 ----------
function update() {
if (!gameActive) return;
// 1. 玩家位置平滑移动到目标位置 (使操作更跟手)
const DX = targetPlayerX - playerX;
// 快速跟随,但保留手感
if (Math.abs(DX) > 0.3) {
playerX += DX * 0.35;
// 避免微小抖动
if (Math.abs(DX) < 0.5) {
playerX = targetPlayerX;
}
} else {
playerX = targetPlayerX;
}
playerX = clampPlayerX(playerX);
// 同步目标位置以防超出边界后不一致
targetPlayerX = clampPlayerX(targetPlayerX);
// 2. 移动障碍物 & 碰撞检测 & 移除屏幕外
const playerRect = {
x: playerX,
y: playerY,
width: PLAYER_WIDTH,
height: PLAYER_HEIGHT,
};
for (let i = obstacles.length - 1; i >= 0; i--) {
const obs = obstacles[i];
obs.y += OBSTACLE_SPEED;
// 碰撞检测 (仅当游戏活跃时)
if (gameActive && checkCollision(playerRect, obs)) {
endGame();
return; // 立即停止更新,避免继续处理
}
// 移除超出底部的障碍物,并增加分数
if (obs.y > CH + 50) {
obstacles.splice(i, 1);
if (gameActive) {
score += 1;
updateScoreDisplay();
}
}
}
// 3. 生成新障碍物 (游戏活跃时才生成)
if (gameActive) {
frameCounter++;
if (frameCounter >= SPAWN_FRAME_INTERVAL) {
spawnObstacle();
frameCounter = 0;
// 动态略微调整生成间隔,增加随机性
// 保持在30~45之间变化
// 但为了稳定,使用固定基准加小波动
const variation = Math.floor(Math.random() * 12) - 4; // -4~7
frameCounter = -variation; // 技巧:让下次间隔变化
}
}
}
// ---------- 绘制图形 ----------
function draw() {
ctx.clearRect(0, 0, CW, CH);
// ------- 绘制装饰云朵 (背景) -------
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
drawCloud(40, 120, 30);
drawCloud(280, 200, 35);
drawCloud(70, 320, 28);
drawCloud(300, 430, 32);
drawCloud(150, 70, 38);
// ------- 绘制障碍物 (石块/陨石) -------
for (let obs of obstacles) {
// 石块渐变
const gradient = ctx.createLinearGradient(obs.x, obs.y, obs.x + 10, obs.y + obs.height);
gradient.addColorStop(0, '#5d4a3a');
gradient.addColorStop(0.5, '#4a3728');
gradient.addColorStop(1, '#2f231a');
ctx.fillStyle = gradient;
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 8;
ctx.beginPath();
// 不规则石块效果
const w = obs.width;
const h = obs.height;
const x = obs.x;
const y = obs.y;
ctx.moveTo(x + 5, y);
ctx.lineTo(x + w - 6, y + 4);
ctx.lineTo(x + w, y + h/2);
ctx.lineTo(x + w - 5, y + h - 5);
ctx.lineTo(x + w/2, y + h);
ctx.lineTo(x + 3, y + h - 7);
ctx.lineTo(x, y + h/3);
ctx.closePath();
ctx.fill();
// 高光线
ctx.shadowBlur = 4;
ctx.fillStyle = '#7d6b5c';
ctx.beginPath();
ctx.moveTo(x + 8, y + 5);
ctx.lineTo(x + w - 12, y + 8);
ctx.lineTo(x + w - 9, y + h/2 - 2);
ctx.lineTo(x + 6, y + h/2 - 3);
ctx.closePath();
ctx.fill();
// 重置阴影
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
// 裂纹
ctx.strokeStyle = '#1a120a';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(x + 10, y + 12);
ctx.lineTo(x + 18, y + 25);
ctx.lineTo(x + 13, y + 34);
ctx.stroke();
}
// ------- 绘制玩家飞机 -------
ctx.shadowColor = '#00000080';
ctx.shadowBlur = 12;
const px = playerX;
const py = playerY;
// 机身主色
const planeGrad = ctx.createLinearGradient(px, py, px + PLAYER_WIDTH*0.8, py + PLAYER_HEIGHT);
planeGrad.addColorStop(0, '#ffd966');
planeGrad.addColorStop(0.6, '#f0a500');
planeGrad.addColorStop(1, '#c47a00');
ctx.fillStyle = planeGrad;
// 绘制飞机主体 (喷气式风格)
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2, py - 4); // 机头
ctx.lineTo(px + PLAYER_WIDTH-8, py + PLAYER_HEIGHT-12); // 右尾翼
ctx.lineTo(px + PLAYER_WIDTH-4, py + PLAYER_HEIGHT-6);
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT-10); // 尾部中心
ctx.lineTo(px + 4, py + PLAYER_HEIGHT-6);
ctx.lineTo(px + 8, py + PLAYER_HEIGHT-12); // 左尾翼
ctx.closePath();
ctx.fill();
// 机翼
ctx.fillStyle = '#e59400';
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2, py + 12);
ctx.lineTo(px - 3, py + 28);
ctx.lineTo(px + 10, py + 32);
ctx.lineTo(px + PLAYER_WIDTH/2, py + 24);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2, py + 12);
ctx.lineTo(px + PLAYER_WIDTH+3, py + 28);
ctx.lineTo(px + PLAYER_WIDTH-10, py + 32);
ctx.lineTo(px + PLAYER_WIDTH/2, py + 24);
ctx.closePath();
ctx.fill();
// 驾驶舱
ctx.fillStyle = '#aee2ff';
ctx.shadowColor = '#ffffff80';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.ellipse(px + PLAYER_WIDTH/2, py + 10, 7, 9, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.ellipse(px + PLAYER_WIDTH/2-2, py + 8, 2.5, 3.5, 0, 0, Math.PI*2);
ctx.fill();
// 引擎火焰
ctx.shadowBlur = 14;
ctx.fillStyle = '#ff7b24';
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2-5, py + PLAYER_HEIGHT-8);
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT+6);
ctx.lineTo(px + PLAYER_WIDTH/2+5, py + PLAYER_HEIGHT-8);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffdd55';
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2-3, py + PLAYER_HEIGHT-6);
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT);
ctx.lineTo(px + PLAYER_WIDTH/2+3, py + PLAYER_HEIGHT-6);
ctx.closePath();
ctx.fill();
// 重置阴影
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
// ------- 游戏结束画面 -------
if (!gameActive) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = '#0a1424';
ctx.fillRect(0, 0, CW, CH);
ctx.globalAlpha = 1;
ctx.fillStyle = 'white';
ctx.font = 'bold 28px "Segoe UI", system-ui';
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 16;
ctx.fillText('💥 坠毁', CW/2, CH/2-20);
ctx.font = '18px system-ui';
ctx.fillText('点击"起飞"继续', CW/2, CH/2+45);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
}
}
function drawCloud(x, y, size) {
ctx.fillStyle = 'rgba(255, 255, 250, 0.25)';
ctx.beginPath();
ctx.arc(x, y, size*0.6, 0, Math.PI*2);
ctx.arc(x+size*0.5, y-size*0.2, size*0.7, 0, Math.PI*2);
ctx.arc(x-size*0.4, y-size*0.1, size*0.55, 0, Math.PI*2);
ctx.arc(x+size*0.2, y+size*0.2, size*0.5, 0, Math.PI*2);
ctx.fill();
}
// ---------- 游戏循环 ----------
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ---------- 事件处理 (触摸/鼠标适配) ----------
function handleStart(e) {
e.preventDefault();
if (!gameActive) return; // 游戏结束时不处理移动
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const canvasRect = canvas.getBoundingClientRect();
// 计算相对于canvas内部的x坐标 (按比例缩放)
const scaleX = CW / canvasRect.width; // canvas物理像素与CSS宽度比例
const relativeX = (clientX - canvasRect.left) * scaleX;
// 设定目标位置:飞机中心对准触摸点
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
playerX = targetPlayerX; // 立即吸附,提升响应
// 记录触摸点id用于移动跟踪
if (e.touches) {
activeTouchId = e.touches[0].identifier;
}
}
function handleMove(e) {
e.preventDefault();
if (!gameActive) return;
// 对于触摸事件,需要找到匹配的触摸点
if (e.touches) {
if (activeTouchId === null) return;
let currentTouch = null;
for (let i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === activeTouchId) {
currentTouch = e.touches[i];
break;
}
}
if (!currentTouch) return;
const canvasRect = canvas.getBoundingClientRect();
const scaleX = CW / canvasRect.width;
const relativeX = (currentTouch.clientX - canvasRect.left) * scaleX;
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
} else {
// 鼠标移动 (用于桌面调试)
const canvasRect = canvas.getBoundingClientRect();
const scaleX = CW / canvasRect.width;
const relativeX = (e.clientX - canvasRect.left) * scaleX;
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
}
}
function handleEnd(e) {
e.preventDefault();
// 触摸结束,清除活动触摸点
if (e.touches) {
// 如果剩余触摸点为空,则重置activeTouchId
if (e.touches.length === 0) {
activeTouchId = null;
} else {
// 仍有其他手指,尝试切换到第一个
activeTouchId = e.touches[0].identifier;
}
} else {
// 鼠标松开不做特殊处理
activeTouchId = null;
}
}
// 绑定事件
function bindEvents() {
// 触摸事件
canvas.addEventListener('touchstart', handleStart, { passive: false });
canvas.addEventListener('touchmove', handleMove, { passive: false });
canvas.addEventListener('touchend', handleEnd);
canvas.addEventListener('touchcancel', handleEnd);
// 鼠标事件 (便于开发调试和部分设备)
canvas.addEventListener('mousedown', handleStart);
window.addEventListener('mousemove', (e) => {
// 只有按下鼠标左键时才处理移动
if (e.buttons === 1 && gameActive) {
e.preventDefault();
const canvasRect = canvas.getBoundingClientRect();
const scaleX = CW / canvasRect.width;
const relativeX = (e.clientX - canvasRect.left) * scaleX;
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
}
});
window.addEventListener('mouseup', (e) => {
// 不做特殊操作
});
// 重启按钮
document.getElementById('restartButton').addEventListener('click', (e) => {
restartGame();
});
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
e.preventDefault();
restartGame();
});
}
// 启动一切
bindEvents();
restartGame(); // 初始状态
gameLoop();
})();
</script>
</body>
</html>