Если вы не читали вторую часть, то можете прочитать ее здесь.

Время игры

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

Итак, во-первых, в большинстве игр есть игровой цикл, который более или менее соответствует следующему потоку:

int main() {
    while (theGameIsRunning) {
        update();
        render();
    }
    return 0;
}

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

int main() {
    while (theGameIsRunning) {
        waitForInput();
        performInputTasks(); 
        render(); // write the game state to the command line
    }
    return 0;
}

Концептуально думайте о каждом пользовательском вводе как о «фрейме». Мы предоставим пользователю либо набор опций («1) вещь», «2) вторую вещь» и т. д.), либо, возможно, набор команд («разыграть карту 1», «использовать эффект 4», и т. д.), которые пользователь может набирать как в старой текстовой ролевой игре. Когда пользователь вводит команду, это начало «кадра» — введенная пользователем задача завершается в performInputTasks(), а затем снова печатается состояние игры, чтобы показать пользователю результаты его команды.

Игровой агент (ИИ) будет делать ходы в разделе performInputTasks() после того, как пользователь закончит свой ход. Мы просто покажем действия агента через текстовый интерфейс. Единственная точка фактического взаимодействия с агентом связана с использованием эффектов карт, которые заставляют игрока делать выбор, или когда происходит бой — для этих событий мы будем хранить состояние агента и предлагать пользователю принять решение, после чего мы продолжим. логика агента по мере необходимости. Не так много карт, которые вызывают взаимодействие, но битва будет основным моментом принятия решений для игрока, поэтому очень важно встроить эту функциональность.

Итак, у нас есть карты и примерное представление о том, каким должен быть геймплей, но недостаточно иметь набор из Deck объектов и называть его хорошим — мы должны разработать способ взаимодействия пользователя и агентов с игровым состоянием. эффективным и ремонтопригодным образом. Это означает, что нам нужно спроектировать сам объект Game.

Было бы очень просто иметь экземпляр Game синглтона «божественного класса», который сам все обрабатывает… и состоит из 15000 строк и неудобен в сопровождении. Однако, если вы читали предыдущие статьи, вы знаете, что здесь мы делаем не так. Мы собираемся создать что-то красивое и модульное, а это значит, что нам нужно найти способ отделить пользовательский интерфейс игры от игровой модели и каким-то образом вставить нашу бизнес-логику посередине. Это означает, что нам нужно принять решение о шаблонах.

MVC против MVP против MVVM

В последнее время я возился с шаблонами проектирования в качестве хобби, потому что я странный, поэтому я думал о том, какой шаблон будет наиболее точно представлять то, что мы пытаемся достичь с этой первоначальной идеей игры. Я разбил его на трихотомию Model-View-Controller (MVC), Model-View-Presenter (MVP) и Model-View-ViewModel (MVVM). Несмотря на схожие шаблоны, они имеют небольшие различия в реализации, и эти различия поразительно трудно понять, используя информацию из Интернета. Я собираюсь подробно описать свое понимание различий ниже и, надеюсь, обосновать свое решение в конце.

Обратите внимание, что в приведенных ниже описаниях модель — это представление наших данных (для нас это объект Game), а представление — это способ отображения этого представления. (текстовый пользовательский интерфейс для нас).

  • Модель-представление-контроллер (MVC)

В шаблоне MVC у вас есть контроллер, который прослушивает входные события из представления и выполняет бизнес-логику непосредственно в отношении модели, при этом представление обновляется напрямую. Это основной и самый простой из шаблонов Model-View-Whatever, который служит основой для описания других. Часто этот шаблон будет использовать ViewController вместо отдельного представления и контроллера (например, в iOS), но это не всегда так, и концептуально они являются отдельными логическими единицами. Как видно из диаграммы ниже, пользователь обычно использует контроллер для управления моделью, которая затем обновляет само представление.

  • Model-View-Presenter (MVP)

MVP во многом похож на MVC, за исключением того, что MVP использует Presenter (или контролирующий контроллер в некоторых источниках) вместо контроллера, а докладчик управляет обновление как модели, ипредставления. Существует два типа MVP: активная версия, которая использует прямую привязку данных как к модели, так и к представлению и обновляет представление непосредственно как часть изменения модели, и пассивная версия, которая использует шаблон наблюдателя для прослушивания обновлений модели, а не для их непосредственного вызова (подумайте об API доступа к диску по сравнению с REST API, где больше бизнес-логики может быть на сервере и отдельно от презентатора и модели). MVP более популярен в Android, где важно модульное разделение бизнес-логики от иерархии модель-представление — это упрощает модульное тестирование бизнес-логики, особенно с такими фреймворками, как Mockito и Robolectric, которые позволяют легко имитировать представление, модель и презентатор.

  • Модель-представление-ViewModel (MVVM)

С MVVM вы перекладываете ответственность за обновление модели и представления на ViewModel, который обычно использует шаблон Observer для обновления представления, когда в модели происходят изменения, и соответствующим образом делегирует обновления представления модели. . Одна приятная вещь в этом шаблоне, хотя она и не применима к этому проекту, — это возможность иметь отношение вида «один ко многим» к ViewModel, что может быть полезно для более сложных макетов. MVVM популярен в таких вещах, как ASP и Silverlight, а также в формах Windows, где связь «один ко многим» пригодится.

Если вы читали этот раздел и все еще ломаете голову, есть отличная дискуссия о stackoverflow и очень хороший доклад от Microsoft. В каком-то смысле это очень тонкая тема с множеством живых и активных дискуссий в Интернете, поэтому, если вы не можете понять это, не волнуйтесь.

В любом случае, я выбрал MVP, отчасти потому, что в последнее время много работаю над Android, и поэтому он ближе всего к моей голове, а также потому, что он обеспечивает наиболее модульную архитектуру (на мой взгляд), и я у меня есть планы на эту кодовую базу помимо статей здесь, поэтому модульность, расширяемость и сложность — все это имеет для меня первостепенное значение.

Дизайн высокого уровня

Теперь, когда мы определились с MVP, базовая архитектура, которую мы будем использовать для игрового цикла, выглядит следующим образом:

Здесь InputLayer и GameDisplay являются просто разными концами интерфейса командной строки и просто действуют как делегаты ввода и вывода, на которые воздействует и использует GameView, который является компонентом логического представления. GameModel — это модельная часть, а Presenter — это, конечно же, презентирующая часть.

Немного глубже (и несколько фрагментов)

Теперь нам нужно создать интерфейсы для объектов, включая наш презентатор. Во-первых, GameModel:

GameModel — это простой класс модели, хранящий все данные, которые потребуются модели — экземпляры Player, общедоступные Decks, отслеживание фаз поворота, все девять ярдов.

Обратите внимание, что в GameModel нет бизнес-логики, просто чтение и запись. Вся бизнес-логика (полагаю, игровая логика) будет передана презентатору, и именно презентер должен обработать представление, запускающее эту логику, и заставить представление отображать ее. Вот почему GameModel является простым базовым классом — его легко смоделировать, сразу же протестировать, и он просто действует как хранилище для состояния, которое он не может изменить сам.

Далее наш GameView с простым интерфейсом:

Это может расшириться, когда мы добавим в игру больше функций. Обратите внимание, что прямо сейчас мы передаем пользователю объекты, которые хотим описать, — это способ сохранить инкапсуляцию GameModel только для себя. У докладчика будет ссылка на представление и на модель, но не будет ссылки на представление в модели и наоборот.

В идеале объекты, которые мы передаем таким образом, также должны быть неизменяемыми, поэтому, даже если бы мы не имели контроля над внутренними элементами представления, мы все равно были бы уверены, что представление не попытается изменить объекты, которые мы запрашиваем. его отображать. В Java это сложнее, чем в таком языке, как, скажем, Swift, поэтому пока я не беспокоюсь о его реализации. Просто имейте это в виду, если вы решите следовать на своем собственном языке.

Интерфейс GameView используется в классе BaseGameDisplay, который затем является подклассом TextBasedGameDisplay. Кстати, здесь мы используем что-то вроде архаичного, но эффективного шаблона — обычно в этой настройке используется шаблон адаптера с BaseGameDisplay вместо подкласса BaseGameViewAdapter, который предоставляет значения по умолчанию для всех методов интерфейса GameView. Java давно добавила ключевое слово default в интерфейсы, поэтому вы можете предоставить реализацию по умолчанию для методов и удалить большую часть рассуждений, лежащих в основе шаблона адаптера. Кроме того, здесь TextBasedGameDisplay в любом случае потребуется реализовать каждый метод интерфейса GameView, независимо от значений по умолчанию, чтобы поддерживать все функции. Поскольку BaseGameDisplay может не иметь представления об обязанностях своих подклассов, помимо необходимости переопределения функций GameView, он служит логическим базовым классом, но не обязательно обязательным. Иногда органический рост кодовой базы — это хорошо, и нужно будет посмотреть, пройдет ли этот класс испытание временем.

Теперь у нас есть модель, которая не может сама изменить состояние игры, и представление, отображающее только существующее состояние игры. Так что же изменяет состояние игры? Ведущий. Наш базовый класс GamePresenter выглядит так:

Обратите внимание, что мы создаем презентатора, обертывающего GameModel, а затем вызываем bind() с GameView. На первый взгляд это кажется немного странным, но идея состоит в том, что презентер оборачивает одну модель, а затем может быть привязан к любому представлению, которое реализует надлежащий интерфейс. Примером этого является Android, где один презентатор используется для привязки к одному фрагменту, но не зависит от того, какой фрагмент привязывается в любой момент времени.

Мы превратим GamePresenter в TextBasedGamePresenter, часть которого приведена ниже:

Не беспокойтесь о GameRulesImpl, мы пойдем по этому пути в следующей статье. Просто обратите внимание, что TextBasedGamePresenter выступает в качестве делегата между представлением и моделью, приказывая представлению выполнять действия и выполняя некоторую бизнес-логику в модели (метод drawCard является бизнес-логикой в ​​этом фрагменте). Я оставлю это в качестве упражнения для читателя, чтобы реализовать остальную часть класса, если вы будете следовать дома, или вы всегда можете проверить серию git repo.

Подведение итогов

Вау, это было много, чтобы написать.

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

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

В любом случае, вот еще несколько щенков!