Это третий пост из серии статей, описывающих мой процесс создания бессерверной многопользовательской игры — Mafia.
В этой статье рассказывается, как я создал базовую систему лобби, расширив основу из моего предыдущего поста.
Лямбда-дизайн
Как я упоминал в своей первой статье об этом проекте, я решил сгруппировать код на основе объекта, если он влияет. Каждая функция Lambda в моем приложении выполняет определенную задачу, а группы Lambda называются контроллерами.
Все функции Lambda в контроллере используют одни и те же файлы и код, но имеют альтернативные точки входа.
Возможно, это плохой подход, однако пока я считаю его интуитивно понятным.
Чтобы поддерживать согласованный код API и структуру ответов, я решил использовать Powertools for AWS Lambda. Этот пакет также позволит мне легко реализовать отслеживание трафика Lambda, если я захочу сделать это в будущем.
Пример API
Ниже приведен пример фрагмента API для обработки действий в лобби. Обратите внимание, что хотя код содержит два маршрута API, процесс развертывания создаст выделенную лямбду для каждого из них.
# very basic api example # Controller that handles all Lobby actions from core.controllers import LobbyController # API route and target function @app.get('/lobby') def get_lobbies(): return LobbyController().get_lobbies() @app.post('/lobby/host') def host_lobby(): details = ... # from payload return LobbyController().host_lobby()
Определение LobbyController
API предоставляет пользователям следующие методы:
- HostLobby
- Присоединяйтесь к лобби
- GetLobby
- GetLobbies
- ВыйтиЛобби
- Стартовое лобби (если хост)
- TerminateLobby (для администратора)
Эти методы читают/записывают в таблицу DynamoDB и извлекают/обновляют/создают записи лобби по мере необходимости. Некоторые действия, такие как присоединение к лобби или выход из него, требуют информирования всех других пользователей в лобби. Для этого я решил использовать AWS IoT Core — подробнее об этом позже.
Это новая диаграмма инфраструктуры AWS после добавления LobbyController (планируется серый):
Схема сущности DynamoDB
Одна из проблем при работе с DynamoDB заключается в том, что все элементы возвращаются в формате JSON. Эта структура была управляемой в небольшом масштабе, но я обнаружил, что она нежизнеспособна по мере роста сложности проекта.
Подводя итог моей борьбе простыми словами; при чтении элемента из DynamoDB не было возможности узнать, без каких полей содержался элемент:
- A — при условии, что поле существует и обработка ошибок
- B — запоминание всех полей для всех элементов
- C — просмотр базы данных в качестве напоминания
Чтобы уменьшить умственное напряжение, вызванное вышеизложенным, я решил объявить все элементы DynamoDB как классы данных — аналогично типичной схеме реляционной базы данных.
С этой целью я остановился на базовой Entity:
@dataclass class Entity: id: str createdAt: int def to_dict(self, exclude_none=True) -> dict: # exclude_none=True removes all keys from the output dict where the value is None if exclude_none: return {k: v for k, v in asdict(self).items() if v is not None} else: return asdict(self)
Все последующие элементы наследуются от базовой сущности. Ниже показано, как выглядит объект User
:
@dataclass class User(Entity): displayName: str lobby: Optional[str] = None game: Optional[str] = None isAdmin: Optional[bool] = None type: str = "USER" @classmethod def new(cls, user_id: str, name: str) -> User: return cls(id=user_id, displayName=name, createdAt=timestamp()) @classmethod def from_primary(cls, user: dict) -> User: return cls(**id(user)) @classmethod def from_gsi1(cls, user: dict) -> User: pass def to_ddb(self) -> dict: item = self.to_dict(exclude_none=True) item['PK'] = item.pop('id') item['SK'] = 'A' return item
Я также решил использовать @classmethod
, как показано в этом фрагменте кода. Декоратор @classmethod
позволяет использовать альтернативные конструкторы классов, которые я использую в зависимости от источника данных. Например, функция from_primary()
указывает, как должна создаваться сущность User
при чтении из первичного индекса таблицы Dynamo.
У меня есть сущности для Lobby
и LobbyUser
, созданные в похожем стиле.
Всплывающее уведомление
Я использую AWS IoT Core для всех своих push-уведомлений по следующим причинам:
- Простота
- Масштабируемость
- Скорость
- специи? (на самом деле я точно не знаю об этом)
Простота
IoT Core — это сервис WebSocket, который по умолчанию предоставляется с каждым аккаунтом AWS. Он использует протокол MQTT, который следует структуре pub/sub. Клиенты подписываются на «тему» и получают все сообщения, опубликованные в этой теме.
Масштабируемость
IoT Core был разработан для поддержки миллионов устройств, и, поскольку это полностью управляемый сервис, все масштабирование выполняется AWS. Потребитель (я 😭) просто платит в зависимости от объема использования.
Скорость и цена
Здесь я сделал некоторые предположения.
Решением, отличным от IoT Core, будет использование функции Lambda, настроенной как коннектор WebSocket. Когда пользователи подключаются, Lambda будет сохранять их connectionId
в таблице Dynamo, а когда сообщения должны быть отправлены пользователю, их connectionId
можно получить.
Недостатки этого подхода, и почему я в конечном итоге выбрал IoT Core, заключаются в том, что для каждого сообщения, отправляемого пользователем, все цели должны быть прочитаны из БД. Я чувствовал, что это будет слишком медленно и, скорее всего, очень дорого.
Я считаю, что IoT Core будет быстрее и дешевле, чем базовый вариант Lambda WebSocket, но время покажет.
Внешний интерфейс
Функциональность лобби на внешнем интерфейсе представляет собой сочетание получения данных API и обновлений IoT.
Получение, размещение, присоединение и выход из лобби — все это действия, которые могут выполняться независимо от других клиентов, поэтому они выполняются через HTTP API.
Вот короткое видео на YouTube, демонстрирующее систему лобби с точки зрения двух пользователей, включая переход от лобби к игре (подробнее об играх в следующем посте).
Это все для этого обновления. Я добился гораздо большего прогресса, но не совсем готов упаковать его в рецензию.
Если вы заинтересованы в проекте и хотите получать более частые обновления, загляните на дискорд-сервер (https://discord.gg/EcGx9h2fwy). Как всегда, рад ответить на любые вопросы, технические или связанные с игрой.
Ваше здоровье