
Архитектура игры - это структура вашей игры, набор различных сущностей, правила их взаимодействия друг с другом. Концепт, принципы, в соответствии с которыми, данные в вашей игре преобразуются и передаются.
Зачем нужна архитектура?
Без продуманной архитектуры типичная картина: один класс Game на 2000 строк, где перемещение, отрисовка, ввод и логика врагов - всё в кучу. Меняешь управление - ломается физика. Добавляешь врага - ломается UI.
Ваша игра - это не один большой кусок кода, который только и делает, что “играет”. Каким бы вы принципам разработки ни следовали, основная идея заключается в том, чтобы разбить необъятную задачу “сделать игру” на кучу маленьких задач. Т.е. применить декомпозицию.
И в этом хорошо помогает продумывание архитектуры игры.
Когда у вас есть список требований и задач к игре, вы можете четко сказать: моя игра должна делать x, y, z. Допустим, отвечать на нажатия клавиш, перемещать игрока, обрабатывать столкновения, просчитывать физику и т.д. Как все это реализовать? Где писать код?
Такие и многие другие вопросы появляются в голове. Давайте скажем теперь так: у нас будет некая сущность (объект) InputManager, которая будет обрабатывать действия пользователя, т.е. она будет получать информацию о нажатии клавиш от операционной системы или фреймворка, приводить к удобному для вас виду (например, вам не важно, была ли нажата клавиша A или Ф - вы всегда работаете с английской раскладкой), а дальше передает эту информацию туда, куда надо.
Куда? Другим системам. Как другие системы могут получать её? В курсе (C#) вам рассказывали про событийную модель, поэтому возьмем её как пример (конечно, информацию между различными сущностями можно передавать по-разному).
Упрощенно рассмотрим событие как список методов для вызова.
Итак, InputManager хочет куда-то передать информацию, для этого он определяет внутри себя событие WasPressedAKey() и активирует его в нужный момент. Предположим, у нас есть некая сущность Player, которая соответствует игроку, при её создании мы подписываемся на событие WasPressedAKey(). И когда оно сработает, мы можем его обработать.
Благодаря подобному подходу мы четко знаем, как реализовать перемещение игрока: Обработать нажатие в InputManager, а затем написать реакцию игрока в Player.
Обратите внимание: InputManager ничего не знает о Player. Он просто кричит "нажали A!", а кто услышит - не его забота. Завтра мы добавим второго игрока или AI - и InputManager менять не придётся. Это и есть слабая связанность.
Продумывание архитектуры приложения - довольно творческая задача, где есть различные принципы, советы, паттерны, но нет парочки законов, которые четко скажут вам, что делать. Любой паттерн имеет свои плюсы и минусы, которые надо иметь в виду.
И для того, чтобы выбрать нужную архитектуру вам нужно знать, что именно вы хотите сделать: основная фича игры, список возможностей (сохранение прогресса, мультиплеер и прочее), различные сценарии использования в игре. Т.е. ТЗ.
Архитектура может существовать на разных “уровнях”. Предположим, что у вашей игры есть определенная архитектура, по которой вы её разрабатываете (например, MVC).
В контексте MVC у вас есть 3 сущности - Model/View/Controller. Можно сказать, что у вас будет 3 экземпляра соответствующих классов, а можно сказать, что каждая из этих сущностей является системой, целым “организмом”, состоящим из множества различных мелких сущностей.
Например, View - это набор отдельных классов, которые ответственны за отображение соответствующих элементов.
Но эти системы тоже должны как-то обеспечивать взаимодействие отдельных компонентов внутри себя. Т.е. у них тоже есть внутренняя архитектура большой составной сущности View. Тоже MVC? Но MVC больше предназначен для работы с пользовательскими интерфейсами. Поэтому нам нужно что-то другое. Например, наследование.** Наследование** - фундаментальный механизм ООП: если X является разновидностью Y, то X наследуется от Y.
Котенок - это Млекопитающее. Следовательно, котенок должен потреблять молоко. Информация об этом хранится в родительском классе Млекопитающее.
Мы определили набор каких-то элементов и правила работы с ними. Получили архитектуру, пусть и в меньшем масштабе, в контексте отдельной системы.
Возвращаясь к примеру с MVC: мы можем сказать, что все подсущности из View наследуются от общего класса MainViewParent, который определяет основные правила отображения, а его потомки модифицируют некоторые из них.
Что нам это даст?
Таким образом, мы можем рассматривать наше приложение на разных масштабах. И на каждом масштабе мы выбираем какую-то архитектуру, определяющую то, как мы пишем код.
Давайте рассмотрим некоторые архитектуры, принципы ПО, применяющиеся в разработке игр.
Грубо говоря, их можно разделить на три уровня. На верхнем - паттерны, определяющие структуру всего проекта (MVC, ECS). На уровне взаимодействия - способы передачи данных между системами (события, конечные автоматы). На уровне отдельных механик - конкретные решения для конкретных задач (игровой цикл, сеть, порядок обновления).
Разобравшись с архитектурой, можно переходить к конкретным алгоритмам - поиск путей, процедурная генерация, ИИ врагов и т.д. Архитектура определяет как организован код, алгоритмы - что этот код делает.
| Тема | О чём |
|---|---|
| Семейство Model-View | MVC, MVP, MVVM - разделение данных, отображения и управления |
| ECS и EC | Entity-Component и Entity-Component-System - композиция вместо наследования |
| ООП | Наследование, полиморфизм и их проблемы |
| Событийная модель | Observer / pub-sub - как передавать информацию между системами |
| Конечные автоматы | Управление состояниями персонажей и сцен |
| Основной игровой цикл | Game loop, deltaTime, порядок обновления систем |
| Порядок обновления | Fixed timestep, swept collision, детерминизм физики |
| Сетевая архитектура | Client-server, prediction, reconciliation - мультиплеер |
| Типичные ошибки | God object, флаговый ад, логика в Draw и другие антипаттерны |
| Следующий шаг: алгоритмы | Алгоритмы |