Это третий пост из серии статей, описывающих мой процесс создания бессерверной многопользовательской игры — 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). Как всегда, рад ответить на любые вопросы, технические или связанные с игрой.

Ваше здоровье