Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиПростой
Время на прочтение11 мин
Количество просмотров2K

В первой части этой стати мы с нейросетью 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>
Теги:
Хабы:
+2
Комментарии0

Публикации

Ближайшие события