Интерфейс — это контракт (договор). Он говорит: «Я гарантирую, что у этого объекта будут вот такие методы и свойства», но не говорит, как именно они работают.
В архитектуре игр (особенно стратегий) интерфейсы — это главный инструмент для создания кода, который легко расширять, тестировать и поддерживать.
- Простая аналогия
- Синтаксис в C#
- Зачем это нужно в вашей игре?
- Причина 1: Полиморфизм (Единое управление разными объектами)
- Причина 2: Абстракция Бэкенда (PlayFab vs Свой сервер)
- Причина 3: Тестируемость (Mocking)
- Интерфейс vs Абстрактный класс
- Принципы правильной работы с интерфейсами
- Interface Segregation Principle (ISP)
- Dependency Injection (Внедрение зависимостей)
- Именование
- Пример архитектуры для стратегии
- Подводные камни
- Итог
Простая аналогия
Представьте розетку.
- Интерфейс: Форма дырок и напряжение (220В).
- Реализация: В розетку можно включить чайник, лампу, зарядку или компьютер.
- Выгода: Вам не нужно менять проводку в стене, чтобы включить новый прибор. Главное, чтобы у прибора была подходящая вилка (реализация интерфейса).
В коде:
- Интерфейс: IDamageable (получить урон).
- Реализация: Soldier, Building, Tree (все могут получать урон, но по-разному).
Синтаксис в C#
Интерфейсы начинаются с буквы I (по соглашению) и содержат только подписи методов (без тела).
// 1. Объявление интерфейса
public interface IDamageable
{
void TakeDamage(int amount); // Только подпись, без кода!
int Health { get; } // Только свойство
}
// 2. Реализация интерфейса в классе
public class Soldier : MonoBehaviour, IDamageable
{
public int Health { get; private set; } = 100;
public void TakeDamage(int amount)
{
Health -= amount;
if (Health <= 0) Die();
}
void Die() { /* Логика смерти солдата */ }
}
public class Building : MonoBehaviour, IDamageable
{
public int Health { get; private set; } = 1000;
public void TakeDamage(int amount)
{
Health -= amount;
if (Health <= 0) DestroyBuilding();
}
void DestroyBuilding() { /* Логика разрушения здания */ }
}Зачем это нужно в вашей игре?
Три главные причины:
Причина 1: Полиморфизм (Единое управление разными объектами)
Например, если у вас игра стратегия. В ней сотни типов объектов: юниты, здания, ресурсы, декорации.
Без интерфейсов вам пришлось бы писать так:
// ❌ ПЛОХО: Много проверок типов
if (obj is Soldier) { ((Soldier)obj).TakeDamage(10); }
else if (obj is Building) { ((Building)obj).TakeDamage(10); }
else if (obj is Resource) { ((Resource)obj).TakeDamage(10); }С интерфейсом код становится универсальным:
// ✅ ХОРОШО: Работает для любого объекта с интерфейсом
public void ApplyExplosionDamage(Vector3 center, int damage)
{
var hitObjects = Physics.OverlapSphere(center, radius);
foreach (var hit in hitObjects)
{
// Получаем компонент, реализующий интерфейс
var damageable = hit.GetComponent<IDamageable>();
if (damageable != null)
{
damageable.TakeDamage(damage); // Не важно, кто это: танк или стена
}
}
}Выгода: Вы можете добавить новый тип объекта (например, Hero), реализовать у него IDamageable, и взрывы автоматически начнут наносить ему урон без изменения кода взрыва.
Причина 2: Абстракция Бэкенда (PlayFab vs Свой сервер)
Не важно какой у вас бекенд, например PlayFab и свой сервер. Интерфейсы позволяют не зависеть от выбора.
// 1. Создаем контракт для данных
public interface IDataService
{
void SavePlayerProgress(PlayerData data);
void LoadPlayerProgress(System.Action<PlayerData> callback);
}
// 2. Реализация для PlayFab
public class PlayFabService : IDataService
{
public void SavePlayerProgress(PlayerData data) { /* Код PlayFab SDK */ }
public void LoadPlayerProgress(System.Action<PlayerData> callback) { /* Код PlayFab SDK */ }
}
// 3. Реализация для своего сервера (ASP.NET)
public class CustomApiService : IDataService
{
public void SavePlayerProgress(PlayerData data) { /* Код UnityWebRequest к вашему API */ }
public void LoadPlayerProgress(System.Action<PlayerData> callback) { /* Код UnityWebRequest */ }
}
// 4. Использование в игре
public class GameManager : MonoBehaviour
{
// Мы зависим от интерфейса, а не от конкретного класса
private IDataService _dataService;
void Start()
{
// Можно легко поменять реализацию в одном месте
_dataService = new PlayFabService();
// Или: _dataService = new CustomApiService();
_dataService.LoadPlayerProgress(OnDataLoaded);
}
}Выгода: PlayFab конечно наладить проще, но если вдруг он станет дорогим или вы решите писать свой сервер, вы меняете одну строку в коде, а не переписываете всю игру.
Причина 3: Тестируемость (Mocking)
Интерфейсы позволяют тестировать логику без реального сервера или рекламы.
// Интерфейс для рекламы
public interface IAdService
{
void ShowInterstitial();
}
// Реализация для продакшена (реальная реклама)
public class UnityAdsService : IAdService { /* Реальный SDK */ }
// Реализация для тестов (пустышка)
public class MockAdService : IAdService
{
public void ShowInterstitial() { Debug.Log("Реклама показана (тест)"); }
}Вы можете запустить игру в редакторе с MockAdService, чтобы не ждать загрузки реальной рекламы каждый раз.
Интерфейс vs Абстрактный класс
Частый вопрос: чем отличается interface от abstract class?
|
Характеристика
|
Интерфейс (interface)
|
Абстрактный класс (abstract class)
|
|---|---|---|
|
Суть
|
«Что объект может делать» (Capability)
|
«Кем объект является» (Identity)
|
|
Наследование
|
Класс может реализовать множество интерфейсов
|
Класс может наследовать только один класс
|
|
Код
|
Только подписи (обычно)
|
Может содержать готовый код и поля
|
|
Пример
|
IDamageable, IMovable, ISaveable |
Character, Building, Unit |
Правило:
- Используйте Абстрактный класс, если у объектов много общего кода (например, у всех юнитов есть здоровье, позиция, анимация).
- Используйте Интерфейс, если нужно добавить способность разным объектам (например,
ISellable— можно продать и здание, и юнита, и ресурс).
// Комбинированный подход
public abstract class Unit : MonoBehaviour
{
public int Health; // Общие данные
protected void Move() { /* Общая логика */ }
}
// Юнит может быть и[Unit], и [IDamageable], и [IMovable]
public class Archer : Unit, IDamageable, ITargetable
{
// Реализация методов интерфейсов
}Принципы правильной работы с интерфейсами
Interface Segregation Principle (ISP)
Не создайте «жирные» интерфейсы. Лучше много маленьких, чем один большой.
- ❌ Плохо:
IGameObject(содержит методыMove,Shoot,Build,Sell,Talk). - ✅ Хорошо:
IMovable,IAttacker,IBuildable,ISellable.
Так вам не придется реализовывать метод Shoot() для здания, которое стрелять не может.
Dependency Injection (Внедрение зависимостей)
Не создавайте объекты внутри класса через new. Передавайте их через конструктор или поле.
// ❌ Жесткая связность
public class Shop : MonoBehaviour
{
private PlayFabService _service = new PlayFabService(); // Привязка к конкретному классу
}
// ✅ Гибкая архитектура
public class Shop : MonoBehaviour
{
private IDataService _service; // Зависимость от интерфейса
public void Init(IDataService service)
{
_service = service;
}
}В Unity это часто делается через Awake или внешний «Composer» класс.
Именование
Всегда используйте префикс I.
IInventoryINetworkManagerIQuest
Это сразу говорит другому разработчику: «Это интерфейс, здесь нет реализации».
Пример архитектуры для стратегии
Вот как интерфейсы связывают Клиент, Экономику и Бэкенд:
// 1. Интерфейсы (Чистая архитектура)
public interface ICurrencyService { void AddGold(int amount); int GetGold(); }
public interface IBuildingSystem { void StartConstruction(BuildingType type); }
public interface INotificationService { void Show(string message); }
// 2. Реализации (Инфраструктура)
public class PlayFabCurrencyService : ICurrencyService { /* ... */ }
public class LocalBuildingSystem : IBuildingSystem { /* ... */ }
public class UINotificationService : INotificationService { /* ... */ }
// 3. Использование (Геймплей)
public class CityManager : MonoBehaviour
{
private ICurrencyService _currency;
private IBuildingSystem _building;
private INotificationService _notify;
// Внедряем зависимости извне
public void Init(ICurrencyService currency, IBuildingSystem building, INotificationService notify)
{
_currency = currency;
_building = building;
_notify = notify;
}
public void OnBuildButtonClicked()
{
if (_currency.GetGold() >= 100)
{
_currency.AddGold(-100);
_building.StartConstruction(BuildingType.Barracks);
_notify.Show("Стройка началась!");
}
else
{
_notify.Show("Недостаточно золота!");
}
}
}Что это дает:
- CityManager не знает про PlayFab. Ему всё равно, откуда берутся деньги.
- Вы можете заменить PlayFabCurrencyService на ServerCurrencyService без изменения CityManager.
- Вы можете легко протестировать CityManager, передав ему MockCurrencyService.
Подводные камни
- Нельзя serialize в Inspector: Поля типа
interfaceне отображаются в Инспекторе Unity.- Решение: Используйте поля конкретного типа (
MonoBehaviour) для ссылки в инспекторе, а в коде приводите к интерфейсу, либо используйте систему Dependency Injection (например, Zenject/VContainer).
- Решение: Используйте поля конкретного типа (
- Производительность: Вызов методов через интерфейс чуть медленнее, чем прямой вызов (виртуальный вызов).
- Реальность: В стратегиях это незаметно. Не оптимизируйте это, если у вас не тысячи вызовов в кадр.
- Over-engineering: Не создавайте интерфейс для каждого класса. Если класс один и не будет заменен — интерфейс не нужен.
Итог
Если вы делаете онлайн-стратегию интерфейсы критически важны в трех местах:
- Взаимодействие с бэкендом: (
IDataService) — чтобы легко менять провайдера (PlayFab ↔ Свой сервер). - Взаимодействие объектов на карте: (
IDamageable,IInteractable) — чтобы универсализировать логику кликов и боя. - Монетизация и сервисы: (
IAdService,IAnalyticsService) — чтобы легко отключать рекламу или менять SDK.
Используйте интерфейсы там, где вы ожидаете изменения реализации в будущем. Это спасёт вас от переписывания всего проекта через год.





