AI生成2D跳跃跑酷游戏|开发全流程总结帖
哈喽各位开发者小伙伴~ 最近用AI豆包辅助完成了一款轻量2D跳跃跑酷小游戏,纯原生HTML5+Canvas+JavaScript开发,无任何框架依赖,浏览器打开就能玩!今天整理了完整的开发总结,从需求梳理到最终交付,全程AI参与逻辑搭建、代码生成和细节优化,新手也能轻松复刻,话不多说,直接上干货👇一、项目概况:AI助力,快速落地轻量小游戏
本次项目核心是开发一款「易上手、轻量级、有基础可玩性」的2D跑酷游戏,全程由AI主导需求拆解、架构设计、代码生成和BUG修复,人工仅做需求确认和细节微调,真正实现「高效开发、低门槛落地」。
🎮 游戏核心玩法:角色固定左侧,仅支持小跳(鼠标点击)、大跳(空格键)躲避障碍,红旗从右侧解锁移动,与角色重合即通关,共8个关卡,逐关提升难度,兼顾趣味性和容错性。
💻 技术栈:HTML5(结构)+ Canvas 2D(渲染)+ 原生JavaScript(逻辑)+ CSS3(样式),纯原生无依赖,无需额外配置,复制代码保存为HTML即可运行。
⏱️ 开发周期:短平快落地,从需求提出到最终测试交付,全程AI高效输出,大幅缩短开发时间,避免人工编写重复代码的繁琐。
二、AI主导的开发全流程(附核心亮点)
整个开发过程遵循「需求梳理→架构设计→功能开发→样式优化→细节迭代→测试交付」的逻辑,AI全程把控每一个环节,既保证代码规范性,又兼顾游戏体验。
- 需求梳理:AI精准拆解核心需求,明确开发边界
初期仅提出「做一款2D跳跃跑酷,有障碍、有关卡、有通关条件」的基础需求,AI快速拆解出具体功能清单和玩法定位,避免需求模糊导致的开发返工:
- 核心玩法:固定角色+双跳跃模式+障碍躲避+红旗通关,新手易上手,无复杂操作;
- 基础功能:关卡系统(8关)、生命值系统、障碍系统(3种伤害类型)、界面系统(5个核心界面)、操作提示;
- 视觉要求:简洁统一,黑框白字清晰可读,按钮有交互动效,适配不同PC屏幕。
亮点:AI自动补充需求细节,比如「非强制无伤通关」「每关提升生命值上限」等容错设计,贴合新手玩家体验。
- 架构设计:AI模块化拆解,代码解耦易维护
AI采用「分层设计+参数集中管理」的思路,搭建清晰的项目架构,避免代码混乱,后续修改和迭代更便捷:
- 结构分层:游戏容器→画布渲染层→UI面板→过场界面,层级清晰,互不干扰;
- 参数集中:通过全局CONFIG常量,统一管理所有游戏参数(画布尺寸、角色属性、障碍伤害、关卡规则等),修改参数无需改动业务逻辑;
- 状态管理:单gameState变量控制游戏全状态(开始、规则、游戏中、通关、失败),避免逻辑冲突。
亮点:AI采用面向对象思路封装游戏对象(玩家、障碍、红旗),每个对象独立初始化、更新、绘制,代码复用性高,调试时可精准定位问题。
- 功能开发:AI自动生成核心代码,全程无需人工编写重复逻辑
核心功能由AI逐模块生成,从画布初始化到游戏主循环,从碰撞检测到界面跳转,每一行代码都有清晰注释,人工仅需确认功能是否符合预期:
- 渲染模块:Canvas 2D绘制背景、角色、障碍、红旗,基于requestAnimationFrame实现稳定主循环,帧率60fps无卡顿;
- 物理与操作:AI实现跳跃抛物线物理逻辑(初速度+重力加速度),添加防连跳判断,避免误操作;
- 核心逻辑:障碍随机生成、碰撞检测(矩形检测,高效简易)、红旗解锁与通关判断、关卡切换与生命值更新;
- 交互模块:自动绑定所有按钮事件、鼠标/键盘操作事件,实现界面无缝跳转。
亮点:AI自动处理边缘场景,比如「障碍超出画布自动删除」「碰撞后避免重复扣血」「红旗未解锁时不显示」等,减少人工调试成本。
- 样式优化与细节迭代:AI响应反馈,快速修复问题
初期开发完成后,反馈「返回开始界面按钮未显示」「规则界面文字挤压」「按钮样式不统一」等问题,AI快速响应,精准修复,同时优化视觉体验:
- 样式优化:统一按钮样式(渐变背景、hover缩放动效),文字添加黑框白字样式,规则界面采用深色半透明背景,提升可读性;
- BUG修复:修复按钮显示/响应问题、碰撞检测误判、连跳BUG等,全程无需人工定位代码问题;
- 细节补充:添加大跳操作提示、关卡开始脉冲提示,规则界面新增「障碍物尺寸与伤害关联」提示,优化玩家指引;
- 响应式适配:使用CSS clamp()函数,适配不同PC屏幕,避免界面错乱。
- 测试交付:AI输出完整可运行代码,附带测试指南
开发完成后,AI输出完整的HTML文件(CSS+JS内嵌),附带详细的测试维度和图片尺寸规范,人工仅需进行简单的功能验证:
- 测试维度:功能测试(操作、界面、关卡)、兼容性测试(主流浏览器)、体验测试(帧率、难度)、鲁棒性测试(极端操作);
- 资源规范:为所有图片(背景、角色、障碍、过场界面)定义最佳像素尺寸,替换图片即可使用,无需调整代码;
- 交付物:单一HTML文件,无任何依赖,直接浏览器打开运行,便于分享和二次开发。
三、AI开发小游戏的优势与感悟
✅ 核心优势
- 高效快捷:无需人工编写重复代码(如主循环、碰撞检测、事件绑定),大幅缩短开发周期,新手也能快速落地项目;
- 逻辑严谨:AI自动处理边缘场景和异常逻辑,减少BUG产生,调试成本低;
- 易于维护:代码结构清晰、模块化强、注释完整,后续修改参数、新增功能都很便捷;
- 门槛极低:无需熟练掌握Canvas、物理逻辑等知识点,只要明确需求,AI就能完成核心开发。
💡 开发感悟
本次项目全程由AI主导,深刻感受到AI在轻量游戏开发中的优势——它能快速将模糊的需求转化为具体的代码,自动规避很多人工容易忽略的细节(如边缘场景处理、代码解耦)。对于新手来说,这不仅是一个可直接运行的游戏项目,更是一个优质的学习案例,通过阅读AI生成的代码,能快速理解Canvas渲染、游戏主循环、碰撞检测等核心知识点。
同时,AI也并非万能,它需要清晰的需求指引,遇到模糊的需求时,需要人工补充确认;后续若想提升游戏体验(如添加音效、动画、更多关卡),也可基于AI生成的代码进行二次扩展。
附上游戏核心代码(可直接复制运行),感兴趣的小伙伴可以自行下载测试、二次开发~ 有任何问题,也可以评论区交流,AI也能快速帮忙解决哦!✨
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>2D跳跃跑酷 - Runrunrun</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: linear-gradient(to bottom, #1a2980, #26d0ce);
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
padding: 10px;
}
#gameContainer {
position: relative;
width: min(100%, 800px);
height: min(calc(100vh - 20px), 500px);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border-radius: 10px;
overflow: hidden;
background-color: #1a1a1a;
cursor: pointer;
}
#gameCanvas {
display: block;
width: 100%;
height: 100%;
}
/* 核心样式:所有文字的黑色背景框+白色文字 */
.text-bg {
background: rgba(0, 0, 0, 0.8) !important;
color: #ffffff !important;
padding: 6px 10px !important;
border-radius: 6px !important;
display: inline-block !important;
line-height: 1.5 !important;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5) !important;
}
.text-bg-block {
background: rgba(0, 0, 0, 0.8) !important;
color: #ffffff !important;
padding: 8px 12px !important;
border-radius: 8px !important;
display: block !important;
width: 100%;
max-width: 600px;
}
/* UI面板样式 */
#uiPanel {
position: absolute;
top: 10px;
left: 10px;
font-size: clamp(12px, 2vw, 16px);
z-index: 10;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
#levelDisplay {
position: absolute;
top: 10px;
right: 10px;
font-size: clamp(12px, 2vw, 16px);
z-index: 10;
}
/* 过场界面通用样式 */
.screen-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 20;
padding: 20px;
gap: 20px; /* 增大间距,确保按钮显示完整 */
}
/* 规则界面深色背景 */
#rulesScreen {
display: none;
background: rgba(20, 20, 20, 0.95); /* 深色背景 */
}
#startScreen { display: flex; background: url('start.png') no-repeat center/cover; }
#gameOverScreen { display: none; background: url('fail.png') no-repeat center/cover; }
#levelCompleteScreen { display: none; background: url('success.png') no-repeat center/cover; }
#levelStartScreen { display: none; }
/* 标题样式 */
h1 {
font-size: clamp(24px, 8vw, 48px);
margin: 0;
text-align: center;
}
h2 {
font-size: clamp(20px, 6vw, 36px);
margin: 0;
text-align: center;
}
/* 普通文字 */
p {
font-size: clamp(14px, 3vw, 20px);
margin: 0;
max-width: 100%;
text-align: center;
}
/* 按钮通用样式(确保所有按钮显示正常) */
.game-btn {
background: linear-gradient(to bottom, #FF5722, #E64A19);
color: white !important;
border: none;
padding: clamp(10px, 3vw, 15px) clamp(20px, 6vw, 30px);
font-size: clamp(16px, 3vw, 20px);
border-radius: 50px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
min-width: 150px; /* 增大最小宽度,确保按钮不被压缩 */
margin-top: 15px;
display: block; /* 块级显示,确保独占一行 */
}
.game-btn:hover { transform: scale(1.05); box-shadow: 0 7px 20px rgba(0, 0, 0, 0.4); }
/* 血条 */
.health-bar {
width: clamp(100px, 20vw, 150px);
height: 15px;
background: #333;
border-radius: 10px;
overflow: hidden;
margin-top: 5px;
}
.health-fill {
height: 100%;
background: linear-gradient(to right, #ff0000, #ff8c00);
border-radius: 10px;
transition: width 0.3s;
}
/* 障碍说明 */
.obstacle-info {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
margin-top: 10px;
}
.obstacle-item {
display: flex;
align-items: center;
gap: 5px;
margin: 5px 0;
font-size: clamp(12px, 2vw, 14px);
}
.obstacle-img { width: 15px; height: 15px; object-fit: cover; border-radius: 2px; }
/* 大跳提示 */
#jumpIndicator {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-size: clamp(12px, 2vw, 14px);
z-index: 10;
display: none;
white-space: nowrap;
}
/* 开始提示 */
#clickToStart {
font-size: clamp(16px, 4vw, 24px);
animation: pulse 2s infinite;
text-align: center;
}
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="500"></canvas>
<!-- 血条区域 -->
<div id="uiPanel">
<div class="text-bg">生命值: <span id="healthValue">100</span></div>
<div class="health-bar">
<div class="health-fill" id="healthBar" style="width: 100%"></div>
</div>
</div>
<!-- 关卡数 -->
<div id="levelDisplay" class="text-bg">关卡: <span id="levelNumber">1</span>/8</div>
<!-- 大跳提示 -->
<div id="jumpIndicator" class="text-bg">空格键触发大跳!</div>
<!-- 开始界面 -->
<div id="startScreen" class="screen-overlay">
<h1 class="text-bg">2D跳跃跑酷</h1>
<p class="text-bg-block">角色固定左侧,红旗从最右侧左移,重合即通关!</p>
<!-- 规则按钮(统一类名) -->
<button id="rulesButton" class="game-btn">查看游戏规则</button>
<!-- 障碍说明 -->
<div class="obstacle-info">
<div class="obstacle-item text-bg"><img src="obs1.png" class="obstacle-img"><span>普通障碍 (-10血)</span></div>
<div class="obstacle-item text-bg"><img src="obs2.png" class="obstacle-img"><span>危险障碍 (-20血)</span></div>
<div class="obstacle-item text-bg"><img src="obs3.png" class="obstacle-img"><span>致命障碍 (-30血)</span></div>
<div class="obstacle-item text-bg" style="margin-left:20px;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="red">
<path d="M12 0L14 6H24L16 10L18 16L12 12L6 16L8 10L0 6H10L12 0Z"/>
</svg>
<span>终点红旗(重合即通关)</span>
</div>
</div>
<button id="startButton" class="game-btn">开始游戏</button>
</div>
<!-- 游戏规则界面(深色背景 + 新增提示语) -->
<div id="rulesScreen" class="screen-overlay">
<h2 class="text-bg">游戏规则</h2>
<div class="text-bg-block" style="text-align: left; max-height: 60vh; overflow-y: auto; line-height: 1.8;">
<p><strong>1. 操作方式</strong></p>
<p>• 鼠标点击画布:小跳(高度适中,精准越过单个障碍)</p>
<p>• 按空格键:大跳(高度适配,缓慢下落越过连续障碍)</p>
<br>
<p><strong>2. 通关条件</strong></p>
<p>• 当红旗距离≤20米时,会从最右侧出现并向左移动</p>
<p>• 红旗与左侧角色重合即通关当前关卡</p>
<br>
<p><strong>3. 失败条件</strong></p>
<p>• 碰到障碍物会扣血,生命值耗尽则游戏结束</p>
<p>• 不同障碍扣血量不同:普通障碍-10血,危险障碍-20血,致命障碍-30血</p>
<br>
<p><strong>4. 关卡机制</strong></p>
<p>• 共8个关卡,每关红旗初始距离递增20米</p>
<p>• 每关通关后生命值上限会提升20点</p>
<br>
<p><strong>💡 游戏提示</strong></p>
<p>越大越长的障碍物伤害越高哦~所以并非必须无伤通关,选择最优受伤策略通关即可!</p>
</div>
<!-- 返回按钮(确保显示,统一类名) -->
<button id="backToStartButton" class="game-btn">返回开始界面</button>
</div>
<!-- 关卡开始界面 -->
<div id="levelStartScreen" class="screen-overlay">
<h2 class="text-bg">第 <span id="currentLevelNum">1</span> 关</h2>
<p id="levelDescription" class="text-bg-block">红旗初始距离20米,≤20米时从最右侧出现!</p>
<div id="clickToStart" class="text-bg">鼠标点击以开始</div>
</div>
<!-- 游戏结束界面 -->
<div id="gameOverScreen" class="screen-overlay">
<h2 class="text-bg">游戏结束!</h2>
<p class="text-bg-block">你完成了 <span id="finalLevel">0</span> 个关卡</p>
<button id="restartButton" class="game-btn">重新开始</button>
</div>
<!-- 关卡完成界面 -->
<div id="levelCompleteScreen" class="screen-overlay">
<h2 class="text-bg">关卡完成!</h2>
<p class="text-bg-block">红旗与角色重合,成功通关本关!</p>
<p class="text-bg-block">进入第 <span id="nextLevel">2</span> 关</p>
<p class="text-bg-block">生命值上限提升至 <span id="newMaxHealth">120</span></p>
<button id="nextLevelButton" class="game-btn">继续</button>
</div>
</div>
<script>
// 游戏核心配置
const CONFIG = {
totalLevels: 8,
pixelPerMeter: 5,
background: { groundY: 420, imgSrc: 'bg.png' },
flag: {
initDistanceBase: 20,
initDistanceStep: 20,
showDistance: 20,
reduceSpeed: 6,
moveSpeed: 4,
poleWidth: 6,
poleHeight: 120,
flagWidth: 40,
flagHeight: 30
},
obstacle: {
moveSpeed: 4,
generateProb: 0.008,
minSpace: 50,
groupStartLevel: 2,
groupProb: 0.2,
groupMaxCount: 2,
groupInnerSpace: 80,
heightRange: { min: 30, max: 70 },
baseWidth: 64,
probWeights: { obs1: 0.6, obs2: 0.3, obs3: 0.1 }
},
obstacleTypes: [
{ name: "普通障碍", img: 'obs1.png', damage: 10, maxLevel: 8 },
{ name: "危险障碍", img: 'obs2.png', damage: 20, minLevel: 2, maxLevel: 8 },
{ name: "致命障碍", img: 'obs3.png', damage: 30, minLevel: 4, maxLevel: 8 }
],
player: {
x: 100,
width: 40,
height: 60,
smallJumpPower: 18,
smallJumpGravity: 0.6,
bigJumpPower: 22,
bigJumpGravity: 0.4,
groundOffset: 0
},
health: { initialMax: 100, increasePerLevel: 20 },
fps: 60
};
// DOM元素获取(确保包含所有按钮)
const DOM = {
canvas: document.getElementById('gameCanvas'),
startScreen: document.getElementById('startScreen'),
rulesScreen: document.getElementById('rulesScreen'),
levelStartScreen: document.getElementById('levelStartScreen'),
gameOverScreen: document.getElementById('gameOverScreen'),
levelCompleteScreen: document.getElementById('levelCompleteScreen'),
healthValue: document.getElementById('healthValue'),
healthBar: document.getElementById('healthBar'),
levelNumber: document.getElementById('levelNumber'),
finalLevel: document.getElementById('finalLevel'),
nextLevel: document.getElementById('nextLevel'),
newMaxHealth: document.getElementById('newMaxHealth'),
jumpIndicator: document.getElementById('jumpIndicator'),
currentLevelNum: document.getElementById('currentLevelNum'),
levelDescription: document.getElementById('levelDescription'),
startButton: document.getElementById('startButton'),
rulesButton: document.getElementById('rulesButton'),
backToStartButton: document.getElementById('backToStartButton'),
restartButton: document.getElementById('restartButton'),
nextLevelButton: document.getElementById('nextLevelButton'),
gameContainer: document.getElementById('gameContainer')
};
// 画布初始化
const ctx = DOM.canvas.getContext('2d');
const canvasW = DOM.canvas.width;
const canvasH = DOM.canvas.height;
// 游戏状态
let gameState = 'start';
let player = { jumpType: 'none' };
let obstacles = [];
let endFlag = {};
let levelState = {
currentLevel: 1,
maxHealth: CONFIG.health.initialMax,
currentHealth: CONFIG.health.initialMax,
flagInitDistance: 0,
flagCurrentDistance: 0
};
let timeAccumulator = 0;
const frameTime = 1000 / CONFIG.fps;
// 图片预加载
const preloadedImages = {
bg: new Image(),
player: new Image(),
start: new Image(),
success: new Image(),
fail: new Image(),
obs1: new Image(),
obs2: new Image(),
obs3: new Image()
};
preloadedImages.bg.src = CONFIG.background.imgSrc;
preloadedImages.player.src = 'player.png';
preloadedImages.start.src = 'start.png';
preloadedImages.success.src = 'success.png';
preloadedImages.fail.src = 'fail.png';
preloadedImages.obs1.src = 'obs1.png';
preloadedImages.obs2.src = 'obs2.png';
preloadedImages.obs3.src = 'obs3.png';
// 初始化玩家
function initPlayer() {
player = {
x: CONFIG.player.x,
y: CONFIG.background.groundY - CONFIG.player.height,
width: CONFIG.player.width,
height: CONFIG.player.height,
velocityY: 0,
onGround: true,
groundY: CONFIG.background.groundY - CONFIG.player.height,
jumpType: 'none'
};
}
// 初始化红旗
function initEndFlag() {
const groundY = CONFIG.background.groundY;
endFlag = {
x: canvasW + 100,
y: groundY - CONFIG.flag.poleHeight,
poleWidth: CONFIG.flag.poleWidth,
poleHeight: CONFIG.flag.poleHeight,
flagWidth: CONFIG.flag.flagWidth,
flagHeight: CONFIG.flag.flagHeight,
isShow: false,
isExist: true,
isInitedShowPos: false
};
levelState.flagCurrentDistance = levelState.flagInitDistance;
}
// 初始化关卡
function initLevel(level = 1) {
const flagInitDistance = CONFIG.flag.initDistanceBase + (level - 1) * CONFIG.flag.initDistanceStep;
levelState = {
currentLevel: level,
maxHealth: CONFIG.health.initialMax + (level - 1) * CONFIG.health.increasePerLevel,
currentHealth: CONFIG.health.initialMax + (level - 1) * CONFIG.health.increasePerLevel,
flagInitDistance: flagInitDistance,
flagCurrentDistance: flagInitDistance
};
obstacles = [];
timeAccumulator = 0;
initEndFlag();
initPlayer();
updateHealthUI();
updateLevelUI();
}
// 更新玩家状态
function updatePlayer() {
if (gameState !== 'playing') return;
let currentGravity = 0;
if (player.jumpType === 'small') currentGravity = CONFIG.player.smallJumpGravity;
else if (player.jumpType === 'big') currentGravity = CONFIG.player.bigJumpGravity;
player.velocityY += currentGravity;
player.y += player.velocityY;
if (player.y >= player.groundY) {
player.y = player.groundY;
player.velocityY = 0;
player.onGround = true;
player.jumpType = 'none';
}
}
// 小跳逻辑
function smallJump() {
if (!player.onGround || gameState !== 'playing') return;
player.velocityY = -CONFIG.player.smallJumpPower;
player.onGround = false;
player.jumpType = 'small';
}
// 大跳逻辑
function bigJump() {
if (!player.onGround || gameState !== 'playing') return;
player.velocityY = -CONFIG.player.bigJumpPower;
player.onGround = false;
player.jumpType = 'big';
DOM.jumpIndicator.style.display = 'block';
setTimeout(() => DOM.jumpIndicator.style.display = 'none', 1000);
}
// 更新红旗状态
function updateEndFlag(deltaTime) {
if (gameState !== 'playing' || !endFlag.isExist) return;
timeAccumulator += deltaTime;
if (timeAccumulator >= 1000) {
levelState.flagCurrentDistance = Math.max(0, levelState.flagCurrentDistance - CONFIG.flag.reduceSpeed);
timeAccumulator = 0;
}
if (!endFlag.isShow && levelState.flagCurrentDistance <= CONFIG.flag.showDistance) {
endFlag.isShow = true;
endFlag.x = canvasW - endFlag.poleWidth;
endFlag.isInitedShowPos = true;
}
if (endFlag.isShow && endFlag.isInitedShowPos) {
endFlag.x -= CONFIG.flag.moveSpeed;
endFlag.x = Math.max(0, endFlag.x);
}
const tolerance = 8;
if (endFlag.x >= CONFIG.player.x - tolerance && endFlag.x <= CONFIG.player.x + tolerance) {
levelComplete();
endFlag.isExist = false;
}
if (endFlag.x < 0) {
levelComplete();
endFlag.isExist = false;
}
}
// 按权重选择障碍类型
function selectObstacleTypeByWeight(availableTypes) {
const weightMap = {};
let totalWeight = 0;
availableTypes.forEach(type => {
const key = type.img.split('.')[0];
weightMap[key] = CONFIG.obstacle.probWeights[key];
totalWeight += weightMap[key];
});
const randomVal = Math.random() * totalWeight;
let currentWeight = 0;
for (const type of availableTypes) {
const key = type.img.split('.')[0];
currentWeight += weightMap[key];
if (randomVal <= currentWeight) return type;
}
return availableTypes[0];
}
// 生成障碍
function generateObstacle() {
if (gameState !== 'playing') return;
if (Math.random() > CONFIG.obstacle.generateProb) return;
const availableTypes = CONFIG.obstacleTypes.filter(t => {
const min = t.minLevel || 1;
const max = t.maxLevel || CONFIG.totalLevels;
return levelState.currentLevel >= min && levelState.currentLevel <= max;
});
if (availableTypes.length === 0) return;
let startX = canvasW + 50;
if (obstacles.length > 0) {
const lastObstacle = obstacles[obstacles.length - 1];
startX = Math.max(startX, lastObstacle.x + lastObstacle.width + CONFIG.obstacle.minSpace);
}
const isGroup = levelState.currentLevel >= CONFIG.obstacle.groupStartLevel && Math.random() < CONFIG.obstacle.groupProb;
const groupCount = isGroup ? Math.floor(Math.random() * CONFIG.obstacle.groupMaxCount) + 1 : 1;
const groundY = CONFIG.background.groundY;
for (let i = 0; i < groupCount; i++) {
const selectedType = selectObstacleTypeByWeight(availableTypes);
let width = 0;
const baseW = CONFIG.obstacle.baseWidth;
if (selectedType.img === 'obs1.png') width = baseW * 1.25 * 1.25;
else if (selectedType.img === 'obs2.png') width = baseW * 1.25;
else width = baseW;
const height = Math.random() * (CONFIG.obstacle.heightRange.max - CONFIG.obstacle.heightRange.min) + CONFIG.obstacle.heightRange.min;
const currentX = startX + i * (width + CONFIG.obstacle.groupInnerSpace);
obstacles.push({
x: currentX,
y: groundY - height,
width: width,
height: height,
img: preloadedImages[selectedType.img.split('.')[0]],
damage: selectedType.damage
});
}
}
// 更新障碍状态
function updateObstacles() {
if (gameState !== 'playing') return;
for (let i = 0; i < obstacles.length; i++) {
obstacles[i].x -= CONFIG.obstacle.moveSpeed;
if (obstacles[i].x < -obstacles[i].width) {
obstacles.splice(i, 1);
i--;
}
}
}
// 碰撞检测
function checkObstacleCollision() {
if (gameState !== 'playing') return;
for (let i = 0; i < obstacles.length; i++) {
const obs = obstacles[i];
if (
player.x + player.width > obs.x + 5 &&
player.x < obs.x + obs.width - 5 &&
player.y + player.height > obs.y + 5 &&
player.y < obs.y + obs.height - 5
) {
levelState.currentHealth = Math.max(0, levelState.currentHealth - obs.damage);
updateHealthUI();
obstacles.splice(i, 1);
i--;
if (levelState.currentHealth <= 0) gameOver();
break;
}
}
}
// 更新UI
function updateHealthUI() {
DOM.healthValue.textContent = levelState.currentHealth;
DOM.healthBar.style.width = `${(levelState.currentHealth / levelState.maxHealth) * 100}%`;
}
function updateLevelUI() {
const level = levelState.currentLevel;
const flagInitDis = levelState.flagInitDistance;
DOM.levelNumber.textContent = level;
DOM.currentLevelNum.textContent = level;
DOM.levelDescription.textContent = `红旗初始距离${flagInitDis}米,≤20米时从最右侧出现!`;
}
// 绘制红旗
function drawEndFlag() {
if (!endFlag.isShow || !endFlag.isExist) return;
ctx.fillStyle = '#333333';
ctx.fillRect(endFlag.x, endFlag.y, endFlag.poleWidth, endFlag.poleHeight);
ctx.fillStyle = '#ff0000';
ctx.beginPath();
const flagTopX = endFlag.x + endFlag.poleWidth;
const flagTopY = endFlag.y;
ctx.moveTo(flagTopX, flagTopY);
ctx.lineTo(flagTopX + endFlag.flagWidth, flagTopY);
ctx.lineTo(flagTopX + endFlag.flagWidth, flagTopY + endFlag.flagHeight);
ctx.lineTo(flagTopX + endFlag.flagWidth / 2, flagTopY + endFlag.flagHeight - 5);
ctx.lineTo(flagTopX, flagTopY + endFlag.flagHeight);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ff4444';
ctx.beginPath();
ctx.moveTo(flagTopX, flagTopY + 2);
ctx.lineTo(flagTopX + endFlag.flagWidth - 2, flagTopY + 2);
ctx.lineTo(flagTopX + endFlag.flagWidth - 2, flagTopY + endFlag.flagHeight - 2);
ctx.lineTo(flagTopX + endFlag.flagWidth / 2, flagTopY + endFlag.flagHeight - 7);
ctx.lineTo(flagTopX, flagTopY + endFlag.flagHeight - 2);
ctx.closePath();
ctx.fill();
}
// 游戏流程控制
function showLevelStart() {
gameState = 'levelStart';
hideAllScreens();
DOM.levelStartScreen.style.display = 'flex';
updateLevelUI();
}
function gameStart() {
gameState = 'playing';
hideAllScreens();
}
function levelComplete() {
gameState = 'levelComplete';
hideAllScreens();
DOM.levelCompleteScreen.style.display = 'flex';
const nextLevel = levelState.currentLevel + 1;
DOM.nextLevel.textContent = nextLevel > CONFIG.totalLevels ? levelState.currentLevel : nextLevel;
DOM.newMaxHealth.textContent = levelState.maxHealth + CONFIG.health.increasePerLevel;
}
function gameOver() {
gameState = 'gameOver';
hideAllScreens();
DOM.gameOverScreen.style.display = 'flex';
DOM.finalLevel.textContent = levelState.currentLevel;
if (levelState.currentLevel === CONFIG.totalLevels) {
DOM.gameOverScreen.querySelector('h2').textContent = '恭喜通关!';
DOM.gameOverScreen.querySelector('p').innerHTML = '你成功通过所有8关,跑酷大神诞生!';
}
}
function hideAllScreens() {
DOM.startScreen.style.display = 'none';
DOM.rulesScreen.style.display = 'none';
DOM.levelStartScreen.style.display = 'none';
DOM.gameOverScreen.style.display = 'none';
DOM.levelCompleteScreen.style.display = 'none';
}
// 绘制游戏
function draw() {
ctx.clearRect(0, 0, canvasW, canvasH);
ctx.drawImage(preloadedImages.bg, 0, 0, canvasW, canvasH);
drawEndFlag();
ctx.drawImage(preloadedImages.player, player.x, player.y, player.width, player.height);
obstacles.forEach(obs => ctx.drawImage(obs.img, obs.x, obs.y, obs.width, obs.height));
}
// 游戏主循环
let lastTime = Date.now();
function gameLoop() {
const now = Date.now();
const deltaTime = now - lastTime;
lastTime = now;
updatePlayer();
generateObstacle();
updateObstacles();
updateEndFlag(deltaTime);
checkObstacleCollision();
draw();
requestAnimationFrame(gameLoop);
}
// 绑定事件(重点修复返回按钮事件)
function bindEvents() {
// 规则按钮跳转
DOM.rulesButton.addEventListener('click', () => {
hideAllScreens();
DOM.rulesScreen.style.display = 'flex';
});
// 返回开始界面按钮事件(核心修复)
DOM.backToStartButton.addEventListener('click', () => {
hideAllScreens();
DOM.startScreen.style.display = 'flex'; // 显式显示开始界面
});
DOM.startButton.addEventListener('click', () => {
hideAllScreens();
initLevel(1);
showLevelStart();
});
DOM.restartButton.addEventListener('click', () => {
initLevel(1);
showLevelStart();
});
DOM.nextLevelButton.addEventListener('click', () => {
const nextLevel = levelState.currentLevel + 1;
if (nextLevel <= CONFIG.totalLevels) {
initLevel(nextLevel);
showLevelStart();
} else {
gameOver();
}
});
DOM.levelStartScreen.addEventListener('click', gameStart);
DOM.gameContainer.addEventListener('click', smallJump);
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
bigJump();
}
});
}
// 初始化游戏
function initGame() {
initPlayer();
initLevel(1);
bindEvents();
gameLoop();
}
window.addEventListener('load', initGame);
</script>
</body>
</html>
四、项目成果与后续可扩展方向
🎯 最终成果
一款完整可运行的2D跳跃跑酷游戏,具备:
- 5个核心界面(开始、规则、关卡开始、通关、失败),界面跳转流畅;
- 8个关卡,难度梯度合理,容错性高,新手易上手;
- 完整的操作、障碍、红旗、生命值系统,玩法完整;
- 统一的视觉样式,响应式适配,运行流畅无卡顿。
🔧 后续可扩展方向(AI可快速实现)
- 添加音效:跳跃、碰撞、通关、失败音效,提升游戏氛围感;
- 新增功能:积分系统、排行榜、角色皮肤、更多障碍类型;
- 视觉升级:添加角色跳跃动画、障碍移动动画、背景滚动效果;
- 适配优化:适配移动端触摸操作,扩大适用场景。
游戏网址:http://www.silencer76.com/nowcoderRunrunrun/
#你都用AI做什么##AI“智障”时刻##Prompt分享##牛客AI体验站#
字节跳动公司福利 1366人发布