Портируем старую игру в жанре «shoot 'em up» на JavaScript на коленке

https://habr.com/ru/company/icl_services/blog/508850/
  • Блог компании ICL Services
  • Ненормальное программирование
  • JavaScript
  • Разработка игр


Имеется древняя игрушка LaserAge, которая написана на Flash (на очень древнем Macromedia Flash 4) и работает только под Windows. В детстве она мне очень понравилась, поэтому я решил для души портировать её, чтобы можно было играть с браузера со всех устройств.


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


При уничтожении всех противников на уровне происходит переключение на следующий уровень. Всего 100 уровней.


В терминах игры уровень — волна (Wave), а несколько волн объединены в большой уровень (Level), который представляет из себя просто смену заднего фона, т.е. всего 4 больших уровня в каждом из которых 25 волн. В последней волне большого уровня обычно бывает босс — противник с огромным значением жизни и мощным оружием.


https://github.com/EntityFX/laseroid/blob/master/doc/LaserAgeNext.png?raw=true


[TOC]


Бизнес логика игры


Игровое пространство


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


Область движения игрока ограничена так, что он не может сталкиваться с кораблями противника, а корабли противника с игроком.


https://github.com/EntityFX/laseroid/blob/master/doc/Stage.png?raw=true


Оружие


Оружием обладает космический корабль игрока и корабли противника.
Оружие игрока может быть ручным (стреляет при нажатии мыши) и дополнительное автоматическое (стреляет периодами).


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


Оружие космического корабля игрока


  • Торпеда — стреляет маленькими ракетами
    • Одинарная Торпеда — 1 уровень апгрейда
    • Двойная — 2 уровень апгрейда
    • Тройная — 3 уровень апгрейда
  • Автоматические пушки
    • Дополнительная автоматическая Торпеда слева корабля — 4 уровень апгрейда
    • Дополнительная автоматическая Торпеда справа корабля — 5 уровень апгрейда
  • Зелёная плазма — 6 и 7 уровень апгрейда (увеличивается скорострельность)
  • Фиолетовая плазма — 8 уровень апгрейда (наносит урон всем противникам по траектории полёта)
  • Зелёный лазер — 9 уровень (наносит урон всем противникам, а также активно одну секунду, тем самым можно задеть соседних противников)

Дополнительное оружие:


  • Красная плазма — 15-19 уровень (наносит урон всем противникам, а также активно одну секунду, тем самым можно задеть соседних противников)
  • Зелёная плазма — 20-24 уровень
  • Синяя плазма — 25-29 уровень апгрейда
  • Фиолетовая плазма — 30-34 уровень апгрейда
  • Фиолетовая плазма — 30-34 уровень апгрейда
  • Дополнительная автоматическая Торпеда слева стреляет желтой плазмой — 35 — 39 уровень апгрейда
  • Дополнительная автоматическая Торпеда справа стреляет желтой плазмой — 40+ уровень апгрейда

Таблица с характеристиками оружия игрока


Оружие Hit Points Скорость спрайта Интенсивность Тип Дополнительно Вид
Торпеда 1 5 25 Торпеда Одинарная, двойная, тройная
Автоматическая Торпеда 1 5 50 Торпеда Слева и Справа
Зелёная плазма 3 7 30 Торпеда
Фиолетовая плазма 2 8 30 Торпеда Атакует до 3х целей
Красная плазма 2 4 30 Торпеда
Синяя плазма 4 4.5 30 Торпеда
Жёлтая плазма 2 3.8 40 Торпеда Только автоматическая
Зелёный Лазер 4 - 15/55 Лазер Атакует до 5ти целей одновременно

Таблица с конфигурацией оружия игрока в зависимости от уровня жизни


Уровень жизни Конфигурация оружия
1 Торпеда
2 Торпеда + Торпеда
3 Торпеда + Торпеда + Торпеда
4 Торпеда + Торпеда + Торпеда + Автоматическая торпеда слева
5 Торпеда + Торпеда + Торпеда + Автоматическая торпеда слева + справа
6 Зелёная плазма + Автоматическая торпеда слева + справа
7 Зелёная плазма + Автоматическая торпеда слева + справа
8 Фиолетовая плазма + Автоматическая торпеда слева + справа
9 Зелёный лазер + Автоматическая торпеда слева + справа
15 — 19 Зелёный лазер + Красная плазма + Автоматическая торпеда слева + справа
20 — 24 Зелёный лазер + Красная плазма + Автоматическая торпеда слева + справа
25 — 29 Зелёный лазер + Синяя плазма + Автоматическая торпеда слева + справа
30 — 34 Зелёный лазер + Фиолетовая плазма + Автоматическая торпеда слева + справа
35 — 39 Зелёный лазер + Фиолетовая плазма + Автоматическая желтая плазма слева + торпеда справа
40+ Зелёный лазер + Фиолетовая плазма + Автоматическая желтая плазма слева + желтая плазма справа

Оружие противников


Таблица с конфигурацией оружия противников


Оружие Скорость спрайта Тип
Торпеда 2.5 Торпеда
Красная плазма 3.5 Торпеда
Синяя плазма 4.5 Торпеда
Зелёная плазма 5 Торпеда
Синяя Торпеда 3 Торпеда
Жёлтая плазма 3.2 — 3.8 Торпеда
Белая плазма 4 — 6 Торпеда
Зелёный Лазер - Лазер

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


Интенсивность оружия противников может иметь один и более временных слотов, в каждом отдельно задаётся минимальное и максимальное время фреймов и число повторов. Слот может быть паузой или активным состоянием (стреляет).


Пример конфигурации оружия:


"torpedo": {
    "sprite": "Bullet1_1.png", //картинка спрайта
    "isRandomIntensity": false, //нужно ли переключать случайно слоты - true или по порядку - false
    "intensity": [
        //слот 0
        {
            "min": 50, //минимальное число фреймов
            "max": 200, //максимальное число фреймов
            "type": "pause" //pause - оружие неактивно, shoot - активное (стреляет)
        },
        //слот 1
        {
            "min": 100,
            "max": 200,
            "type": "shoot"
        },
        {
            "min": 50,
            "max": 80,
            "type": "pause"
        },
        {
            "min": 30,
            "max": 100,
            "repeat": 2
        }
    ],
    "speed": 2.5, //скорость
    "type": "bullet", //тип оружия
    "sound": "alienTorpedo"
}

Действующие лица


Корабль игрока


Корабль игрока может перемещаться в ограниченной области, чтобы не пересекаться с кораблями противников.


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


Оружие активирует при удержании левой клавиши мыши (тапом и удержанием по экрану на мобильном телефоне).


Противники


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


Корабль противника Жизнь Тип Движения Оружие Вид
Чужой 1 2 Обычный Нормальное горизонтальное Торпеда
Чужой 2 4 Обычный Нормальное все направления Торпеда
Быстрый чужой 10 Обычный Быстрое горизонтальное Торпеда (Интенсивная)
Фрегат чужого 10 Обычный Нормально-быстрое все направления Красная плазма
Броневик чужого 10 Обычный Медленное вниз Торпеда (Очень интенсивная)
Быстрый Фрегат чужого 30 Обычный Медленное вниз (следит за игроком) Красная плазма (Очень интенсивная)
Красный истребитель 30 Обычный Медленное вниз (следит за игроком) Синяя плазма
Зелёный истребитель 30 Обычный Быстро вертикально Синяя плазма
Чужой 1 модификация 2 Обычный Нормальное горизонтальное Синяя Торпеда
Бомбардировщик 30 Обычный Нормальное все направления (следит за игроком) Зелёная плазма
Тяжёлый Чужой 30 Обычный Нормальное все направления Торпеда
Тяжёлый Фрегат Чужого 35 Обычный Нормальное все направления Синяя Торпеда + Синяя Торпеда
Тяжёлый броневик 35 Обычный Нормальное вниз Жёлтая Плазма + Жёлтая Плазма + Жёлтая Плазма + Жёлтая Плазма
Линкор 100 Босс Нормальное все направления Синяя плазма (очень интенсивная) + Зелёная плазма (очень интенсивная)
Крейсер 250 Босс Нормальное все направления Зелёная плазма (сверх интенсивная)
Тяжёлый Крейсер 500 Босс Быстрое все направления Жёлтая Плазма + Жёлтая Плазма + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Белая плазма + Белая плазма
Эпичный Тяжёлый Крейсер 1000 (восстанавливается) Босс Быстрое все направления Жёлтая Плазма + Жёлтая Плазма + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Синяя Торпеда + Белая плазма + Белая плазма+ Зелёная плазма (очень интенсивная)

JSON-конфигурация противника:


"alien10": {
    "life": 35,
    "weapons": [
        {
            "weapon": "blueTorpedo",
            "position": {
                "x": -6,
                "y": 0
            }
        },
        {
            "weapon": "blueTorpedo",
            "position": {
                "x": 6,
                "y": 0
            }
        }
    ],
    "sprite": "AlienShip10_1.png",
    "movement": "horizontalFast",
    "killPoints": 2100
}

JSON-конфигурация движения противника :


"horizontalFast": {
    "movements": [
        {
            "type": "freeMovement", //freeMovement - обычное, followPlayer - следит за игроком (движется в направление)
            "speedDelta": {
                "vx": -6,
                "vy": 0
            },
            "intensity": [ //интенсивность движения в виде слотов
                {
                    "min": 20,
                    "max": 150
                },
                {
                    "min": 150,
                    "max": 350
                }
            ]
        }
    ]
}

Бонусы


Специальный вид противника


https://raw.githubusercontent.com/EntityFX/laseroid/master/resources/laser-age/graphics/PowerUps_1.png, который не имеет оружия и при уничтожении порождает спрайт с бонусом


https://raw.githubusercontent.com/EntityFX/laseroid/master/resources/laser-age/graphics/Upgrade.png, который должен поймать корабль игрока. Если игрок поймает бонус, то увеличивается его уровень (жизнь).


Уровни


Каждый уровень содержит множество различного вида противников, которые расположены определённым образом. Также уровень может содержать один и более бонусов.


JSON-конфигурация уровня :


        "2": {
            "level": 1, 
            "enemies": [ // список противников
                {
                    "id": "alien1",
                    "position": {
                        "x": 200,
                        "y": 35
                    }
                },
                //...
                {
                    "id": "alien1",
                    "position": {
                        "x": 525,
                        "y": 40
                    }
                }
            ],
            "bonuses": [ // список бонусов
                {
                    "id": "bonus1",
                    "position": {
                        "x": 350,
                        "y": 10
                    }
                }
            ]
        },

Выбор JavaScript библиотеки для реализации


Я просмотрел множество библиотек графики для JavaScript, но остановился на Hexi JS: https://github.com/kittykatattack/hexi .


Возможности библиотеки:


  • Простота
  • Рисование примитивов
  • Рисование просты интерфейсов (кнопки, события)
  • Перемещение, масштабирование, вращение
  • Рисование спрайтов
    • Анимированные спрайты
    • Работа со спрайтами как с объектами
    • Загрузка спрайтов в виде большой текстуры-атласа. Можно разместить множество изображений в одном файлы и на выходе получить одну большую текстуру и JSON файл с описанием спрайтов (область, смещение)
  • Логика столкновений
  • Работа с устройствами ввода (клавиатура), тач-скрин.

Пример текстуры-атласа создаваемого с помощью программы TexturePacker


https://github.com/EntityFX/laseroid/blob/master/doc/ships-atlas-texture.png?raw=true


Звуковая библиотека: https://github.com/kittykatattack/sound.js


Возможности библиотеки:


  • Простота
  • Воспроизведение звуков
  • Воспроизведение музыки
  • Эффекты

Архитектура


Общая диаграмма классов:


https://github.com/EntityFX/laseroid/blob/master/doc/diagrams/game.png?raw=true


Ядро игры


https://github.com/EntityFX/laseroid/blob/master/doc/diagrams/core.png?raw=true


Класс Main


Является точкой входа и контейнером игрового кода.


Поля:


  • resources — содержит список всех загружаемых ресурсов (текстуры, звук, json)
  • sounds — словарь звуков: Ключ — название, Значение — путь
  • gameScene — объект HexiJS на
  • game — экземпляр объекта Game
  • hexi — инстанс HexiJS
  • gameStorage — сохраняет состояние игры в localStorage

Методы:


  • init() — инициализирует HexiJS
  • load() — загружает ресурсы (текстуры, звук, json)
  • setup() — устанавливает игровую область, события нажатия кнопок, запускает фоновую музыку
  • playLoop() — точка изменения состояния игры (считает движение, коллизии, снаряды, перерисовывает пространство).
  • saveGame() — сохраняет игру
  • loadGame() — загружает игру

Пример списка ресурсов текущей реализации игры:


Main.resources = [
        "images/environment1.png",
        "images/environment2.png",
        "images/environment3.png",
        "images/environment4.png",
        "images/interface.png",
        "images/life-icon.png",

        "images/ships-texture.json",
        "images/bullet-texture.json",

        "sounds/alien-torpedo-shoot.wav",
        "sounds/alien-red-plasma-shoot.wav",
        "sounds/hero-torpedo-shoot.wav",
        "sounds/explode.wav",
        "sounds/hero-green-plasma-shoot.wav",
        "sounds/alien-green-plasma-shoot.wav",
        "sounds/alien-blue-torpedo-shoot.wav",
        "sounds/alien-yellow-laser.wav",
        "sounds/pulse-plasma.wav",
        "sounds/laser.wav",

        "sounds/track0.ogg",
        "sounds/track1.ogg",
        "sounds/track2.ogg",
        "sounds/track3.ogg",
        "sounds/track4.ogg",

        "data/hero-configuration.json",
        "data/levels-configuration.json",
        "data/enemy-configuration.json",
        "data/ui-configuration.json",
    ];

Класс Game


Основной класс игры.


Поля:


  • level — информация о уровне. Значение: { "wave": 1 //номер волны, "type": 1 }
  • score — информация об очках. Значение: {"points": 0 }
  • bulletsController — Экземпляр класса BulletsController. Управляет поведение торпед и лазеров оружия
  • enemyController — Экземпляр класса EnemyController. Управляет поведением всех противников на уровне (в т.ч. и бонусами)
  • player — Экземпляр Player
  • hexi — экземпляр класса Hexi (ссылка)
  • game — экземпляр объекта Game
  • gameStorage — экземпляр объекта GameStorage

Методы:


  • clearShips() — очистка всех проиивников, бонусов
  • setupLevel() — настроить уровень (добавить противников, бонусы, расстановка)
  • nextLevel() — переход на следующий уровень
  • previousLevel() — переход на предыдущий уровень
  • forwardLevel() — перепрыгнуть на несколько уровней вперёд (на 5)
  • rewindLevel() — перепрыгнуть на несколько уровней назад (на 5)
  • restoreState(gameState: JSON) — восстановить по объекту gameState
  • resetGame() — сбросить игру (начать сначала)
  • update() — обновить игровой мир
  • enemyDestroyed() — обработчик срабатывает при уничтожении всех противников

Класс GameStorage


Сохраняет и загружает состояние игры .


Поля:


  • game — экземпляр объекта Game

Методы:


  • save() — сохранить состояние игры
  • load() — загрузить состояние игры

Класс InputDevice


Работает с событиями устройств ввода: click и touch кнопок, нажатие клавиш клавиатуры.


Поля:


  • game — экземпляр объекта Game

Методы:


  • init() — инициализирует все обработчики события и callback'и
  • loadTapped() — нажата кнопка "Load"
  • storeTapped() — нажата кнопка "Store"
  • resetTapped() — нажата кнопка "Reset"
  • pauseTapped() — нажата кнопка "Pause"

Иерархия классов действующих лиц


https://github.com/EntityFX/laseroid/blob/master/doc/diagrams/actors.png?raw=true


Actor


Класс участника.


Поля:


  • hexi — экземпляр класса Hexi (ссылка)
  • game — экземпляр объекта Game
  • life — текущее значение жизни
  • initialLife — начальное значение жизни
  • sprite — экземпляр класса Hexi.Sprite
  • shipConfiguration — конфигурация бонуса

Методы:


  • move() — переместить действующее лицо
  • update() — обновить действующее лицо
  • setPosition(position: {x, y}) — установить по координатам

WeaponedActor


Класс участника (противник или игрок) обладающем оружием.


Поля:


  • automatedWeapons — массив автоматических оружий
  • canShoot — мжет ли стрелять
  • isWeaponShooting — активено ли оружие

Методы:


  • startShoot() — запустить выстрелы оружием
  • stopShoot() — остановить выстрелы оружием
  • onShootStarted() — обработчик события, что запущены выстрелы оружием
  • onShootStopped() — обработчик события, что остановлены выстрелы оружием
  • updateShooting() — выполняет алгоритмы выстрелов

Enemy


Класс противника.


Поля:


  • type — тип противника
  • syncWeapons — массив конфигураций для синхронного оружия
  • movementEngine — экземпляр класса MovementEngine

Методы:


  • setWeapon() — установить оружие используя текущую конфигурацию
  • shootWithWeapon() — выполняет выстрел противником
  • setLifeLine() — рисует линию жизни противника
  • hit() — проверяет столкновение торпед (лазера) игрока с текущим противником

MovementEngine


Класс управляющий движением.


Для придания сложности движения, используется конфигурация со слотами. В каждом слоте задаётся вектор направления vx, vy и интенсивность. Имеется возможность отключения отражения от нижней границы и режим слежения за игроком (противник всегда движется за игроком).


Поля:


  • movementsConfiguration — конфигурация движения
  • firstMovementConfiguration — первый элемент из списка конфигураций
  • movementItensity — интенсивность движения
  • movementItensityCounter — счётчик интенсивности движения
  • movementItensitySlot — номер слота интенсивности
  • isBounceBottom — флаг на проверку отражения от нижней границы. Если false, то противник не отражается от нижней границы

Методы:


  • setMovement() — настраивает движение
  • updateMovement() — обновляет движение по конфигурации движения

Player


Класс игрока.


Поля:


  • weapons — массив оружия игрока
  • collisionSprite — спрайт коллизии (торпеды противника сталкиваются со спрайтом коллизии, а не спрайтом игрока)
  • weaponLifeLevels — значения уровня жизни для проверки на апгрейд оружия
  • invisibilityCounter — счётчик невидимости от торпед (нужен для того, чтобы при столкновении с торпедой противника игрок стал временно недосягаем для других торпед)

Методы:


  • upgrade() — апгрейд игрока (+1 жизнь)
  • downgrade() — даунгрейд игрока (+1 жизнь)
  • shootWithLaser(currentWeapon, weapon) — выстрел лазером
  • shootWithBullets(currentWeapon, weapon) — выстрел торпедой
  • setWeapon() — установить оружие используя текущую конфигурацию
  • setLife(life: number) — установить значение жизни (меняет оружие в соответствии со значением жизни)
  • hitUpgrade(upgradeItem) — проверить столкновение со спрайтом апгрейда

Bonus


Класс Бонуса. При уничтожении порождает спрайт апгрейда.


Поля:


  • type — тип бонуса
  • movementEngine — экземпляр класса MovementEngine
  • upgradeBonus — конфигурация апгрейда

Методы:


  • shootWithUpgrade(upgradeBonus: JSON) — породить спрайт апгрейда

EnemyController


Управляет состоянием противников, бонусов, апгрейдами.


Поля:


  • enemies — массив всех противников на уровне
  • bonuses — массив всех бонусных кораблей на уровне
  • player — объект игрока
  • upgrades — массив всех спрайтов апгрейда

Методы:


  • isLevelCompleted() — проверка на завершённость уровня (уничтожены все противники и бонусы, пойманы апгрейды)
  • update() — обновляет состояние всех противников
  • clear() — очистка уровня от проиивников, бонусов

BulletsController


Управляет состоянием торпед (перемещение), лазерами игрока и противников.


Поля:


  • playerBullets — массив торпед игрока
  • enemyBullets — массив торпед всех противников
  • explosionSplashes — массив спрайтов взрыва
  • playerLaser — состояние спрайта лазера игрока (Если оружие доступно).

Методы:


  • update() — обновляет состояние всех торпед, лазеров
  • clear() — очищает уровень от всех торпед, лазеров
  • updatePlayerBullets() — изменяет состояние всех торпед игрока
  • updatePlayerLaser() — изменяет состояние лазера игрока
  • updateEnemyBullets() — изменяет состояние всех торпед противника
  • updateExplosions() — изменяет состояние всех взрывов

Выводы


Я постарался достаточно подробно рассказать как портировать существуюшую игру на JavaScript.


Игру можно модифицировавть и снабдить своими текстурами, музыкой, сделать собственную конфигурацию уровня или вариацию всей игры.


Очень хотелось бы получить от вас комментарии по архитектуре игры, варианты рефакторинга, по именованию методов и т.д.


Согласен, архитектура не очень идеальная и есть куда стремиться!


Спасибо и интересных Вам проектов!


Ссылки


http://laseroid.azurewebsites.net/ — сама игра
https://github.com/EntityFX/laseroid — исходный код игры

Currently unrated

Recent Posts

Archive

2020
2019
2018
2017
2016
2015
2014

Categories

Authors

Feeds

RSS / Atom