🏃 极简线条跑酷 · 黑白版
by RocketTiger92466 lines17.9 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: #f0f0f0; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #e0e0e0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Courier New', 'PingFang SC', monospace;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
}
.game-wrapper {
position: relative;
border: 4px solid #111;
border-radius: 12px;
overflow: hidden;
box-shadow: 8px 8px 0 #00000020;
transition: transform 0.1s ease;
max-width: 95vw;
max-height: 90vh;
}
.game-wrapper:active { transform: scale(0.995); }
canvas { display: block; max-width: 100%; height: auto; }
.hint-bar {
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
background: #fff; color: #111; padding: 6px 18px; border-radius: 20px;
font-size: 13px; letter-spacing: 0.5px; pointer-events: none;
border: 2px solid #111; transition: opacity 0.3s; font-weight: bold;
}
</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 hintBar = document.getElementById('hintBar');
const WIDTH = 800, 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 = 1800; // 重力 px/s²
const JUMP_VEL = -620; // 跳跃初速度 px/s
const SPEED = 320; // 障碍物速度 px/s
const SCORE_RATE = 10; // 每秒得分
const SPAWN_MIN = 0.5, SPAWN_MAX = 1.5;// 障碍物生成间隔范围 (秒)
// -------------------- 状态 --------------------
const STATE = { WAIT: 0, PLAY: 1, OVER: 2 };
let gameState = STATE.WAIT;
let score = 0, scoreAcc = 0;
let playerY = GROUND_Y, playerVy = 0, onGround = true;
let obstacles = [], spawnTimer = 0;
let shake = 0, overAlpha = 0, restartCD = 0;
let lastTime = performance.now();
// -------------------- 简易音效 (保留,但不影响黑白风格) --------------------
let audioCtx = null;
function getCtx() {
if (!audioCtx) try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
if (audioCtx?.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function beep(freq, dur, type='square', vol=0.06) {
const ctx = getCtx(); if(!ctx) return;
const t = ctx.currentTime;
const o = ctx.createOscillator(), g = ctx.createGain();
o.type = type; o.frequency.setValueAtTime(freq, t);
g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.001, t+dur);
o.connect(g); g.connect(ctx.destination);
o.start(t); o.stop(t+dur);
}
function sfxJump() { beep(600,0.08); setTimeout(()=>beep(800,0.06),40); }
function sfxHit() { beep(80,0.3,'triangle',0.1); }
function sfxScore() { beep(1200,0.04,'sine',0.03); }
// -------------------- 障碍物生成 --------------------
function spawn() {
const sizes = [32, 42, 52, 62, 74];
const h = sizes[Math.floor(Math.random()*sizes.length)];
const w = 12 + h*0.15;
obstacles.push({ x: WIDTH+10, y: GROUND_Y-h, w, h });
}
// -------------------- 碰撞检测 (收缩框) --------------------
function hitTest(pBox, obs) {
const m = 6;
const px = pBox.x+m, py = pBox.y+m, pw = pBox.w-m*2, ph = pBox.h-m*2;
const ox = obs.x - obs.w/2 + m*0.6, oy = obs.y + m*0.4, ow = obs.w - m*1.2, oh = obs.h - m*0.8;
return px < ox+ow && px+pw > ox && py < oy+oh && py+ph > oy;
}
function playerBox() {
return { x: PLAYER_X-14, y: playerY-58, w: 28, h: 58 };
}
// -------------------- 重置 --------------------
function reset() {
playerY = GROUND_Y; playerVy = 0; onGround = true;
obstacles = []; score = 0; scoreAcc = 0; spawnTimer = 0.6;
shake = 0; overAlpha = 0; restartCD = 0;
lastTime = performance.now();
gameState = STATE.PLAY;
hintBar.style.opacity = '0';
}
// -------------------- 绘制函数 (极简黑白线条风格) --------------------
function drawBackground() {
// 纯白背景
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, WIDTH, GROUND_Y);
// 极细横线表示远方
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 0.5;
for (let y=40; y<GROUND_Y; y+=35) {
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(WIDTH,y); ctx.stroke();
}
}
function drawGround() {
// 地面:纯白底色,黑色粗线边缘
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, GROUND_Y, WIDTH, HEIGHT-GROUND_Y);
ctx.strokeStyle = '#111';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, GROUND_Y); ctx.lineTo(WIDTH, GROUND_Y);
ctx.stroke();
// 地面纹理:短斜线
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
for (let x=0; x<WIDTH; x+=25) {
const offset = (x*7)%30;
ctx.beginPath();
ctx.moveTo(x, GROUND_Y+4);
ctx.lineTo(x-6, GROUND_Y+14);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x+12, GROUND_Y+2);
ctx.lineTo(x+6, GROUND_Y+12);
ctx.stroke();
}
// 底部粗线
ctx.strokeStyle = '#111';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, HEIGHT-1); ctx.lineTo(WIDTH, HEIGHT-1);
ctx.stroke();
}
// 极简火柴人
function drawPlayer(y, jumping) {
const headR = 12;
const headY = y - 58 + headR;
const bodyTop = headY + headR;
const bodyBottom = y - 16;
const legSpread = jumping ? 8 : 11;
const armY = bodyTop + 5;
ctx.save();
if (jumping) { ctx.translate(PLAYER_X, y); ctx.rotate(-0.07); ctx.translate(-PLAYER_X, -y); }
// 所有绘制使用黑色线条,无填充
ctx.strokeStyle = '#111';
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// 腿
ctx.beginPath();
ctx.moveTo(PLAYER_X, bodyBottom);
ctx.lineTo(PLAYER_X-legSpread, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(PLAYER_X, bodyBottom);
ctx.lineTo(PLAYER_X+legSpread, y);
ctx.stroke();
// 身体
ctx.beginPath();
ctx.moveTo(PLAYER_X, bodyTop);
ctx.lineTo(PLAYER_X, bodyBottom);
ctx.stroke();
// 手臂
const armAngle = jumping ? -0.8 : 0.3;
const armLen = 14;
ctx.beginPath();
ctx.moveTo(PLAYER_X, armY);
ctx.lineTo(PLAYER_X - Math.cos(armAngle)*armLen, armY - Math.sin(armAngle)*armLen);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(PLAYER_X, armY);
ctx.lineTo(PLAYER_X + Math.cos(armAngle)*armLen, armY - Math.sin(armAngle)*armLen);
ctx.stroke();
// 头部 (空心圆)
ctx.lineWidth = 2.2;
ctx.beginPath();
ctx.arc(PLAYER_X, headY, headR, 0, Math.PI*2);
ctx.stroke();
// 眼睛 (小点)
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(PLAYER_X-3.5, headY-2, 1.8, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(PLAYER_X+3.5, headY-2, 1.8, 0, Math.PI*2);
ctx.fill();
// 微笑
ctx.strokeStyle = '#111';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(PLAYER_X, headY+3, 4, 0.2*Math.PI, 0.8*Math.PI);
ctx.stroke();
ctx.restore();
}
// 极简仙人掌 (直线条构成)
function drawCactus(obs) {
const cx = obs.x, w = obs.w, h = obs.h;
const left = cx - w/2, right = cx + w/2, top = obs.y, bottom = obs.y + h;
ctx.strokeStyle = '#111';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
// 主体矩形
ctx.beginPath();
ctx.moveTo(left, top);
ctx.lineTo(right, top);
ctx.lineTo(right, bottom);
ctx.lineTo(left, bottom);
ctx.closePath();
ctx.stroke();
// 中间竖线
ctx.lineWidth = 1.5;
ctx.strokeStyle = '#333';
ctx.beginPath();
ctx.moveTo(cx, top+4);
ctx.lineTo(cx, bottom-4);
ctx.stroke();
// 侧枝 (根据高度)
if (h >= 42) {
ctx.strokeStyle = '#111';
ctx.lineWidth = 2;
// 左枝
const branchY = top + h*0.35;
ctx.beginPath();
ctx.moveTo(left, branchY);
ctx.lineTo(left - w*0.55, branchY);
ctx.lineTo(left - w*0.55, branchY - h*0.2);
ctx.stroke();
// 右枝 (更高的仙人掌才有)
if (h >= 52) {
const branchY2 = top + h*0.5;
ctx.beginPath();
ctx.moveTo(right, branchY2);
ctx.lineTo(right + w*0.55, branchY2);
ctx.lineTo(right + w*0.55, branchY2 - h*0.18);
ctx.stroke();
}
}
// 顶部小刺
ctx.fillStyle = '#111';
for (let i=-1; i<=1; i++) {
ctx.beginPath();
ctx.arc(cx + i*4, top-1, 1.8, 0, Math.PI*2);
ctx.fill();
}
}
// UI:左上角分数 (黑白)
function drawUI() {
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#111';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(16, 14, 140, 38, 20);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#111';
ctx.font = 'bold 18px "Courier New", monospace';
ctx.textAlign = 'left';
ctx.fillText(`${Math.floor(score)}`, 30, 40);
}
// 游戏结束覆盖层
function drawGameOver() {
if (overAlpha <= 0) return;
const alpha = Math.min(1, overAlpha);
ctx.fillStyle = `rgba(255,255,255,${alpha*0.85})`;
ctx.fillRect(0,0,WIDTH,HEIGHT);
ctx.fillStyle = '#111';
ctx.font = 'bold 36px "Courier New", monospace';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', WIDTH/2, HEIGHT/2-20);
ctx.font = 'bold 20px "Courier New", monospace';
ctx.fillText(`SCORE: ${Math.floor(score)}`, WIDTH/2, HEIGHT/2+30);
const pulse = 0.7 + 0.3*Math.sin(performance.now()*0.005);
ctx.font = '16px "Courier New", monospace';
ctx.fillStyle = `rgba(0,0,0,${pulse})`;
ctx.fillText('[ 空格键 ] 重新开始', WIDTH/2, HEIGHT/2+65);
}
// -------------------- 更新逻辑 --------------------
function update(dt) {
const dtClamp = Math.min(dt, 0.1);
if (shake > 0) shake = Math.max(0, shake - dtClamp*12);
if (restartCD > 0) restartCD = Math.max(0, restartCD - dtClamp);
if (gameState === STATE.PLAY) {
// 计分
scoreAcc += dtClamp;
const prev = Math.floor(score);
score = scoreAcc * SCORE_RATE;
if (Math.floor(score) > prev && Math.floor(score)%10===0 && score>0) sfxScore();
// 重力
if (!onGround) {
playerVy += GRAVITY * dtClamp;
playerY += playerVy * dtClamp;
if (playerY >= GROUND_Y) {
playerY = GROUND_Y; playerVy = 0; onGround = true;
}
}
// 障碍物生成与移动
spawnTimer -= dtClamp;
if (spawnTimer <= 0) {
spawn();
spawnTimer = SPAWN_MIN + Math.random()*(SPAWN_MAX-SPAWN_MIN);
}
for (const obs of obstacles) obs.x -= SPEED * dtClamp;
while (obstacles.length && obstacles[0].x < -60) obstacles.shift();
// 碰撞检测
const pBox = playerBox();
for (const obs of obstacles) {
if (hitTest(pBox, obs)) {
gameState = STATE.OVER;
overAlpha = 0; shake = 0.4; restartCD = 0.5;
sfxHit();
hintBar.style.opacity = '1';
hintBar.textContent = '[ 空格键 ] 重新开始';
break;
}
}
}
if (gameState === STATE.OVER && overAlpha < 1.5) overAlpha += dtClamp*2;
}
// -------------------- 渲染 --------------------
function render() {
ctx.clearRect(0,0,WIDTH,HEIGHT);
let sx=0, sy=0;
if (shake>0) { sx=(Math.random()-0.5)*shake*12; sy=(Math.random()-0.5)*shake*10; }
ctx.save();
ctx.translate(sx, sy);
drawBackground();
drawGround();
for (const obs of obstacles) drawCactus(obs);
drawPlayer(playerY, !onGround || playerY<GROUND_Y);
ctx.restore();
drawUI();
drawGameOver();
if (gameState === STATE.WAIT) {
ctx.fillStyle = '#111';
ctx.font = 'bold 22px "Courier New", monospace';
ctx.textAlign = 'center';
ctx.fillText('点击屏幕 或 按空格键 开始', WIDTH/2, HEIGHT/2-10);
ctx.font = '14px "Courier New", monospace';
ctx.fillText('极简跑酷 · 黑白线条', WIDTH/2, HEIGHT/2+25);
}
}
// -------------------- 循环 --------------------
function loop(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
if (dt>0 && dt<0.5) update(dt);
render();
requestAnimationFrame(loop);
}
// -------------------- 输入 --------------------
function jumpAction() {
if (gameState === STATE.WAIT) {
reset();
if (onGround) { playerVy = JUMP_VEL; onGround = false; sfxJump(); }
return;
}
if (gameState === STATE.PLAY && onGround) {
playerVy = JUMP_VEL; onGround = false; sfxJump();
}
if (gameState === STATE.OVER && restartCD <= 0) {
reset();
}
}
window.addEventListener('keydown', e => {
if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') {
e.preventDefault();
jumpAction();
}
});
canvas.addEventListener('click', e => { e.preventDefault(); jumpAction(); });
canvas.addEventListener('touchstart', e => { e.preventDefault(); jumpAction(); }, {passive: false});
canvas.addEventListener('dblclick', e => e.preventDefault());
// 初始画面
function drawInit() {
ctx.clearRect(0,0,WIDTH,HEIGHT);
drawBackground();
drawGround();
drawPlayer(GROUND_Y, false);
drawUI();
ctx.fillStyle = '#111';
ctx.font = 'bold 22px "Courier New", monospace';
ctx.textAlign = 'center';
ctx.fillText('点击屏幕 或 按空格键 开始', WIDTH/2, HEIGHT/2-10);
ctx.font = '14px "Courier New", monospace';
ctx.fillText('极简跑酷 · 黑白线条', WIDTH/2, HEIGHT/2+25);
}
drawInit();
hintBar.style.opacity = '1';
hintBar.textContent = '[ 空格 / 点击 ] 跳跃';
lastTime = performance.now();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 🏃 极简线条跑酷 · 黑白版
Creator: RocketTiger92
Libraries: none
Complexity: complex (466 lines, 17.9 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-mspxnsc0" to link back to the original. Then publish at arcadelab.ai/publish.