556 lines
18 KiB
HTML
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> |