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

Обзор

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

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. Эти библиотеки могли бы быть достойны отдельных статей, но думаю, их суть ясна из их названий.

Ссылки