🏃 简笔画小人跑酷 - 跳跃游戏
by RocketTiger92991 lines38.7 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🏃 简笔画小人跑酷 - 跳跃游戏</title>
<style>
:root {
--bg: #1a1a2e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--bg);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
cursor: pointer;
}
.game-wrapper {
position: relative;
border-radius: 20px;
overflow: hidden;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.5),
0 0 0 3px rgba(255, 255, 255, 0.08),
0 0 0 6px rgba(0, 0, 0, 0.2),
0 0 120px rgba(100, 180, 255, 0.15);
transition: transform 0.15s ease, box-shadow 0.15s ease;
max-width: 95vw;
max-height: 90vh;
}
.game-wrapper:active {
transform: scale(0.995);
box-shadow:
0 15px 45px rgba(0, 0, 0, 0.5),
0 0 0 3px rgba(255, 255, 255, 0.06),
0 0 0 6px rgba(0, 0, 0, 0.2),
0 0 100px rgba(100, 180, 255, 0.1);
}
canvas {
display: block;
border-radius: 17px;
max-width: 100%;
height: auto;
}
.hint-bar {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.65);
color: #fff;
padding: 8px 20px;
border-radius: 25px;
font-size: 14px;
letter-spacing: 0.5px;
pointer-events: none;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: opacity 0.4s ease;
}
</style>
</head>
<body>
<div class="game-wrapper" id="gameWrapper">
<canvas id="gameCanvas"></canvas>
<div class="hint-bar" id="hintBar">⌨️ 空格键 或 🖱️ 点击屏幕跳跃</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const wrapper = document.getElementById('gameWrapper');
const hintBar = document.getElementById('hintBar');
// --- 画布尺寸 ---
const WIDTH = 800;
const HEIGHT = 420;
canvas.width = WIDTH;
canvas.height = HEIGHT;
// --- 响应式缩放 ---
function resizeCanvas() {
const scale = Math.min(
(window.innerWidth * 0.95) / WIDTH,
(window.innerHeight * 0.88) / HEIGHT,
1.0
);
canvas.style.width = WIDTH * scale + 'px';
canvas.style.height = HEIGHT * scale + 'px';
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// --- 游戏常量 ---
const GROUND_Y = 340; // 地面线(脚底位置)
const PLAYER_X = 130; // 玩家水平位置
const GRAVITY_PER_SEC = 1800; // 重力加速度 px/s² (60fps下每帧0.5px)
const JUMP_VELOCITY = -600; // 跳跃初速度 px/s (60fps下约-10.3px/帧)
const OBSTACLE_SPEED = 220; // 障碍物速度 px/s
const SCORE_PER_SECOND = 10; // 每秒得分
const MIN_SPAWN_INTERVAL = 0.5; // 最小生成间隔(秒)
const MAX_SPAWN_INTERVAL = 1.5; // 最大生成间隔(秒)
// --- 游戏状态 ---
const STATE = { WAITING: 'waiting', PLAYING: 'playing', GAME_OVER: 'gameOver' };
let gameState = STATE.WAITING;
let score = 0;
let scoreAccumulator = 0;
let playerY = GROUND_Y; // 玩家脚底Y坐标
let playerVelocityY = 0;
let isOnGround = true;
let obstacles = [];
let spawnTimer = 0;
let particles = [];
let clouds = [];
let screenShake = 0;
let gameOverAlpha = 0;
let restartCooldown = 0;
let lastTime = performance.now();
// --- 音效系统 (Web Audio API) ---
let audioCtx = null;
function getAudioContext() {
if (!audioCtx) {
try {
audioCtx = new(window.AudioContext || window.webkitAudioContext)();
} catch (e) {
audioCtx = null;
}
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
return audioCtx;
}
function playSound(freq, duration, type = 'square', vol = 0.08, freqEnd = null) {
const ctx = getAudioContext();
if (!ctx) return;
try {
const t = ctx.currentTime;
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(freq, t);
if (freqEnd) {
oscillator.frequency.linearRampToValueAtTime(freqEnd, t + duration);
}
gainNode.gain.setValueAtTime(vol, t);
gainNode.gain.exponentialRampToValueAtTime(0.001, t + duration);
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.start(t);
oscillator.stop(t + duration);
} catch (e) {
// 静默处理
}
}
function sfxJump() {
playSound(420, 0.1, 'square', 0.06, 680);
setTimeout(() => playSound(520, 0.07, 'square', 0.04, 750), 40);
}
function sfxHit() {
playSound(60, 0.35, 'sawtooth', 0.12, 25);
playSound(90, 0.25, 'triangle', 0.08, 40);
}
function sfxScore() {
playSound(880, 0.06, 'sine', 0.03, 1100);
}
// --- 初始化云朵 ---
function initClouds() {
clouds = [];
for (let i = 0; i < 5; i++) {
clouds.push({
x: Math.random() * WIDTH,
y: 25 + Math.random() * 130,
w: 60 + Math.random() * 100,
h: 25 + Math.random() * 35,
speed: 15 + Math.random() * 35,
opacity: 0.35 + Math.random() * 0.45,
});
}
}
initClouds();
// --- 粒子系统 ---
function spawnParticles(x, y, count, color, spreadX = 3, spreadY = 4, life = 0.5) {
for (let i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * spreadX * 2,
vy: -Math.random() * spreadY * 8 - 2,
life: life + Math.random() * 0.3,
maxLife: life + 0.3,
color: color,
size: 1.5 + Math.random() * 3.5,
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 400 * dt; // 粒子重力
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
}
function drawParticles(ctx) {
for (const p of particles) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = p.color.replace('1)', `${alpha})`).replace('rgb',
'rgba');
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
}
// --- 重置游戏 ---
function resetGame() {
playerY = GROUND_Y;
playerVelocityY = 0;
isOnGround = true;
obstacles = [];
particles = [];
score = 0;
scoreAccumulator = 0;
spawnTimer = 0.6;
screenShake = 0;
gameOverAlpha = 0;
restartCooldown = 0;
lastTime = performance.now();
gameState = STATE.PLAYING;
hintBar.style.opacity = '0';
}
// --- 生成障碍物 ---
function spawnObstacle() {
const heightOptions = [
{ h: 32, w: 16, label: 'tiny' },
{ h: 42, w: 18, label: 'small' },
{ h: 52, w: 20, label: 'medium' },
{ h: 62, w: 22, label: 'large' },
{ h: 74, w: 24, label: 'xlarge' },
];
const choice = heightOptions[Math.floor(Math.random() * heightOptions.length)];
obstacles.push({
x: WIDTH + 10,
y: GROUND_Y - choice.h,
width: choice.w,
height: choice.h,
label: choice.label,
passed: false,
});
}
// --- 碰撞检测 ---
function checkCollision(playerBox, obs) {
const obsBox = {
x: obs.x - obs.width / 2,
y: obs.y,
w: obs.width,
h: obs.height,
};
// 缩小一点碰撞框,更公平
const margin = 5;
const px = playerBox.x + margin;
const py = playerBox.y + margin;
const pw = playerBox.w - margin * 2;
const ph = playerBox.h - margin * 2;
const ox = obsBox.x + margin * 0.6;
const oy = obsBox.y + margin * 0.4;
const ow = obsBox.w - margin * 1.2;
const oh = obsBox.h - margin * 0.8;
return (
px < ox + ow &&
px + pw > ox &&
py < oy + oh &&
py + ph > oy
);
}
function getPlayerCollisionBox() {
const headRadius = 13;
const totalHeight = 58;
const footY = playerY;
const topY = footY - totalHeight;
const boxWidth = 28;
return {
x: PLAYER_X - boxWidth / 2,
y: topY,
w: boxWidth,
h: totalHeight,
};
}
// --- 绘制函数 ---
function drawSky(ctx) {
const gradient = ctx.createLinearGradient(0, 0, 0, GROUND_Y);
gradient.addColorStop(0, '#b8dff5');
gradient.addColorStop(0.45, '#d4eaf7');
gradient.addColorStop(0.85, '#e8f4fb');
gradient.addColorStop(1, '#f0f7fc');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, WIDTH, GROUND_Y);
// 太阳光晕
const sunX = WIDTH * 0.78;
const sunY = 65;
const sunGrad = ctx.createRadialGradient(sunX, sunY, 20, sunX, sunY, 180);
sunGrad.addColorStop(0, 'rgba(255,252,235,0.9)');
sunGrad.addColorStop(0.25, 'rgba(255,248,220,0.5)');
sunGrad.addColorStop(0.6, 'rgba(255,240,200,0.08)');
sunGrad.addColorStop(1, 'rgba(255,235,180,0)');
ctx.fillStyle = sunGrad;
ctx.fillRect(0, 0, WIDTH, GROUND_Y);
}
function drawClouds(ctx) {
for (const cloud of clouds) {
ctx.fillStyle = `rgba(255,255,255,${cloud.opacity})`;
const cx = cloud.x;
const cy = cloud.y;
const w = cloud.w;
const h = cloud.h;
// 多个椭圆组成云朵
ctx.beginPath();
ctx.ellipse(cx, cy, w * 0.5, h * 0.45, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx - w * 0.28, cy + h * 0.08, w * 0.35, h * 0.38, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + w * 0.3, cy + h * 0.05, w * 0.38, h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx - w * 0.1, cy - h * 0.2, w * 0.33, h * 0.42, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + w * 0.12, cy - h * 0.15, w * 0.3, h * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawGround(ctx) {
// 草地主体
const grassGrad = ctx.createLinearGradient(0, GROUND_Y, 0, HEIGHT);
grassGrad.addColorStop(0, '#7bc34d');
grassGrad.addColorStop(0.15, '#6aaf3c');
grassGrad.addColorStop(0.5, '#558b2f');
grassGrad.addColorStop(1, '#3e6b1f');
ctx.fillStyle = grassGrad;
ctx.fillRect(0, GROUND_Y, WIDTH, HEIGHT - GROUND_Y);
// 地面线
ctx.strokeStyle = '#5c9e2f';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(0, GROUND_Y);
ctx.lineTo(WIDTH, GROUND_Y);
ctx.stroke();
// 地面线高光
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, GROUND_Y + 1);
ctx.lineTo(WIDTH, GROUND_Y + 1);
ctx.stroke();
// 草丛纹理
const grassBlades = 90;
for (let i = 0; i < grassBlades; i++) {
const gx = (i * WIDTH / grassBlades + Math.sin(i * 7.3) * 8 + WIDTH * 0.03) % WIDTH;
const gy = GROUND_Y + 3 + Math.abs(Math.cos(i * 4.7)) * 14;
const gh = 8 + Math.abs(Math.sin(i * 3.1)) * 16;
const sway = Math.sin(i * 2.3 + performance.now() * 0.001) * 3;
ctx.strokeStyle = `rgba(${100 + Math.random()*40},${140 + Math.random()*50},${40 + Math.random()*35},0.55)`;
ctx.lineWidth = 1.2 + Math.random() * 0.8;
ctx.beginPath();
ctx.moveTo(gx, gy);
ctx.quadraticCurveTo(gx + sway, gy - gh * 0.6, gx + sway * 1.4, gy - gh);
ctx.stroke();
}
// 小花朵点缀
for (let i = 0; i < 15; i++) {
const fx = (i * 53 + 17) % WIDTH;
const fy = GROUND_Y + 5 + (i * 31 % 28);
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.beginPath();
ctx.arc(fx, fy, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,240,150,0.8)';
ctx.beginPath();
ctx.arc(fx, fy, 1.1, 0, Math.PI * 2);
ctx.fill();
}
}
function drawStickFigure(ctx, footY, isJumping) {
const headRadius = 13;
const headY = footY - 58 + headRadius;
const neckY = headY + headRadius;
const bodyBottom = footY - 16;
const bodyTop = neckY;
const bodyMid = (bodyTop + bodyBottom) / 2;
const armY = bodyTop + 5;
ctx.save();
// 跳跃时的轻微旋转
if (isJumping) {
const rotateAngle = -0.08;
ctx.translate(PLAYER_X, footY);
ctx.rotate(rotateAngle);
ctx.translate(-PLAYER_X, -footY);
}
// 阴影
if (isOnGround && !isJumping) {
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(PLAYER_X, footY + 1, 12, 4, 0, 0, Math.PI * 2);
ctx.fill();
}
// 腿部
const legSpread = isJumping ? 7 : 10;
ctx.strokeStyle = '#2c2c2c';
ctx.lineWidth = 3.5;
ctx.lineCap = 'round';
// 左腿
ctx.beginPath();
ctx.moveTo(PLAYER_X, bodyBottom);
ctx.lineTo(PLAYER_X - legSpread, footY);
ctx.stroke();
// 右腿
ctx.beginPath();
ctx.moveTo(PLAYER_X, bodyBottom);
ctx.lineTo(PLAYER_X + legSpread, footY);
ctx.stroke();
// 身体
ctx.strokeStyle = '#2c2c2c';
ctx.lineWidth = 3.8;
ctx.beginPath();
ctx.moveTo(PLAYER_X, bodyTop);
ctx.lineTo(PLAYER_X, bodyBottom);
ctx.stroke();
// 手臂
const armLength = 16;
const armAngle = isJumping ? -0.6 : 0.25;
ctx.lineWidth = 3;
// 左臂
ctx.beginPath();
ctx.moveTo(PLAYER_X, armY);
ctx.lineTo(PLAYER_X - Math.cos(armAngle) * armLength, armY - Math.sin(armAngle) * armLength);
ctx.stroke();
// 右臂
ctx.beginPath();
ctx.moveTo(PLAYER_X, armY);
ctx.lineTo(PLAYER_X + Math.cos(armAngle) * armLength, armY - Math.sin(armAngle) * armLength);
ctx.stroke();
// 鞋子
ctx.fillStyle = '#3a3a3a';
ctx.beginPath();
ctx.arc(PLAYER_X - legSpread, footY - 1, 3.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(PLAYER_X + legSpread, footY - 1, 3.5, 0, Math.PI * 2);
ctx.fill();
// 头部
const headGrad = ctx.createRadialGradient(PLAYER_X - 2, headY - 3, 2, PLAYER_X, headY, headRadius);
headGrad.addColorStop(0, '#fdf5e6');
headGrad.addColorStop(0.7, '#f0d9b5');
headGrad.addColorStop(1, '#d4a574');
ctx.fillStyle = headGrad;
ctx.beginPath();
ctx.arc(PLAYER_X, headY, headRadius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#2c2c2c';
ctx.lineWidth = 2.2;
ctx.stroke();
// 眼睛
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.arc(PLAYER_X - 4, headY - 3, 2.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(PLAYER_X + 4, headY - 3, 2.2, 0, Math.PI * 2);
ctx.fill();
// 微笑
ctx.strokeStyle = '#5a3a2a';
ctx.lineWidth = 1.3;
ctx.beginPath();
ctx.arc(PLAYER_X, headY + 2, 5, 0.15 * Math.PI, 0.85 * Math.PI);
ctx.stroke();
// 跳跃时的汗滴
if (isJumping && playerVelocityY < -100) {
ctx.fillStyle = 'rgba(150,200,255,0.7)';
ctx.beginPath();
ctx.arc(PLAYER_X + 16, headY - 6, 2.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawCactus(ctx, obs) {
const cx = obs.x;
const baseY = obs.y + obs.height;
const h = obs.height;
const w = obs.width;
const halfW = w / 2;
ctx.save();
// 主体阴影
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.roundRect(cx - halfW + 3, obs.y + 3, w, h, halfW * 0.8);
ctx.fill();
// 主体
const bodyGrad = ctx.createLinearGradient(cx - halfW, 0, cx + halfW, 0);
bodyGrad.addColorStop(0, '#4a8c3f');
bodyGrad.addColorStop(0.35, '#5da04f');
bodyGrad.addColorStop(0.65, '#4d9442');
bodyGrad.addColorStop(1, '#35702b');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.roundRect(cx - halfW, obs.y, w, h, halfW * 0.8);
ctx.fill();
ctx.strokeStyle = '#2d5a22';
ctx.lineWidth = 1.6;
ctx.stroke();
// 纹理竖线
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
for (let lx = cx - halfW * 0.4; lx <= cx + halfW * 0.4; lx += halfW * 0.5) {
ctx.beginPath();
ctx.moveTo(lx, obs.y + 4);
ctx.lineTo(lx, obs.y + h - 4);
ctx.stroke();
}
// 分支(较高的仙人掌才有)
if (h >= 42) {
const branchY = obs.y + h * 0.35;
const branchLen = w * 0.7;
const branchH = h * 0.28;
// 左分支
drawCactusBranch(ctx, cx - halfW, branchY, -branchLen, branchH, bodyGrad);
// 右分支
if (h >= 52) {
const branchY2 = obs.y + h * 0.5;
drawCactusBranch(ctx, cx + halfW, branchY2, branchLen, branchH * 0.85, bodyGrad);
}
}
// 顶部小刺
ctx.fillStyle = '#d4e8c0';
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.arc(cx + i * halfW * 0.5, obs.y - 1, 2, 0, Math.PI * 2);
ctx.fill();
}
// 小花(大型仙人掌)
if (h >= 62) {
const flowerX = cx;
const flowerY = obs.y - 3;
ctx.fillStyle = '#ff6b8a';
ctx.beginPath();
ctx.arc(flowerX, flowerY, 4.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffb347';
ctx.beginPath();
ctx.arc(flowerX, flowerY, 2.2, 0, Math.PI * 2);
ctx.fill();
// 花瓣
for (let p = 0; p < 5; p++) {
const angle = (p / 5) * Math.PI * 2;
ctx.fillStyle = '#ff8da8';
ctx.beginPath();
ctx.arc(flowerX + Math.cos(angle) * 4, flowerY + Math.sin(angle) * 4, 2.8, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
function drawCactusBranch(ctx, startX, startY, len, h, bodyGrad) {
ctx.fillStyle = '#4a8c3f';
ctx.beginPath();
ctx.roundRect(startX + (len > 0 ? 0 : len), startY, Math.abs(len), h, 7);
ctx.fill();
ctx.strokeStyle = '#2d5a22';
ctx.lineWidth = 1.3;
ctx.stroke();
// 小臂延伸
const tipX = startX + len;
const tipY = startY + h / 2;
ctx.fillStyle = '#5da04f';
ctx.beginPath();
ctx.roundRect(tipX - 4, tipY - h * 0.6, 8, h * 1.2, 5);
ctx.fill();
ctx.strokeStyle = '#2d5a22';
ctx.lineWidth = 1;
ctx.stroke();
}
function drawGameOverOverlay(ctx) {
if (gameOverAlpha <= 0) return;
// 暗色遮罩
ctx.fillStyle = `rgba(0,0,0,${Math.min(0.55, gameOverAlpha)})`;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// 主面板
const panelW = 340;
const panelH = 190;
const panelX = WIDTH / 2 - panelW / 2;
const panelY = HEIGHT / 2 - panelH / 2 - 10;
const alpha = Math.min(1, gameOverAlpha * 2);
ctx.save();
ctx.globalAlpha = alpha;
// 面板背景
const panelGrad = ctx.createLinearGradient(panelX, panelY, panelX, panelY + panelH);
panelGrad.addColorStop(0, 'rgba(40,40,50,0.94)');
panelGrad.addColorStop(1, 'rgba(25,25,35,0.96)');
ctx.fillStyle = panelGrad;
ctx.beginPath();
ctx.roundRect(panelX, panelY, panelW, panelH, 20);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 2;
ctx.stroke();
// 标题
ctx.fillStyle = '#ff6b6b';
ctx.font = 'bold 34px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.textAlign = 'center';
ctx.fillText('游戏结束', WIDTH / 2, panelY + 52);
// 分数
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.fillText(`🏆 最终得分:${Math.floor(score)}`, WIDTH / 2, panelY + 95);
// 提示
ctx.fillStyle = '#ffd700';
ctx.font = '17px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
const pulse = 0.7 + 0.3 * Math.sin(performance.now() * 0.005);
ctx.globalAlpha = alpha * pulse;
ctx.fillText('按 空格键 重新开始', WIDTH / 2, panelY + 140);
ctx.restore();
}
function drawUI(ctx) {
// 左上角分数
const scoreX = 22;
const scoreY = 38;
// 分数背景
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.beginPath();
ctx.roundRect(scoreX - 8, scoreY - 26, 155, 40, 22);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.lineWidth = 1.5;
ctx.stroke();
// 分数文字
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 18px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`⭐ ${Math.floor(score)} 分`, scoreX + 6, scoreY + 1);
}
// --- 更新逻辑 ---
function update(dt) {
// 限制最大步长防止跳帧问题
const clampedDt = Math.min(dt, 0.1);
// 更新云朵
for (const cloud of clouds) {
cloud.x -= cloud.speed * clampedDt;
if (cloud.x + cloud.w < -20) {
cloud.x = WIDTH + 20;
cloud.y = 25 + Math.random() * 130;
}
}
// 更新粒子
updateParticles(clampedDt);
// 屏幕震动衰减
if (screenShake > 0) {
screenShake = Math.max(0, screenShake - clampedDt * 12);
}
// 游戏结束冷却
if (restartCooldown > 0) {
restartCooldown = Math.max(0, restartCooldown - clampedDt);
}
if (gameState === STATE.PLAYING) {
// 计分
scoreAccumulator += clampedDt;
const prevScoreInt = Math.floor(score);
score = scoreAccumulator * SCORE_PER_SECOND;
if (Math.floor(score) > prevScoreInt && Math.floor(score) % 10 === 0 && Math.floor(score) > 0) {
sfxScore();
}
// 玩家物理
if (!isOnGround) {
playerVelocityY += GRAVITY_PER_SEC * clampedDt;
playerY += playerVelocityY * clampedDt;
// 落地检测
if (playerY >= GROUND_Y) {
playerY = GROUND_Y;
playerVelocityY = 0;
isOnGround = true;
// 落地粒子
spawnParticles(PLAYER_X, GROUND_Y, 8, 'rgb(180,200,160)', 6, 3, 0.4);
}
}
// 生成障碍物
spawnTimer -= clampedDt;
if (spawnTimer <= 0) {
spawnObstacle();
spawnTimer = MIN_SPAWN_INTERVAL + Math.random() * (MAX_SPAWN_INTERVAL - MIN_SPAWN_INTERVAL);
}
// 更新障碍物
for (const obs of obstacles) {
obs.x -= OBSTACLE_SPEED * clampedDt;
}
// 移除屏幕外的障碍物
while (obstacles.length > 0 && obstacles[0].x < -60) {
obstacles.shift();
}
// 碰撞检测
const playerBox = getPlayerCollisionBox();
for (const obs of obstacles) {
if (checkCollision(playerBox, obs)) {
// 游戏结束
gameState = STATE.GAME_OVER;
gameOverAlpha = 0;
screenShake = 0.5;
restartCooldown = 0.5;
sfxHit();
spawnParticles(PLAYER_X, playerY - 20, 25, 'rgb(255,100,80)', 8, 6, 0.7);
spawnParticles(PLAYER_X, playerY - 20, 15, 'rgb(255,200,50)', 6, 5, 0.5);
hintBar.style.opacity = '1';
hintBar.textContent = '💥 游戏结束!按 空格键 重新开始';
break;
}
}
}
// 游戏结束动画
if (gameState === STATE.GAME_OVER && gameOverAlpha < 1.5) {
gameOverAlpha += clampedDt * 1.8;
}
}
// --- 渲染 ---
function render(ctx) {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
// 屏幕震动偏移
let shakeX = 0;
let shakeY = 0;
if (screenShake > 0) {
shakeX = (Math.random() - 0.5) * screenShake * 18;
shakeY = (Math.random() - 0.5) * screenShake * 14;
}
ctx.save();
ctx.translate(shakeX, shakeY);
drawSky(ctx);
drawClouds(ctx);
drawGround(ctx);
// 绘制障碍物
for (const obs of obstacles) {
drawCactus(ctx, obs);
}
// 绘制玩家
const isJumping = !isOnGround || playerY < GROUND_Y;
drawStickFigure(ctx, playerY, isJumping);
// 绘制粒子
drawParticles(ctx);
ctx.restore();
// UI(不受震动影响)
drawUI(ctx);
drawGameOverOverlay(ctx);
// 等待状态提示
if (gameState === STATE.WAITING) {
const pulse = 0.6 + 0.4 * Math.sin(performance.now() * 0.004);
ctx.fillStyle = `rgba(0,0,0,0.5)`;
ctx.font = 'bold 20px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.textAlign = 'center';
ctx.fillText('👆 点击屏幕 或按 空格键 开始游戏', WIDTH / 2, HEIGHT / 2 - 15);
ctx.fillStyle = `rgba(255,255,255,${0.6 + 0.4 * Math.sin(performance.now()*0.005)})`;
ctx.font = '15px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.fillText('跳过仙人掌,存活越久分数越高!', WIDTH / 2, HEIGHT / 2 + 25);
}
}
// --- 游戏循环 ---
function gameLoop(timestamp) {
const dt = (timestamp - lastTime) / 1000; // 转换为秒
lastTime = timestamp;
if (dt > 0 && dt < 0.5) {
update(dt);
}
render(ctx);
requestAnimationFrame(gameLoop);
}
// --- 输入处理 ---
function handleJump() {
if (gameState === STATE.WAITING) {
resetGame();
gameState = STATE.PLAYING;
hintBar.style.opacity = '0';
// 初始跳跃
if (isOnGround) {
playerVelocityY = JUMP_VELOCITY;
isOnGround = false;
sfxJump();
spawnParticles(PLAYER_X, GROUND_Y, 6, 'rgb(200,220,240)', 5, 4, 0.35);
}
return;
}
if (gameState === STATE.PLAYING) {
if (isOnGround) {
playerVelocityY = JUMP_VELOCITY;
isOnGround = false;
sfxJump();
spawnParticles(PLAYER_X, GROUND_Y, 6, 'rgb(200,220,240)', 5, 4, 0.35);
}
return;
}
if (gameState === STATE.GAME_OVER && restartCooldown <= 0) {
resetGame();
hintBar.style.opacity = '0';
return;
}
}
function handleRestart() {
if (gameState === STATE.GAME_OVER && restartCooldown <= 0) {
resetGame();
hintBar.style.opacity = '0';
}
}
// 键盘事件
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') {
e.preventDefault();
if (gameState === STATE.GAME_OVER) {
handleRestart();
} else {
handleJump();
}
}
});
// 鼠标点击
canvas.addEventListener('click', (e) => {
e.preventDefault();
if (gameState === STATE.GAME_OVER) {
handleRestart();
} else {
handleJump();
}
});
// 触摸事件(移动端)
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameState === STATE.GAME_OVER) {
handleRestart();
} else {
handleJump();
}
}, { passive: false });
// 双击防止缩放
canvas.addEventListener('dblclick', (e) => {
e.preventDefault();
});
// --- 初始绘制 ---
function drawInitialFrame() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
drawSky(ctx);
drawClouds(ctx);
drawGround(ctx);
drawStickFigure(ctx, GROUND_Y, false);
drawUI(ctx);
// 等待提示
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.font = 'bold 20px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.textAlign = 'center';
ctx.fillText('👆 点击屏幕 或按 空格键 开始游戏', WIDTH / 2, HEIGHT / 2 - 15);
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '15px "PingFang SC","Microsoft YaHei","Segoe UI",sans-serif';
ctx.fillText('跳过仙人掌,存活越久分数越高!', WIDTH / 2, HEIGHT / 2 + 25);
}
// --- 启动 ---
drawInitialFrame();
hintBar.style.opacity = '1';
hintBar.textContent = '⌨️ 空格键 或 🖱️ 点击屏幕跳跃';
lastTime = performance.now();
requestAnimationFrame(gameLoop);
console.log('🏃 简笔画小人跑酷游戏已就绪!');
console.log(' - 按空格键或点击屏幕跳跃');
console.log(' - 躲避仙人掌障碍物');
console.log(' - 每存活1秒获得10分');
console.log(' - 游戏结束后按空格键重新开始');
console.log(' 🎮 祝你玩得开心!');
})();
</script>
</body>
</html>Game Source: 🏃 简笔画小人跑酷 - 跳跃游戏
Creator: RocketTiger92
Libraries: none
Complexity: complex (991 lines, 38.7 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-rockettiger92" to link back to the original. Then publish at arcadelab.ai/publish.