🦖 GXUST AI通识课 | 从“一句话需求”到“可玩游戏”:我用TRAE复刻Chrome小恐龙(附完整代码 + 二段跳彩蛋)

本文适用对象:零编程基础小白 / AI编程工具尝鲜者 / 想做小游戏的实践党
工具:字节跳动出品 AI 编程工具 —— TRAE(https://www.trae.ai/)
游戏参考:Chrome 离线恐龙游戏
最终成果:一个完整的跳跃跑酷游戏 HTML 文件,支持二段跳,复制即玩

📌 写在前面

这学期选了 AI 通识课,作业要求体验四个不同类别的 AI 工具。我选了:

  • AI写作(已完成)
  • AI图像(待更新)
  • AI编程(本文主角:TRAE)
  • AI聊天助手(待更新)

今天写 AI编程篇。我会完整展示 三次基础提示词迭代 + 一次二段跳升级 的全过程,以及最终生成的完整游戏代码。


Part 1️⃣ 提示词工程:三次基础迭代

1.1 第一次尝试:过于简单,AI只给了一个“骨架”🤨

提示词(初版)
帮我写一个像 Chrome 小恐龙的游戏

结果诊断

  • ❌ 恐龙能出现,能跳跃,但没有障碍物
  • ❌ 没有碰撞检测,没有分数
  • ❌ 严格来说不算一个“游戏”

教训:把AI当“心有灵犀”的队友不现实。你不说清楚,它就只给最低标准的答案。

1.2 第二次尝试:加细节,效果立竿见影 ✅

提示词(进阶版)
用 HTML/CSS/JS 制作一个 Chrome 离线恐龙游戏的复刻版本。
要求:

1.使用 Canvas 作为游戏画布

2.恐龙用矩形代替,仙人掌用矩形代替,背景灰色/白色

3.按空格或上箭头让恐龙跳跃,有重力物理效果

4.随机生成仙人掌从右侧向左移动

5.碰撞检测:恐龙碰到仙人掌后游戏结束并显示分数

6.分数随时间累积,显示在右上角

7.游戏结束后按“R”键或点击“重新开始”可以重置游戏

改进点

  • ✅ 跳跃物理、仙人掌生成、分数累积都有了
  • ✅ 游戏基本可玩

仍存在的问题

  • ⚠️ 跳跃手感偏硬
  • ⚠️ 仙人掌间隔有时不合理
  • ⚠️ 难度不会增长

1.3 第三次优化:给足“性能参数”,AI给出成品 🚀

提示词(终版)
1.游戏需要包含以下完整功能:
1.1 恐龙位置固定在左侧(x=100),矩形30x30
1.2 跳跃机制:重力0.6,跳跃初速度-10,落地判定
1.3 仙人掌15x30矩形,初始x=800,速度5向左移动
1.4 生成间隔:min80帧,max120帧(用计数器实现)
1.5 分数系统:每帧+1,碰撞时停止加分
1.6 碰撞检测:矩形重叠
1.7 游戏结束显示文字和分数,按R键重置

2.额外功能:
2.1 游戏开始前显示“按空格开始游戏”
2.2 最高分记录(localStorage存储)
2.3 游戏难度随分数提升:每增加500分,仙人掌速度+0.5

3.技术约束:
3.1 原生JS + Canvas,无第三方库
3.2 游戏循环使用requestAnimationFrame
3.3 禁止空格键滚动页面

效果:请看下方 Part 2 的成果展示,基本达到了我的预期。


Part 1.5 🐾 彩蛋升级:我给恐龙加了个二段跳

基础版玩了几把后,我觉得不过瘾——原版只能跳一次。于是我又给 TRAE 输入了新提示词:

提示词(二段跳版)
在上一个版本的小恐龙游戏基础上,增加二段跳功能:

恐龙在空中时可以再跳跃一次(总共可以跳两次)

落地后重置跳跃次数为2

在空中时,如果有剩余跳跃次数,按空格可以再次起跳

在恐龙头顶用一个小图标显示剩余跳跃次数

TRAE 自动分析了现有代码,帮我完成了:

  • ✅ 新增 jumpsRemaining 变量,初始值2
  • ✅ 修改 jump() 函数:只有剩余次数>0才能跳,每次消耗一次
  • ✅ 落地时重置跳跃次数为2
  • ✅ 绘制时在恐龙头顶显示剩余次数(橙色小翅膀图标)

全程没动一行代码,二段跳完美实现


Part 2️⃣ 成果展示:TRAE生成的完整游戏代码(带二段跳)

将下面的代码复制到一个 index.html 文件中,双击用浏览器打开即可游玩。

游戏特色

  • 经典跑酷:跳跃躲避仙人掌
  • 二段跳:空中可以再跳一次
  • 动态难度:分数越高,仙人掌速度越快
  • 最高分记录:关闭浏览器也不丢失
```html
<!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>Chrome Dino Game - 二段跳版</title>
    <style>
        * { user-select: none; -webkit-tap-highlight-color: transparent; }
        body {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex; justify-content: center; align-items: center;
            min-height: 100vh; margin: 0; padding: 20px;
            font-family: 'Courier New', monospace;
        }
        .game-container {
            background: #f7f7f7; padding: 20px; border-radius: 24px;
            box-shadow: 0 20px 35px rgba(0,0,0,0.2);
        }
        canvas { display: block; margin: 0 auto; border-radius: 12px; cursor: pointer; }
        .info {
            margin-top: 15px; text-align: center; font-size: 14px; color: #555;
        }
        .info span {
            background: #333; color: #ffd966; padding: 4px 10px;
            border-radius: 20px; font-weight: bold; margin: 0 4px;
        }
        button {
            margin-top: 15px; padding: 8px 24px; font-size: 16px;
            font-family: inherit; font-weight: bold; background: #333;
            color: white; border: none; border-radius: 40px; cursor: pointer;
        }
        button:active { transform: scale(0.96); }
        @media (max-width: 860px) { canvas { width: 100%; height: auto; } }
    </style>
</head>
<body>
<div>
    <div class="game-container">
        <canvas id="gameCanvas" width="800" height="200"></canvas>
        <div class="info">
            🎮 空格 / ↑ 跳跃(二段跳可用) &nbsp;&nbsp;|&nbsp;&nbsp;
            💾 最高分:<span id="highScoreSpan">0</span> &nbsp;&nbsp;|&nbsp;&nbsp;
            ⚡ 当前速度倍率:<span id="speedFactorSpan">1.00</span>
        </div>
        <div style="text-align: center;">
            <button id="restartButton">🔄 重新开始</button>
        </div>
    </div>
</div>

<script>
    (function() {
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');

        let gameRunning = false;
        let gameOverFlag = false;
        let animationId = null;

        const DINO_WIDTH = 30, DINO_HEIGHT = 30;
        let dinoY = canvas.height - DINO_HEIGHT;
        let dinoVelocity = 0;
        const GRAVITY = 0.6, JUMP_POWER = -10;
        let jumpsRemaining = 2;
        const MAX_JUMPS = 2;

        let obstacles = [];
        const OBSTACLE_WIDTH = 15, OBSTACLE_HEIGHT = 30;
        let spawnGapMin = 70, spawnGapMax = 110;
        let nextSpawnCounter = 0;

        let baseSpeed = 5, currentSpeed = baseSpeed;
        let score = 0, highScore = 0;

        const highScoreSpan = document.getElementById('highScoreSpan');
        const speedFactorSpan = document.getElementById('speedFactorSpan');
        const restartBtn = document.getElementById('restartButton');

        try {
            const saved = localStorage.getItem('dinoHighScore');
            if (saved && !isNaN(parseInt(saved))) {
                highScore = parseInt(saved);
                highScoreSpan.innerText = highScore;
            }
        } catch(e) {}

        function updateHighScoreUI() { highScoreSpan.innerText = highScore; }
        function updateSpeedUI() { speedFactorSpan.innerText = (currentSpeed / baseSpeed).toFixed(2); }

        function setNextSpawnCounter() {
            const gap = spawnGapMin + Math.floor(Math.random() * (spawnGapMax - spawnGapMin));
            nextSpawnCounter = gap;
        }

        function resetGame() {
            dinoY = canvas.height - DINO_HEIGHT;
            dinoVelocity = 0;
            obstacles = [];
            score = 0;
            currentSpeed = baseSpeed;
            gameRunning = true;
            gameOverFlag = false;
            jumpsRemaining = MAX_JUMPS;
            setNextSpawnCounter();
            updateSpeedUI();
            if (animationId === null) startGameLoop();
        }

        function jump() {
            if (!gameRunning) return false;
            if (jumpsRemaining > 0) {
                dinoVelocity = JUMP_POWER;
                jumpsRemaining--;
                return true;
            }
            return false;
        }

        function updateGame() {
            if (!gameRunning) return;

            dinoVelocity += GRAVITY;
            dinoY += dinoVelocity;
            if (dinoY >= canvas.height - DINO_HEIGHT) {
                dinoY = canvas.height - DINO_HEIGHT;
                dinoVelocity = 0;
                jumpsRemaining = MAX_JUMPS;
            }
            if (dinoY < 0) { dinoY = 0; if (dinoVelocity < 0) dinoVelocity = 0; }

            for (let i = 0; i < obstacles.length; i++) obstacles[i].x -= currentSpeed;
            obstacles = obstacles.filter(obs => obs.x + OBSTACLE_WIDTH > 0);

            if (nextSpawnCounter <= 0) {
                obstacles.push({ x: canvas.width, width: OBSTACLE_WIDTH, height: OBSTACLE_HEIGHT, y: canvas.height - OBSTACLE_HEIGHT });
                setNextSpawnCounter();
            } else {
                nextSpawnCounter--;
            }

            const dinoRect = { x: 100, y: dinoY, w: DINO_WIDTH, h: DINO_HEIGHT };
            for (let obs of obstacles) {
                const obsRect = { x: obs.x, y: obs.y, w: OBSTACLE_WIDTH, h: OBSTACLE_HEIGHT };
                if (dinoRect.x < obsRect.x + obsRect.w && dinoRect.x + dinoRect.w > obsRect.x &&
                    dinoRect.y < obsRect.y + obsRect.h && dinoRect.y + dinoRect.h > obsRect.y) {
                    gameRunning = false;
                    gameOverFlag = true;
                    if (score > highScore) {
                        highScore = score;
                        updateHighScoreUI();
                        localStorage.setItem('dinoHighScore', highScore);
                    }
                    return;
                }
            }

            score++;
            let targetSpeed = baseSpeed + Math.floor(score / 500) * 0.5;
            if (targetSpeed > baseSpeed * 3) targetSpeed = baseSpeed * 3;
            currentSpeed = targetSpeed;
            updateSpeedUI();
        }

        function draw() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            ctx.beginPath();
            ctx.moveTo(0, canvas.height - 5);
            ctx.lineTo(canvas.width, canvas.height - 5);
            ctx.strokeStyle = "#888";
            ctx.lineWidth = 2;
            ctx.stroke();

            for (let obs of obstacles) {
                ctx.fillStyle = "#2d6a2f";
                ctx.fillRect(obs.x, obs.y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT);
                ctx.fillStyle = "#1e4a1f";
                ctx.fillRect(obs.x + 3, obs.y - 6, 4, 6);
                ctx.fillRect(obs.x + 8, obs.y - 9, 4, 9);
            }

            ctx.fillStyle = "#3a3a3a";
            ctx.fillRect(100, dinoY, DINO_WIDTH, DINO_HEIGHT);
            ctx.fillStyle = "white";
            ctx.fillRect(122, dinoY + 6, 6, 6);
            ctx.fillStyle = "#000";
            ctx.fillRect(124, dinoY + 8, 3, 3);
            ctx.fillStyle = "#e05a5a";
            ctx.fillRect(120, dinoY + 20, 8, 4);

            if (gameRunning && jumpsRemaining > 0 && dinoY < canvas.height - DINO_HEIGHT - 1) {
                ctx.font = "bold 14px 'Courier New'";
                ctx.fillStyle = "#ff8800";
                ctx.fillText("✈️ " + jumpsRemaining, 100, dinoY - 5);
            }

            ctx.font = "bold 20px 'Courier New'";
            ctx.fillStyle = "#2c3e50";
            ctx.fillText("Score: " + Math.floor(score), canvas.width - 130, 35);
            ctx.font = "16px 'Courier New'";
            ctx.fillStyle = "#555";
            ctx.fillText("Best: " + highScore, canvas.width - 130, 65);

            if (!gameRunning && !gameOverFlag) {
                ctx.font = "bold 18px 'Courier New'";
                ctx.fillStyle = "#111";
                ctx.fillText("⚡ PRESS SPACE TO START ⚡", canvas.width/2 - 150, canvas.height/2);
            } else if (gameOverFlag) {
                ctx.font = "bold 26px 'Courier New'";
                ctx.fillStyle = "#c0392b";
                ctx.fillText("💀 GAME OVER 💀", canvas.width/2 - 120, canvas.height/2 - 20);
                ctx.font = "16px 'Courier New'";
                ctx.fillStyle = "#2c3e50";
                ctx.fillText("Press SPACE or R to restart", canvas.width/2 - 135, canvas.height/2 + 20);
            }
        }

        function gameLoop() { updateGame(); draw(); animationId = requestAnimationFrame(gameLoop); }
        function startGameLoop() { if (animationId) return; animationId = requestAnimationFrame(gameLoop); }
        function startGame() {
            if (!gameRunning && gameOverFlag) resetGame();
            else if (!gameRunning && !gameOverFlag) resetGame();
        }

        function handleKeydown(e) {
            const key = e.key;
            if (key === ' ' || key === 'ArrowUp' || key === 'Space') {
                e.preventDefault();
                if (!gameRunning) startGame();
                else jump();
            }
            if (key === 'r' || key === 'R') { e.preventDefault(); resetGame(); }
        }
        function handleCanvasClick(e) {
            e.preventDefault();
            if (!gameRunning) startGame();
            else jump();
        }

        restartBtn.addEventListener('click', () => resetGame());
        window.addEventListener('keydown', handleKeydown);
        canvas.addEventListener('click', handleCanvasClick);
        window.addEventListener('keydown', function(e) { if (e.code === 'Space' && e.target === document.body) e.preventDefault(); });

        gameRunning = false;
        gameOverFlag = false;
        dinoY = canvas.height - DINO_HEIGHT;
        jumpsRemaining = MAX_JUMPS;
        setNextSpawnCounter();
        startGameLoop();
    })();
</script>
</body>
</html>

Part 3️⃣ 总结测评

3.1 TRAE vs 通用聊天助手

TRAE(字节出品)

  • 中文场景适配:⭐⭐⭐⭐(中文原生,直接说中文需求)
  • 代码集成度:⭐⭐⭐⭐(生成即运行,不用复制粘贴)
  • 上下文管理:⭐⭐⭐⭐(超大上下文,可管理整个项目)
  • 交互友好度:⭐⭐⭐(有 IDE 学习成本,但 SOLO 模式简单)
  • 价格:完全免费(国内版)

通用聊天助手(如 ChatGPT/豆包)

  • 中文场景适配:⭐⭐⭐(需要翻译或手动调整)
  • 代码集成度:⭐⭐(需手动复制到编辑器测试)
  • 上下文管理:⭐⭐(窗口有限,大项目容易忘记需求)
  • 交互友好度:⭐⭐⭐⭐⭐(对话即用,零门槛)
  • 价格:部分免费/付费

3.2 个人评价

👍 优点

  1. 中文原生优化,直接打中文就能生成代码,不用翻译。
  2. SOLO 模式实现“动嘴编程”:AI 独立完成从需求到代码的全流程。
  3. 代码质量高:二段跳改动没有破坏原有任何功能,物理手感舒适。
  4. 完全免费:对学生党非常友好。

👎 不足

  1. 国内版模型与国际版不同(豆包 vs Claude),习惯 Claude 的用户可能需要适应。
  2. SOLO 模式有学习曲线:建议先搭建核心功能,再逐步叠加细节。
  3. 提示词依然最关键:你说得越细,AI 做得越好。

3.3 三条经验总结

  1. 分步走 —— 先做出最小可玩版本,再逐步叠加分数、难度、二段跳等细节。
  2. 给具体参数 —— 直接说“重力 0.6、跳跃初速度 -10”比“让跳跃自然一点”效果好 10 倍。
  3. 增加功能时,说清楚触发条件、次数限制和可视化反馈 —— 例如“在空中可以再跳一次,落地重置,头顶显示剩余次数”。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐