В этой статье я хочу представить вам мой фреймворк, реализующий идеи чистой архитектуры адаптированные для игровых проектов. Данный фреймворк определяет основные слои вашего проекта, сущности и сервисы, а так же содержит минимальный набор утилит.
Обзор
Для начала давайте рассмотрим из чего состоит этот фреймворк.
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)

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

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

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

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

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

Laeslaraftor
16.05.2026 22:00Да, MonoGame это другое. Это было гениальным решением сделать фреймворкосодержащий продукт для игр, в названии которого:
Game - это пустой абстрактный класс с
OnDisposeInternal()Framework - это когда проект состоит из базовых классов с абстрактными методами, которые доступны только внутри своей борки.
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 начнут проверять, жив ли ещё сам разработчик

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

Laeslaraftor
16.05.2026 22:00Первая же ссылка по запросу «C# dispose pattern»: https://learn.microsoft.com/ru-ru/dotnet/standard/garbage-collection/implementing-dispose
В целом, я посмотрел твой репозиторий с этой библиотекой, заодно ещё заглянул в тот что с Unity проектом. У меня просто слов нет, чтобы описать всё что ты там понаписал. Ты явно пытаешься натянуть стиль Java на C#. Я ещё никогда так не угорал, когда читал чужой код. Там же буквально в каждой строчке какие то пёрлы

Denis535 Автор
16.05.2026 22:00Какие пёрлы? И чем вам не нравится Java стиль?

Laeslaraftor
16.05.2026 22:00Не нравится тем что это C#, если бы код был на Java то вопросов бы не было. И вот те самые пёрлы:
Кто в здравом уме будет писать using внутри namespace в каждом файле? Это прям нишевая фишка, которая может пригодиться раз в жизни.
Открывающиеся фигурные скобки в C# следует писать с новой строчки, ты либо в блокноте код фигачишь, либо вручную зачем то в настройках убрал перенос на новую строку.
Куча модификаторов на каждом методе. Зачем это надо? Зачем ты добавляешь методу internal и потом в настройках прописываешь InternalsVisibleTo? На ровном месте всё усложняешь
Зачем то постоянно переопределяешь методы. Вот эти все
private protected override abstract. Зачем это делается?Постоянные проверки
Check.Operation.Alive, как будто у тебя идёт работа с нативной памятью. Если у тебя даже намёка нету на работу с нативной памятью, то зачем эти миллиард проверок на то что объект был освобождён? Ну и про неправильную реализациюIDisposableя уже писалМожно не писать в каждом файле
#nullable enable, можно просто в настройках проекта поставить<Nullable>enable</Nullable>Вот эти твои *Base1234 тоже ни к чему тут, надо нормально классы обзывать, а не цифры в конце добавлять
ProgramBase2 - это прям сок, настолько раздутых дженериков я ещё никогда не видел
default! для readonly полей… В чём проблема сделать protected конструктор? Вот реально с нифига выбираешь самые странные решения. У тебя в ProgramBase2, 4 свойства с init, в каждом по 2 проверки, итого при создании экземпляра у тебя в моменте будет 8 проверок идти, половина из которых передовые
Божественное название (((фреймворка)))
10 пунктов для этого фреймворкосодержащего продукта. Если говорить и про тот Unity проект, то там вообще песня

Denis535 Автор
16.05.2026 22:00И в чем тут проблема?

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

Laeslaraftor
16.05.2026 22:00А если серьёзно, ты реально не видишь проблем в своём коде? Или ты просто прикалываешься? Ты называешь это фреймворком, но при этом твой “фреймворк” ничего кроме абстракций не даёт. Такую шляпу можно самому за полчаса накатать, и то лучше выйдет. Вот представь: чтобы написать hello world, тебе сначала надо сделать свою реализацию абстрактного
System.String, а для этого реализоватьGetType()иGetHashCode(). Прикольно? Вот и будущим пользователям твоего фреймворка так же весело. Фреймворк - это когда он даёт готовый функционал: рендеринг, звук, загрузку ресурсов, пайплайн ассетов. А ты просто шаблоны накатал, которые ещё и написаны кучеряво, и назвал это GameFramework. Переименуй тогда уж в BoilerplateBase.Pro
DEugene
А зачем столько "пустых" ничего не делающих абстракций? Бесконечные abstract *Base классы с private protected override заглушками - просто визуальный бесполезный шум.
А еще код не органично выглялит для dotnet. Скорее похоже на рукописи древних джавистов или плюсовиков, лет так дцать назад.
Ну и самое главное, из статьи не понятно какую проблему решает этот фреймворк? Где он дает буста при разработке игр? Пока визуально кажется что он лишь добавляет когнитивной нагрузки - пойди сначала разберись, что вообще есть, от чего надо понаследоваться и что пооверрадить чтобы что-то нужное получить.
mopsicus
T-классы по-моему в Делфи были)