Пишем игры: космический шутер

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

Импорт текстур

Откройте ct.js и создайте новый проект под названием «SpaceShooter».

Теперь скачайте набор текстур с сайта Kenney. Он бесплатен и отлично подходит для создания прототипов, во время изучения движка и просто для тестов.

Вы также можете найти набор нужных текстур в папке ct.js/examples/SpaceShooter_assets.

Нам понадобятся вот такие ассеты:

Теперь откройте вкладку «Текстуры» в верхней части окна ct.IDE и перетащите их в редактор. Можно также нажать кнопку «Импортировать», чтобы найти их вручную.

Для каждого из изображений на панели текстур появится карточка. Давайте откроем PlayerShip и настроим его. Мы увидим жёлтую фигуру — она определяет форму столкновения. На данный момент она покрывает слишком много пустого пространства, особенно над крыльями. Чтобы исправить это, воспользуемся инструментами в левом столбце.

Для начала, нажмите кнопку «Центр изображения», чтобы его ось располагалась в центре корабля.

Теперь выберите пункт "Ломаная / Многоугольник" под категорией "Маска столкновений". Добавьте пару дополнительных точек и переместите их с помощью мыши, чтобы получившийся многоугольник напоминал форму корабля.

Нажмите «Сохранить» и перейдите к следующей текстуре — LaserRed. Как и в случае с кораблём, давайте установим его ось в центр, нажав «Центр изображения». Затем выберите маску-круг прямо под этой кнопкой. Жёлтая маска столкновения после этого отразится в виде круга.

Следующая текстура, Laser_Blue, тоже должна быть центрирована. Поскольку форма столкновения должна охватывать всё изображение, можно нажать кнопку «Заполнить», чтобы автоматизировать настройку маски.

Оба астероида больше похожи на многоугольники с их вогнутыми фигурами или острыми углам. Установите их форму столкновения на ломаную / многоугольник, и не забудьте установить их ось в центр текстуры.

Фигура EnemyShip тоже лучше описывается как многоугольник.

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

Создание первых типов и их расположение в комнате

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

Нажмите вкладку «Типы» в верхней части экрана и создайте новый тип для корабля игрока. После нажатия кнопки «Создать», нажмите на большого призрачного кота в левой колонке. За этим котом кроется выбор текстур. Нажмите на карточку с вашим кораблём — теперь он должен появиться в левой колонке редактора.

Теперь давайте переименуем тип в PlayerShip, чтобы потом не вспоминать страшные коды по-умолчанию во время программирования.

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

Теперь давайте разместим созданные типы в комнате. Чтобы создать комнату, нажмите вкладку «Комнаты» в верхней части окна ct.IDE и нажмите кнопку «Добавить». Затем откройте созданную комнату, нажав на нее.

Немного объясню, как использовать редактор комнат. Перво-наперво, в левой колонке можно указать имя комнаты и размер видимой области (размер камеры).

В ct.js комнаты бесконечны и могут перемещаться в любом направлении. Вы можете размещать объекты внутри и снаружи рамки видимой области.

Здесь также есть раздел «События комнаты», который определяет игровую логику для каждой отдельной комнаты. Здесь можно описать графический интерфейс или сценарий уровня, с триггерами и скриптами.

Ниже этой кнопки — панель с копиями и фонами. Нужно выбрать копию на соответствующей вкладке, чтобы потом щелчком мыши поместить её на карте. Чтобы отключить добавление новых копий, нажмите инструмент «Выбрать и переместить». С этим инструментом, а также в любой момент нажатием колёсика можно передвигать текущий вид на комнату. Колёсиком и кнопками в верхней части экрана можно изменить масштаб комнаты.

Если вы активно перемещались по комнате и вдруг потерялись, нажмите кнопку «В центр», чтобы вернуться к начальным координатам.

Можно установить сетку, нажав на кнопку в правом нижнем углу. Повторное нажатие отключит сетку.

Наконец, вы можете переместить все копии в комнате одновременно, нажав кнопку со значком «переместить» в верхнем левом углу. Можно это также делать с помощью инструмента «Выбрать и переместить» — тогда можно через Shift выделить часть копий.

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

Теперь добавьте фон. Перейдите на вкладку «Фоны» и нажмите «Добавить», затем выберите BG. Он заполнит весь экран.

Хоть фоны и рисуются всегда перед копиями, если они находятся на одном уровне по глубине (по умолчанию этот уровень равен везде 0), лучше изменить глубину. Нажмите на гайку слева от значка фона в левом столбце и в поле «Глубина» введите -5. Так мы сообщаем движку, что этот фон размещён ниже, чем другие копии и фоны. Глубина является третьей координатной осью. Она идёт вверх, а X и Y уходят в стороны.

Теперь сохраните проект и нажмите кнопку «Скомпилировать и запустить» наверху. На данный момент у вас будет игровой проект с неподвижными кораблями и астероидами.

Добавление передвижения

Обработка ввода пользователя — самая важная задача в игре. В этом разделе мы сделаем так, чтобы корабль игрока двигался при нажатии стрелок клавиатуры или WASD.

Чтобы обрабатывать ввод с клавиатуры, понадобится подключить модуль клавиатуры. Нажмите на вкладку «Котомоды», найдите модуль keyboard слева, выберите его, а затем нажмите большую красную кнопку немного справа, чтобы включить его (хотя он может быть включен по умолчанию). Затем добавьте модули mouse, random и place — они нам тоже потом понадобятся.

Добавление действий

Действия в ct.js — это такие сущности, которые объединяют различные методы ввода в события и позволяют прослушивать ввод игрока в коде. Вы можете прочитать больше о них тут.

Сейчас давайте создадим базовую схему управления для нашего шутера. Откройте вкладку «Настройки», затем нажмите кнопку с надписью «Редактировать действия». Нам нужно определить три различных действия: для стрельбы лазерными пулями, для горизонтального перемещения и для вертикального перемещения.

Сначала нажмите кнопку «Добавить действие», затем введите имя первого действия. Нажмите кнопку «Добавить метод ввода», чтобы привязать к нему события кнопок. Используйте поиск, чтобы быстро отфильтровать доступные методы ввода.

Создайте три действия, как на картинке выше. Назначьте множитель -1 для keyboard.ArrowUp, keyboard.KeyW, keyboard.ArrowLeft и keyboard.A, чтобы эти клавиши двигали корабль в противоположном направлпении, нежели keyboard.KeyS, keyboard.KeyD и т.д.

Написание движения

Откройте вкладку «Типы» сверху и нажмите вкладку Кадр.

TIP

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

Напишите следующий код:

/**
 * Перемещение корабля
 * См. Настройки > Редактировать действия
 * и "Действия" в документации.
 */

this.x += 8 * ct.delta * ct.actions.MoveX.value; // Передвижение по оси X


/**
 * Проверим, вышел ли корабль за пределы камеры
 */
if (this.x < 0) { // Корабль перешёл левую границу?
    this.x = 0; // Вернуть его к левой границе
}
if (this.x > ct.viewWidth) { // Корабль перешёл правую границу?
    this.x = ct.viewWidth; // Вернуть его к правой границе
}

this.move();

Здесь мы используем созданные действия. Сначала мы пытаемся перемещать корабль по горизонтали (по X, строка 6). ct.actions.MoveX вернёт 1, если мы нажали стрелку на правой клавиатуре или клавишу "D", и вернет -1, если игрок нажмёт стрелку влево или клавишу "A". Если ничего не нажимать, оно вернёт 0, отключив горизонтальное движение.

ct.delta необходим для компенсации возможных лагов и падений FPS. Обычно оно равно «1» и особо ничего не делает, но ускорит движение, если часть кадров была пропущена.

Наконец, мы умножаем наше промежуточное значение скорости на требуемую скорость 8.

После мы проверяем, вышла ли X-координата корабля за рамки камеры. Здесь 0 означает левую сторону вида, а ct.viewWidth означает размер вида по горизонтали, или правую сторону.

Самостоятельно!

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

Движение противников и астероидов

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

Вражеские корабли

Откройте вкладку «Типы», затем нажмите EnemyShip. Перейдите к событию «Код создания» и добавьте этот код:

this.speed = 3;
this.direction = 270;

Здесь мы используем встроенные переменные для перемещения. Редактирование координат вручную удобно для обработки ввода игрока, но для большинства задач лучше использовать эти переменные, поскольку они автоматизируют большинство вещей — например, с ними не надо писать ct.delta. Здесь this.speed означает скорость копии, а this.direction — направление движения.

TIP

В ct.js направление измеряется в градусах, начиная слева и продолжаясь против часовой стрелки. 0° означает движение вправо, 90° означает вверх, 180° — влево, а 270° — вниз.

Если мы перейдём к событию Кадр, мы увидим этот маленький кусочек кода:

this.move();

Эта строка как раз читает встроенные переменные и перемещает копию в соответствии с ними. Без неё this.speed и this.direction будут бессмысленными.

Есть и другие встроенные переменные, которые вы можете найти на странице документации к ct.types.

Мы изменим код Кадр, чтобы враги уничтожали себя, если выходят за край экрана.

this.move();

if (this.y > ct.height + 80) {
    this.kill = true;
}

Самостоятельно!

Что, если вражеские корабли будут двигаться по диагонали, зигзагообразно?

Астероиды

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

Откройте тип Asteroid_Medium в соответствующей вкладке и в событии Код создания напишите это:

this.speed = ct.random.range(1, 3);
this.direction = ct.random.range(270 - 30, 270 + 30);

Событие Кадр будет такое же, как и у EnemyShip.

this.move();

if (this.y > ct.height + 80) {
    this.kill = true;
}

Сделайте то же самое для другого астероида.

Сохраните проект и нажмите кнопку «Запустить» вверху. Вражеский корабль будет медленно двигаться ко дну, а астероиды будут двигаться более хаотично. Если вы обновите страницу, астероиды будут двигаться в новом направлении.

Снаряды и коллизии

Пора добавить пару пушек 😎

Откройте код Кадр у PlayerShip и добавьте вот такой код:

if (ct.actions.Shoot.pressed) {
    ct.types.copy('Laser_Blue', this.x, this.y);
}

Это первый раз, когда мы добавляем новые копии программно. Ура!

TIP

ct.types.copy — очень важная функция, которая создаёт новую копию в текущей комнате. Сначала мы пишем имя типа в кавычках, затем мы записываем координаты, по которым мы должны создать копию, по горизонтальной и вертикальной осям соответственно. this.x означает горизонтальное расположение текущей копии, а this.y — вертикальное.

Объединив данные, мы создаём луч лазера прямо под нашим кораблём. Лучи будут появляться при нажатии клавиши пробела.

Now let's move to the Laser_Blue itself. We will define its movement with default variables.

this.speed = 18;
this.direction = 90;

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

if (this.y < -40) {
    this.kill = true;
}

this.move();

Следующий этап — обработка столкновений. Лучше написать всю логику столкновений в коде вражеских кораблей и астероидов, потому что они будут реагировать по-разному. Так в коде лазера будет меньше мусора.

Перейдите к коду Кадр у EnemyShip. Добавьте следующий код:

var collided = ct.place.meet(this, this.x, this.y, 'Laser_Blue');
if (collided) {
    collided.kill = true;
    this.kill = true;
}

Метод ct.place.meet проверяет, не сталкивается ли данная копия с другими копиями определённого типа, как если бы она была помещена в заданные координаты. Для этого примера нам нужно проверить, сталкивается ли наша текущая копия (this, вражеский корабль) в своей текущей позиции (this.x, this.y) с лазерными пулями ('Laser_Blue'). Метод возвращает либо попавшуюся копию, либо false, поэтому нам нужно проверить, вернул ли он верное значение.

TIP

В модуле ct.place есть ещё методы. Откройте раздел «Котомоды, а затем щёлкните модуль place слева. Откройте документацию нажатием на вкладку справа.

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

Скопируйте точно такой же код в Asteroid_Medium. Нам также понадобится этот код в Asteroid_Big, но мы сделаем так, чтобы большие астероиды разбивались на два меньших:

var collided = ct.place.meet(this, this.x, this.y, 'Laser_Blue');
if (collided) {
    collided.kill = true;
    this.kill = true;
    ct.types.copy('Asteroid_Medium', this.x, this.y);
    ct.types.copy('Asteroid_Medium', this.x, this.y);
}

Если вы запустите игру, вы сможете уничтожать вражеские корабли и астероиды. Большие астероиды должны разбиваться на более мелкие.

Пули противников

Вражеские корабли тоже должны стрелять. Добавьте следующий код в EnemyShip в код создания:

this.bulletTimer = 60;

Так мы настроим наш таймер, чтобы вражеский корабль стрелял с определённым интервалом. Мы будем уменьшать значение this.bulletTimer с каждым кадром и сбрасывать его после выстрела. 60 означает, что мы будем ждать 1 секунду (60 кадров), прежде чем выстрелить первую пулю.

Добавьте этот код в раздел Кадр:

this.bulletTimer -= ct.delta;
if (this.bulletTimer <= 0) {
    this.bulletTimer = 180;
    ct.types.copy('Laser_Red', this.x, this.y + 32);
}

this.bulletTimer -= ct.delta; означает, что мы уменьшаем значение this.bulletTimer на длину кадра. ct.delta обычно равно 1, но на низком FPS будет становиться больше, чтобы компенсировать лаги, и чтобы стрельба была плавной.

Когда переменная таймера дойдёт до нуля, мы сбросим его, устанавливая this.bulletTimer новое значение, и создадим красную лазерную пулю. Написав this.y + 32, мы создаём пулю немного ниже корабля.

Давайте напишем код для красных пуль. Добавьте этот код в раздел «Код создания» для Laser_Red:

this.speed = 8;
this.direction = 270;

this.rotation = ct.random.deg();

this.rotation поворачивает текстуру копии. ct.random.deg() возвращает случайное значение от 0 до 360, что полезно для задания углов.

TIP

Ещё есть this.scale.x и this.scale.y, которые растягивают текстуру по горизонтали и вертикали соответственно, а также this.alpha, которая меняет прозрачноть (0 сделает копию невидимой, 1 — полностью непрозрачной).

Код раздела Кадр будет выглядеть так:

if (this.y > ct.height + 40) {
    this.kill = true;
}

this.move();

this.rotation += 4 * ct.delta;

this.rotation += 4 * ct.delta; будет каждый кадр вращать текстуру примерно на 4 градуса.

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

Создание копий с течением времени

Откройте комнату Main в разделе с комнатами. Удалите существующие астероиды и врагов, щёлкнув по ним правой кнопкой мыши, или сотрите их левой кнопкой мыши, удерживая клавишу Ctrl.

Затем нажмите кнопку «События комнаты» слева.

В комнатах те же самые события, что и в копиях.

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

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

Для этого настройте два таймера в коде создания:

this.asteroidTimer = 20;
this.enemyTimer = 180;

Затем добавьте этот код, чтобы генерировать врагов:

this.asteroidTimer -= ct.delta;
if (this.asteroidTimer <= 0) {
    this.asteroidTimer = ct.random.range(20, 200);
    ct.types.copy(ct.random.dice('Asteroid_Big', 'Asteroid_Medium'), ct.random(ct.viewWidth), -100);
}

this.enemyTimer -= ct.delta;
if (this.enemyTimer <= 0) {
    this.enemyTimer = ct.random.range(180, 400);
    ct.types.copy('EnemyShip', ct.random(ct.viewWidth), -100);
}

Это всё, что нужно для генерации астероидов и врагов!

TIP

ct.random.dice возвращает одно из предоставленных значений. Можно перечислить любые значения, включая числа, строки, сложные объекты. В нашем же случае есть 50% вероятность того, что будет создан 'Asteroid_Big' и 50% того, что это будет 'Asteroid_Medium'.

ct.random.range(a, b) возвращает случайное числовое значение от a и b.

ct.random(b) — то же самое, что и ct.random.range(0, b).

Жизни, счёт и графический интерфейс

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

Добавление и рисование счёта

Счёт для нас — это числовая переменная, хранящаяся глобально. В нашем случае лучше разместить его внутри комнаты. Откройте комнату «Main» и нажмите кнопку «События комнаты». Добавьте этот код в раздел «При создании»:

this.score = 0;

this.scoreLabel = new PIXI.Text('Score: ' + this.score);
this.addChild(this.scoreLabel);
this.scoreLabel.x = 30;
this.scoreLabel.y = 30;

Здесь мы создаём переменную с именем score, затем мы создаем текстовый блок с помощью new PIXI.Text ('Some text'), сохраняем его в this.scoreLabel и добавляем в комнату с помощью this.addChild (this.scoreLabel);. Потом мы размещаем его так, чтобы он отображался в верхнем левом углу, с отступом 30 пикселей на каждой стороне.

Мы также должны добавить эту строку на вкладку Draw, чтобы текст обновлялся каждый кадр:

this.scoreLabel.text = 'Score: ' + this.score;

Теперь откроем код Кадр у EnemyShip и добавим ct.room.score += 100; туда, где корабль уничтожается от столкновения с пулями игрока. Код будет выглядеть так:

this.move();

if (this.y > ct.height + 80) {
    this.kill = true;
}

var collided = ct.place.meet(this, this.x, this.y, 'Laser_Blue');
if (collided) {
    collided.kill = true;
    this.kill = true;
    ct.room.score += 100;
}

this.bulletTimer -= ct.delta;
if (this.bulletTimer <= 0) {
    this.bulletTimer = 180;
    ct.types.copy('Laser_Red', this.x, this.y + 32);
}

TIP

ct.room указывает на текущую комнату.

Сделайте то же самое с астероидами. Измените добавляемый счёт по вашему желанию.

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

Текст может быть нарисован с помощью предопределённых стилей, которые содержат цвет заливки, стиль обводки, настройки шрифта, тень. Они создаются в разделе Интерфейс вверху экрана. Создайте его, нажав кнопку «Создать». Вы увидите редактор стилей, который имеет левую панель с вкладками для настройки свойств и изображение для предварительного просмотра справа.

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

Перейдите на вкладку «Заливка», активируйте её и выберите тип заливки «Сплошная». Выберите подходящий цвет; я выбрал что-то похожее на цвета корабля игрока.

Добавьте тень или границу, или и то, и другое! Затем сохраните изменения, нажав кнопку «Применить» в левом нижнем углу.

Назовите созданный стиль ScoreText. Его можно переименовать, щёлкнув по нему правой кнопкой мыши в списке стилей.

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

this.asteroidTimer = 20;
this.enemyTimer = 180;

this.score = 0;
this.scoreLabel = new PIXI.Text('Score: ' + this.score, ct.styles.get('ScoreText'));
this.addChild(this.scoreLabel);
this.scoreLabel.x = 30;
this.scoreLabel.y = 30;

TIP

ct.styles.get('Style'); загружает данный стиль. Вы можете использовать его внутри конструктора PIXI.Text для стилизации создаваемых блоков текста.

Если вы запустите игру, счёт будет отображаться с созданным стилем. Ура!

Отображение жизней и их подсчёт

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

this.lives = 3;
this.livesLabel = new PIXI.Text('Lives: ' + this.lives, ct.styles.get('ScoreText'));
this.addChild(this.livesLabel);
this.livesLabel.x = ct.viewWidth - 200;
this.livesLabel.y = 30;

И нам нужен этот код, чтобы обновлять текст:

this.livesLabel.text = 'Lives: ' + this.lives;

Самостоятельно!

Создайте новый стиль и примените его к метке «Lives».

Теперь нужно добавить код, чтобы корабль игрока удалял по одной жизни при столкновении. Мы могли бы использовать ct.place.meet и проверять столкновение с определёнными типами, как мы использовали его в коде астероидов и врагов, но давайте сгруппируем их в одну группу столкновений. Это позволит написать меньше кода и не потребует никаких изменений, если мы добавим больше врагов, ракет или астероидов разного размера.

Чтобы добавить копии в группу столкновений, мы должны добавить эту строку кода в событии создания для всех необходимых типов:

this.ctype = 'Hostile';

Добавьте эту строку в Код создания астероидов, вражеских кораблей и красных лазеров.

Теперь добавьте этот код в Кадр корабля игрока:

var hostile = ct.place.occupied(this, this.x, this.y, 'Hostile');
if (hostile) {
    hostile.kill = true;
    ct.room.lives --;
    if (ct.room.lives <= 0) {
        this.kill = true;
        setTimeout(function() {
            ct.rooms.switch('Main');
        }, 1000);
    }
}

ct.place.occupied похож на ct.place.meet, что мы уже использовали, но этот метод работает с группами столкновений, а не с типами.

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

setTimeout — это стандартная функция браузера, которая выполняет функцию через заданное количество миллисекунд. Здесь мы ждём одну секунду (1000 миллисекунд), а затем перезапускаем комнату.

TIP

setTimeout может показаться более качественным способом работы с отложенными событиями, нежели чем написание таймеров. Разница в том, что таймеры существуют пока жив их владелец, а setTimeout будет срабатывать при любых обстоятельствах, даже если вызвавшая его копия была удалена из комнаты. (На самом деле, есть способ отменить setTimeout, но он неудобен при работе с разными копиями. Всё, забудьте!)

В нашем случае нужно, чтобы комната была перезапущена, когда корабля игрока не будет на экране, поэтому мы используем setTimeout. Мы используем таймеры для стрельбы и создания врагов, потому что мы не хотим, чтобы пули появлялись случайным образом после их уничтожения.

Сохраните ваш проект и протестируйте его. Теперь у вас есть маленький, но полностью рабочий космический шутер! Есть много способов улучшить эту игру:

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

Вот мой результат улучшения этого проекта: Catsteroids.

Вы также можете начать новый проект, если не увлекаетесь космическими шутерами 😄