AI生成2D跳跃跑酷游戏|开发全流程总结帖

哈喽各位开发者小伙伴~ 最近用AI豆包辅助完成了一款轻量2D跳跃跑酷小游戏,纯原生HTML5+Canvas+JavaScript开发,无任何框架依赖,浏览器打开就能玩!今天整理了完整的开发总结,从需求梳理到最终交付,全程AI参与逻辑搭建、代码生成和细节优化,新手也能轻松复刻,话不多说,直接上干货👇一、项目概况:AI助力,快速落地轻量小游戏

本次项目核心是开发一款「易上手、轻量级、有基础可玩性」的2D跑酷游戏,全程由AI主导需求拆解、架构设计、代码生成和BUG修复,人工仅做需求确认和细节微调,真正实现「高效开发、低门槛落地」。

🎮 游戏核心玩法:角色固定左侧,仅支持小跳(鼠标点击)、大跳(空格键)躲避障碍,红旗从右侧解锁移动,与角色重合即通关,共8个关卡,逐关提升难度,兼顾趣味性和容错性。

💻 技术栈:HTML5(结构)+ Canvas 2D(渲染)+ 原生JavaScript(逻辑)+ CSS3(样式),纯原生无依赖,无需额外配置,复制代码保存为HTML即可运行。

⏱️ 开发周期:短平快落地,从需求提出到最终测试交付,全程AI高效输出,大幅缩短开发时间,避免人工编写重复代码的繁琐。

二、AI主导的开发全流程(附核心亮点)

整个开发过程遵循「需求梳理→架构设计→功能开发→样式优化→细节迭代→测试交付」的逻辑,AI全程把控每一个环节,既保证代码规范性,又兼顾游戏体验。

  1. 需求梳理:AI精准拆解核心需求,明确开发边界

初期仅提出「做一款2D跳跃跑酷,有障碍、有关卡、有通关条件」的基础需求,AI快速拆解出具体功能清单和玩法定位,避免需求模糊导致的开发返工:

  • 核心玩法:固定角色+双跳跃模式+障碍躲避+红旗通关,新手易上手,无复杂操作;
  • 基础功能:关卡系统(8关)、生命值系统、障碍系统(3种伤害类型)、界面系统(5个核心界面)、操作提示;
  • 视觉要求:简洁统一,黑框白字清晰可读,按钮有交互动效,适配不同PC屏幕。

亮点:AI自动补充需求细节,比如「非强制无伤通关」「每关提升生命值上限」等容错设计,贴合新手玩家体验。

  1. 架构设计:AI模块化拆解,代码解耦易维护

AI采用「分层设计+参数集中管理」的思路,搭建清晰的项目架构,避免代码混乱,后续修改和迭代更便捷:

  • 结构分层:游戏容器→画布渲染层→UI面板→过场界面,层级清晰,互不干扰;
  • 参数集中:通过全局CONFIG常量,统一管理所有游戏参数(画布尺寸、角色属性、障碍伤害、关卡规则等),修改参数无需改动业务逻辑;
  • 状态管理:单gameState变量控制游戏全状态(开始、规则、游戏中、通关、失败),避免逻辑冲突。

亮点:AI采用面向对象思路封装游戏对象(玩家、障碍、红旗),每个对象独立初始化、更新、绘制,代码复用性高,调试时可精准定位问题。

  1. 功能开发:AI自动生成核心代码,全程无需人工编写重复逻辑

核心功能由AI逐模块生成,从画布初始化到游戏主循环,从碰撞检测到界面跳转,每一行代码都有清晰注释,人工仅需确认功能是否符合预期:

  • 渲染模块:Canvas 2D绘制背景、角色、障碍、红旗,基于requestAnimationFrame实现稳定主循环,帧率60fps无卡顿;
  • 物理与操作:AI实现跳跃抛物线物理逻辑(初速度+重力加速度),添加防连跳判断,避免误操作;
  • 核心逻辑:障碍随机生成、碰撞检测(矩形检测,高效简易)、红旗解锁与通关判断、关卡切换与生命值更新;
  • 交互模块:自动绑定所有按钮事件、鼠标/键盘操作事件,实现界面无缝跳转。

亮点:AI自动处理边缘场景,比如「障碍超出画布自动删除」「碰撞后避免重复扣血」「红旗未解锁时不显示」等,减少人工调试成本。

  1. 样式优化与细节迭代:AI响应反馈,快速修复问题

初期开发完成后,反馈「返回开始界面按钮未显示」「规则界面文字挤压」「按钮样式不统一」等问题,AI快速响应,精准修复,同时优化视觉体验:

  • 样式优化:统一按钮样式(渐变背景、hover缩放动效),文字添加黑框白字样式,规则界面采用深色半透明背景,提升可读性;
  • BUG修复:修复按钮显示/响应问题、碰撞检测误判、连跳BUG等,全程无需人工定位代码问题;
  • 细节补充:添加大跳操作提示、关卡开始脉冲提示,规则界面新增「障碍物尺寸与伤害关联」提示,优化玩家指引;
  • 响应式适配:使用CSS clamp()函数,适配不同PC屏幕,避免界面错乱。
  1. 测试交付: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体验站#
全部评论
b站通关视频:https://www.bilibili.com/video/BV1nHFMz3Epu/
1 回复 分享
发布于 昨天 19:25 河南

相关推荐

评论
3
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务