Пишем игры: космический шутер
Давайте сделаем небольшую космическую стрелялку с астероидами, лазерами и вражескими кораблями! В этом туториале вы узнаете, как импортировать ресурсы, реагировать на пользовательский ввод, перемещать объекты и обнаруживать коллизии.
Импорт текстур
Откройте 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.cgroup = '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.
Вы также можете начать новый проект, если не увлекаетесь космическими шутерами 😄