This page has been translated automatically.
Видеоуроки
Интерфейс
Основы
Продвинутый уровень
Подсказки и советы
Основы
Программирование на C#
Рендеринг
Профессиональный уровень (SIM)
Принципы работы
Свойства (properties)
Компонентная Система
Рендер
Физика
Браузер SDK 2
Лицензирование и типы лицензий
Дополнения (Add-Ons)
Демонстрационные проекты
API Samples
Редактор UnigineEditor
Обзор интерфейса
Работа с ассетами
Контроль версий
Настройки и предпочтения
Работа с проектами
Настройка параметров ноды
Setting Up Materials
Настройка свойств
Освещение
Sandworm
Использование инструментов редактора для конкретных задач
Расширение функционала редактора
Встроенные объекты
Ноды (Nodes)
Объекты (Objects)
Эффекты
Декали
Источники света
Geodetics
World-ноды
Звуковые объекты
Объекты поиска пути
Player-ноды
Программирование
Основы
Настройка среды разработки
Примеры использования
C++
UnigineScript
Унифицированный язык шейдеров UUSL (Unified UNIGINE Shader Language)
Плагины
Форматы файлов
Материалы и шейдеры
Rebuilding the Engine Tools
Интерфейс пользователя (GUI)
Двойная точность координат
Справочник API
Animations-Related Classes
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
IG Plugin
CIGIConnector Plugin
Rendering-Related Classes
VR-Related Classes
Работа с контентом
Оптимизация контента
Материалы
Визуальный редактор материалов
Material Nodes Library
Miscellaneous
Input
Math
Matrix
Textures
Art Samples
Учебные материалы

Интерфейсы и абстрактные классы C#

Dependency Management in C# Classes
Управление зависимостями в классах C##

Projects where classes create everything they need by themselves often have common architectural problems, because they handle their own dependencies directly. For example, a class might internally create services like a custom player HUD or input settings. This causes several issues:Проекты, в которых классы создают все, что им нужно, самостоятельно, часто сталкиваются с общими архитектурными проблемами, поскольку они напрямую обрабатывают свои собственные зависимости. Например, класс может создавать внутренние сервисы, такие как пользовательский интерфейс проигрывателя или настройки ввода. Это вызывает ряд проблем:

  • Tight Coupling: Using specific implementations directly within a class creates tight connections between the class and those implementations. Modifying or replacing these dependencies, such as changing an input handler from keyboard to gamepad, inevitably requires changing the dependent class itself, violating the open-closed principle. Тесная связь: Использование конкретных реализаций непосредственно в классе создает тесные связи между классом и этими реализациями. Изменение или замена этих зависимостей, например, изменение обработчика ввода с клавиатуры на геймпад, неизбежно требует изменения самого зависимого класса, что нарушает принцип "открыто-закрыто".
  • Poor Maintainability: Dependency setup logic often becomes duplicated across the application. If multiple components individually configure the same dependency, it becomes difficult to manage and understand the configurations, and thus harder to maintain. Плохая ремонтопригодность: Логика настройки зависимостей часто дублируется в приложении. Если несколько компонентов по отдельности настраивают одну и ту же зависимость, становится трудно управлять конфигурациями и понимать их, а следовательно, и поддерживать.
  • Difficulty in Unit Testing: Hard-coded dependencies severely limit testability. For example, when a class internally creates a complex dependency like an audio manager, it becomes difficult to replace it with a mock during testing, which makes unit tests less reliable. Сложность модульного тестирования: Жестко заданные зависимости серьезно ограничивают возможность тестирования. Например, когда класс внутренне создает сложную зависимость, такую как audio manager, во время тестирования становится трудно заменить ее макетом, что делает модульные тесты менее надежными.
  • Reduced Reusability and Modularity: Components tied to certain dependencies become less flexible. This makes it harder to reuse them in different situations. If a component is directly linked to a specific manager or subsystem, you can't easily move it or use it somewhere else. Уменьшена возможность повторного использования и модульность: Компоненты, привязанные к определенным зависимостям, становятся менее гибкими. Это затрудняет их повторное использование в различных ситуациях. Если компонент напрямую связан с определенным менеджером или подсистемой, вы не сможете легко переместить его или использовать в другом месте.

Dependency Injection
Внедрение зависимостей#

Dependency Injection (DI) is a way to write code that's flexible and easy to manage. Rather than creating or directly referencing its dependencies, a class receives them from outside. Внедрение зависимостей (DI) - это способ написания гибкого и простого в управлении кода. Вместо того, чтобы создавать свои зависимости или напрямую ссылаться на них, класс получает их извне.

Classes depend on an abstraction (an interface or abstract class) rather than a concrete implementation. This approach adheres to the Dependency Inversion Principle (DIP), which states that high-level modules should not depend on low-level modules; both should depend on abstractions. Moreover, abstractions themselves should not depend on details – the details should depend on the abstraction.Классы зависят от абстракции (интерфейса или абстрактного класса), а не от конкретной реализации. Этот подход основан на принципе инверсии зависимостей (DIP), который гласит, что модули высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций. Более того, сами абстракции не должны зависеть от деталей – детали должны зависеть от абстракции.

By following DIP, you reduce direct coupling and make it easier to replace components without affecting higher-level logic.Следуя DIP, вы уменьшаете прямую связь и упрощаете замену компонентов, не затрагивая логику более высокого уровня.

Abstraction diagram

To address issues above and build more flexible, testable, and maintainable systems in UNIGINE, you can use components that implement interfaces or inherit from abstract classes. C# Component System supports this approach and makes it easier to program to abstractions rather than concrete implementations and decouple components from specific dependencies.Чтобы устранить вышеуказанные проблемы и создать более гибкие, тестируемые и обслуживаемые системы в UNIGINE, вы можете использовать компоненты, которые реализуют интерфейсы или наследуются от абстрактных классов. Система компонентов C# поддерживает этот подход и упрощает программирование на основе абстракций, а не конкретных реализаций, и отделяет компоненты от конкретных зависимостей.

Interfaces
Интерфейсы#

Interfaces are a mechanism for defining contracts that classes can implement. Any class that implements an interface agrees to provide that functionality. Интерфейсы - это механизм определения контрактов, которые могут реализовывать классы. Любой класс, реализующий интерфейс, соглашается предоставлять эту функциональность.

Using interfaces for DI is a common pattern to achieve maximum decoupling. The approach to working with interfaces in UNIGINE can be summarized as follows:Использование интерфейсов для DI является распространенным способом достижения максимальной развязки. Подход к работе с интерфейсами в UNIGINE можно резюмировать следующим образом:

  1. Define an Interface. Определите интерфейс.

    Identify the behavior or functionality that needs to be provided, and define an interface declaring the required methods. This interface serves as a contract that any specific component must fulfill.Определите поведение или функциональность, которые необходимо обеспечить, и определите интерфейс, объявляющий необходимые методы. Этот интерфейс служит контрактом, который должен выполняться любым конкретным компонентом.

    Исходный код (C#)
    public interface IShootable {
    	public void Shoot();
    }
    Примечание
    You can create an empty *.cs file right in the Asset Browser window.Вы можете создать пустой файл *.cs прямо в окне Asset Browser.

    Empty .cs file creation

  2. Implement the Interface in a Component. Реализуйте интерфейс в компоненте.

    Create one or more components that implement this interface. They will contain the actual code to perform the work, but from the outside they will be accessed via the interface. You can have multiple implementations of the same interface coexisting, which is a powerful way to swap behaviors. The component that implements the interface is called a service.Создайте один или несколько компонентов, реализующих этот интерфейс. Они будут содержать фактический код для выполнения работы, но доступ к ним извне будет осуществляться через интерфейс. У вас может быть несколько одновременных реализаций одного и того же интерфейса, что является эффективным способом обмена поведением. Компонент, реализующий интерфейс, называется сервисом.

    Исходный код (C#)
    public class WizardStuff : Component, IShootable
    {
    	public void Shoot()
    	{
    		Log.MessageLine("The wizard stuff shooted with fireball");
    	}
    }
    
    public class MagicWand : Component, IShootable
    {
    	public void Shoot()
    	{
    		Log.MessageLine("The magic wand shooted with electric zap");
    	}
    }
    
    public class Bow : Component, IShootable
    {
    	public void Shoot()
    	{
    	Log.MessageLine("The bow shooted with arrow");
    	}
    }

    Nodes with components

  3. Depend on the Interface in the Client. Зависит от интерфейса в клиенте.

    Any component that needs to use the special behavior should not directly instantiate concrete service class. Instead, it should rely on a reference to an interface type. The component that depends on the service is called a client.Любой компонент, которому необходимо использовать специальное поведение, не должен напрямую создавать экземпляр конкретного класса сервиса. Вместо этого он должен полагаться на ссылку на тип интерфейса. Компонент, зависящий от сервиса, называется клиентом.

    Исходный код (C#)
    public class Player : Component
    {
    	[ShowInEditor]
    	IShootable mainPlayerWeapon = null;
    
    	[ShowInEditor]
    	IShootable sparePlayerWeapon = null;
    
    	void Update()
    	{
    		if (Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT))
    		{
    			mainPlayerWeapon?.Shoot();
    		}
    
    		if (Input.IsMouseButtonDown(Input.MOUSE_BUTTON.RIGHT))
    		{
    			sparePlayerWeapon?.Shoot();
    		}
    	}
    }
  4. Inject the Concrete Implementation. Внедрите конкретную реализацию.

    This step is where the wiring happens. The interface reference in the client must be assigned a concrete object instance that implements the interface. There are a couple of ways to do this using UNIGINE features:На этом этапе происходит подключение. Ссылке на интерфейс в клиенте должен быть присвоен конкретный экземпляр объекта, который реализует интерфейс. Есть несколько способов сделать это, используя функции UNIGINE:

    • Via the Editor. In UNIGINE you can expose the interface field in the component parameters. By default, parameters are displayed or hidden in the UI in accordance with access modifiers: public - displayed, otherwise - hidden. But you can show a private or protected one by specifying the corresponding visibility attribute ShowInEditor as shown above in the code sample. Через редактор. В UNIGINE вы можете открыть поле интерфейса в параметрах компонента. По умолчанию параметры отображаются или скрываются в пользовательском интерфейсе в соответствии с модификаторами доступа: общедоступные - отображаются, в противном случае - скрыты. Но вы можете показать закрытый или защищенный файл, указав соответствующий атрибут видимости ShowInEditor, как показано выше в примере кода.

      Then, you simply attach a node with an attached component that implements your specific interface.Затем вы просто подключаете узел с подключенным компонентом, который реализует ваш конкретный интерфейс.

      Using interfaces in Editor

    • Via the API. Or you can locate the interface using the engine API. Через API. Или вы можете найти интерфейс с помощью API движка.

      Исходный код (C#)
      // Getting IShootable
      var newWeapon = obj.GetComponent<IShootable>();
      // If newWeapon is not null, it will be new main player weapon
      mainPlayerWeapon = newWeapon ?? mainPlayerWeapon;

      A common pattern is calling GetComponent<>() method in the client's code. The engine will search for a component that implements the IShootable interface, as in our example, and return a reference to it.Распространенным шаблоном является вызов метода GetComponent<>() в клиентском коде. Движок выполнит поиск компонента, реализующего интерфейс IShootable, как в нашем примере, и вернет ссылку на него.

      The key point is that the client code does not instantiate the service directly - instead, it receives a reference or obtains it from the environment.Ключевым моментом является то, что клиентский код не создает экземпляр службы напрямую - вместо этого он получает ссылку или извлекает ее из среды.

Abstract Classes
Абстрактные классы#

Interfaces aren't the only way to invert dependencies – abstract classes can also serve as the abstraction layer between a client and the concrete implementation. The workflow for using abstract classes is very similar to the interface approach, with a few differences:Интерфейсы – не единственный способ инвертировать зависимости. Абстрактные классы также могут служить в качестве уровня абстракции между клиентом и конкретной реализацией. Рабочий процесс использования абстрактных классов очень похож на интерфейсный подход, с некоторыми отличиями:

  1. Define an Abstract Base Class. Определите абстрактный базовый класс.

    Create an abstract class that declares the necessary methods and optionally provides common functionality or default behavior shared across implementations. Since it's abstract, it can't be instantiated directly - instead, it serves as a template that concrete subclass components must follow.Создайте абстрактный класс, который объявляет необходимые методы и, при необходимости, предоставляет общие функциональные возможности или поведение по умолчанию, общие для всех реализаций. Поскольку он абстрактен, его нельзя создать напрямую - вместо этого он служит шаблоном, которому должны следовать компоненты конкретного подкласса.

    Исходный код (C#)
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    [Component(PropertyGuid = "AUTOGENERATED_GUID")]
    public abstract class Toggleable : Component
    {
    	[ShowInEditor]
    	private bool isToggled = false;
    
    	public bool Toggled
    	{
    		get => isToggled;
    		set
    		{
    			if (value != isToggled)
    			{
    				bool ok = value ? On() : Off();
    				isToggled = isToggled ^ ok;
    			}
    		}
    	}
    
    	public bool Toggle() => isToggled = isToggled ^ (isToggled ? Off() : On());
    
    	protected abstract bool On();
    	protected abstract bool Off();
    }
  2. Create Subclasses that Inherit from the Abstract Class. Создайте подклассы, которые наследуются от абстрактного класса.

    Implement one or more classes that extend the abstract base class. Each subclass must implement the abstract methods, providing its own behavior.Реализуйте один или несколько классов, расширяющих абстрактный базовый класс. Каждый подкласс должен реализовывать абстрактные методы, обеспечивающие его собственное поведение.

    Исходный код (C#)
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")]
    public class Lamp : Toggleable
    {
    	[ParameterColor]
    	public vec4 emission_color = vec4.WHITE;
    
    	protected override bool On()
    	{
    		Log.MessageLine("Lamp::On()");
    		return SetEmissionColor(emission_color);
    	}
    
    	protected override bool Off()
    	{
    		Log.MessageLine("Lamp::Off()");
    		return SetEmissionColor(vec4.ZERO);
    	}
    
    	private bool SetEmissionColor(vec4 emission_color)
    	{
    		Object obj = (Object)node;
    		if (obj == null)
    			return false;
    
    		for (var surface = 0; surface < obj.NumSurfaces; surface += 1)
    			obj.SetMaterialParameterFloat4("emission_color", emission_color, surface);
    
    		return true;
    	}
    
    	private void Init()
    	{
    		SetEmissionColor(Toggled ? emission_color : vec4.ZERO);
    	}
    }
    Исходный код (C#)
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    [Component(PropertyGuid = "AUTOGENERATED_GUID")]
    public class Fan : Toggleable
    {
    	public float rotation_speed = 120;
    	private float target_speed = 0;
    	private float actual_speed = 0;
    
    	protected override bool On()
    	{
    		Log.MessageLine("Fan::On()");
    		target_speed = rotation_speed;
    		return true;
    	}
    
    	protected override bool Off()
    	{
    		Log.MessageLine("Fan::Off()");
    		target_speed = 0;
    		return true;
    	}
    
    	private void Init()
    	{
    		target_speed = Toggled ? rotation_speed : 0;
    	}
    
    	private void Update()
    	{
    		actual_speed = MathLib.Lerp(actual_speed, target_speed, Game.IFps);
    		node.Rotate(0, 0, actual_speed * Game.IFps);
    	}
    }

    Nodes with abstract components

  3. Inject a Concrete Subclass Instance into the Client Class. Добавьте конкретный экземпляр подкласса в клиентский класс.

    Similar to the interface case, the client that needs the functionality holds a reference of the abstract base class type. Then, because of polymorphism, you can pass specific implementations of the abstract class to the client class in two following ways:Как и в случае с интерфейсом, клиент, которому требуется функциональность, содержит ссылку на абстрактный базовый класс. Затем, благодаря полиморфизму, вы можете передать конкретные реализации абстрактного класса клиентскому классу двумя следующими способами:

    • Via the Editor. Simply attach a node with the attached component, that inherits your abstract base class in a node Parameters window: Через редактор. Просто прикрепите узел с подключенным компонентом, который наследует ваш абстрактный базовый класс, в окне node Parameters:

      Using abstract classes in Editor

    • Via the API. Retrieving abstract base classes via the API works in a similar way as interfaces. Через API. Извлечение абстрактных базовых классов через API работает аналогично интерфейсам.

      Исходный код (C#)
      public class Toggler : Component
      {
      	private void Update()
      	{
      		// Some logic for intersection with togglable object...
      		if (obj)
      		{
      			var toggleable = obj.GetComponent<Toggleable>();
      			if (toggleable)
      			{
      				toggleable.Toggle();
      			}
      		}
      	}
      }

      In this case, the client code requests a component that derives from an abstract class. The engine traverses the available components and returns one that matches the expected base type, such as Toggleable.В этом случае клиентский код запрашивает компонент, который является производным от абстрактного класса. Обработчик просматривает доступные компоненты и возвращает тот, который соответствует ожидаемому базовому типу, например Toggleable.

Choosing the Right Abstraction Mechanism
Выбор правильного механизма абстрагирования#

Interfaces and abstract classes both define behaviour without binding to concrete implementations, but they differ in intent and capabilities:Интерфейсы и абстрактные классы определяют поведение без привязки к конкретным реализациям, но они различаются по назначению и возможностям:

  • Interfaces are ideal for defining behaviour that can be implemented by any class, regardless of its place in the hierarchy. They also support multiple inheritance by allowing a class to implement multiple interfaces.Интерфейсы идеально подходят для определения поведения, которое может быть реализовано любым классом, независимо от его места в иерархии. Они также поддерживают множественное наследование, позволяя классу реализовывать несколько интерфейсов.
  • Abstract classes are more appropriate when there is a need to share common implementation details, such as fields or methods across a group of related types.Абстрактные классы более подходят, когда есть необходимость в совместном использовании общих деталей реализации, таких как поля или методы, для группы связанных типов.

It's important to use abstractions when they bring clear benefits - such as easier testing, multiple implementations, or shared logic. But if there's only one concrete use case and no need for reuse or substitution, keeping things simple is often the better choice.Важно использовать абстракции, когда они приносят очевидные преимущества, такие как упрощение тестирования, множественность реализаций или общая логика. Но если есть только один конкретный вариант использования и нет необходимости в повторном использовании или замене, то зачастую лучшим выбором является простота.

Using External Dependency Injection Frameworks
Использование фреймворков внедрения внешних зависимостей#

You can configure dependencies manually within the engine - using the editor and components to wire things together. This method gives full control over how and when dependencies are resolved. Alternatively, external Dependency Injection frameworks can be used. They allow you to:Вы можете настроить зависимости в движке вручную, используя редактор и компоненты для объединения элементов. Этот метод дает полный контроль над тем, как и когда разрешаются зависимости. В качестве альтернативы можно использовать внешние платформы Dependency Injection. Они позволяют вам:

  • Use familiar tools from the .NET ecosystem.Используйте знакомые инструменты из экосистемы .NET.
  • Simplify dependency management in larger projects.Упростите управление зависимостями в более крупных проектах.
  • Access advanced DI features like scopes, signals, or event-based injection.Получите доступ к расширенным функциям DI, таким как области видимости, сигналы или внедрение на основе событий.

Below are examples of how to set up some popular external DI frameworks.Ниже приведены примеры того, как настроить некоторые популярные внешние DI-фреймворки.

Setting up Zenject in a UNIGINE Project
Настройка Zenject в проекте UNIGINE#

  1. Download the non-Unity build from the official Zenject GitHub repositoryЗагрузите сборку, отличную от Unity, из официального репозитория Zenject на GitHub
  2. Extract the downloaded archived *.dll files into your project's bin folderИзвлеките загруженные архивные файлы *.dll в папку bin вашего проекта
  3. Add references to the *.dll files in your project:Добавьте ссылки на файлы *.dll в свой проект:

    • Option 1: Using Your IDE Вариант 1: Использование вашей IDE

      1. Open your project in your preferred IDE.Откройте свой проект в предпочитаемой вами среде IDE.
      2. Open the Reference Manager or the equivalent interface for managing project references.Откройте Менеджер ссылок или аналогичный интерфейс для управления ссылками на проекты.
      3. Use the Browse option to locate the required DLLs in the project's bin folder.Используйте опцию Обзор, чтобы найти требуемый DLLs в папке bin проекта.
      4. Select previously added files.Выберите ранее добавленные файлы.
      5. Confirm the selection to add them as references in your project.Подтвердите выбор, чтобы добавить их в качестве ссылок в свой проект.
    • Option 2: Manually edit the *.csproj file Вариант 2: Вручную отредактируйте файл *.csproj

      Open your project *.csproj file and add:Откройте файл вашего проекта *.csproj и добавьте:

      Исходный код (XML)
      <ItemGroup>
      	<Reference Include="Zenject">
      		<HintPath>bin\Zenject.dll</HintPath>
      	</Reference>
      	<Reference Include="Zenject-Signals">
      		<HintPath>bin\Zenject-Signals.dll</HintPath>
      	</Reference>
      	<Reference Include="Zenject-usage">
      		<HintPath>bin\Zenject-usage.dll</HintPath>
      	</Reference>
      </ItemGroup>
  4. Restart your IDE (and Editor, if needed) to ensure the changes are recognized.Перезапустите IDE (и редактор, если необходимо), чтобы убедиться, что изменения распознаны.
  5. You're now ready to use the Zenject in your project. For usage details, refer to the Zenject documentation.Теперь вы готовы использовать Zenject в своем проекте. Для получения подробной информации об использовании обратитесь к документации Zenject.

    Исходный код (C#)
    public interface ISomeInterface
    {
    	void Send(string message);
    }
    
    
    public class SomeImplementation : Component, ISomeInterface
    {
    	public void Send (string message)
    	{
    		Log.MessageLine(message);
    	}
    }
    Исходный код (C#)
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    using Zenject;
    [Component(PropertyGuid = "AUTOGENERATED_GUID")]
    public class ClientCode : Component
    {
    	void Init()
    	{
    		var container = new DiContainer();
    
    		container.Bind<ISomeInterface>().To<SomeImplementation>().AsSingle();
    
    		var foo = container.Resolve<ISomeInterface>();
    
    		foo.Send("Hello, Zenject!");
    	}
    }

Setting up Microsoft DI in a UNIGINE Project
Настройка Microsoft DI в проекте UNIGINE#

  1. Open your project in your preferred IDE.Откройте свой проект в предпочитаемой вами среде IDE.
  2. Install the NuGet package for Microsoft.Extensions.DependencyInjection:Установите пакет NuGet для Microsoft.Extensions.DependencyInjection:

    • Via the built-in IDE NuGet Package Manager, orЧерез встроенную среду IDE NuGet Package Manager, или
    • Using the .NET CLI command:Использование команды .NET CLI:

      Исходный код
      dotnet add package Microsoft.Extensions.DependencyInjection
  3. That's it - Microsoft's DI framework is now available in your project. You can build more complex setups using scopes, lifetimes, and modules - see the official docs for details.Вот и все - платформа Microsoft's DI теперь доступна в вашем проекте. Вы можете создавать более сложные настройки, используя области действия, время жизни и модули - подробности смотрите в официальных документах .

    Исходный код (C#)
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    using Microsoft.Extensions.DependencyInjection;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")]
    
    public class ClientCode : Component
    {
    	void Init()
    	{
    		var services = new ServiceCollection();
    
    		services.AddTransient<ISomeInterface, SomeImplementation>();
    
    		var serviceProvider = services.BuildServiceProvider();
    
    		var foo = serviceProvider.GetRequiredService<ISomeInterface>();
    
    		foo.Send("Hello, .NET DI!");
    	}
    }

Информация, представленная на данной странице, актуальна для версии UNIGINE 2.20 SDK.

Последнее обновление: 16.04.2025
Build: ()