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

Обзор

Для начала давайте рассмотрим из чего состоит этот фреймворк.

System

  • Check - набор проверок для аргументов и операций.

  • DisposableBase - освобождаемый объект. Содержит некоторый дополнительный функционал.

  • IDependencyProvider - локатор служб. Предоставляет запрашиваемые объекты и значения.

GameFramework.Pro

  • Main - корень проекта.

    • ProgramBase - главная сущность проекта. Создает другие сущности и предоставляет запрашиваемые зависимости.

  • UI - аудио-графический пользовательский интерфейс.

    • ThemeBase - сущность аудио темы. Проигрывает музыкальные плейлисты.

    • PlayListBase

    • ScreenBase - сущность графического экрана. Показывает дерево виджетов.

    • WidgetBase

    • ViewableWidgetBase

    • RouterBase - сервис менеджера состояния. Предоставляет методы для загрузки главного меню, загрузки/перезагрузки/выгрузки игры, а так же выхода из приложения.

  • App - модель приложения.

    • ApplicationBase - сущность приложения. Выполняет инициализацию, запуск главного цикла, запуск игры, а так же предоставляет хранилище и подобное.

  • Game - модель домена / бизнеса (в нашем случае самой игры).

    • GameBase - сущность игры. Содержит информацию об игре, игровые правила, состояние и сущности игроков.

    • PlayerBase - сущность игрока. Содержит информацию об игроке и состояние. Так же может обрабатывать ввод.

    • WorldBase - сущность мира. Содержит все остальные сущности.

    • EntityBase - любой значимый для игры объект.

Детальный обзор

Теперь давайте рассмотрим некоторые компоненты этого фреймворка более детально.

Program

// Программа содержит другие сущности и сервисы,
// а так же реализует паттерн локатор служб.
// Заметьте, что локатор служб считается анти-паттерном,
// но другие решения приводят к оверкодингу.
public abstract class ProgramBase : DisposableBase {
  
    public ProgramBase();
    private protected override void OnDisposeInternal();
  
}
// Каждая сущность и сервис разделены на две класса: базовый и расширенный.
// Все расширенные классы добавляют поддержку IDependencyProvider.
public abstract class ProgramBase2<TTheme, TScreen, TRouter, TApplication> : ProgramBase, IDependencyProvider
    where TTheme : ThemeBase
    where TScreen : ScreenBase
    where TRouter : RouterBase
    where TApplication : ApplicationBase {

    protected TTheme Theme { get; init; }
    protected TScreen Screen { get; init; }
    protected TRouter Router { get; init; }
    protected TApplication Application { get; init; }

    public ProgramBase2();
    private protected override void OnDisposeInternal();

    object? IDependencyProvider.GetValue(Type type, object? argument);

}

Theme

// Тема содержит плейлист,
// который проигрывает текущий список аудио треков.
// Заметьте, что я использовал машину состояний,
// которая позволляет вам легко управлять текущим плейлистом.
public abstract class ThemeBase : DisposableBase {

    protected StateMachine Machine { get; }

    public ThemeBase();
    private protected override void OnDisposeInternal();

}
// Заметьте, что вместо наследования,
// я использовал двухсторонюю связь между плейлистом и состоянием.
public abstract class PlayListBase {
    public sealed class State2 : State {

        public PlayListBase PlayList { get; }

        public State2(PlayListBase playList);
        protected override void OnDispose();

        protected override void OnActivate(object? argument);
        protected override void OnDeactivate(object? argument);

    }

    public State2 State { get; }

    public PlayListBase();
    protected internal abstract void OnDispose();
    private protected virtual void OnDisposeInternal();

    protected internal abstract void OnActivate(object? argument);
    protected internal abstract void OnDeactivate(object? argument);

}
public abstract class ThemeBase2<TRouter, TApplication> : ThemeBase
    where TRouter : RouterBase
    where TApplication : ApplicationBase {

    protected IDependencyProvider Provider { get; }
    protected TRouter Router { get; }
    protected TApplication Application { get; }

    public ThemeBase2();
    private protected override void OnDisposeInternal();

}
public abstract class PlayListBase2 : PlayListBase {

    protected IDependencyProvider Provider { get; }

    public PlayListBase2();
    private protected override void OnDisposeInternal();

}

Screen

// Экран содержит дерево виджетов,
// которые рисуют текущий пользовательский интерфейс.
// Заметьте, что я использовал машину дерева,
// которая позволляет вам легко управлять иерархией виджетов.
public abstract class ScreenBase : DisposableBase {

    protected TreeMachine Machine { get; }

    public ScreenBase();
    private protected override void OnDisposeInternal();

}
// Заметьте, что вместо наследования,
// я использовал двухсторонюю связь между виджетом и нодой.
public abstract class WidgetBase {
    public sealed class Node2 : Node {

        public WidgetBase Widget { get; }

        public Node2(WidgetBase widget);
        protected override void OnDispose();

        protected override void OnActivate(object? argument);
        protected override void OnDeactivate(object? argument);

        protected override void Sort(List<INode> children);

    }

    public Node2 Node { get; }

    public WidgetBase();
    protected internal abstract void OnDispose();
    private protected virtual void OnDisposeInternal();

    protected internal abstract void OnActivate(object? argument);
    protected internal abstract void OnDeactivate(object? argument);

    protected internal virtual void OnBeforeDescendantActivate(INode descendant, object? argument);
    protected internal virtual void OnAfterDescendantActivate(INode descendant, object? argument);
    protected internal virtual void OnBeforeDescendantDeactivate(INode descendant, object? argument);
    protected internal virtual void OnAfterDescendantDeactivate(INode descendant, object? argument);

    protected internal virtual void Sort(List<INode> children);

}
public abstract class ViewableWidgetBase : WidgetBase {

    public object View { get; protected init; }

    internal ViewableWidgetBase();
    private protected override void OnDisposeInternal();

}
public abstract class ViewableWidgetBase<TView> : ViewableWidgetBase
    where TView : notnull {

    protected new TView View { get; init; }

    public ViewableWidgetBase();
    private protected override void OnDisposeInternal();

}
public abstract class ScreenBase2<TRouter, TApplication> : ScreenBase
    where TRouter : RouterBase
    where TApplication : ApplicationBase {

    protected IDependencyProvider Provider { get; }
    protected TRouter Router { get; }
    protected TApplication Application { get; }

    public ScreenBase2();
    private protected override void OnDisposeInternal();

}
public abstract class WidgetBase2 : WidgetBase {

    protected IDependencyProvider Provider { get;}

    public WidgetBase2();
    private protected override void OnDisposeInternal();

}
public abstract class ViewableWidgetBase2<TView> : ViewableWidgetBase<TView>
    where TView : notnull {

    protected IDependencyProvider Provider { get;}

    public ViewableWidgetBase2();
    private protected override void OnDisposeInternal();

}

Router

// Роутер предоставляет ссылки на другие сущности.
public abstract class RouterBase : DisposableBase {
  
    public RouterBase();
    private protected override void OnDisposeInternal();
  
}
public abstract class RouterBase2<TTheme, TScreen, TApplication> : RouterBase
    where TTheme : ThemeBase
    where TScreen : ScreenBase
    where TApplication : ApplicationBase {

    protected IDependencyProvider Provider { get; }
    protected TTheme Theme { get; }
    protected TScreen Screen { get; }
    protected TApplication Application { get; }

    public RouterBase2();
    private protected override void OnDisposeInternal();

}

Application

// Приложение предоставляет лишь провайдер зависимостей.
// Остальные сущности являются такими же простыми,
// поэтому я не буду продолжать описывать их.
public abstract class ApplicationBase : DisposableBase {
  
    public ApplicationBase();
    private protected override void OnDisposeInternal();
  
}
public abstract class ApplicationBase2 : ApplicationBase {
  
    protected IDependencyProvider Provider { get; }
  
    public ApplicationBase2();
    private protected override void OnDisposeInternal();
  
}

Дополнение

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

Я могу предложить следующую классификацию классов:

  • Entity - объект названный существительным, который, в идеале, только обрабатывает события и не предоставляет никакого API.

  • Service - объект названный отглагольным существительным, который, в идеале, только предоставляет API для других сущностей.

  • Value - примитив или структура содержащая другие значения. Иногда такие структуры имеют суффиксы Info или Desc.

  • Utility - неймспейс для определенного множества статических методов.

Так же я могу предложить следующие классификации:

  • Для свойств классов:

    • Property/Data

    • Property/Info - информация об объекте.

    • Property/Info/Question - вопрос к объекту.

    • Property/Info/Assertion - утверждение об объекте.

    • Property/Info/Directive - инструкция объекту.

    • Property/Reference/Object - ссылка на объект.

    • Property/Reference/Delegate - ссылка на логику.

    • Property/Reference/Event - ссылка на обработчики события.

  • Для методов классов:

    • Method/Constructor

    • Method/Destructor

    • Method/Command/Query - команда на получение данных.

    • Method/Command/Request - команда на выполнение работы.

    • Method/Handler - реакция на событие.

  • Для атрибутов классов:

    • Attribute/Info

    • Attribute/Info/Directive

Заключение

В заключении, я хочу отметить, что большую сложность фреймворка я выделил в две отдельные библиотеки StateMachine.Pro и TreeMachine.Pro. Эти библиотеки могли бы быть достойны отдельных статей, но думаю, их суть ясна из их названий.

Ссылки

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


  1. DEugene
    16.05.2026 22:00

    А зачем столько "пустых" ничего не делающих абстракций? Бесконечные abstract *Base классы с private protected override заглушками - просто визуальный бесполезный шум.

    А еще код не органично выглялит для dotnet. Скорее похоже на рукописи древних джавистов или плюсовиков, лет так дцать назад.

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


    1. mopsicus
      16.05.2026 22:00

      T-классы по-моему в Делфи были)


  1. shai_hulud
    16.05.2026 22:00

    Как будто смотришь в чей-то ящик проношенных труханов.


  1. Sektor2350
    16.05.2026 22:00

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


    1. Denis535 Автор
      16.05.2026 22:00

      И сколько времени потребуется, чтобы набросать подобные абстракции? Теперь, каждый опытный и не опытный разработчик может использовать мой фремворк. Разве это плохо? Многие проекты, кстати, вообще не имеют четкой архитектуры. Для решения этой проблемы и были придуманы фреймворки типа ribs. Что именно выглядит не очень? У вас есть идеи получше? Предлагайте!


      1. Laeslaraftor
        16.05.2026 22:00

        ribs решил проблему навигации в uber. Твой (((фреймворк))) решил проблему нехватки private protected override void OnDisposeInternal() в природе


  1. Mr_Dr_Pr_Patrick
    16.05.2026 22:00

    Перед разработкой чего надо всегда спросить себя: а не сделал ли кто подобное? А если и сделал в чем мой продукт лучше, выделяется. Чем не угодил MonoGame?

    Как по мне большая проблема дотнета это отсутствие нативна. Только и дело что врапить Cpp.


    1. Denis535 Автор
      16.05.2026 22:00

      А кто-то делал подобное? MonoGame это совершенно другое.


      1. Laeslaraftor
        16.05.2026 22:00

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

        1. Game - это пустой абстрактный класс с OnDisposeInternal()

        2. Framework - это когда проект состоит из базовых классов с абстрактными методами, которые доступны только внутри своей борки.

        3. Pro - это Private pRotected Override void OnDisposeInternal()

        Вся индустрия на MonoGame, Unity и Godot, не подозревая, что настоящий геймдев - это когда PlayListBase знает про State2, а State2 знает про PlayListBase, и вместе они дружно вызывают друг другу OnDisposeInternal

        Жду GameFramework.Pro.Max.Ultra, где добавят sealed-класс PlayerBase3<TPlayer, TWorld, TInput, TDisposeHandler, TDisposeHandler2>, а в Check.Operation.Alive начнут проверять, жив ли ещё сам разработчик


        1. Denis535 Автор
          16.05.2026 22:00

          Возможно мелкие недочеты остались, но в целом все правильно.


  1. Laeslaraftor
    16.05.2026 22:00

    Зачем каждому методу добавлять кучу модификаторов? protected internal abstract полностью убивает расширяемость, так как метод с такой комбинацией модификаторов невозможно реализовать из другой сборки, соответственно все Base1234 классы идут лесом. И тебе явно стоит изучить как правильно реализовывать IDisposable


    1. Denis535 Автор
      16.05.2026 22:00

      И как же правильно надо реализовывать IDisposable?


      1. Laeslaraftor
        16.05.2026 22:00

        Первая же ссылка по запросу «C# dispose pattern»: https://learn.microsoft.com/ru-ru/dotnet/standard/garbage-collection/implementing-dispose

        В целом, я посмотрел твой репозиторий с этой библиотекой, заодно ещё заглянул в тот что с Unity проектом. У меня просто слов нет, чтобы описать всё что ты там понаписал. Ты явно пытаешься натянуть стиль Java на C#. Я ещё никогда так не угорал, когда читал чужой код. Там же буквально в каждой строчке какие то пёрлы


        1. Denis535 Автор
          16.05.2026 22:00

          Какие пёрлы? И чем вам не нравится Java стиль?


          1. Laeslaraftor
            16.05.2026 22:00

            Не нравится тем что это C#, если бы код был на Java то вопросов бы не было. И вот те самые пёрлы:

            1. Кто в здравом уме будет писать using внутри namespace в каждом файле? Это прям нишевая фишка, которая может пригодиться раз в жизни.

            2. Открывающиеся фигурные скобки в C# следует писать с новой строчки, ты либо в блокноте код фигачишь, либо вручную зачем то в настройках убрал перенос на новую строку.

            3. Куча модификаторов на каждом методе. Зачем это надо? Зачем ты добавляешь методу internal и потом в настройках прописываешь InternalsVisibleTo? На ровном месте всё усложняешь

            4. Зачем то постоянно переопределяешь методы. Вот эти все private protected override abstract. Зачем это делается?

            5. Постоянные проверки Check.Operation.Alive, как будто у тебя идёт работа с нативной памятью. Если у тебя даже намёка нету на работу с нативной памятью, то зачем эти миллиард проверок на то что объект был освобождён? Ну и про неправильную реализацию IDisposable я уже писал

            6. Можно не писать в каждом файле #nullable enable, можно просто в настройках проекта поставить <Nullable>enable</Nullable>

            7. Вот эти твои *Base1234 тоже ни к чему тут, надо нормально классы обзывать, а не цифры в конце добавлять

            8. ProgramBase2 - это прям сок, настолько раздутых дженериков я ещё никогда не видел

            9. default! для readonly полей… В чём проблема сделать protected конструктор? Вот реально с нифига выбираешь самые странные решения. У тебя в ProgramBase2, 4 свойства с init, в каждом по 2 проверки, итого при создании экземпляра у тебя в моменте будет 8 проверок идти, половина из которых передовые

            10. Божественное название (((фреймворка)))

            10 пунктов для этого фреймворкосодержащего продукта. Если говорить и про тот Unity проект, то там вообще песня


            1. Denis535 Автор
              16.05.2026 22:00

              И в чем тут проблема?


              1. Laeslaraftor
                16.05.2026 22:00

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


              1. Laeslaraftor
                16.05.2026 22:00

                А если серьёзно, ты реально не видишь проблем в своём коде? Или ты просто прикалываешься? Ты называешь это фреймворком, но при этом твой “фреймворк” ничего кроме абстракций не даёт. Такую шляпу можно самому за полчаса накатать, и то лучше выйдет. Вот представь: чтобы написать hello world, тебе сначала надо сделать свою реализацию абстрактного System.String, а для этого реализовать GetType() и GetHashCode(). Прикольно? Вот и будущим пользователям твоего фреймворка так же весело. Фреймворк - это когда он даёт готовый функционал: рендеринг, звук, загрузку ресурсов, пайплайн ассетов. А ты просто шаблоны накатал, которые ещё и написаны кучеряво, и назвал это GameFramework. Переименуй тогда уж в BoilerplateBase.Pro