🎮ArcadeLab

--ARCADELAB title:晚风跑酷·横屏道具版 description:横屏丝滑屋顶跑酷|纯手势操控:上滑跳、

by CrystalMeteor49
409 lines12.5 KB🛠️ p5.js (creative coding)
▶ Play
--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.