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

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

Что такое внедрение зависимостей?

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

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

Понимание внедрения зависимостей

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

В C# внедрение зависимостей можно реализовать, например, с помощью техники, называемой «внедрение конструктора» (constructor injection). Но давайте сначала разберем ключевые понятия:

  • Зависимость (Dependency):
    «Зависимость» — это объект, от которого зависит функциональность класса. Например, если вы создаете игру, персонаж может зависеть от оружия, чтобы атаковать врагов. В программном обеспечении зависимостями могут быть любые объекты, от источников данных до других классов или сервисов.

  • Внедрение (Injection):
    «Внедрение» (или «Инъекция») подразумевает процесс предоставления классу необходимых зависимостей. Вместо того чтобы класс создавал свои собственные зависимости, они предоставляются извне.

Unity: Проблема с ключевым словом new

  • Вы не можете напрямую создавать объекты MonoBehaviour с помощью ключевого слова new, потому что MonoBehaviour — это особый тип класса, которым Unity управляет внутренне. Поэтому вы не можете использовать здесь конструкторы.

public class Player : MonoBehaviour {

public int name;

}

public class User : MonoBehaviour {

  Player p1 = new Player();
}

Этот код не сработает и выдаст предупреждение: «Вы пытаетесь создать MonoBehaviour, используя ключевое слово new».

Чтобы создать объект, нам нужно использовать вместо ключевого слова «new» функцию Instantiate(). Однако Instantiate() принимает определенный список параметров: позиция, поворот и родительский объект. Мы не можем передать сюда другие ссылки, поэтому мы не можем использовать в Unity стандартные подходы к внедрению зависимостей. О решении этой проблемы мы поговорим позже.

В контексте разработки игр сама Unity предоставляет нам некоторую помощь в управлении и внедрении зависимостей. Unity имеет функцию под названием «Inversion of Control (IoC) Container», которая может управлять внедрением зависимостей.

Инверсия контроля

Инверсия контроля (IoC) — это идея, согласно которой контроль над потоком программы инвертируется или передается фреймворку или контейнеру вместо того, чтобы контролироваться самим кодом приложения.

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

В контексте разработки игр сама Unity может помочь в управлении и внедрении зависимостей. Unity предоставляет функцию под названием «Inversion of Control (IoC) Container», которая может управлять внедрением зависимостей.

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

Конструктор

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

  • Конструкторы вызываются автоматически при создании нового инстанса класса и помогают убедиться, что объект правильно инициализирован и готов к использованию.

Особенности внедрения зависимостей в классическом C# и Unity

Давайте рассмотрим наглядный пример, чтобы понять, как работает внедрение зависимостей в классическом программировании на C# и в Unity. Разобрав этот пример, вы поймете, как внедрение зависимостей работает в каждом контексте и в чем его преимущества.

Допустим, вы создаете простое консольное приложение, в котором есть класс Character, зависящий от класса Weapon. Вот как можно реализовать внедрение этой зависимости без Unity:

  • Определение интерфейсов:
    Создайте интерфейсы, определяющие поведение оружия, например IWeapon.

  • Реализация внедрения зависимостей:
    Создайте класс Character, чтобы он принимал различные реализации оружия через внедрения конструктора.

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

public interface IWeapon
{
  void Attack();
}

public class Sword : IWeapon
{
  public void Attack()
  {
      Console.WriteLine("Swinging sword!");
  }
}

public class Bow : IWeapon
{
  public void Attack()
  {
      Console.WriteLine("Firing bow!");
  }
}
  • Реализация внедрения зависимостей:

public class Character
{
    private IWeapon weapon;

    public Character(IWeapon weapon)
    {
        this.weapon = weapon;
    }

    public void AttackEnemy()
    {
        weapon.Attack();
    }
}

class Program
{
    static void Main(string[] args)
    {
        IWeapon sword = new Sword();
        Character character = new Character(sword);

        character.AttackEnemy();
    }
}

В Unity классы MonoBehaviour не используют традиционные конструкторы так же, как обычные классы C#. Это меняет наш подход к реализации внедрения зависимостей в Unity по сравнению с классическим программированием на C#. Вместо внедрения конструктора в Unity используются другие техники, часто подразумевающие использование публичных полей и ScriptableObjects.

Пример в Unity

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

  • Начните с определения интерфейса, который представляет подсчет очков:

public interface IScoreManager
{
    void AddScore(int points);
    int GetScore();
}          
  • Реализация ScoreManager:

public class ScoreManager : IScoreManager
{
  private int score;

  public void AddScore(int points)
  {
      score += points;
  }

  public int GetScore()
  {
      return score;
  }
}
  • Используем внедрение зависимостей:
    Теперь мы хотим внедрить IScoreManager в класс MonoBehaviour, которому он нужен. Это можно сделать с помощью публичных полей в инспекторе Unity:

using UnityEngine;

public class Player : MonoBehaviour
{
    public IScoreManager scoreManager;

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Coin"))
        {
            scoreManager.AddScore(10);
            Destroy(other.gameObject);
        }
    }
}

В редакторе Unity прикрепите скрипт Player к GameObject у вашего игрока. Затем вам нужно создать GameObject с прикрепленным к нему скриптом ScoreManager. Этот GameObject будет служить вашим DI‑контейнером. Присвойте инстанс ScoreManager полю scoreManager в скрипте Player, используя инспектор.

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

Внедрение зависимостей в коде

Если вы хотите внедрять зависимости прямо в коде, а не с помощью инспектора в Unity, вы можете использовать ручной подход. Это подразумевает создание необходимых инстансов в коде и передачу их соответствующим компонентам при инстанцировании.

  • Скрипт Player с внедрением свойств:
    Используйем внедрение свойств для внедрения инстанса IScoreManager в скрипт Player:

using UnityEngine;

public class Player : MonoBehaviour
{
    // Свойство для внедрения ScoreManager
    public IScoreManager ScoreManager { get; set; }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Coin") && ScoreManager != null)
        {
            ScoreManager.AddScore(10);
            Destroy(other.gameObject);
        }
    }
}
  • Скрипт GameManager:
    В скрипте GameManager вы можете создать инстансы и внедрить зависимости следующим образом:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    private void Start()
    {
        IScoreManager scoreManager = new ScoreManager();

        // Создайте GameObject с компонентом Player
        GameObject playerGameObject = new GameObject("Player");
        Player player = playerGameObject.AddComponent();

        // Внедряем зависимость ScoreManager
        player.ScoreManager = scoreManager;

        // Остальной код инициализации…
    }
}

В этом примере мы создаем новый GameObject с именем «Player» и добавляем к нему компонент MonoBehaviour Player с помощью метода AddComponent. Затем мы вручную внедряем зависимость ScoreManager в инстанс Player через свойство ScoreManager.

Сторонний плагин для внедрения зависимостей :-

  • Extenject, также известный как Zenject, — это мощный фреймворк внедрения зависимостей и инверсии контроля для разработки игр Unity. Этот фреймворк упрощает процесс управления и внедрения зависимостей ваши GameObject»ы. С помощью Extenject вы можете легко создавать гибкий и модульный код, определяя, как различные компоненты работают вместе посредством внедрения.

  • Ссылка: extenject‑dependency‑injection

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

  • Благодаря внедрению зависимостей извне, а не внутреннему созданию их классами, этот подход способствует свободному соединению компонентов, упрощает тестирование и позволяет легко вносить изменения и расширения.

Заключение

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

Научиться создавать игры с нуля можно на онлайн-курсе "Unity Game Developer. Basic". На странице курса можно ознакомиться с полной программой обучения и посмотреть записи открытых уроков.

Комментарии (0)