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

Создание другого спрайта

На данный момент у меня нет спрайта для представления ракеты. Что я сделаю, так это открою новую сцену в Unity и импортирую 3D-объект ракеты. Я изменю свойства основной камеры в сцене, чтобы использовать ортогональную проекцию, и изменю свойство Очистить флажки на >сплошной цвет. Я обновлю сплошной цвет, чтобы он отличался от цветов на ракете. Я наведу ракету и отрегулирую направленное освещение.

В Unity есть класс ScreenCapture со статическим методом CaptureScreenshot. У него есть перегруженные методы, но я буду использовать тот, который принимает только один строковый параметр, который будет местом для сохранения снимка экрана. Я создам сценарий с именем ScreenCap, который прикреплю к игровому объекту MainCamera.

using UnityEngine;

public class ScreenCap : MonoBehaviour
{
  void Update()
  {
    if(Input.GetKeyDown(KeyCode.B))
    {
      ScreenCapture.CaptureScreenshot("Assets/Screenshots/Missile.png");
    }
  }
}

Теперь у меня есть снимок экрана, и я открою GIMP (GNU Image Manipulation Program), бесплатный инструмент для редактирования изображений. В GIMP я создам новое изображение из строки меню, выбрав Файл -> Создать. В новом окне Создать новое изображение я задаю ему одинаковую ширину и высоту со значением, равным степени двойки.

Ширина и высота не обязательно должны быть одинаковыми, но зачем степень двойки? Это рекомендуемый размер текстуры Unity. Если графический процессор не поддерживает размер текстуры NPOT (не степень двойки), Unity придется выполнить дополнительную работу, которая потребует больше памяти и замедлит загрузку текстур.

Затем я нажимаю и перетаскиваю изображение ракеты в GIMP, объединяю слои изображения на панели Слои и добавляю альфа-канал. к однослойному. Добавление альфа-канала позволит сделать части изображения прозрачными.

Затем я воспользуюсь Инструментом «Выбор по цвету» и нажму на цвет фона. Это выделяет все пиксели, окружающие ракету. Я продолжу и воспользуюсь Инструментом "Ластик", чтобы удалить фон.

Теперь изображение готово, я экспортирую его из GIMP через строку меню в разделе Файл и импортирую в Unity как спрайт. Я создам новый GameObject с компонентом Sprite Renderer и Capsule Collider 2D с пометкой Missile. В компоненте Sprite Renderer я обновлю спрайт только что импортированным. В Capsule Collider 2D я отмечаю свойство IsTrigger и настраиваю размер коллайдера.

Мне нужен след за ракетой, поэтому я добавлю дочерний GameObject с компонентом SpriteRenderer и Animator с пометкой Trail. Я обновлю параметры, чтобы они были похожи на то, как я настроил сломанный двигатель на корабле игрока.

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

Круто, теперь кодировать.

Обновление/рефакторинг: перечисления

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

  • Иметь способ общаться с SpawnManager, чтобы найти ближайшую вражескую цель (если она есть)
  • Выясните, как вращать игровые объекты, у которых нет 2D Rigidbody (я обновил угловую скорость Rigidbody2D на астероиде, чтобы повернуть его)

Наряду с этим я также решил реорганизовать код, чтобы я перестал использовать целые числа и логические значения для представления бонусов и режимов стрельбы. Я использовал перечисления. Что такое перечисление? Это тип значения, который определяется набором именованных констант, основной тип которого является целочисленным типом (byte, short, int и т. д.). . Я переработал сценарии Powerup и Player, чтобы использовать перечисление PowerupType и FiringMode.

using UnityEngine;

public enum PowerupType
{
  TripleShot,
  HomingMissile,
  HealthCollectible,
  AmmoCollectible,
  Shield,
  SpeedBoost
}

public class Powerup : MonoBehaviour
{
  ...
  [SerializeField]
  private PowerupType _powerup;
  
  private void OnTriggerEnter2D(Collider2D collision)
  {
    if(collision != null)
    {
      if(collision.CompareTag("Player"))
      {
        switch(_powerup)
        {
          case PoweupType.TripleShot:
          case PowerupType.HomingMissile:
            FiringMode mode = _powerup == PowerupType.TripleShot ? FiringMode.TripleShot : FiringMode.HomingMissile;
            collision.GetComponent<Player>().EnableWeapon(mode);
            break;
          case PowerupType.SpeedBoost:
            collision.GetComponent<Player>().EnableSpeedBoost();
            break;
          case PowerupType.EnableShield:
            collision.GetComponent<Player>().EnableShield();
            break;
          case PowerupType.AmmoCollectible:
            collision.GetComponent<Player>().AddAmmo();
            break;
          case PowerupType.HealthCollectible:
            collision.GetComponent<Player>().UpdateLives(1);
            break;
          ...
        }
         ...
      }
     }
    }
    ...
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum FiringMode
{
  Default,
  HomingMissile,
  TripleShot
}

public class Player : MonoBehaviour
{
  ...
  private FiringMode _firingMode;

  private float _weaponCooldownDuration;

  [SerializeField]
  private float _laserCooldownDuration = .2f;

  [SerializeField]
  private GameObject _missilePrefab;
  [SerializeField]
  private AudioClip _missileAudioClip;
  [SerializeField]
  private float _missileCooldownDuration = 1f;

  private void FireWeapon()
  {
    if(_canFire && Input.GetKeyDown(KeyCode.Space))
    {
      if(_ammoCurrentCount > 0)
      {
        ...
        switch(_firingMode)
        {
          case FiringMode.Default:
          case FiringMode.TripleShot:
            List<Laser> lasers = new List<Laser>();
            if(FiringMode.Default == _firingMode)
              lasers.Add(Instantiate(_laserPrefab, _laserSpawnTransform.position, _laserSpawnTransform.rotation).GetComponent<Laser>());
            else
              lasers.AddRange(Instantiate(_tripleShotPrefab, _laserSpawnTransform.position, _laserSpawntransform.rotation).GetComponentInChildren<Laser>());
            
            foreach(Laser laser in lasers)
              laser.InitializeFiring(1);
            
            _audioSource.PlayOneShot(_laserAudioClip);
            break;

          case FiringMode.HomingMissile:
            //Missile logic
            break;
        }

        _canFire = false;
        StartCoroutine(ResetWeaponCooldown());
      }
    }
  }

  private IEnumerator ResetWeaponCooldown()
  {
    yield return new WaitForSeconds(_weaponCooldownDuration);
    _canFire = true;
  }

  public void EnableWeapon(FiringMode mode)
  {
    _firingMode = mode;
    
    if(_resetWeaponCoroutine != null)
      StopCoroutine(_resetWeaponRoutine);
    
    PowerupType powerup = _firingMode == 
      FiringMode.TripleShot ? PowerupType.TripleShot : PowerupType.HomingMissile;
    
    _weaponCooldownDuration = mode == 
      FiringMode.HomingMissile ? _missileCooldownDuration : _laserCooldownDuration;
    
    _resetWeaponRoutine = StartCoroutine(ResetPowerup(powerup));
  }

  private IEnumerator ResetPowerup(PowerupType powerup)
  {
    switch(powerup)
    {
      case PowerupType.TripleShot:
      case PowerupType.HomingMissile:
        _firingMode = FiringMode.Default;
        _weaponCooldownDuration = _laserCooldownDuration;
        break;
      case PowerupType.SpeedBoost:
        _isSpeedBoostEnabled = false;
        break;
    }
  }

  public void EnableSpeedBoost()
  {
    ...
    _resetSpeedBoostRoutine = StartCoroutine(ResetPowerup(PowerupType.SpeedBoost));
  }
  ...
}

Обновление/рефакторинг: Overrides, Seeking Missile, Quaternions и List‹T›

Поскольку класс Missile должен был использовать ту же логику, что и класс Laser, я немного изменил класс Laser и сделал Missileкласс дочерний от него. Я использовал ключевое слово виртуальный для методов, функциональность которых я хотел расширить в классе Laser. Я использовал ключевое слово override в сигнатурах этих методов в классе Missile для реализации дополнительной логики. Чтобы выполнить логику базового класса в переопределенных методах, я смогу использовать ключевое слово base, представляющее базовый класс, и вызывать метод из него.

using UnityEngine;

public class Laser : MonoBehaviour
{

    void OnTriggerEnter2D(Collider2D collision)
    {
        CollisionCheck(ref collision);
    }
    #endregion

    #region Methods
    protected void CollisionCheck(ref Collider2D collision)
    {
        if (collision != null)
        {
            if (collision.CompareTag("Player") && _isEnemyWeapon)
            {
                collision.GetComponent<Player>().UpdateLives(-1);
                Destroy(gameObject);
            }
        }
    }

    // Move laser till it's out of the viewport
    // at the moment, it is assumed to be moving to the top
    protected virtual void Move()
    {
      ...
    }

    public virtual void InitializeFiring(int owner)
    {
      ...
    }
    ...
}

Следующее, что я сделал, это настроил диспетчер спавна, чтобы найти ближайшего врага. Для этого мне нужно отслеживать появляющихся врагов, а не отслеживать их, когда игрок их уничтожает. Я использовал класс List‹T›, находящийся в пространстве имен System.Collections.Generic, которое можно рассматривать как общий динамический массив. С помощью List‹T› я могу легко добавлять и удалятьобъекты определенного типа из него >не создавая вручную для него больше места по сравнению с массивом. Я обновил SpawnManager, чтобы отслеживать врагов, и создал метод для получения преобразования врага, ближайшего к игроку, с пометкой FindTheNearestEnemyToThePlayer. Мне пришлось обновить сценарии Enemy и EnemyDestroyedBehaviour, чтобы позволить SpawnManager удалить их из списка. Я также добавил код в скрипт SpawnManager, чтобы запускать усилитель самонаводящейся ракеты через определенные промежутки времени.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnManager : MonoBehaviour
{
    ...

    private List<GameObject> _enemies = new List<GameObject>();
    [SerializeField]
    private Player _player;
    private int _homingMissileSpawnInterval = 10;


    private IEnumerator SpawnEnemy()
    {
        while (_canSpawn)
        {
            ...

             _enemies.Add(Instantiate(
                _enemyPrefab,
                spawnLocation,
                Quaternion.AngleAxis(180, Vector3.forward),
                _enemyContainer.transform));
            ...

        }

    }


    private IEnumerator SpawnHomingMissile()
    {
        while (_canSpawn)
        {
            yield return new WaitForSeconds(_homingMissileSpawnInterval);

            Vector3 spawnLocation = Camera.main.ViewportToWorldPoint(
                new Vector3(
                    Random.Range(LEFT_BOUND, RIGHT_BOUND),
                    TOP_BOUND,
                    Camera.main.WorldToViewportPoint(transform.position).z
                ));
            Instantiate(
                _powerups[5],
                spawnLocation,
                Quaternion.identity
                );
            _powerupSpawnCount++;
        }
    }


    public void StartWave()
    {
        _player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
        ...
    }


    private IEnumerator SpawnPowerupAtEnemyPosition(Vector3 position, float delay)
    {
        yield return new WaitForSeconds(delay);
        Instantiate(
            _powerups[3],
            position,
            Quaternion.identity
            );
    }

    public void EnemyDestroyed(GameObject enemy, float powerupSpawnDelayDuration)
    {
        if(_canSpawn)
        {
            _enemies.Remove(enemy);
            Vector3 position = enemy.transform.position;
            _enemiesDestroyedCount++;
            if(_enemiesDestroyedCount % _enemySpawnsBeforeAmmoDrop == 0)
            {
                StartCoroutine(SpawnPowerupAtEnemyPosition(position, powerupSpawnDelayDuration));
            }
        }

    private IEnumerator StartWaveRoutine()
    {
        ...
        StartCoroutine(SpawnHomingMissile());
    }

    //Attempt to findn the enemy closest to the player.
    //Doing a null check whenever attempting to reference
    //the current enemy as it could have been destroyed
    //by the player while running this method.
    public Transform FindNearestEnemyToPlayer()
    {
        Transform closestEnemyTransform = null;
        float distance = -1f;
        foreach (GameObject enemy in _enemies)
        {
            if (_player == null)
                return null;

            if (distance == -1 || closestEnemyTransform == null)
            {
                closestEnemyTransform = enemy != null ? enemy.transform : null;

                if (closestEnemyTransform != null)
                    distance = Vector3.Distance(_player.transform.position, closestEnemyTransform.position);
            }
            else if (enemy != null)
            {
                float newEnemyDistance = Vector3.Distance(_player.transform.position, enemy.transform.position);
                if (newEnemyDistance < distance)
                {
                    closestEnemyTransform = enemy != null ? enemy.transform : closestEnemyTransform;
                    if (enemy != null)
                        distance = newEnemyDistance;
                }
            }
        }

        return closestEnemyTransform;

    }

}
using UnityEngine;


public class Enemy : MonoBehaviour
{
    ...
    private const string SPAWNMANAGER_TAG = "SpawnManager";

    private SpawnManager _spawnManager;
    private bool _wasKilled;

    void Start()
    {
        ...
        _spawnManager = GameObject.FindGameObjectWithTag(SPAWNMANAGER_TAG).GetComponent<SpawnManager>();
        ...
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other != null)
        {
            if (other.CompareTag(PLAYER_TAG))
                other.GetComponent<Player>().UpdateLives(-1);


            if (other.CompareTag(LASER_TAG))
            {
                ...
                _wasKilled = true;
                ...
            }

            ...
                
        }
    }

    public void InformSpawnManager(float powerupSpawnDelayDuration)
    {
        if(_wasKilled)
        _spawnManager.EnemyDestroyed(gameObject, powerupSpawnDelayDuration);
    }

}
using UnityEngine;

public class EnemyDestroyedBehaviour : StateMachineBehaviour
{

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.GetComponent<AudioSource>().Play();
        animator.GetComponent<Enemy>().InformSpawnManager(stateInfo.length * .4f);
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {

        if(stateInfo.normalizedTime >= 1f)
            animator.GetComponent<Enemy>().Die();
    }

}

Наконец, мне нужно было настроить движение и поведение ракеты. Поведение, к которому я стремился, было:

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

Я знал, как перемещать GameObject, обновляя его позицию Transform либо с помощью моего собственного кода, либо с помощью метода Transform Translate. Я рассчитал направление, в котором мне нужно было двигаться, вычитая положение игрока из положения противника и нормализуя его (превратив его в единичный вектор, чтобы он действовал только как направление). Оттуда я углубился в свойство rotation Transform, которое относится к типу Quaternion. Класс Quaternion представляет повороты в Unity и содержит статические методы, помогающие создавать повороты и повороты от одного поворота к другому. Методы, которые я использовал из класса, были LookRotation и RotateTowards.

LookRotation принимает два параметра Vector3: направление вперед и направление вверх. Это выровняет ось Z Преобразования (синяя стрелка, когда выбран GameObject) с направлением вперед и осью X (красная стрелка когда выбран GameObject) с перекрестным произведением прямого и восходящего направлений. Затем ось Y будет выровнена с перекрестным произведением между выровненными осью Z и осью X.

RotateTowards принимает три параметра: 2 кватерниона, помеченных от и до, и число с плавающей запятой, помеченное maxDegreesDelta. Как следует из названий параметров кватерниона, этот метод вернет поворот, который поворачивается не более чем на maxDegreesDelta от от кватернионак к кватерниону. . Если maxDegreesDelta отрицательно, метод будет вращаться до тех пор, пока не будет направлен в противоположное направление от к Кватернион. С учетом этой информации и обновлений сценариев SpawnManager и Laser я, наконец, обновил сценарий Missile и добавил код в Player скрипт для запуска ракеты.

using UnityEngine;

public class Missile : Laser
{
    #region Variables
    private Transform _targetTransform;
    [SerializeField]
    private GameObject _explosionPrefab;
    [SerializeField]
    private float _maxDegreesDelta;
    private SpawnManager _spawnManager;
    [SerializeField]
    private float _movementDelay = .7f;
    private float _movementDelayTimer;
    [SerializeField]
    private float _rotationDelay;
    private bool _canRotate;
    #endregion

    #region UnityMethods

    private void Start()
    {
        _spawnManager = GameObject.FindGameObjectWithTag("SpawnManager").GetComponent<SpawnManager>();
    }

    void Update()
    {
        Move();
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        CollisionCheck(ref collision);
    }

    #endregion

    #region Methods
    private IEnumerator DespawnRoutine()
    {
        yield return new WaitForSeconds(8);
        Detonate();
    }

    private IEnumerator StartRotationRoutine()
    {
        yield return new WaitForSeconds(_rotationDelay);
        _canRotate = true;
    }

    private void Detonate()
    {
        _canMove = false;
        Instantiate(_explosionPrefab, transform.position, Quaternion.identity);
        Destroy(gameObject);
    }

    public override void InitializeFiring(int owner)
    {
        base.InitializeFiring(owner);
        _movementDelayTimer = Time.time + _movementDelay;
        StartCoroutine(StartRotationRoutine());
        StartCoroutine(DespawnRoutine());
    }

    protected override void Move()
    {
        if(_canMove)
        {

            if(_targetTransform != null)
            {
                Vector3 moveDirection = 
                  (_targetTransform.transform.position - transform.position).normalized;
                
                if (Time.time > _movementDelayTimer)
                {

                    //transform.position += (moveDirection * _speed * Time.deltaTime);
                    transform.Translate(moveDirection * _speed * Time.deltaTime, Space.World);
                }
                else
                    transform.Translate(Vector3.up * _speed * .6f * Time.deltaTime);

                if(_canRotate)
                {
                    Quaternion targetRotation = 
                      Quaternion.LookRotation(transform.forward, moveDirection);

                    transform.rotation = 
                      Quaternion.RotateTowards(transform.rotation, targetRotation, _maxDegreesDelta);
                }
 
            }
            else
            {
                transform.Translate(transform.up * _speed * .6f * Time.deltaTime);
                FindTarget();
            }
        }


    }

    private void FindTarget()
    {
        if (_isEnemyWeapon)
        {
            GameObject player = GameObject.FindGameObjectWithTag("Player");

            if (player != null)
            {
                _targetTransform = player.transform;
            }
            else
            {
                Destroy(gameObject);
            }
        }
        else
        {
            _targetTransform = _spawnManager.FindNearestEnemyToPlayer();
        }
    }

    #endregion

}
using UnityEngine;
...
public class Player : MonoBehaviour
{
    //Attempt to fire weapon
    private void FireWeapon()
    {
        if(_canFire && Input.GetKeyDown(KeyCode.Space))
        {
            if(_ammoCurrentCount > 0)
            {
                _ammoCurrentCount--;
                _uiManager.UpdateAmmoText(_ammoCurrentCount);

                switch (_firingMode)
                {
                    ...
                    case FiringMode.HomingMissile:
                        Missile missile = Instantiate(_missilePrefab, _laserSpawnTransform.position, _laserSpawnTransform.rotation).GetComponent<Missile>();
                        missile.InitializeFiring(1);
                        _audioSource.PlayOneShot(_missileAudioClip);
                        break;
                }                
                ...
            }
            ...
        }        
    }
}

Круто, теперь у нас есть способ нацелиться на ближайшего врага и еще одно полезное усиление!