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

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

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

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

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

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

❄ Льдины исчезают через секунду после касания

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В итоге нам удалось сделать платформер за пару часов. Его можно было бы масштабировать и улучшать, но мы получили достаточное качество и продолжительность игры, чтобы понять: нейросети с этим справились. Итоговый результат – готовая игра. В нее вы можете поиграть по ссылке: 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>
2
5 комментариев