Использование нейросетей в разработке игр. Часть 2. Делаем платформер

от автора

В первой части этой стати мы с нейросетью Qwen пытались создать аналог Pong! в зимнем сеттинге (Снежинка вместо шарика и на фоне падает снег). И у нас получилась вполне рабочая и симпатичная игра. Посмотреть саму игру вы можете тут:
https://chat.qwen.ai/s/deploy/386f16fb-096d-4fe2-b706-a8c72374825c

Не знаю, как долго она будет храниться на серверах, поэтому не могу ручаться за работоспособность ссылки для тех, кто увидит статью сильно позже её выхода.

Итак, с понгом Qwen справился, настало время для более сложных испытаний. Посмотрим, как он справится с платформером. Буду давать ему все тот же «Снежный» сеттинг. Поэтому прыгать мы будем по льдам. Я написал о том, что я хочу ИИ и получил такой ответ:

Qwen:
✅ Пример: что будет в игре

🎄 Снежный персонаж прыгает по льдинкам

🏆 Счёт: сколько льдинок прошёл

🔁 Рестарт при падении

Я: Хорошо, давай.

Код первой итерации платформера составил 335 строчек. Игра выглядела так:

Что ж, для прототипа неплохо, но это был даже далеко не MVP. Например, яйцо прыгало только вверх и не управлялось вправо-влево. Да и при чем тут вообще яйцо?

Давайте попробуем заменить это яйцо на что-нибудь более тематическое, например, снеговика. А дальше будем разбираться с механиками игры.

Снеговик получился забавный, но мы тут за тем, чтобы оценить возможности кодинга, а не дизайна, поэтому пусть будет так. Разве что, по какой-то причине, его модель находится не на платформе, а над ней. Исправим.

Далее я попытался добавить автоматическую генерацию платформ для бесконечной игры.

Тут возникла первая серьезная проблема: платформы генерировались слишком далеко от персонажа (он просто не доставал до них прыжком) и слишком близко друг к другу. После нескольких попыток я решил отключить автоматическую генерацию и бесконечную игру.

Была еще одна проблема: с каждым прыжком нашего персонажа камера опускалась вниз вместо того, чтобы следовать за ним наверх. В итоге уже на 5-6 прыжке мы теряли снеговика из виду. Эту проблему тоже решили короткой перепиской с Qwen и получили более-менее играбельный платформер. Правда, уровень состоял всего из 7 платформ и при прохождении уровня ничего не происходило. Снеговик просто гордо стоял на вершине. Хотя по промпту игра должна была перевести игрока на новый уровень.

Я добавил флаг на последнюю платформу и при достижении флага должен был запуститься экран победы. Но ничего не произошло. Снеговик все так же грустно стоял на вершине, но уже рядом с флагом.

Пусть и не сразу, но нам удалось решить вопрос с окончанием уровня. После того, как персонаж наступал на последнюю платформу стало выходить окно с поздравлением и предложением начать второй уровень:

На втором уровне игрока должно было ждать усложнение: некоторые платформы должны двигаться. Никаких проблем с движущимися платформами не возникло, персонаж взаимодействовал с ними аналогично статичным. Единственной проблемой второго уровня оказалось то, что он бы последним. Поэтому, после его прохождения выходило окно с поздравлением игрока и предложением пройти второй уровень снова. Давайте попробуем добавить третий уровень и, например, окно победы в игре.

Далее мы с Qwen создали третий уровень, где все платформы двигались. Сначала это вызвало проблему: первая платформа, с которой стартовал снеговик тоже двигалась и почти сразу сбрасывала игрока. Эта проблема была быстро решена – первую и последнюю платформу я сделал статичной, а остальные двигались. Дальше, после прохождения финального уровня открывалось окно, в котором игроку сообщалось о его победе: все уровни были пройдены, а игра закончена.

В итоге нам удалось сделать платформер за пару часов. Его можно было бы масштабировать и улучшать, но мы получили достаточное качество и продолжительность игры, чтобы понять: нейросети с этим справились. Итоговый результат – готовая игра. В нее вы можете поиграть по ссылке: https://chat.qwen.ai/s/deploy/69dea8a8-a8e2-434a-805b-c963a62ad593

Что нам не удалось?

Не удалось решить проблему с прилипанием персонажа к платформе. Если персонаж встаёт на движущуюся платформу, то он падает после того, как она выезжает из-под него. Также по какой-то причине игра ускоряется с каждым следующим уровнем. Видимо, это происходит из-за того, что ИИ считает, что ускорение механик усложняет игру. Попытки исправить это ни к чему не привели. Автоматическая генерация уровней тоже вышла неудачной.

Что нам удалось?

Мы создали платформер из трех уровней в снежном сеттинге. Игровым персонажем у нас выступил снеговик, а на фоне шел снег. Сами платформы были похожи на льдины. Каждый последующий уровень был сложнее предыдущего, а по прохождении игры вы получали поздравления и предложение начать игру сначала. Основные проблемы присутствовали на этапе создания первого уровня и ключевых механик, далее игра масштабировалась достаточно быстро. Итоговый код игры составил ровно 600 строчек.

На создание игры было потрачено около двух часов времени и больше ничего. Сама нейросеть абсолютно бесплатна.

Итак, готовы ли нейросети заменить разработчиков? Пока на этот вопрос сложно ответить, для этого нужно устроить ей по-настоящему сложный вызов. Как насчет того, чтобы сделать «Героев меча и магии 3»? Попробуем в третьей части этой стати.

<!DOCTYPE html> <html lang="ru"> <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>Снежный прыжок</title>     <style>         * {             margin: 0;             padding: 0;             box-sizing: border-box;         }                  body {             background: linear-gradient(to bottom, #1e3c72, #2a5298);             overflow: hidden;             display: flex;             justify-content: center;             align-items: center;             min-height: 100vh;             font-family: Arial, sans-serif;         }                  #game-container {             position: relative;             width: 400px;             height: 600px;             background: linear-gradient(to top, #000428, #004e92);             border: 4px solid #4facfe;             border-radius: 20px;             overflow: hidden;             box-shadow: 0 0 20px rgba(79, 172, 254, 0.5);         }                  #score {             position: absolute;             top: 20px;             left: 20px;             color: white;             font-size: 24px;             font-weight: bold;             text-shadow: 0 0 5px rgba(0, 0, 0, 0.7);             z-index: 5;         }                  #best-score {             position: absolute;             top: 60px;             left: 20px;             color: #ffd700;             font-size: 18px;             font-weight: bold;             text-shadow: 0 0 5px rgba(0, 0, 0, 0.7);             z-index: 5;         }                  #hint {             position: absolute;             top: 90px;             left: 20px;             color: rgba(255, 255, 255, 0.7);             font-size: 14px;             z-index: 5;         }                  #game-over, .level-complete {             position: absolute;             top: 0;             left: 0;             width: 100%;             height: 100%;             background: rgba(0, 0, 0, 0.9);             color: white;             display: flex;             flex-direction: column;             justify-content: center;             align-items: center;             z-index: 30;             opacity: 0;             pointer-events: none;             transition: opacity 0.5s;         }                  #game-over.active, .level-complete.active {             opacity: 1;             pointer-events: all;         }                  #restart-btn, #next-level-btn, #restart-all-btn {             margin-top: 20px;             padding: 12px 24px;             background: #4facfe;             color: white;             border: none;             border-radius: 10px;             cursor: pointer;             font-size: 18px;         }                  #next-level-btn {             background: #00f2fe;             box-shadow: 0 0 10px rgba(0, 242, 254, 0.5);         }                  .snow-effect {             position: absolute;             top: 0;             left: 0;             width: 100%;             height: 100%;             pointer-events: none;             z-index: 1;         }                  .snow {             position: absolute;             width: 6px;             height: 6px;             background: white;             border-radius: 50%;             opacity: 0.8;         }          #player {             position: absolute;             width: 40px;             height: 50px;             transform: translate(-50%, -50%);             transition: transform 0.1s;             z-index: 10;         }          .head {             position: absolute;             width: 24px;             height: 24px;             background: white;             border-radius: 50%;             top: 0;             left: 8px;             box-shadow: 0 0 5px rgba(255, 255, 255, 0.6);         }          .body {             position: absolute;             width: 30px;             height: 30px;             background: white;             border-radius: 50%;             bottom: 0;             left: 5px;             box-shadow: 0 0 5px rgba(255, 255, 255, 0.6);         }          .eye {             position: absolute;             width: 5px;             height: 5px;             background: #333;             border-radius: 50%;             top: 12px;         }          .eye.left {             left: 12px;         }          .eye.right {             left: 19px;         }          .mouth {             position: absolute;             width: 10px;             height: 3px;             background: #333;             border-radius: 2px;             top: 18px;             left: 15px;         }          .arm {             position: absolute;             width: 6px;             height: 18px;             background: #ddd;             border-radius: 3px;             top: 12px;         }          .arm.left {             left: 2px;             transform: rotate(20deg);         }          .arm.right {             right: 2px;             transform: rotate(-20deg);         }          #player.jump .arm.left {             animation: wave-left 0.5s infinite alternate;         }          #player.jump .arm.right {             animation: wave-right 0.5s infinite alternate;         }          @keyframes wave-left {             0% { transform: rotate(20deg); }             100% { transform: rotate(40deg); }         }          @keyframes wave-right {             0% { transform: rotate(-20deg); }             100% { transform: rotate(-40deg); }         }          #player.squish {             transform: translate(-50%, -50%) scaleY(0.8);         }          #player.fall {             animation: blink 0.3s infinite;         }          @keyframes blink {             0%, 80% { opacity: 1; }             90%, 100% { opacity: 0.5; }         }          .platform {             position: absolute;             width: 100px;             height: 15px;             background: linear-gradient(to bottom, #a0e7ff, #d0f0ff);             border-radius: 10px;             box-shadow: 0 0 8px rgba(255, 255, 255, 0.4);             opacity: 1;             transition: opacity 0.3s;         }          .flag {             position: absolute;             width: 12px;             height: 20px;             background: #ff4757;             border-radius: 2px 0 0 2px;         }          .flag::after {             content: '';             position: absolute;             top: 0;             right: -8px;             width: 10px;             height: 10px;             background: #00f;             clip-path: polygon(0 0, 100% 0, 50% 100%);         }     </style> </head> <body>     <div id="game-container">         <div id="score">Счёт: 0</div>         <div id="best-score">Рекорд: 0</div>         <div id="hint">Подсказка: Доберись до флага!</div>                  <div id="player">             <div class="head"></div>             <div class="body"></div>             <div class="eye left"></div>             <div class="eye right"></div>             <div class="mouth"></div>             <div class="arm left"></div>             <div class="arm right"></div>         </div>                  <div class="snow-effect" id="snow-effect"></div>                  <div id="game-over">             <h2>Вы упали!</h2>             <p>Счёт: <span id="final-score">0</span></p>             <button id="restart-btn">Начать сначала</button>         </div>          <div class="level-complete" id="level-complete">             <h2>Уровень пройден! 🎉</h2>             <button id="next-level-btn">Следующий уровень</button>         </div>          <div class="level-complete" id="final-win">             <h2>🎉 Победа! Все уровни пройдены!</h2>             <button id="restart-all-btn">Начать сначала</button>         </div>     </div>      <script>         const player = document.getElementById('player');         const gameContainer = document.getElementById('game-container');         const scoreElement = document.getElementById('score');         const bestScoreElement = document.getElementById('best-score');         const gameOverScreen = document.getElementById('game-over');         const levelCompleteScreen = document.getElementById('level-complete');         const finalWinScreen = document.getElementById('final-win');         const finalScoreElement = document.getElementById('final-score');         const restartBtn = document.getElementById('restart-btn');         const nextLevelBtn = document.getElementById('next-level-btn');         const restartAllBtn = document.getElementById('restart-all-btn');         const snowEffect = document.getElementById('snow-effect');          const gameWidth = 400;         const gameHeight = 600;         const playerWidth = 40;         const playerHeight = 50;         const platformWidth = 100;         const platformHeight = 15;         const playerSpeed = 6;          let playerX = gameWidth / 2 - playerWidth / 2;         let playerY = 550 - playerHeight / 2;         let velocityY = 0;         let gravity = 0.6;         let jumpPower = -13.5;         let isJumping = false;         let cameraY = 0;         let score = 0;         let platforms = [];         let gameActive = true;         let bestScore = 0;         let currentLevel = 1;         let levelFinished = false;          const keys = { w: false, a: false, d: false };          function createSnowflakes() {             for (let i = 0; i < 30; i++) {                 const snow = document.createElement('div');                 snow.classList.add('snow');                 snow.style.left = `${Math.random() * 100}%`;                 snow.style.top = `${Math.random() * 100}%`;                 snow.style.opacity = Math.random() * 0.7 + 0.3;                 snow.dataset.speed = Math.random() * 1.5 + 0.5;                 snowEffect.appendChild(snow);             }         }          function animateSnowflakes() {             const snows = document.querySelectorAll('.snow');             if (!snows.length) return;             snows.forEach(snow => {                 let top = parseFloat(snow.style.top) + parseFloat(snow.dataset.speed);                 if (top > 100) top = -5;                 snow.style.top = `${top}%`;             });         }          function createLevel1() {             platforms = [                 { x: 150, y: 550, touched: false, moving: false },                 { x: 250, y: 480, touched: false, moving: false },                 { x: 100, y: 410, touched: false, moving: false },                 { x: 200, y: 340, touched: false, moving: false },                 { x: 300, y: 270, touched: false, moving: false },                 { x: 120, y: 200, touched: false, moving: false },                 { x: 220, y: 130, touched: false, moving: false },                 { x: 150, y: 60, touched: false, moving: false }             ];         }          function createLevel2() {             platforms = [                 { x: 180, y: 550, touched: false, moving: false },                 { x: 280, y: 480, touched: false, moving: true, direction: 1, speed: 1 },                 { x: 80, y: 410, touched: false, moving: true, direction: -1, speed: 1.2 },                 { x: 200, y: 340, touched: false, moving: false },                 { x: 300, y: 270, touched: false, moving: true, direction: 1, speed: 1.5 },                 { x: 100, y: 200, touched: false, moving: true, direction: -1, speed: 1 },                 { x: 220, y: 130, touched: false, moving: false },                 { x: 150, y: 60, touched: false, moving: false }             ];         }          // --- ИСПРАВЛЕНИЕ: стартовая платформа — статичная ---         function createLevel3() {             platforms = [                 { x: 180, y: 550, touched: false, moving: false }, // ✅ Статичная                 { x: 200, y: 480, touched: false, moving: true, direction: -1, speed: 2.2 },                 { x: 180, y: 410, touched: false, moving: true, direction: 1, speed: 2.5 },                 { x: 200, y: 340, touched: false, moving: true, direction: -1, speed: 2 },                 { x: 180, y: 270, touched: false, moving: true, direction: 1, speed: 2.3 },                 { x: 200, y: 200, touched: false, moving: true, direction: -1, speed: 2.1 },                 { x: 180, y: 130, touched: false, moving: false },                 { x: 150, y: 60, touched: false, moving: false }             ];         }          function renderPlatforms() {             document.querySelectorAll('.platform').forEach(p => p.remove());             document.querySelectorAll('.flag').forEach(f => f.remove());              platforms.forEach((p, index) => {                 const platform = document.createElement('div');                 platform.classList.add('platform');                 platform.style.left = `${p.x}px`;                 platform.style.top = `${p.y - cameraY}px`;                 platform.dataset.y = p.y;                 gameContainer.appendChild(platform);                  if (index === platforms.length - 1) {                     const flag = document.createElement('div');                     flag.classList.add('flag');                     flag.style.left = `${p.x + platformWidth - 20}px`;                     flag.style.top = `${p.y - cameraY - 15}px`;                     gameContainer.appendChild(flag);                 }             });         }          function updateMovingPlatforms() {             platforms.forEach(p => {                 if (p.moving) {                     p.x += p.direction * p.speed;                     if (p.x <= 50 || p.x >= gameWidth - platformWidth - 50) {                         p.direction *= -1;                     }                 }             });         }          function jump() {             if (isJumping) return;             velocityY = jumpPower;             isJumping = true;         }          function checkCollision() {             const playerBottom = playerY + playerHeight / 2;             const playerCenterX = playerX + playerWidth / 2;              for (let p of platforms) {                 if (                     playerBottom >= p.y &&                      playerBottom <= p.y + 10 &&                     playerCenterX >= p.x &&                      playerCenterX <= p.x + platformWidth &&                     velocityY > 0                 ) {                     isJumping = false;                     velocityY = 0;                     playerY = p.y - playerHeight / 2;                      if (!p.touched) {                         p.touched = true;                         score++;                         scoreElement.textContent = `Счёт: ${score}`;                         setTimeout(() => {                             const el = document.querySelector(`.platform[data-y="${p.y}"]`);                             if (el) el.style.opacity = '0';                         }, 500);                     }                     return true;                 }             }             return false;         }          function gameLoop() {             if (!gameActive) return;              if (keys.a) playerX = Math.max(0, playerX - playerSpeed);             if (keys.d) playerX = Math.min(gameWidth - playerWidth, playerX + playerSpeed);             if (keys.w && !isJumping) jump();              velocityY += gravity;             playerY += velocityY;              checkCollision();              updateMovingPlatforms();              const targetCameraY = Math.max(0, playerY - gameHeight * 0.4);             cameraY += (targetCameraY - cameraY) * 0.1;              if (playerY > gameHeight + 100) {                 endGame();             }              // --- ПРОВЕРКА ПОБЕДЫ ---             const lastPlatform = platforms[platforms.length - 1];             const playerBottom = playerY + playerHeight / 2;             const playerCenterX = playerX + playerWidth / 2;              if (                 !levelFinished &&                 playerBottom >= lastPlatform.y - 10 &&                 playerBottom <= lastPlatform.y + 20 &&                 playerCenterX >= lastPlatform.x &&                 playerCenterX <= lastPlatform.x + platformWidth             ) {                 console.log(`🎉 Уровень ${currentLevel} пройден!`);                 levelFinished = true;                  setTimeout(() => {                     if (currentLevel < 3) {                         levelCompleteScreen.classList.add('active');                     } else {                         finalWinScreen.classList.add('active');                     }                 }, 600);             }              player.classList.remove('jump', 'squish', 'fall');             if (velocityY < 0) player.classList.add('jump');             else if (velocityY > 5) player.classList.add('fall');             if (!isJumping) player.classList.add('squish');              player.style.left = `${playerX + playerWidth / 2}px`;             player.style.top = `${playerY - cameraY}px`;              renderPlatforms();             animateSnowflakes();             requestAnimationFrame(gameLoop);         }          function endGame() {             gameActive = false;             finalScoreElement.textContent = score;             gameOverScreen.classList.add('active');         }          function startLevel(level) {             if (level > 3) return;              playerX = gameWidth / 2 - playerWidth / 2;             playerY = 550 - playerHeight / 2;             velocityY = 0;             cameraY = 0;             isJumping = false;             gameActive = true;             levelFinished = false;              scoreElement.textContent = `Счёт: ${score}`;             gameOverScreen.classList.remove('active');             levelCompleteScreen.classList.remove('active');             finalWinScreen.classList.remove('active');              if (level === 1) createLevel1();             else if (level === 2) createLevel2();             else if (level === 3) createLevel3();              currentLevel = level;             requestAnimationFrame(gameLoop);         }          restartBtn.addEventListener('click', () => {             score = 0;             startLevel(1);         });          nextLevelBtn.addEventListener('click', () => {             startLevel(currentLevel + 1);         });          restartAllBtn.addEventListener('click', () => {             score = 0;             startLevel(1);         });          document.addEventListener('keydown', (e) => {             const key = e.key.toLowerCase();             if (key === 'w') keys.w = true;             if (key === 'a') keys.a = true;             if (key === 'd') keys.d = true;             if (key === ' ') {                 e.preventDefault();                 jump();             }         });          document.addEventListener('keyup', (e) => {             const key = e.key.toLowerCase();             if (key === 'w') keys.w = false;             if (key === 'a') keys.a = false;             if (key === 'd') keys.d = false;         });          // Запуск         if (document.readyState === 'loading') {             document.addEventListener('DOMContentLoaded', () => {                 createSnowflakes();                 startLevel(1);             });         } else {             createSnowflakes();             startLevel(1);         }     </script> </body> </html> 


ссылка на оригинал статьи https://habr.com/ru/articles/935740/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *