Как сделать игру, похожую на Space Invaders

В этом уроке по Unity вы узнаете, как создать классическую двухмерную космическую стрелялку, похожую на Space Invaders.

Версия: C# 7.3, Unity 2020.3, Unity

Space Invaders, известная в Японии как Supēsuinbēdā (スペースインベーダー), — одна из самых известных ретро-игр в мире. Выпущенная для игровых автоматов в 1978 году японской игровой компанией Taito, она быстро стала хитом.

Классическая аркадная игра Invaders в Unity

Лазерная пушка, стреляющая пулями, представляет игрока. Игрок может спрятаться за любой из четырех торчек (トーチカ), также известных как доты.

Есть три типа захватчиков: крабы, кальмары и осьминоги. Они появляются роем из нескольких рядов и перемещаются сверху вниз экрана. Краб-захватчик стал культовым символом, повсеместно ассоциирующимся с аркадами и играми в целом.

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

В этом уроке вы воспроизведете основные особенности игрового процесса Space Invaders с помощью Unity. По пути вы узнаете, как:

  • Создавать и перемещать рой захватчиков.
  • Заставить захватчиков во главе роя стрелять лазерными пулями.
  • Двигать и стрелять в ответ как игрок.
  • Менять темп музыки и скорость роя в зависимости от количества прибитых врагов.

Примечание. В этом уроке предполагается, что у вас уже есть базовый опыт работы с Unity и промежуточные знания C#. Кроме того, предполагается, что вы используете C# 7 и Unity 2020.3.

Приступая к работе

Скачайте материалы проекта, нажав кнопку «Скачать материалы урока» вверху страницы.

Распакуйте zip-файл в удобное место. После извлечения вы увидите папки Starter и Final проекта. Откройте начальный проект в Unity.

Проект содержит несколько папок, которые помогут вам начать работу. Откройте Assets/RW, и вы найдете следующие каталоги:

  • Animations: Содержит готовые анимации и аниматор для префабов пуль и взрывов.
  • Prefabs: Содержит все префабы, которые использует проект.
  • Resources: Содержит шрифты и текстуры.
  • Scenes: Содержит основную сцену, с которой вы будете работать.
  • Scripts: Есть все скрипты проекта. Большинство из них — пустые оболочки, которые вы заполните.
  • Sounds: Имеет все звуковые эффекты и музыкальный файл.
  • Sprites: Содержит все пиксельные изображения для проекта.

Перейдите к RW/Scenes и откройте Main. Нажмите на Play, и вы увидите красную пушку в центре нижней части экрана и услышите музыкальный цикл. Кроме того, вы увидите ярлыки UI Score и Lives.

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

Начальная позиция в окне Scene редактора Unity показывает пушку, игровой счет и число жизней

Остановите игру и выберите Edit > Project Settings > Physics 2D. Взгляните на матрицу столкновений слоев. Обратите внимание, что в этом проекте есть два пользовательских слоя: Hero и Enemy.

Матрица слоев для Physics2D столкновений в Unity

Обнаружение столкновений Physics 2D для игровых объектов в слое Enemy работает только с игровыми объектами в слое Hero. Игровые объекты слоя Hero работают как с Enemy, так и с другими игровыми объектами слоя Hero. Эта информация будет важна позже.

Теперь взгляните на игровые объекты в окне Hierarchy. К некоторым из них прикреплены пользовательские компоненты, над которыми вы будете работать в данном уроке.

Окно Hierarchy проекта в Unity

Game Controller имеет прикрепленные компоненты Invader Swarm, Torchka Manager и Game Manager. В дополнение к этому есть компонент Audio Source, который вы будете использовать для звуковых эффектов.

Окно Inspector игрового объекта Game Controller в Unity

Music содержит компоненты Music Control и Audio Source. Audio Source воспроизводит Beats, расположенные в папке RW/Sounds. Это основная музыка в игре. В настоящее время у музыки фиксированный темп, но позже вы сделаете его динамичным.

Игровой объект Music в окне Inspector редактора Unity

CANNON содержит компоненты Cannon Control и Box Collider 2D для обнаружения столкновений. Обратите внимание, что его слой установлен на Hero. У него также есть два непосредственных дочерних элемента: Sprite, связанный с ним спрайт, и Muzzle, пустой игровой объект, представляющий позицию для создания экземпляра пули во время стрельбы.

Компонент CANNON в окне Inspector редактора Unity

Main Camera является основной и единственной камерой в игре и содержит компонент Audio Listener. Вид ортографический со смещением позиции -10 по оси Z.

Для всех остальных игровых объектов Z установлено равным 0. На самом деле, для остальных предположим, что ось Z не существует, так как это 2D-игра, и вы будете придерживаться их позиционирования только по осям X и Y.

INVADERS Spawn Start и TORCHKA Spawn Center — это два пустых вспомогательных игровых объекта, предком которых является игровой объект Helpers. Их позиции помогут вам позже породить рой захватчиков и прикрытий.

Canvas и Event System предназначены для пользовательского интерфейса. У Canvas есть следующие дочерние элементы:

  • Score Text: Отображает счет.
  • Lives Text: Вы будете использовать его для отображения оставшихся жизней игрока.
  • Game Over Panel: На этой панели отображается сообщение Game Over, когда у игрока заканчиваются жизни.
  • All Clear: Вы будете использовать эту панель для отображения сообщения All Clear, когда игрок уничтожит всех захватчиков.
  • Restart Button: Эта кнопка перезапускает игру.

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

Реализация элементов управления игроком

Выберите скрипт CannonControl, прикрепленный к CANNON. Откройте его в редакторе кода. Вставьте следующий код внутри класса:

[SerializeField] 
private float speed = 500f;

private void Update()
{
    if (Input.GetKey(KeyCode.D)) 
    {
        transform.Translate(speed * Time.deltaTime, 0, 0);
    }
    else if (Input.GetKey(KeyCode.A)) 
    {
        transform.Translate(-speed * Time.deltaTime, 0, 0);
    }
}

Вот разбор кода:

  • Переменная speed управляет скоростью пушки.
  • Внутри метода Update вы проверяете, удерживает ли игрок клавиши D или A. Если игрок удерживает D, пушка движется вправо со скоростью speed * Time.deltaTime каждый кадр. Если игрок удерживает кнопку A, то пушка перемещается влево на такое же расстояние.

Сохраните файл. Вернитесь в Unity и выберите Play внутри редактора. Теперь вы можете перемещать корабль, используя D и A.

Теперь пушку в игровом окне Scene редактора Unity можно двигать, используя клавиши

Далее вы добавите механику стрельбы, используя префаб Bullet внутри RW/Prefabs.

Выберите Bullet и взгляните на Inspector.

Префаб имеет компонент Kinematic Rigidbody 2D. Это кинематика, потому что вы не будете полагаться на физику для перемещения пули, а вместо этого будете переводить ее с помощью скрипта.

Слой Rigidbody 2D установлен на Hero и имеет компонент Box Collider 2D для обнаружения столкновений. Также есть компонент Bullet, который пока ничего не делает.

Откройте скрипт Bullet в редакторе кода. Добавьте следующий код внутри класса:

[SerializeField] 
private float speed = 200f;

[SerializeField] 
private float lifeTime = 5f;

internal void DestroySelf()
{
    gameObject.SetActive(false);
    Destroy(gameObject);
}

private void Awake()
{
    Invoke("DestroySelf", lifeTime);
}

private void Update()
{
    transform.Translate(speed * Time.deltaTime * Vector2.up);
}

private void OnCollisionEnter2D(Collision2D other)
{
    DestroySelf();
}

В приведенном выше коде вы:

  • Используйте метод Update, чтобы перемещать пулю по скорости speed * Time.deltaTime каждый кадр вверх.
  • Используйте DestroySelf, чтобы уничтожить игровой объект пулю. Перед вызовом Destroy вы отключаете пулю, потому что процесс уничтожения занимает несколько кадров, а отключение происходит почти мгновенно.
  • Awake вызывает DestroySelf для автоматического уничтожения пули через несколько секунд lifetime. DestroySelf также вызывается, когда пуля сталкивается с другим игровым объектом.

Вам понадобятся звуковые эффекты во время стрельбы. Внутри RW/Scripts откройте GameManager и добавьте в класс следующее:

internal static GameManager Instance;

[SerializeField] 
private AudioSource sfx;

internal void PlaySfx(AudioClip clip) => sfx.PlayOneShot(clip);

private void Awake()
{
    if (Instance == null) 
    {
        Instance = this;
    }
    else if (Instance != this) 
    {
        Destroy(gameObject);
    }
}

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

Чтобы использовать пулю, вам нужно создать ее экземпляр. Снова откройте CannonControl и добавьте следующее после объявления speed:

[SerializeField] 
private Transform muzzle;

[SerializeField] 
private AudioClip shooting;

[SerializeField] 
private float coolDownTime = 0.5f;

[SerializeField] 
private Bullet bulletPrefab;

private float shootTimer;

Затем, после кода движения, внутри Update, вставьте:

shootTimer += Time.deltaTime;
if (shootTimer > coolDownTime && Input.GetKey(KeyCode.Space))
{
    shootTimer = 0f;

    Instantiate(bulletPrefab, muzzle.position, Quaternion.identity);
    GameManager.Instance.PlaySfx(shooting);
}

Этот код увеличивает ShootTimer каждый кадр, пока не достигнет coolDownTime. Если игрок удерживает клавишу пробела, код сбрасывает ShootTimer и создает экземпляр bulletPrefab в muzzle.position. Он также воспроизводит звуковой эффект стрельбы с помощью PlaySfx внутри GameManager.

Сохраните все и вернитесь в Unity.

Вернитесь к префабу Bullet. Обратите внимание на новые поля для компонента Bullet.

Префаб Bullet в окне инспектора редактора Unity

Теперь выберите Game Controller из Hierarchy. Установите Sfx для Game Manager на Audio Source, подключенный к компоненту Game Controller.

Окно Inspector показывает компонент игрового контролллера Audio Source в редакторе Unity

Выберите CANNON в иерархии. В Cannon Control сначала установите Muzzle в Muzzle Transform, дочерний элемент CANNON. Затем установите Bullet Prefab на Bullet из RW/Prefabs. И наконец, установите для параметра Shooting значение CannonBullet, которое находится в разделе RW/Sounds.

Компонент Cannon Control в окне Inspector редактора Unity

Сохраните и поиграйте. Теперь вы можете перемещать пушку и стрелять лазерными пулями, удерживая нажатой клавишу Space.

Теперь пушка может стрелять лазерными пулями в редакторе Unity

Ваш герой готов! Теперь пришло время добавить злодеев. В следующем разделе вы добавите рой захватчиков.

Создание захватчиков

Как все антагонисты начинают свою жизнь в играх? Spawning конечно! Вы сделаете это дальше.

Порождение захватчиков

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

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

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

Теперь выберите Invader Swarm, подключенный к игровому контроллеру. Откройте связанный скрипт в редакторе кода. Добавьте следующий код внутри класса:

[System.Serializable]
private struct InvaderType
{
    public string name;
    public Sprite[] sprites;
    public int points;
    public int rowCount;
}

Вот разбор кода:

  • Этот код определяет структуру InvaderType.
  • В массиве sprites хранятся все спрайты, связанные с типом захватчика, которые SimpleAnimator использует для анимации захватчика.
  • rowCount — это количество строк, которые захватчик будет иметь в рое.
  • name хранит имя типа для захватчика, а в points хранится игровой счет, если игрок уничтожает захватчика этого типа. Они пригодятся позже.

Теперь добавьте следующее прямо под InvaderType:

[Header("Spawning")]
[SerializeField] 
private InvaderType[] invaderTypes;

[SerializeField] 
private int columnCount = 11;

[SerializeField] 
private int ySpacing;

[SerializeField]
private int xSpacing;

[SerializeField] 
private Transform spawnStartPoint;

private float minX;

Вы будете использовать все эти поля для spawn логики объектов:

  • InvaderTypes: представляет все используемые типы захватчиков.
  • columnCount: общее количество столбцов для роя.
  • ySpacing: расстояние между каждым захватчиком в рое по оси Y.
  • xSpacing: расстояние между каждым захватчиком в рое по оси X.
  • spawnStartPoint: точка появления первого захватчика.
  • minX: cохраняет минимальное значение позиции X для роя.

Затем вставьте следующий код после всех объявлений переменных:

private void Start()
{
    minX = spawnStartPoint.position.x;

    GameObject swarm = new GameObject { name = "Swarm" };
    Vector2 currentPos = spawnStartPoint.position;

    int rowIndex = 0;
    foreach (var invaderType in invaderTypes)
    {
        var invaderName = invaderType.name.Trim();
        for (int i = 0, len = invaderType.rowCount; i < len; i++)
        {
            for (int j = 0; j < columnCount; j++)
            {
                var invader = new GameObject() { name = invaderName };
                invader.AddComponent<SimpleAnimator>().sprites = invaderType.sprites;
                invader.transform.position = currentPos;
                invader.transform.SetParent(swarm.transform);

                currentPos.x += xSpacing;
            }

            currentPos.x = minX;
            currentPos.y -= ySpacing;

            rowIndex++;
        }
    }
}

Вот разбор кода:

  1. Этот код устанавливает minX внутри Start. Затем он создает пустой игровой объект с именем Swarm.
  2. xSpacing и ySpacing обновляют currentPos по осям X и Y соответственно.
  3. currentPos.x увеличивается после каждой итерации захватчика в строке. После завершения строки значение currentPos.y уменьшается.
  4. В цикле по элементам invaderTypes для каждого захватчика вы выполняете итерацию по строкам, чтобы создать отдельные игровые объекты захватчика в позиции currentPos.
  5. xSpacing и ySpacing обновляют currentPos по осям X и Y соответственно.
  6. Каждому созданному игровому объекту-захватчику присваивается имя invaderName. Затем вы добавляете компонент SimpleAnimator и назначаете его массив спрайтов sprites, связанным с invaderType.
  7. И наконец, захватчик становится дочерним элементом Swarm, и его позиция устанавливается на currentPos.

Сохраните все и вернитесь в Unity.

Выберите Game Controller в иерархии. На Invader Swarm установите:

  • Y Spacing на 25
  • X Spacing на 25
  • Spawn Start Point на INVADERS Spawn Start, которая является дочерним элементом Helpers.

Затем установите размер Invader Types на 3 и установите поля-члены следующим образом:

  • 0: Установите для Name значение SQUID, для Points30, а для Row Count1.
  • 1: Установите для Name значение CRAB, для Points20, а для Row Count1.
  • 2: Установите Name на OCTOPUS, для Points на 10 и Row Count на 2.

Теперь перейдите в RW/Sprites и посмотрите на таблицу спрайтов INVADERS. Это не оригинальные Space Invaders, но они подойдут для этого урока.

Вернитесь к Invader Swarm и настройте его следующим образом, используя таблицу спрайтов:

Для записи SQUID установите список Sprites, чтобы он содержал следующие спрайты из таблицы спрайтов в следующем порядке:

  • bugs_invaders_0
  • bugs_invaders_5
  • bugs_invaders_9
  • bugs_invaders_4

Выполните то же упражнение, но на этот раз для CRAB, используя следующие спрайты:

  • bugs_invaders_13
  • bugs_invaders_18

И наконец, назначьте спрайты для OCTOPUS, используя:

  • bugs_invaders_7
  • bugs_invaders_2

Установка значений для поля Invader Types компонента игрового контроллера Invader Swarm в окне Inspector редактора Unity

Вот визуальная ссылка на то, как все должно выглядеть сейчас:

Теперь игра создает захватчиков в редакторе Unity

Сохраните и поиграйте. Обратите внимание на появление роя и появление захватчиков в одном месте.

Чудесно! Но они не двигаются. Вы исправите это в следующем разделе.

Перемещение захватчиков

Вернитесь к скрипту InvaderSwarm.cs и добавьте следующее после существующих объявлений переменных:

[Space]
[Header("Movement")]
[SerializeField] 
private float speedFactor = 10f;

private Transform[,] invaders;
private int rowCount;
private bool isMovingRight = true;
private float maxX;
private float currentX;
private float xIncrement;

Все эти переменные помогут двигать рой:

  • speedFactor на данный момент представляет собой скорость, с которой захватчики перемещаются по оси X. Позже скорость будет зависеть от темпа музыки, поэтому фактическая скорость будет произведением этих двух величин.
  • invaders хранят Transforms всех созданных игровых объектов invader.
  • rowCount хранит общее количество строк в рое.
  • isMovingRight представляет направление движения и по умолчанию имеет значение true.
  • maxX — максимальная позиция X для движения роя.
  • currentX представляет общую позицию X роя.
  • xIncrement — это значение за кадр, которое перемещает захватчиков по оси X.

Теперь в метод Start добавьте следующий код прямо над int rowIndex = 0;:

foreach (var invaderType in invaderTypes)
{
    rowCount += invaderType.rowCount;
}
maxX = minX + 2f * xSpacing * columnCount;
currentX = minX;
invaders = new Transform[rowCount, columnCount];

Этот код вычисляет общее количество строк и сохраняет его внутри rowCount. Затем вы вычисляете maxX на основе общего количества столбцов и расстояния между каждым захватчиком. Изначально currentX устанавливается в позицию X точки spawnStartPoint.

Вы объявили массив invaders. Чтобы заполнить его, вам понадобится еще одна строка кода.

Вставьте следующую строку в самый внутренний цикл for, прямо над currentPos.x += xSpacing;:

invaders[rowIndex, j] = invader.transform;

Эта строчка кода заботится о заселении захватчиками.

И наконец, сразу после метода Start вставьте:

private void Update()
{
    xIncrement = speedFactor * Time.deltaTime;
    if (isMovingRight)
    {
        currentX += xIncrement;
        if (currentX < maxX) 
        {
            MoveInvaders(xIncrement, 0);
        }
        else 
        {
            ChangeDirection();
        }
    }
    else
    {
        currentX -= xIncrement;
        if (currentX > minX) 
        {
            MoveInvaders(-xIncrement, 0);
        }
        else 
        {
            ChangeDirection();
        }
    }
}

private void MoveInvaders(float x, float y)
{
    for (int i = 0; i < rowCount; i++)
    {
        for (int j = 0; j < columnCount; j++)
        {
            invaders[i, j].Translate(x, y, 0);
        }
    }
}

private void ChangeDirection()
{
    isMovingRight = !isMovingRight;
    MoveInvaders(0, -ySpacing);
}

Вот разбор кода:

  • MoveInvaders принимает два значения с плавающей запятой: x и y. Он перемещает каждый Transform в invaders на одно и то же значение по осям X и Y соответственно.
  • ChangeDirection переключает isMovingRight и перемещает рой вниз на величину ySpacing.
  • Внутри метода Update вы вычисляете xIncrement и обновляете currentX в зависимости от направления каждого кадра.
  • Вы используете currentX, чтобы проверить, приближается ли позиция X роя к порогу. Если да, вы вызываете ChangeDirection. Если нет, вы перемещаете рой с помощью MoveInvaders.

Сохраните все. Вернитесь в Unity и нажмите на Play. Вы увидите, как движется рой захватчиков. Находясь в игровом режиме, попробуйте разные значения Speed Factor роя захватчиков и посмотрите, как он повлияет на скорость роя.

Теперь захватчики в игре на Unity передвигаются с нарастающей скоростью по экрану, а затем вниз

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

Заставить захватчиков стрелять лазерами

Вы будете использовать вариант префаба Bullet для захватчиков. Перейдите в RW/Prefabs и найдите EnemyBullet. Это то же самое, что и Bullet, за исключением того, что спрайт указывает в противоположном направлении Y, а его слой установлен на Enemy.

Выберите EnemyBullet. Обратите внимание, что скорость пули установлена на -200. Это гарантирует, что пуля движется в направлении, противоположном направлению пушечных пуль, но с той же величиной.

Префаб EnemyBullet в окне Inspector редактора Unity

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

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

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

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

Откройте скрипт InvaderSwarm.cs и добавьте следующую строку после объявления структуры InvaderType:

internal static InvaderSwarm Instance;

Это помогает превратить InvaderSwarm в синглтон.

Затем вставьте следующее прямо над методом Start:

internal Transform GetInvader(int row, int column)
{
    if (row < 0 || column < 0
        || row >= invaders.GetLength(0) || column >= invaders.GetLength(1)) 
    {
        return null;
    }

    return invaders[row, column];
}

private void Awake()
{
    if (Instance == null) 
    {
        Instance = this;
    }
    else if (Instance != this) 
    {
        Destroy(gameObject);
    }
}

GetInvader возвращает Transform захватчика по индексу строки и столбца захватчиков. Awake превращает InvaderSwarm в Singleton, гарантируя, что при запуске игры будет активен только один экземпляр InvaderSwarm.

Теперь выберите префаб BulletSpawner и взгляните на Inspector. Обратите внимание, что к нему прикреплен BulletSpawner.

Также есть Kinematic Rigidbody 2D, Box Collider 2D и слой установлен на Enemy. Вы не будете добавлять коллайдеры к захватчикам, а будете использовать этот коллайдер для обнаружения попаданий пушечных пуль.

Префаб BulletSpawner в окне Inspector редактора Unity

В редакторе кода откройте скрипт BulletSpawner.cs, прикрепленный к BulletSpawner, и добавьте в класс следующее:

internal int currentRow;
internal int column;

[SerializeField] 
private AudioClip shooting;

[SerializeField] 
private GameObject bulletPrefab;

[SerializeField] 
private Transform spawnPoint;

[SerializeField] 
private float minTime;

[SerializeField]
private float maxTime;

private float timer;
private float currentTime;
private Transform followTarget;

internal void Setup()
{
    currentTime = Random.Range(minTime, maxTime);
    followTarget = InvaderSwarm.Instance.GetInvader(currentRow, column);
}

private void Update()
{
    transform.position = followTarget.position;

    timer += Time.deltaTime;
    if (timer < currentTime) 
    {
        return;
    }

    Instantiate(bulletPrefab, spawnPoint.position, Quaternion.identity);
    GameManager.Instance.PlaySfx(shooting);
    timer = 0f;
    currentTime = Random.Range(minTime, maxTime);
}

Вот разбор кода:

  • currentTime представляет время ожидания до выстрела следующей пули. Для него установлено случайное значение между minTime и maxTime.
  • currentRow и column связывают генератор пуль с захватчиком. column задается один раз и не меняется. Но, как вы увидите позже, currentRow обновляется, если пули игрока попадают в этот генератор.
  • Внутри метода Setup() вы устанавливаете followTarget, вызывая GetInvader из экземпляра InvaderSwarm, используя currentRow и column. Вы также устанавливаете начальное значение для currentTime.
  • Внутри метода Update вы обновляете позицию генератора пуль, чтобы она соответствовала followTarget.position. Кроме того, вы увеличиваете таймер до тех пор, пока он не достигнет currentTime. Когда это происходит, вы создаете пулю в точке spawnPoint.position во время воспроизведения звукового эффекта стрельбы, после чего сбрасываете таймер и currentTime.

Сохраните все. Вернитесь в Unity и откройте BulletSpawner в Prefab Mode. Убедитесь, что для Bullet Spawner установлены следующие значения:

  • Shooting на InvaderBullet, расположенному по адресу RW/Sounds.
  • Bullet Prefab на EnemyBullet, расположенного в RW/Prefabs.
  • Spawn Point на SpawnPoint Transform, который является единственным потомком BulletSpawner.
  • Min Time на 1 и Max Time на 10.

Окно Inspector редактора Unity показывает скрипт Bullet Spawner

Чтобы использовать BulletSpawner, вам нужно вернуться к скрипту InvaderSwarm.cs.

Сначала вставьте следующую строку в конце всех объявлений переменных:

[SerializeField] 
private BulletSpawner bulletSpawnerPrefab;

Затем в метод Start добавьте в конце следующие строки:

for (int i = 0; i < columnCount; i++)
{
    var bulletSpawner = Instantiate(bulletSpawnerPrefab);
    bulletSpawner.transform.SetParent(swarm.transform);
    bulletSpawner.column = i;
    bulletSpawner.currentRow = rowCount - 1;
    bulletSpawner.Setup();
}

В этом коде вы создаете генератор пуль и настраиваете его. Вы создаете экземпляр bulletSpawner для каждого столбца роя и устанавливаете его column и currentRow, а затем вызываете его метод Setup. Вы также отправляете bulletSpawner к Swarm, чтобы предотвратить беспорядок в иерархии.

Сохраните все и вернитесь в Unity. Выберите Game Controller из Hierarchy и установите префаб Bullet Spawner для Invader Swarm на BulletSpawner, расположенный в RW/Prefabs.

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

Теперь рой стреляет лазерными пулями по пушке в редакторе Unity

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

Однако чего-то не хватает, так это взрывов. Вы добавите их дальше.

Добавление взрывов

Перейдите к RW/Prefabs. Обратите внимание на префаб Explosion. Это простой GameObject с предварительно созданной анимацией спрайтов, которую вы будете использовать для визуальных эффектов взрыва.

Теперь снова откройте GameManager.cs. Добавьте следующее после объявления sfx:

[SerializeField] 
private GameObject explosionPrefab;

[SerializeField] 
private float explosionTime = 1f;

[SerializeField] 
private AudioClip explosionClip;

internal void CreateExplosion(Vector2 position)
{
    PlaySfx(explosionClip);

    var explosion = Instantiate(explosionPrefab, position,
        Quaternion.Euler(0f, 0f, Random.Range(-180f, 180f)));
    Destroy(explosion, explosionTime);
}

CreateExplosion создает взрыв в позиции со случайным вращением по оси Z и уничтожает его через explosionTime секунд.

Чтобы использовать CreateExplosion, добавьте следующую строку в конец DestroySelf внутри класса Bullet:

GameManager.Instance.CreateExplosion(transform.position);

Сохраните все. Вернитесь в Unity и выберите Game Controller из Hierarchy. Для набора Game Manager:

  • Explosion Prefab для Explosion находится в RW/Prefabs.
  • Explosion Clip для Explosion находится в RW/Sounds.

Компонент Game Manager в окне Inspector редактора Unity

Сохраните сцену и поиграйте. Теперь в игре есть взрывы.

Теперь стрельба пулями в редакторе Unity приводит к взрывам

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

Добавление игрового счета и жизней

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

Реализация жизней игрока

Откройте GameManager.cs и добавьте следующий код после объявлений переменных:

[SerializeField] 
private int maxLives = 3;

[SerializeField] 
private Text livesLabel;

private int lives;

internal void UpdateLives()
{
    lives = Mathf.Clamp(lives - 1, 0, maxLives);
    livesLabel.text = $"Lives: {lives}";
}

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

Добавьте следующее в конце Awake:

lives = maxLives;
livesLabel.text = $"Lives: {lives}";

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

Теперь откройте CannonControl и вставьте следующие строки после объявлений переменных:

[SerializeField] 
private float respawnTime = 2f;

[SerializeField] 
private SpriteRenderer sprite;

[SerializeField] 
private Collider2D cannonCollider;

private Vector2 startPos;

private void Start() => startPos = transform.position;

Затем добавьте следующие строки после метода Update:

private void OnCollisionEnter2D(Collision2D other)
{
    GameManager.Instance.UpdateLives();
    StopAllCoroutines();
    StartCoroutine(Respawn());
}

System.Collections.IEnumerator Respawn()
{
    enabled = false;
    cannonCollider.enabled = false;
    ChangeSpriteAlpha(0.0f);

    yield return new WaitForSeconds(0.25f * respawnTime);

    transform.position = startPos;
    enabled = true;
    ChangeSpriteAlpha(0.25f);

    yield return new WaitForSeconds(0.75f * respawnTime);

    ChangeSpriteAlpha(1.0f);
    cannonCollider.enabled = true;
}

private void ChangeSpriteAlpha(float value)
{
    var color = sprite.color;
    color.a = value;
    sprite.color = color;
}

Вот что происходит:

  • ChangeSpriteAlpha изменяет непрозрачность спрайта пушки.
  • Когда пуля попадает в пушку, GameManager.UpdateLives уменьшает общее количество жизней и запускается сопрограмма Respawn.
  • Respawn сначала отключает cannonCollider и делает пушечный спрайт невидимым. Через несколько мгновений спрайт пушки становится слегка прозрачным, а положение пушки возвращается к startPos. И наконец, он восстанавливает непрозрачность спрайта и снова включает коллайдер.

Сохраните все и вернитесь в Unity. Выберите Game Controller и установите для Lives Label в Game Manager значение Lives Text, которое является дочерним элементом Canvas.

Game Manager в окне Inspector редактора Unity

Для Cannon Control на CANNON установите Sprite на Sprite Renderer, дочернем игровом объекте CANNON. Установите Collider на Box Collider 2D для игрового объекта CANNON.

Компонент Cannon Control в окне Inspector редактора Unity

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

Теперь пушка в игре на Unity теряет жизни при попадании снарядов

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

Реализация счета и окончания игры

Прежде чем сделать что-либо еще, откройте скрипт MusicControl.cs. Вы хотите остановить музыку по окончании игры, поэтому вставьте следующий код внутри класса:

[SerializeField] 
private AudioSource source;

internal void StopPlaying() => source.Stop();

StopPlaying останавливает источник звука при вызове.

Теперь откройте скрипт GameManager.cs и добавьте следующее после объявлений переменных:

[SerializeField] 
private MusicControl music;

[SerializeField] 
private Text scoreLabel;

[SerializeField] 
private GameObject gameOver;

[SerializeField] 
private GameObject allClear;

[SerializeField] 
private Button restartButton;

private int score;

internal void UpdateScore(int value)
{
    score += value;
    scoreLabel.text = $"Score: {score}";
}

internal void TriggerGameOver(bool failure = true)
{
    gameOver.SetActive(failure);
    allClear.SetActive(!failure);
    restartButton.gameObject.SetActive(true);

    Time.timeScale = 0f;
    music.StopPlaying();
}

Затем вставьте следующие строки в конец UpdateLives:

if (lives > 0) 
{
    return;
}

TriggerGameOver();

И наконец, добавьте следующее в конце метода Awake:

score = 0;
scoreLabel.text = $"Score: {score}";
gameOver.gameObject.SetActive(false);
allClear.gameObject.SetActive(false);

restartButton.onClick.AddListener(() =>
{
    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    Time.timeScale = 1f;
});
restartButton.gameObject.SetActive(false);

Вот что делает этот код:

  • allClear хранит ссылку на панель All Clear, которая отображается, когда игрок уничтожает всех захватчиков. gameOver ссылается на панель Game Over, которая показывает, когда у игрока заканчиваются жизни.
  • UpdateScore увеличивает игровой счет на переданное ему значение и обновляет метку пользовательского интерфейса, чтобы отразить изменения.
  • TriggerGameOver показывает панель Game Over, если failure равно значению true. В противном случае отображается панель All Clear. Он также включает restartButton, приостанавливает игру и останавливает музыку.
  • Awake обрабатывает событие onClick для кнопки перезапуска. Он перезагружает сцену при нажатии.

Откройте InvaderSwarm.cs и добавьте в класс следующее после объявлений переменных:

private int killCount;
private System.Collections.Generic.Dictionary<string, int> pointsMap;

internal void IncreaseDeathCount()
{
    killCount++;
    if (killCount >= invaders.Length)
    {
        GameManager.Instance.TriggerGameOver(false);
        return;
    }
}

internal int GetPoints(string alienName)
{
    if (pointsMap.ContainsKey(alienName)) 
    {
        return pointsMap[alienName];
    }
    return 0;
}

Затем вставьте следующую строку прямо над int rowIndex = 0; внутри метода Start:

pointsMap = new System.Collections.Generic.Dictionary<string, int>();

Под строкой прямо под var invaderName = invaderType.name.Trim(); добавьте следующее:

pointsMap[invaderName] = invaderType.points;

Вот разбор кода:

  • pointsMap - это словарь (сопоставление строки с целым числом). Он сопоставляет имя типа захватчика со значением его очков.
  • IncreaseDeathCount отслеживает и обновляет killCount, когда игрок устраняет захватчиков. Когда игрок уничтожает всех захватчиков, TriggerGameOver получает false и отображает панель All Clear.
  • GetPoints возвращает очки, связанные с типом захватчика, передавая его имя в качестве ключа.

И наконец, откройте BulletSpawner.cs, чтобы обрабатывать обнаружение столкновений для захватчиков. Вставьте следующее сразу после метода Update:

private void OnCollisionEnter2D(Collision2D other)
{
    if (!other.collider.GetComponent<Bullet>()) 
    {
        return;
    }

    GameManager.Instance.
        UpdateScore(InvaderSwarm.Instance.GetPoints(followTarget.gameObject.name));

    InvaderSwarm.Instance.IncreaseDeathCount();

    followTarget.GetComponentInChildren<SpriteRenderer>().enabled = false;
    currentRow = currentRow - 1;
    if (currentRow < 0) 
    {
        gameObject.SetActive(false);
    }
    else 
    {
        Setup();
    }
}

Вот что делает этот код:

  • OnCollisionEnter2D возвращает ничего не делая, если объект, который попал в генератор пуль, не был типа Bullet.
  • Если пушечная пуля попала в спаунера, счет и количество убийств обновляются. Кроме того, рендерер спрайтов текущего FollowTarget отключает, а затем обновляет currentRow.
  • Если строк не осталось, GameObject отключен. В противном случае вы вызываете Setup, чтобы обновить файл followTarget.

Вау! Это было много работы. Сохраните все и вернитесь в Unity, чтобы закончить этот шаг.

Выберите Music и установите Music Control на компонент Audio Source на том же игровом объекте.

Компонент Music Control в окне Inspector редактора Unity

Затем выберите Game Controller. Для Game Manager установите:

  • Music на Music Control.
  • Score Label на Score Text.
  • Game Over на Game Over Panel.
  • All Clear на All Clear.
  • Restart Button на Restart Button.

Окно Inspector в Unity показывает компонент Game Manager

Сохраните и поиграйте. Теперь вы можете убить захватчиков! Включите Gizmos в Game View и выберите Swarm, чтобы увидеть, как обновляются генераторы пуль.

Теперь лазерные пули в окне Game редактора Unity поражают и удаляют захватчиков

Убейте всех захватчиков, чтобы увидеть All Clear, или проиграйте, чтобы увидеть панель Game Over. Затем вы можете использовать кнопку Restart, чтобы перезагрузить сцену.

Панель Game Over в окне сцены редактора Unity

Панель All Clear окне сцены редактора Unity

Захватчики немного медлительны и имеют одинаковую скорость повсюду. В следующем разделе вы обновите их скорость в соответствии с темпом музыки.

Добавление простого динамического звука

Перейдите к MusicControl.cs. Откройте его в редакторе кода. Добавьте следующую строку вверху класса:

private readonly float defaultTempo = 1.33f;

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

Теперь вставьте следующее выше StopPlaying:

[SerializeField] 
internal int pitchChangeSteps = 5;

[SerializeField] 
private float maxPitch = 5.25f;

private float pitchChange;

internal float Tempo { get; private set; }

Затем добавьте следующие строки после определения StopPlaying:

internal void IncreasePitch()
{
    if (source.pitch == maxPitch) 
    {
        return;
    }

    source.pitch = Mathf.Clamp(source.pitch + pitchChange, 1, maxPitch);
    Tempo = Mathf.Pow(2, pitchChange) * Tempo;
}

private void Start()
{
    source.pitch = 1f;
    Tempo = defaultTempo;
    pitchChange = maxPitch / pitchChangeSteps;
}

Вот как работает код:

  • Внутри метода Start для source.pitch и Tempo установлены значения по умолчанию.
  • IncreasePitch увеличивает высоту тона исходного звука на величину, определяемую параметром pitchChange, который, в свою очередь, является отношением maxPitch и pitchChangeSteps. maxPitch также устанавливает верхний предел высоты тона.
  • После изменения высоты тона можно рассчитать темп по следующей формуле:

Формула pitchchange = -log(2)(tempo1/tempo2), где tempo1 обозначает темп перед изменением и tempo2 темп после изменения

Теперь откройте скрипт InvaderSwarm.cs и добавьте следующее в конце объявлений переменных:

[SerializeField] 
private MusicControl musicControl;

private int tempKillCount;

В IncreaseDeathCount вставьте в конце следующие строки:

tempKillCount++;
if (tempKillCount < invaders.Length / musicControl.pitchChangeSteps) 
{
    return;
}

musicControl.IncreasePitch();
tempKillCount = 0;

Теперь IncreaseDeathCount отслеживает переменную tempKillCount, чтобы проверить, не превышает ли invaders.Length / musicControl.pitchChangeSteps. Если это так, он вызывает IncreasePitch и tempKillCount.

Это означает, что когда игрок устраняет почти invaders.Length / musicControl.pitchChangeSteps захватчиков, высота звука и темп увеличиваются. Переменная Tempo внутри MusicControl отслеживает обновленный темп.

И наконец, внутри метода Update замените xIncrement = speedFactor * Time.deltaTime; на:

xIncrement = speedFactor * musicControl.Tempo * Time.deltaTime;

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

Сохраните все. Вернитесь в Unity и выберите Game Controller. Установите в компоненте Invader Swarm параметр Music Control на значение Music.

Сохранить и играть.

Окно Inspector в редакторе Unity показывает компонент Invader Swarm

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

В захватчиков стреляют, и рой движется с нарастающей скоростью в окне Game редактора Unity

Есть еще одна небольшая проблема: если вы пропустите каких-либо захватчиков, они продолжат движение, как только достигнут нижней части экрана. Было бы лучше запустить Game Over, если рой достигнет дна.

Для этого откройте InvaderSwarm.cs и добавьте следующее в конце объявлений переменных:

[SerializeField] 
private Transform cannonPosition;

private float minY;
private float currentY;

Затем вставьте следующие строки в начале метода Start:

currentY = spawnStartPoint.position.y;
minY = cannonPosition.position.y;

Затем добавьте следующее в конец ChangeDirection:

currentY -= ySpacing;
if (currentY < minY) 
{
    GameManager.Instance.TriggerGameOver();
}

Вот что делает этот код:

  • Внутри метода Start вы устанавливаете minY в положение Y пушки и currentY в положение Y точки spawnStartPoint.
  • При каждом вызове ChangeDirection уменьшает значение currentY до тех пор, пока оно не станет меньше minY, после чего игра заканчивается и отображается сообщение Game Over.

Сохраните все. Вернитесь в Unity и выберите Game Controller. Установите Cannon Position для Invader Swarm на Transform игрового объекта CANNON.

Окно Inspector в редакторе Unity отображает компонент Invader Swarm

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

Панель Game Over в сцене редактора Unity показывается после того, как захватчики достигают позиции, которая ниже пушки в игре

Вы это сделали!

Куда двигаться дальше?

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

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

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

Вся игра на Unity с торчками!

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

Автор перевода: Jean Winters

Источник: Unity Tutorial: How to Make a Game Like Space Invaders

Комментировать

Почта не публикуется.Обязательные поля отмечены *