codex秒生成的小游戏-火柴人闯关
·
<!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>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0e0e1a;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', system-ui, sans-serif;
overflow: hidden;
}
.game-wrapper {
background: #151528;
border-radius: 18px;
padding: 20px;
box-shadow: 0 8px 50px rgba(0,0,0,0.7);
position: relative;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 0 4px;
}
.score-box {
font-size: 17px;
font-weight: 600;
color: #ccc;
}
.score-box span { color: #f0c040; }
.controls-row {
display: flex;
gap: 8px;
}
.controls-row button {
background: #2d2d50;
border: none;
color: #ccc;
font-size: 13px;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s;
}
.controls-row button:hover { background: #3d3d62; }
canvas {
display: block;
background: #111124;
border-radius: 10px;
width: 800px;
height: 400px;
image-rendering: auto;
}
.footer {
margin-top: 10px;
text-align: center;
font-size: 13px;
color: #555;
}
.footer kbd {
background: #222238;
padding: 2px 9px;
border-radius: 4px;
font-family: inherit;
font-size: 12px;
color: #999;
}
@media (max-width: 860px) {
.game-wrapper { padding: 12px; border-radius: 12px; }
canvas { width: calc(100vw - 40px); height: calc((100vw - 40px) * 0.5); }
}
</style>
</head>
<body>
<div class="game-wrapper">
<div class="header">
<div class="score-box">🏃 <span id="scoreDisplay">0</span>m</div>
<div class="controls-row">
<button id="restartBtn">重新开始</button>
</div>
</div>
<canvas id="gameCanvas" width="800" height="400"></canvas>
<div class="footer"><kbd>空格</kbd> <kbd>↑</kbd> <kbd>W</kbd> 跳跃 · 双击/点两下可二段跳</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('scoreDisplay');
const W = 800, H = 400;
const GROUND_Y = 340; // 地面高度
const GRAVITY = 0.6;
const JUMP_VEL = -9;
const MAX_JUMPS = 2;
// ---- 状态 ----
let gameOver = false;
let score = 0;
let frameCount = 0;
let speed = 5;
let obstacles = [];
let particles = [];
let bgStars = [];
// ---- 火柴人 ----
const stick = {
x: 120, y: GROUND_Y,
vy: 0,
w: 20, h: 44, // 碰撞箱
jumpsLeft: MAX_JUMPS,
grounded: true,
runPhase: 0,
};
// ---- 星星背景 ----
for (let i = 0; i < 50; i++) {
bgStars.push({
x: Math.random() * W,
y: Math.random() * (GROUND_Y - 20),
r: Math.random() * 1.6 + 0.4,
a: Math.random() * 0.5 + 0.2,
});
}
// ---- 地面块(视觉分割) ----
let groundOff = 0;
// ---- 重置 ----
function reset() {
gameOver = false;
score = 0;
frameCount = 0;
speed = 5;
obstacles = [];
particles = [];
stick.y = GROUND_Y;
stick.vy = 0;
stick.jumpsLeft = MAX_JUMPS;
stick.grounded = true;
stick.runPhase = 0;
scoreDisplay.textContent = '0';
}
// ---- 生成障碍物 ----
function spawnObstacle() {
const types = [
{ w: 14, h: 28 }, // 小石块
{ w: 20, h: 36 }, // 中箱子
{ w: 12, h: 40 }, // 高柱子
{ w: 28, h: 22 }, // 矮宽箱
];
const t = types[Math.floor(Math.random() * types.length)];
obstacles.push({
x: W + 20,
y: GROUND_Y - t.h,
w: t.w,
h: t.h,
passed: false,
type: t,
});
}
let spawnTimer = 0;
// ---- 粒子特效 ----
function emitParticles(x, y, count, color, spread) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * spread,
vy: -Math.random() * spread * 0.6 - 1,
life: 1,
decay: 0.015 + Math.random() * 0.025,
size: 2 + Math.random() * 3,
color,
});
}
}
// ---- 更新 ----
function update() {
if (gameOver) return;
frameCount++;
// 速度渐增
speed = 5 + Math.floor(score / 100) * 0.5;
if (speed > 14) speed = 14;
// ---- 火柴人物理 ----
stick.vy += GRAVITY;
stick.y += stick.vy;
// 落地检测
if (stick.y >= GROUND_Y) {
stick.y = GROUND_Y;
stick.vy = 0;
if (!stick.grounded) {
// 落地粒子
emitParticles(stick.x, GROUND_Y, 6, 'rgba(200,200,255,0.4)', 4);
}
stick.grounded = true;
stick.jumpsLeft = MAX_JUMPS;
} else {
stick.grounded = false;
}
// 跑步相位(落地时才积累)
if (stick.grounded) {
stick.runPhase += 0.18 * (speed / 5);
}
// ---- 障碍物 ----
for (let i = obstacles.length - 1; i >= 0; i--) {
const o = obstacles[i];
o.x -= speed;
// 计分
if (!o.passed && o.x + o.w < stick.x) {
o.passed = true;
score++;
scoreDisplay.textContent = score;
}
// 碰撞检测 (AABB)
const sx = stick.x - stick.w/2, sy = stick.y - stick.h;
if (sx < o.x + o.w && sx + stick.w > o.x &&
sy < o.y + o.h && sy + stick.h > o.y) {
gameOver = true;
emitParticles(stick.x, stick.y - stick.h/2, 30, 'rgba(255,100,100,0.8)', 8);
return;
}
// 移除屏幕外
if (o.x + o.w < -20) obstacles.splice(i, 1);
}
// ---- 生成 ----
spawnTimer -= speed;
if (spawnTimer <= 0) {
spawnObstacle();
const minGap = Math.max(80, 140 - score * 0.15);
spawnTimer = minGap + Math.random() * 60;
}
// ---- 粒子 ----
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.08;
p.life -= p.decay;
if (p.life <= 0) particles.splice(i, 1);
}
// 地面滚动
groundOff = (groundOff - speed) % 40;
}
// ---- 绘制火柴人 ----
function drawStickman() {
const cx = stick.x;
const baseY = stick.y; // 脚底
const headR = 8;
const bodyLen = 20;
const limbLen = 16;
const headY = baseY - bodyLen - headR;
ctx.save();
ctx.strokeStyle = '#e8e8f0';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// 跑步 / 跳跃姿态
const inAir = !stick.grounded;
const phase = stick.runPhase;
const swing = (inAir ? 0.5 : Math.sin(phase)) * 0.45;
// 头
ctx.beginPath();
ctx.arc(cx, headY, headR, 0, Math.PI * 2);
ctx.stroke();
// 身体
const neckY = headY + headR;
const hipY = neckY + bodyLen;
ctx.beginPath();
ctx.moveTo(cx, neckY);
ctx.lineTo(cx, hipY);
ctx.stroke();
// 左腿(从胯部到脚)
const legAngleL = 0.6 + swing;
const legAngle = 0.6;
const lx1 = cx, ly1 = hipY;
const lx2 = cx - Math.sin(legAngleL) * limbLen;
const ly2 = ly1 + Math.cos(legAngleL) * limbLen;
ctx.beginPath(); ctx.moveTo(lx1, ly1); ctx.lineTo(lx2, ly2); ctx.stroke();
// 右腿
const legAngleR = 0.6 - swing;
const rx2 = cx - Math.sin(legAngleR) * limbLen;
const ry2 = ly1 + Math.cos(legAngleR) * limbLen;
ctx.beginPath(); ctx.moveTo(cx, ly1); ctx.lineTo(rx2, ry2); ctx.stroke();
// 手臂
const armY = neckY + 4;
const armSwing = (inAir ? 1 : Math.sin(phase + Math.PI)) * 0.4;
const ax1 = cx - Math.sin(0.3 + armSwing) * limbLen * 0.9;
const ay1 = armY + Math.cos(0.3 + armSwing) * limbLen * 0.9;
ctx.beginPath(); ctx.moveTo(cx, armY); ctx.lineTo(ax1, ay1); ctx.stroke();
const ax2 = cx - Math.sin(0.3 - armSwing) * limbLen * 0.9;
const ay2 = armY + Math.cos(0.3 - armSwing) * limbLen * 0.9;
ctx.beginPath(); ctx.moveTo(cx, armY); ctx.lineTo(ax2, ay2); ctx.stroke();
// 眼睛
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.arc(cx - 3, headY - 1, 1.5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(cx + 3, headY - 1, 1.5, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
// ---- 绘制障碍物 ----
function drawObstacle(o) {
const hue = 220 + (o.h % 3) * 15;
ctx.fillStyle = `hsl(${hue}, 20%, 35%)`;
ctx.fillRect(o.x, o.y, o.w, o.h);
// 边框高光
ctx.strokeStyle = `hsl(${hue}, 15%, 50%)`;
ctx.lineWidth = 1.5;
ctx.strokeRect(o.x, o.y, o.w, o.h);
// 顶线高光
ctx.strokeStyle = `hsl(${hue}, 20%, 55%)`;
ctx.beginPath();
ctx.moveTo(o.x + 2, o.y + 2);
ctx.lineTo(o.x + o.w - 2, o.y + 2);
ctx.stroke();
}
// ---- 绘制场景 ----
function drawScene() {
ctx.clearRect(0, 0, W, H);
// ---- 星空 ----
for (const s of bgStars) {
ctx.fillStyle = `rgba(255,255,255,${s.a})`;
ctx.beginPath();
ctx.arc(((s.x - frameCount * 0.15) % W + W) % W, s.y, s.r, 0, Math.PI*2);
ctx.fill();
}
// ---- 远景城市剪影 ----
ctx.fillStyle = '#1a1a32';
const cityOffset = (frameCount * 0.3) % 200;
for (let i = -1; i < 6; i++) {
const bx = i * 160 - cityOffset;
const bh = 40 + ((i * 7 + 3) % 5) * 20;
ctx.fillRect(bx, GROUND_Y - bh, 50, bh);
// 小窗户
ctx.fillStyle = 'rgba(255,200,100,0.06)';
for (let wy = GROUND_Y - bh + 8; wy < GROUND_Y - 12; wy += 12)
for (let wx = bx + 6; wx < bx + 46; wx += 14)
ctx.fillRect(wx, wy, 6, 5);
ctx.fillStyle = '#1a1a32';
}
// ---- 地面 ----
ctx.fillStyle = '#1c1c30';
ctx.fillRect(0, GROUND_Y + 2, W, H - GROUND_Y);
// 地面分割线
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
for (let x = groundOff; x < W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, GROUND_Y + 6); ctx.lineTo(x, GROUND_Y + 14); ctx.stroke();
}
// 地面顶部亮线
ctx.fillStyle = 'rgba(255,255,255,0.06)';
ctx.fillRect(0, GROUND_Y, W, 2);
// ---- 障碍物 ----
for (const o of obstacles) drawObstacle(o);
// ---- 火柴人 ----
drawStickman();
// ---- 粒子 ----
for (const p of particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI*2);
ctx.fill();
}
ctx.globalAlpha = 1;
// ---- 影子 ----
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(stick.x, GROUND_Y + 4, 14, 4, 0, 0, Math.PI*2);
ctx.fill();
// ---- 游戏结束 ----
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff';
ctx.font = 'bold 32px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('💀 撞倒了', W/2, 150);
ctx.font = '20px system-ui, sans-serif';
ctx.fillStyle = '#f0c040';
ctx.fillText(`${score}m`, W/2, 200);
ctx.font = '15px system-ui, sans-serif';
ctx.fillStyle = '#999';
ctx.fillText('点击「重新开始」再来一局', W/2, 250);
}
}
// ---- 跳跃 ----
function jump() {
if (gameOver) return;
if (stick.jumpsLeft > 0) {
stick.vy = JUMP_VEL * (stick.jumpsLeft === MAX_JUMPS ? 1 : 0.85);
stick.jumpsLeft--;
if (stick.jumpsLeft === 0 && !stick.grounded) {
// 二段跳粒子
emitParticles(stick.x, stick.y, 10, 'rgba(150,200,255,0.5)', 6);
}
}
}
// ---- 循环 ----
function gameLoop() {
update();
drawScene();
requestAnimationFrame(gameLoop);
}
// ---- 事件 ----
document.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
e.preventDefault();
jump();
}
});
// 触摸 / 点击跳跃(双击触发二段跳)
let lastTap = 0;
canvas.addEventListener('pointerdown', (e) => {
e.preventDefault();
const now = Date.now();
if (now - lastTap < 300 && stick.jumpsLeft === 1) {
// 快速双击视为二段跳
jump();
lastTap = 0;
} else {
jump();
lastTap = now;
}
});
document.getElementById('restartBtn').addEventListener('click', () => {
reset();
});
// ---- 启动 ----
reset();
gameLoop();
})();
</script>
</body>
</html>
更多推荐




所有评论(0)