В докладе Антон расскажет о транзакционном фреймворке, на котором построен новый проект студии Pushkin: батлер с асинхронным мультиплеером, синхронизацией состояния между устройствами, минимальными требованиями к сети и защитой от читеров.
3. Задачи, решаемые фреймворком
● Поддержка асинхронного мультиплеера
● Игра с нескольких устройств под одним аккаунтом
● Минимальное использование сетевого трафика
● Защита от читеров
● Единая кодовая база для клиента и сервера
● Детерминированные расчёты
● Масштабируемость серверной инфраструктуры
4. Модель данных — основа любой игры
● Хранит данные мира и игрока
● Описывает сущности, логику и правила игры
● Правильно применённая датамодель уменьшает связность
приложения
5. PsData
Плагин датамодели для Unreal Engine
https://github.com/PushkinStudio/PsData
● Const-correctness
● События
● Валидация данных
● Рефлексия и кодогенерация
6. Использование PsData
● Унаследовать тип данных от UPsData
● Объявить поля и метаданные полей с помощью макросов
UCLASS()
class UUserData : public UPsData
{
GENERATED_BODY()
DPROP(int32, LastTransactionTime);
DMETA(Event)
DPROP(FString, Uid);
DMETA(Strict)
DPROP(UMoneyData*, Money);
}
7. Использование PsData
Обращение к объекту:
/** ... */
{
UserData->SetLastTransactionTime(Timestamp);
const auto Money = UserData->GetMoney();
Money->SetGold(100);
Money->SetCredits(10);
}
8. Использование PsData
Создание объекта для неконстантного доступа:
class FUserDataAccessor
{
private:
friend UMyDataController;
static UUserData* GetMutableUserData(URootData* RootData)
{
return RootData->GetMutableUserData();
}
};
9. Использование PsData
Инициализация модели:
void UMyDataController::Init()
{
RootData = NewObject<UFooRootData>();
auto PrototypesData =
FFooPrototypesDataAccessor::GetMutablePrototypes(RootData);
// Deserialize from UDataTable asset
auto Deserializer = FPsDataTableDeserializer(MyDataTable, PrototypesData-
>GetCharacters());
PrototypesData->DataDeserialize(&Deserializer);
//...
}
18. Контроллер журнала
class UTransactionJournalController : public UObject
{
/** ... */
static TOptional<FTransactionJournal> DeserializeJournal(const TSharedRef<FJsonObject>& Object);
/** Serialize current transaction journal to json object */
static TSharedPtr<FJsonObject> SerializeJournal(const FTransactionJournal& Journal);
void SyncJournal(const FSyncDelegate& OnCompleted, bool bForce = false);
/** Mark the transaction as completed into the journal. May initialize journal synchronization */
FGuid CommitTransaction(const FTransactionWithContext& Transaction, const FCommitDelegate&
OnCompleted = FCommitDelegate{});
/** Mark transaction as failed into the journal. May initialize journal synchronization */
FGuid FailTransaction(const FTransactionWithContext& Transaction, const FString& ErrorMessage, const
FCommitDelegate& OnCompleted = FCommitDelegate{});
};
19. Исполнитель транзакций
class UTransactionExecutor : public UObject
{
/** Check if transaction can be applied */
FTransactionCheckResult CheckCanApply(const
FTransactionConstRef& Transaction) const;
/** Apply transaction to data model */
TOptional<FExecutorError> Execute(const FTransactionConstRef&
Transaction);
};
20. Менеджер транзакций
class UTransactionManager : public UActorComponent
{
/** Create a transaction and check whether it can be executed */
bool CheckCanExecute(const FTransactionContext& Context) const;
/** Create and apply transaction using given context and commit it into transaction journal */
void Execute(const FTransactionContext& Context, const FExecuteDelegate& OnComplete);
/** Create and apply all transactions from transaction journal */
void PlayJournal(const FTransactionJournal& Journal, const FPlayJournalDelegate& OnCompleted);
/** Create and apply all transactions from transaction journal */
void RestoreJournal(const FTransactionJournal& Journal, const FRestoreJournalDelegate& OnCompleted);
/** Synchronize transaction journal */
void SyncJournal(const FSyncJournalDelegate& OnCompleted);
};
24. Контроль ошибок на сервере
● Сервер владеет доверенной версией состояния игрока
● Применяя транзакции из журнала, сервер выполняет
CheckCanExecute на каждой
● После применения журнала к состоянию игрока вычисляется хеш
от него
● Хеши на клиенте и сервере должны совпадать
● В случае ошибки CheckCanExecute или несовпадении хешей
возвращается ошибка, после этого клиент забирает последнее
верное состояние с сервера
25. Асинхронные транзакции
● Применяются, если на результат выполнения транзакции могут
повлиять события извне (пользовательский ввод)
● Процесс применения разделён на Begin и Commit
● Begin возвращает Promise, который получает результат
выполнения транзакции и затем вызывает метод завершения
Commit
● Между Begin и Commit события ввода добавляются в
PlayerInputEvents в журнале
28. Планы по улучшению системы
● Избавиться от отдельного Context, перейдя на сериализацию
объекта транзакции
● Шаблонизировать создание объектов транзакции
● Выделить систему в плагин