JUMP简单跳一跳
by RocketTiger92748 lines30.9 KB
<!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">
<title>JUMP简单跳一跳</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 核心修改:改为纵向排列布局 */
html, body {
width: 100%;
min-height: 100vh; /* 保证能包裹下方额外元素 */
background: #E0E0E0;
font-family: "Comic Sans MS", "Marker Felt", "Chalkboard SE", "华文细黑", sans-serif;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
display: flex;
flex-direction: column; /* 垂直堆叠 */
justify-content: center;
align-items: center;
overflow-y: auto; /* 允许屏幕过短时滚动 */
padding: 20px 0; /* 顶部和底部留白,避免阴影被切 */
}
.game-container {
position: relative;
width: 90vw;
max-width: 850px;
height: 50vh;
max-height: 480px;
aspect-ratio: 16 / 9;
background-color: #ffffff;
/* 纸张横线 */
background-image: repeating-linear-gradient(
to bottom,
transparent,
transparent 49px,
#ebebeb 49px,
#ebebeb 50px
);
border: 3px solid #000000;
border-radius: 20px;
box-shadow: 8px 8px 0 #00000020;
overflow: hidden;
touch-action: none;
flex-shrink: 0; /* 防止被压缩 */
}
.game-wrapper:active { transform: scale(0.995); }
#gameCanvas {
display: block;
width: 100%;
height: 100%;
background: transparent;
touch-action: none;
}
/* 遮罩层 */
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.85);
z-index: 10;
text-align: center;
cursor: pointer;
border-radius: 18px;
}
.overlay.hidden { display: none; }
.overlay h1 {
font-size: 48px;
font-weight: bold;
margin-bottom: 20px;
letter-spacing: 4px;
color: #000000;
}
.overlay h2 {
font-size: 36px;
font-weight: bold;
margin-bottom: 15px;
color: #000000;
}
.overlay p {
font-size: 16px;
margin: 6px 0;
color: #333333;
}
.overlay .tip {
margin-top: 20px;
font-size: 15px;
color: #666666;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.score-display {
position: absolute;
top: 20px;
left: 20px;
background: #ffffff;
border: 2px solid #000000;
border-radius: 20px;
padding: 0 18px;
font-size: 28px;
font-weight: bold;
color: #000000;
z-index: 5;
min-width: 30px;
text-align: center;
line-height: 1.4;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 核心修改:蓄力条移除绝对定位,放到画框外面 */
.charge-bar {
position: relative; /* 改为普通文档流 */
margin-top: 25px; /* 与画框保持间距 */
width: 200px;
height: 10px;
border: 2px solid #000000;
border-radius: 5px;
background: #ffffff;
opacity: 0;
transition: opacity 0.1s;
flex-shrink: 0;
align-self: center;
}
.charge-bar.active { opacity: 1; }
.charge-fill {
width: 0%;
height: 100%;
background: #000000;
border-radius: 3px;
transition: width 0.05s linear;
}
.shake {
animation: canvasShake 0.2s ease-in-out;
}
@keyframes canvasShake {
0%, 100% { transform: translate(0, 0); }
25% { transform: translate(-2px, -2px); }
75% { transform: translate(2px, 2px); }
}
@media (max-width: 768px) {
.overlay h1 { font-size: 32px; }
.overlay h2 { font-size: 26px; }
.overlay p { font-size: 13px; }
.score-display { font-size: 22px; padding: 0 14px; top: 12px; left: 12px; }
.charge-bar { width: 150px; margin-top: 15px; }
}
</style>
</head>
<body>
<!-- 游戏画框 -->
<div class="game-container">
<canvas id="gameCanvas"></canvas>
<div id="startScreen" class="overlay">
<h1>简单跳一跳</h1>
<p>长按 W 键 / 按住屏幕 蓄力</p>
<p>松开跳跃,落在柱子上方</p>
<p class="tip">点击 或 按空格键开始</p>
</div>
<div id="gameOverScreen" class="overlay hidden">
<h2>游戏结束</h2>
<p>得分:<span id="finalScore">0</span></p>
<p>最高分:<span id="bestScore">0</span></p>
<p class="tip">点任意处重新开始</p>
</div>
<div id="scoreDisplay" class="score-display">0</div>
</div>
<!-- 蓄力进度条(移到画框正下方) -->
<div id="chargeBar" class="charge-bar">
<div id="chargeFill" class="charge-fill"></div>
</div>
<script>
const CONFIG = {
gravity: 0.6,
maxChargeTime: 1000,
minJumpPower: 7,
maxJumpPower: 16,
jumpAngle: -45,
pillarMinWidth: 27,
pillarMaxWidth: 90,
pillarMinGap: 60,
pillarMaxGap: 180,
pillarMinHeight: 160,
pillarMaxHeight: 100,
playerWidth: 20,
playerHeight: 40,
cameraSmooth: 0.08,
};
const GameState = {
MENU: 'menu',
PLAYING: 'playing',
CHARGING: 'charging',
JUMPING: 'jumping',
GAMEOVER: 'gameover'
};
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.startScreen = document.getElementById('startScreen');
this.gameOverScreen = document.getElementById('gameOverScreen');
this.scoreDisplay = document.getElementById('scoreDisplay');
this.finalScoreEl = document.getElementById('finalScore');
this.bestScoreEl = document.getElementById('bestScore');
this.chargeBar = document.getElementById('chargeBar');
this.chargeFill = document.getElementById('chargeFill');
this.state = GameState.MENU;
this.score = 0;
this.bestScore = parseInt(localStorage.getItem('jumpBestScore')) || 0;
this.cameraX = 0;
this.targetCameraX = 0;
this.cameraLocked = true;
this.chargeStartTime = 0;
this.chargePower = 0;
this.player = null;
this.pillars = [];
this.audioContext = null;
// 记录缩放比例与逻辑宽高
this.scale = 1;
this.logicalWidth = 0;
this.logicalHeight = 0;
this.dpr = 1;
this.init();
}
init() {
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
this.bindEvents();
this.initAudio();
this.gameLoop();
}
// ========== 高清渲染且不破坏坐标 ==========
resizeCanvas() {
this.dpr = window.devicePixelRatio || 1;
const rect = this.canvas.parentElement.getBoundingClientRect();
this.logicalWidth = rect.width;
this.logicalHeight = rect.height;
this.canvas.width = this.logicalWidth * this.dpr;
this.canvas.height = this.logicalHeight * this.dpr;
this.canvas.style.width = this.logicalWidth + 'px';
this.canvas.style.height = this.logicalHeight + 'px';
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.scale(this.dpr, this.dpr);
this.scale = this.logicalWidth / 850;
}
bindEvents() {
const startGameHandler = () => {
if (this.state === GameState.MENU || this.state === GameState.GAMEOVER) this.startGame();
};
this.startScreen.addEventListener('click', startGameHandler);
this.gameOverScreen.addEventListener('click', startGameHandler);
this.startScreen.addEventListener('touchstart', (e) => {
e.preventDefault();
startGameHandler();
}, { passive: false });
this.gameOverScreen.addEventListener('touchstart', (e) => {
e.preventDefault();
startGameHandler();
}, { passive: false });
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (this.state === GameState.MENU || this.state === GameState.GAMEOVER) startGameHandler();
}
if (e.code === 'KeyW' && this.state === GameState.PLAYING) {
e.preventDefault();
this.startCharge();
}
});
document.addEventListener('keyup', (e) => {
if (e.code === 'KeyW' && this.state === GameState.CHARGING) {
e.preventDefault();
this.releaseJump();
}
});
const startChargeHandler = (e) => {
e.preventDefault();
if (this.state === GameState.PLAYING) this.startCharge();
};
this.canvas.addEventListener('mousedown', startChargeHandler);
this.canvas.addEventListener('touchstart', startChargeHandler, { passive: false });
const releaseHandler = (e) => {
e.preventDefault();
if (this.state === GameState.CHARGING) this.releaseJump();
};
document.addEventListener('mouseup', releaseHandler);
this.canvas.addEventListener('touchend', releaseHandler, { passive: false });
}
initAudio() {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.log('Web Audio API not supported');
}
}
playSound(type) {
if (!this.audioContext) return;
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
switch(type) {
case 'charge':
oscillator.frequency.setValueAtTime(200, this.audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(600, this.audioContext.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.1, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1);
oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.1); break;
case 'jump':
oscillator.frequency.setValueAtTime(400, this.audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(800, this.audioContext.currentTime + 0.15);
gainNode.gain.setValueAtTime(0.15, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15);
oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.15); break;
case 'land':
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(150, this.audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(80, this.audioContext.currentTime + 0.08);
gainNode.gain.setValueAtTime(0.12, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.08);
oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.08); break;
case 'gameover':
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(300, this.audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(100, this.audioContext.currentTime + 0.4);
gainNode.gain.setValueAtTime(0.15, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.4);
oscillator.start(); oscillator.stop(this.audioContext.currentTime + 0.4); break;
}
}
vibrate(duration = 50) {
if (navigator.vibrate) navigator.vibrate(duration);
this.canvas.classList.add('shake');
setTimeout(() => this.canvas.classList.remove('shake'), 200);
}
startGame() {
this.score = 0;
this.cameraX = 0;
this.targetCameraX = 0;
this.cameraLocked = true;
this.pillars = [];
this.state = GameState.PLAYING;
this.startScreen.classList.add('hidden');
this.gameOverScreen.classList.add('hidden');
this.scoreDisplay.textContent = '0';
this.generateFirstPillar();
const firstPillar = this.pillars[0];
this.player = {
x: firstPillar.x + firstPillar.width / 2 - CONFIG.playerWidth * this.scale / 2,
y: firstPillar.topY - CONFIG.playerHeight * this.scale,
vx: 0, vy: 0,
width: CONFIG.playerWidth * this.scale,
height: CONFIG.playerHeight * this.scale,
squash: 1,
currentPillarIndex: 0
};
this.generateNextPillar();
if (this.audioContext && this.audioContext.state === 'suspended') this.audioContext.resume();
}
generateFirstPillar() {
const baseY = this.logicalHeight;
const height = 120 * this.scale;
this.pillars.push({
x: 80 * this.scale,
topY: baseY - height,
width: 30 * this.scale,
height: height,
bottomY: baseY
});
}
generateNextPillar() {
const lastPillar = this.pillars[this.pillars.length - 1];
const baseY = this.logicalHeight;
const gap = (CONFIG.pillarMinGap + Math.random() * (CONFIG.pillarMaxGap - CONFIG.pillarMinGap)) * this.scale;
const width = (CONFIG.pillarMinWidth + Math.random() * (CONFIG.pillarMaxWidth - CONFIG.pillarMinWidth)) * this.scale;
const height = (CONFIG.pillarMinHeight + Math.random() * (CONFIG.pillarMaxHeight - CONFIG.pillarMinHeight)) * this.scale;
const topY = baseY - height;
this.pillars.push({
x: lastPillar.x + lastPillar.width + gap,
topY: topY,
width: width,
height: height,
bottomY: baseY
});
}
startCharge() {
this.state = GameState.CHARGING;
this.cameraLocked = true;
this.chargeStartTime = performance.now();
this.chargePower = 0;
this.chargeBar.classList.add('active');
this.playSound('charge');
this.vibrate(30);
}
releaseJump() {
const chargeTime = performance.now() - this.chargeStartTime;
const chargeRatio = Math.min(chargeTime / CONFIG.maxChargeTime, 1);
const power = CONFIG.minJumpPower + (CONFIG.maxJumpPower - CONFIG.minJumpPower) * chargeRatio;
const angleRad = CONFIG.jumpAngle * Math.PI / 180;
this.player.vx = Math.cos(angleRad) * power * this.scale * 0.8;
this.player.vy = Math.sin(angleRad) * power * this.scale * 0.8;
this.state = GameState.JUMPING;
this.cameraLocked = false;
this.chargeBar.classList.remove('active');
this.chargeFill.style.width = '0%';
this.player.squash = 1;
this.playSound('jump');
}
updateCharge() {
const chargeTime = performance.now() - this.chargeStartTime;
const chargeRatio = Math.min(chargeTime / CONFIG.maxChargeTime, 1);
this.chargePower = chargeRatio;
this.chargeFill.style.width = (chargeRatio * 100) + '%';
this.player.squash = 1 - chargeRatio * 0.2;
}
updatePlayer() {
if (this.state !== GameState.JUMPING) return;
this.player.vy += CONFIG.gravity * this.scale;
this.player.x += this.player.vx;
this.player.y += this.player.vy;
this.checkCollisions();
this.targetCameraX = this.player.x - this.logicalWidth * 0.3;
}
checkCollisions() {
const player = this.player;
const playerBottom = player.y + player.height;
const playerLeft = player.x;
const playerRight = player.x + player.width;
for (let i = 0; i < this.pillars.length; i++) {
const pillar = this.pillars[i];
if (pillar.x + pillar.width < player.x - 50 * this.scale) continue;
if (pillar.x > player.x + 300 * this.scale) break;
if (playerRight > pillar.x && playerLeft < pillar.x + pillar.width) {
if (player.vy > 0 && playerBottom >= pillar.topY && playerBottom <= pillar.topY + 20 * this.scale) {
this.landOnPillar(i, pillar);
return;
}
if (playerBottom > pillar.topY + 10 * this.scale) {
if (playerRight > pillar.x && playerLeft < pillar.x && player.x + player.width / 2 < pillar.x) {
this.gameOver();
return;
}
}
}
}
if (player.y > this.logicalHeight + 100 * this.scale) this.gameOver();
}
landOnPillar(pillarIndex, pillar) {
if (pillarIndex > this.player.currentPillarIndex) {
this.score += pillarIndex - this.player.currentPillarIndex;
this.scoreDisplay.textContent = this.score;
this.player.currentPillarIndex = pillarIndex;
this.generateNextPillar();
}
this.player.y = pillar.topY - this.player.height;
this.player.vx = 0;
this.player.vy = 0;
this.state = GameState.PLAYING;
this.cameraLocked = true;
this.player.squash = 0.7;
setTimeout(() => { if (this.player) this.player.squash = 1; }, 100);
this.playSound('land');
this.vibrate(40);
}
gameOver() {
this.state = GameState.GAMEOVER;
this.cameraLocked = true;
if (this.score > this.bestScore) {
this.bestScore = this.score;
localStorage.setItem('jumpBestScore', this.bestScore.toString());
}
this.finalScoreEl.textContent = this.score;
this.bestScoreEl.textContent = this.bestScore;
this.gameOverScreen.classList.remove('hidden');
this.playSound('gameover');
this.vibrate(200);
}
updateCamera() {
if (!this.cameraLocked) {
this.cameraX += (this.targetCameraX - this.cameraX) * CONFIG.cameraSmooth;
}
if (this.cameraX < 0) this.cameraX = 0;
}
update() {
if (this.state === GameState.CHARGING) this.updateCharge();
if (this.state === GameState.JUMPING) this.updatePlayer();
this.updateCamera();
}
// ======================== 绘制区域 ========================
drawPlayer() {
if (!this.player) return;
const ctx = this.ctx;
const p = this.player;
const screenX = p.x - this.cameraX;
const squash = p.squash || 1;
const width = p.width;
const height = p.height * squash;
const baseY = p.y + p.height - height;
const centerX = screenX + width / 2;
ctx.save();
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2 * this.scale;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const headRadius = 11 * this.scale;
const headY = baseY + headRadius;
ctx.beginPath();
ctx.arc(centerX, headY, headRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX + 1 * this.scale, headY + 3 * this.scale, 4 * this.scale, 0, Math.PI);
ctx.stroke();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(centerX - 4 * this.scale, headY - 2 * this.scale, 1.5 * this.scale, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(centerX + 6 * this.scale, headY - 2 * this.scale, 1.5 * this.scale, 0, Math.PI * 2);
ctx.fill();
const bodyTop = headY + headRadius + 2 * this.scale;
const bodyBottom = baseY + height * 0.7;
ctx.beginPath();
ctx.moveTo(centerX, bodyTop);
ctx.lineTo(centerX, bodyBottom);
ctx.stroke();
const armY = bodyTop + (bodyBottom - bodyTop) * 0.3;
const armLength = 8 * this.scale;
ctx.beginPath();
if (this.state === GameState.CHARGING) {
ctx.moveTo(centerX, armY); ctx.lineTo(centerX - armLength, armY - 8 * this.scale);
ctx.moveTo(centerX, armY); ctx.lineTo(centerX + armLength, armY - 8 * this.scale);
} else if (this.state === GameState.JUMPING) {
ctx.moveTo(centerX, armY); ctx.lineTo(centerX - armLength - 2 * this.scale, armY - 4 * this.scale);
ctx.moveTo(centerX, armY); ctx.lineTo(centerX + armLength + 2 * this.scale, armY - 4 * this.scale);
} else {
ctx.moveTo(centerX, armY); ctx.lineTo(centerX - armLength, armY + 5 * this.scale);
ctx.moveTo(centerX, armY); ctx.lineTo(centerX + armLength, armY + 5 * this.scale);
}
ctx.stroke();
const legTop = bodyBottom;
const legBottom = baseY + height;
const legSpread = 5 * this.scale;
ctx.beginPath();
if (this.state === GameState.JUMPING) {
ctx.moveTo(centerX, legTop); ctx.lineTo(centerX - legSpread, legBottom - 5 * this.scale);
ctx.moveTo(centerX, legTop); ctx.lineTo(centerX + legSpread, legBottom - 5 * this.scale);
} else {
ctx.moveTo(centerX, legTop); ctx.lineTo(centerX - legSpread, legBottom);
ctx.moveTo(centerX, legTop); ctx.lineTo(centerX + legSpread, legBottom);
}
ctx.stroke();
ctx.restore();
}
drawPillars() {
const ctx = this.ctx;
ctx.save();
ctx.strokeStyle = '#000000';
ctx.fillStyle = '#ffffff';
ctx.lineWidth = 2 * this.scale;
for (const pillar of this.pillars) {
const screenX = pillar.x - this.cameraX;
// 修复了此处:将 this.canvas.width 替换为逻辑宽度 this.logicalWidth
if (screenX + pillar.width < -50 * this.scale || screenX > this.logicalWidth + 50 * this.scale) continue;
const cX = screenX + pillar.width / 2;
// 1. 画主体柱子
ctx.beginPath();
ctx.rect(screenX, pillar.topY, pillar.width, pillar.height);
ctx.fill();
ctx.stroke();
// 2. 中间竖线 (没到顶)
ctx.save(); // 保存当前线宽状态
ctx.lineWidth = 2.5; // 修改粗细:1=更细,3=更粗,默认你柱子边框是2
const lineTop = pillar.topY + 10 * this.scale;
ctx.beginPath();
ctx.moveTo(cX, lineTop);
ctx.lineTo(cX, pillar.bottomY);
ctx.stroke();
ctx.restore(); // 恢复原本的线宽,不影响后面的线条
// ======= 3. 链锯一样的锯齿边缘 =======
const sawWidth = 6 * this.scale; // 锯齿宽度
const sawHeight = 8 * this.scale; // 锯齿高度
const gap = 14 * this.scale; // 间距
let yPos = pillar.bottomY - 20 * this.scale;
let count = 0;
// 循环生成链锯般错综的小方块
while (yPos > pillar.topY + 4 * this.scale && count < 10) {
// 错位排列(像链锯齿交错)
let offsetLeft = (count % 2 === 0) ? 0 : (4 * this.scale);
let offsetRight = (count % 2 === 0) ? (4 * this.scale) : 0;
// 左侧锯齿块
ctx.fillRect(screenX - sawWidth - offsetLeft, yPos - sawHeight/2, sawWidth, sawHeight);
ctx.strokeRect(screenX - sawWidth - offsetLeft, yPos - sawHeight/2, sawWidth, sawHeight);
// 右侧锯齿块(和左侧错开)
ctx.fillRect(screenX + pillar.width + offsetRight, yPos - sawHeight/2, sawWidth, sawHeight);
ctx.strokeRect(screenX + pillar.width + offsetRight, yPos - sawHeight/2, sawWidth, sawHeight);
yPos -= gap;
count++;
}
}
ctx.restore();
}
render() {
const ctx = this.ctx;
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, this.logicalWidth, this.logicalHeight);
ctx.clip();
ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
if (this.state === GameState.MENU) {
ctx.restore();
return;
}
this.drawPillars();
this.drawPlayer();
ctx.restore();
}
gameLoop() {
this.update();
this.render();
requestAnimationFrame(() => this.gameLoop());
}
}
window.addEventListener('load', () => {
new Game();
});
</script>
</body>
</html>Game Source: JUMP简单跳一跳
Creator: RocketTiger92
Libraries: none
Complexity: complex (748 lines, 30.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" to link back to the original. Then publish at arcadelab.ai/publish.