--ARCADELAB title:晚风跑酷·横屏道具版 description:横屏丝滑屋顶跑酷|纯手势操控:上滑跳、
by CrystalMeteor49409 lines12.5 KB🛠️ p5.js (creative coding)
--ARCADELAB
title:晚风跑酷·横屏道具版
description:横屏丝滑屋顶跑酷|纯手势操控:上滑跳、下滑蹲、左滑换左道、右滑换右道|磁铁/弹跳鞋/喷气背包|高清黄昏城市画质|背景音乐+音效
libraries:p5
emoji:🌆
color:#1a1428
-->
<!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">
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
html,body{width:100vw;height:100vh;overflow:hidden;background:#000;display:flex;align-items:center;justify-content:center;}
#game{width:100vw;height:56.25vw;max-height:100vh;position:relative;overflow:hidden;border:none;}
canvas{width:100%;height:100%;display:block;}
#ui{position:absolute;inset:0;pointer-events:none;color:#fff;font-family:system-ui,sans-serif;}
.scoreText{position:absolute;top:16px;left:16px;font-size:22px;font-weight:bold;text-shadow:0 0 6px #000;}
.musicBtn{pointer-events:auto;position:absolute;top:16px;right:16px;padding:8px 12px;background:rgba(0,0,0,0.55);border:2px solid #fff;border-radius:8px;font-size:15px;}
.tip{position:absolute;top:60px;left:50%;transform:translateX(-50%);padding:6px 14px;background:rgba(0,0,0,0.6);border-radius:6px;font-size:16px;display:none;}
.overPanel{pointer-events:auto;position:absolute;inset:0;background:rgba(0,0,0,0.75);display:none;flex-direction:column;align-items:center;justify-content:center;gap:16px;}
.overPanel h2{font-size:32px;margin:0;}
.overPanel p{font-size:18px;}
.restartBtn{padding:12px 24px;font-size:18px;background:#ff9922;border:none;border-radius:10px;font-weight:bold;}
</style>
</head>
<body>
<div id="game">
<div id="ui">
<div class="scoreText">金币:0|距离:0m</div>
<button class="musicBtn" id="musicBtn">音乐 开</button>
<div class="tip" id="tip"></div>
<div class="overPanel" id="overPanel">
<h2>撞啦!</h2>
<p id="resultText"></p>
<button class="restartBtn" id="restartBtn">重新开始</button>
</div>
</div>
</div>
<script>
let sketch = function(p){
//游戏基础
let lane = 1; //0左 1中 2右
let targetLane = 1;
let speed = 6;
let dist = 0;
let coinCount = 0;
let isJumping = false;
let jumpY = 0;
let jumpVel = 0;
let isSlide = false;
let slideTimer = 0;
let isFlying = false;
let flyTimer = 0;
let magnetTimer = 0;
let superJumpTimer = 0;
let obstacles = [];
let coins = [];
let items = [];
let bgOffset = 0;
let gameOver = false;
let started = false;
//手势
let touchStartX = 0;
let touchStartY = 0;
const SWIPE_THRESHOLD = 40;
//音频
let audioCtx;
let musicOn = true;
let musicPlaying = false;
let musicOsc,musicGain,musicTimer;
p.setup = function(){
let c = p.createCanvas(p.windowWidth, p.windowHeight * 0.5625);
c.parent("game");
p.frameRate(60);
p.noStroke();
resetGame();
//绑定按钮
document.getElementById("musicBtn").onclick = toggleMusic;
document.getElementById("restartBtn").onclick = ()=>{
document.getElementById("overPanel").style.display = "none";
resetGame();
};
//触摸手势
c.elt.addEventListener("touchstart",(e)=>{
if(gameOver) return;
let t = e.touches[0];
touchStartX = t.clientX;
touchStartY = t.clientY;
});
c.elt.addEventListener("touchend",(e)=>{
if(gameOver || !started) {started=true; startBGM(); return;}
let t = e.changedTouches[0];
let dx = t.clientX - touchStartX;
let dy = t.clientY - touchStartY;
if(Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy)){
if(dx < -SWIPE_THRESHOLD) targetLane = p.constrain(targetLane-1,0,2);
if(dx > SWIPE_THRESHOLD) targetLane = p.constrain(targetLane+1,0,2);
}else if(Math.abs(dy) > SWIPE_THRESHOLD && Math.abs(dy) > Math.abs(dx)){
if(dy < -SWIPE_THRESHOLD) doJump();
if(dy > SWIPE_THRESHOLD) doSlide();
}
});
};
function resetGame(){
lane=1;targetLane=1;speed=6;dist=0;coinCount=0;
isJumping=false;jumpY=0;jumpVel=0;isSlide=false;slideTimer=0;
isFlying=false;flyTimer=0;magnetTimer=0;superJumpTimer=0;
obstacles=[];coins=[];items=[];bgOffset=0;gameOver=false;
document.getElementById("overPanel").style.display = "none";
spawnInitial();
}
function spawnInitial(){
for(let i=0;i<12;i++) spawnSegment(i*220);
}
function spawnSegment(zPos){
let r = p.random();
let l = p.floor(p.random(3));
if(r<0.35){
obstacles.push({type:"barrier",lane:l,z:zPos,w:160,h:110});
}else if(r<0.65){
obstacles.push({type:"bar",lane:l,z:zPos,w:140,h:35});
}
//金币
for(let c=0;c<4;c++){
coins.push({lane:p.floor(p.random(3)),z:zPos + p.random(-80,180),y:0});
}
//道具随机
let ir = p.random();
if(ir<0.12){
let it = p.random(["magnet","superjump","jetpack"]);
items.push({type:it,lane:p.floor(p.random(3)),z:zPos + p.random(-60,120)});
}
}
function doJump(){
if(isJumping || isSlide || isFlying) return;
isJumping=true;
jumpVel = superJumpTimer>0 ? 22 : 14;
playTone(520,0.08,0.12);
}
function doSlide(){
if(isJumping || isSlide || isFlying) return;
isSlide=true;
slideTimer=28;
playTone(320,0.07,0.1);
}
p.draw = function(){
if(!started){
p.background(15,12,30);
p.fill(255);p.textAlign(p.CENTER,p.CENTER);p.textSize(26);
p.text("轻点屏幕开始|上滑跳 下滑蹲 左滑换左道 右滑换右道",p.width/2,p.height/2);
return;
}
if(gameOver){
return;
}
//更新进度
dist += speed;
bgOffset += speed*0.6;
if(dist > 300){
speed = p.min(speed+0.0025,11);
}
//跑道平滑换道
lane = p.lerp(lane, targetLane,0.14);
//跳跃物理
if(isJumping){
jumpY += jumpVel;
jumpVel -= 0.72;
if(jumpY <= 0){
jumpY=0;isJumping=false;jumpVel=0;
}
}
if(isSlide){
slideTimer--;
if(slideTimer<=0) isSlide=false;
}
//道具计时
if(magnetTimer>0) magnetTimer--;
if(superJumpTimer>0) superJumpTimer--;
if(isFlying){
flyTimer--;
jumpY = p.sin(p.frameCount*0.08)*8 + 90;
if(flyTimer<=0){
isFlying=false;jumpY=0;
}
}
//生成新路段
let lastZ = obstacles.length ? obstacles[obstacles.length-1].z : 0;
if(lastZ - dist < 700) spawnSegment(lastZ+220);
//清理远处物体
obstacles = obstacles.filter(o=>o.z - dist > -120);
coins = coins.filter(c=>c.z - dist > -120);
items = items.filter(it=>it.z - dist > -120);
//====渲染【高清黄昏画质】====
//天空渐变
let grad = p.drawingContext.createLinearGradient(0,0,0,p.height);
grad.addColorStop(0,"#191035");
grad.addColorStop(0.45,"#442b66");
grad.addColorStop(0.72,"#ff7b44");
grad.addColorStop(1,"#ffc270");
p.drawingContext.fillStyle = grad;
p.rect(0,0,p.width,p.height);
//远景城市剪影
p.fill("#0f0b22");
for(let x=-p.width;x<p.width*2;x+=110){
let h = p.noise(x*0.008 + bgOffset*0.0002)*140 + 60;
p.rect(x, p.height-h-40, 85, h);
}
//跑道透视
drawTrack();
//绘制障碍物
for(let o of obstacles){
let pj = project(o.lane, o.z-dist, 0);
if(!pj.vis) continue;
if(o.type=="barrier"){
p.fill("#3a3a48");
p.rect(pj.x - pj.w/2, pj.y-pj.h, pj.w, pj.h);
p.fill("#555566");
p.rect(pj.x - pj.w/2 + 4, pj.y-pj.h+4, pj.w-8, 14);
}else if(o.type=="bar"){
p.fill("#ffdd22");
p.rect(pj.x - pj.w/2, pj.y - pj.h/2, pj.w, pj.h);
}
//碰撞检测
if(checkCollision(lane,jumpY,isSlide,o)) gameEnd();
}
//绘制金币+磁铁吸附
for(let c of coins){
let pj = project(c.lane, c.z-dist, c.y);
if(!pj.vis) continue;
if(magnetTimer>0){
let dx = (lane - c.lane)*75;
let dz = (dist - c.z);
if(p.sqrt(dx*dx+dz*dz) < 180){
c.lane = p.lerp(c.lane,lane,0.07);
c.z = p.lerp(c.z,dist+40,0.07);
}
}
p.fill("#ffd700");
p.circle(pj.x, pj.y, pj.s*24);
if(pj.s>0.03 && p.dist(lane*75,0,c.lane*75,c.z-dist)<28){
coinCount++;
c.z = -9999;
playTone(880,0.06,0.09);
}
}
//绘制道具
for(let it of items){
let pj = project(it.lane, it.z-dist,0);
if(!pj.vis) continue;
if(it.type=="magnet") p.fill("#ff3366");
else if(it.type=="superjump") p.fill("#22ee66");
else if(it.type=="jetpack") p.fill("#33aaff");
p.circle(pj.x, pj.y, pj.s*32);
//拾取道具
if(p.dist(lane*75,0,it.lane*75,it.z-dist)<32){
if(it.type=="magnet"){magnetTimer=540;showTip("磁铁!自动吸金币");}
if(it.type=="superjump"){superJumpTimer=480;showTip("弹跳鞋!跳超高");}
if(it.type=="jetpack"){isFlying=true;flyTimer=420;showTip("喷气背包!升空!");}
it.z = -9999;
playTone(660,0.12,0.18);
}
}
//玩家小人示意
let pl = project(lane, 0, jumpY);
p.fill("#ffffff");
p.circle(pl.x, pl.y - (isSlide ? 8 : 0), pl.s*22 * (isSlide?0.6:1));
//UI文字
p.select(".scoreText").html(`金币:${coinCount}|距离:${p.floor(dist)}m`);
};
function drawTrack(){
//透视跑道网格
p.strokeWeight(1);
p.stroke("#384460");
p.noFill();
for(let z=0;z<900;z+=55){
let p0 = project(0,z,0);
let p1 = project(2,z,0);
if(p0.vis && p1.vis) p.line(p0.x,p0.y,p1.x,p1.y);
}
for(let la=0;la<=2;la++){
for(let z=0;z<900;z+=90){
let p0 = project(la,z,0);
let p1 = project(la,z+45,0);
if(p0.vis && p1.vis) p.line(p0.x,p0.y,p1.x,p1.y);
}
}
p.noStroke();
}
function project(lane,z,y){
const VP = 280;
let worldX = (lane-1)*75;
if(z < 10) return {vis:false};
let s = VP / z;
let sx = p.width/2 + worldX * s;
let sy = p.height*0.72 - y*s;
return {x:sx, y:sy, s:s, vis:true, w:160*s, h:110*s};
}
function checkCollision(pl,py,sl,obs){
if(isFlying) return false;
let ox = (obs.lane-1)*75;
let px = (pl-1)*75;
let dz = p.abs(obs.z - dist);
if(dz>45) return false;
if(p.abs(ox - px) > 45) return false;
//横杆 蹲可以过
if(obs.type=="bar"){
if(sl) return false;
return true;
}
//高墙:弹跳鞋跳高可以越过
if(obs.type=="barrier"){
if(py > 70 && superJumpTimer>0) return false;
return true;
}
return true;
}
function gameEnd(){
gameOver = true;
document.getElementById("resultText").innerText = `跑了 ${p.floor(dist)} 米,收集 ${coinCount} 金币`;
document.getElementById("overPanel").style.display = "flex";
playTone(180,0.2,0.35);
stopBGM();
}
function showTip(txt){
let t = document.getElementById("tip");
t.innerText = txt;
t.style.display = "block";
setTimeout(()=>t.style.display="none",1200);
}
//音频
function startBGM(){
if(musicPlaying || !musicOn) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
musicPlaying=true;
loopBGM();
}
function loopBGM(){
if(!musicOn || !musicPlaying) return;
playNote([220,262,330,392],0.32);
musicTimer = setTimeout(loopBGM, 1280);
}
function stopBGM(){
musicPlaying=false;
clearTimeout(musicTimer);
if(audioCtx) audioCtx.close();
}
function toggleMusic(){
musicOn = !musicOn;
document.getElementById("musicBtn").innerText = musicOn ? "音乐 开" : "音乐 关";
if(musicOn && started) startBGM();
else stopBGM();
}
function playTone(freq,atk,rel){
if(!audioCtx) return;
let o = audioCtx.createOscillator();
let g = audioCtx.createGain();
o.connect(g);g.connect(audioCtx.destination);
o.frequency.value = freq;
g.gain.setValueAtTime(0,audioCtx.currentTime);
g.gain.linearRampToValueAtTime(0.22,audioCtx.currentTime+atk);
g.gain.linearRampToValueAtTime(0,audioCtx.currentTime+atk+rel);
o.start();o.stop(audioCtx.currentTime+atk+rel+0.02);
}
function playNote(freqs, dur){
if(!audioCtx) return;
for(let f of freqs){
let o = audioCtx.createOscillator();
let g = audioCtx.createGain();
o.connect(g);g.connect(audioCtx.destination);
o.frequency.value = f;
g.gain.setValueAtTime(0.08,audioCtx.currentTime);
g.gain.linearRampToValueAtTime(0,audioCtx.currentTime+dur);
o.start();o.stop(audioCtx.currentTime+dur+0.02);
}
}
p.windowResized = function(){
p.resizeCanvas(p.windowWidth, p.windowHeight * 0.5625);
}
}
new p5(sketch);
</script>
</body>
</html>
Game Source: --ARCADELAB title:晚风跑酷·横屏道具版 description:横屏丝滑屋顶跑酷|纯手势操控:上滑跳、
Creator: CrystalMeteor49
Libraries: p5
Complexity: complex (409 lines, 12.5 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: arcadelab-title-description-crystalmeteor49" to link back to the original. Then publish at arcadelab.ai/publish.