SlideShare a Scribd company logo
1 of 31
Транзакционный фреймворк для
сингловых игр и игр с
асинхронным мультиплеером
Антон Рассадин
Pushkin Studio / MY.GAMES
Зачем фреймворк?
Чтобы выделить общие для нескольких проектов
части кода в отдельные модули или плагины и
переиспользовать.
Задачи, решаемые фреймворком
● Поддержка асинхронного мультиплеера
● Игра с нескольких устройств под одним аккаунтом
● Минимальное использование сетевого трафика
● Защита от читеров
● Единая кодовая база для клиента и сервера
● Детерминированные расчёты
● Масштабируемость серверной инфраструктуры
Модель данных — основа любой игры
● Хранит данные мира и игрока
● Описывает сущности, логику и правила игры
● Правильно применённая датамодель уменьшает связность
приложения
PsData
Плагин датамодели для Unreal Engine
https://github.com/PushkinStudio/PsData
● Const-correctness
● События
● Валидация данных
● Рефлексия и кодогенерация
Использование PsData
● Унаследовать тип данных от UPsData
● Объявить поля и метаданные полей с помощью макросов
UCLASS()
class UUserData : public UPsData
{
GENERATED_BODY()
DPROP(int32, LastTransactionTime);
DMETA(Event)
DPROP(FString, Uid);
DMETA(Strict)
DPROP(UMoneyData*, Money);
}
Использование PsData
Обращение к объекту:
/** ... */
{
UserData->SetLastTransactionTime(Timestamp);
const auto Money = UserData->GetMoney();
Money->SetGold(100);
Money->SetCredits(10);
}
Использование PsData
Создание объекта для неконстантного доступа:
class FUserDataAccessor
{
private:
friend UMyDataController;
static UUserData* GetMutableUserData(URootData* RootData)
{
return RootData->GetMutableUserData();
}
};
Использование 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);
//...
}
Клиенты и сервер
Общий вид системы (клиент)
Контекст транзакции
struct FTransactionContext
{
UPROPERTY()
FString Type;
UPROPERTY()
FString Id;
UPROPERTY()
int32 Amount;
/** More transaction data */
};
Транзакция
struct ITransaction
{
/** Initialize transaction object from context */
virtual void Init(const FTransactionContext& Context) = 0;
/** Perform execution check */
virtual FTransactionCheckResult CheckCanApply(const URootData*
RootData) const = 0;
/** Apply transaction to data model */
virtual bool Apply(URootData* RootData, UUserInfoData* UserInfoData)
const = 0;
};
Конкретная транзакция
struct FTransactionSellItem final : public ITransaction
{
static FTransactionContext CreateContext(int32 CurrentTime, const FString&
ItemId);
void Init(const FTransactionContext& InContext) override;
FTransactionCheckResult CheckCanApply(const URootData* RootData) const
override;
bool Apply(URootData* RootData, UUserInfoData* UserInfoData) const override;
protected:
/** Item to sell */
FString ItemId;
};
Проверка возможности применить
FTransactionCheckResult FTransactionBuyClickerUpgrade::CheckCanApply(const URootData* RootData) const
{
const auto UserInfo = RootData->GetUserInfo();
const auto ClickerData = UserInfo->GetClickerData();
if (ClickerData->GetLastSimulationTime() < Timestamp)
{
return FTransactionCheckResult(FString::Printf(TEXT("%s: advance simulation to current time. (%d
< %d)"), *_FUNC_LINE, ClickerData->GetLastSimulationTime(), Timestamp));
}
const auto ClickerUpgradeInfo = UDataLibrary::GetClickerUpgradeInfo(RootData, ClickerUpgradeType);
const auto NextUpgradeData = ClickerUpgradeInfo.NextClickerUpgradeData;
if (!NextUpgradeData.IsValid())
{
return FTransactionCheckResult(FString::Printf(TEXT("%s: can't upgrade "%s" further, max
level"), *_FUNC_LINE, *(Utils::EnumToString(ClickerUpgradeType))));
}
return true;
}
Применение к датамодели
bool FTransactionBuyClickerUpgrade::Apply(URootData* RootData, UUserInfoData*
UserInfoData) const
{
const auto ClickerUpgradeInfo = UDataLibrary::GetClickerUpgradeInfo(RootData,
ClickerUpgradeType);
const auto NextUpgradeData = ClickerUpgradeInfo.NextClickerUpgradeData.Get();
const auto ClickerManager = FClickerManager::Create(UserInfoData, RootData);
ClickerManager->InstallClickerUpgrade(NextUpgradeData, Timestamp);
ClickerManager->SpendClickerUpgradeParts(NextUpgradeData->GetPrice(), Timestamp);
const auto QuestManager = FQuestManager::Create(UserInfoData, RootData);
QuestManager->HandleRetroactiveQuestEvent(EQuestType::ClickerUpgrade, Timestamp);
return true;
}
Журнал
struct FTransactionJournalEntry
{
GENERATED_BODY()
UPROPERTY()
FGuid Id;
UPROPERTY()
FTransactionContext Context;
};
struct FTransactionJournal
{
/** Array of applied transactions */
UPROPERTY()
TArray<FTransactionJournalEntry> Transactions;
/** Input events produced by player during async transactions */
UPROPERTY()
TArray<FPlayerInputEvent> PlayerInputEvents;
}
Контроллер журнала
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{});
};
Исполнитель транзакций
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);
};
Менеджер транзакций
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);
};
Синхронизация журнала
PsWebServer
https://github.com/PushkinStudio/PsWebServer
● Плагин на основе библиотеки CivetWeb
● Журнал сериализуется FJsonObjectConverter
Инициализация сервера
void UServer::Start(const FString& InVersion, bool bInVerbose)
{
WebServer = UPsWebServerLibrary::ConstructWebServer(this);
WebServer->StartServer();
// Register handlers
AddHandler<UServerPingHandler>(TEXT("/api/ping"));
AddHandler<UServerShutdownHandler>(TEXT("/api/shutdown"));
AddHandler<UServerJournalHandler>(TEXT("/api/journal"));
}
template <class T>
void UServer::AddHandler(const FString& Endpoint)
{
static_assert(std::is_base_of<UServerHandler, T>::value, "T must be a child of UServerHandler");
const auto ExactEndpoint = Endpoint + TEXT("$");
auto Handler = NewObject<T>(this);
Handler->SetServer(this);
WebServer->AddHandler(Handler, ExactEndpoint);
}
Серверный GameMode
void AMyGameModeServer::BeginPlay()
{
InitDataController();
InitTransactionManager(GetDataController()->GetRootData());
GetTransactionManager()->DisableJournal();
/** ... */
const auto OnCompleted = FBattleLevelPreloadedDelegate::CreateUObject(this,
&AGameModeServer::StartServer);
GetBattleController()->PreloadBattleLevels(OnCompleted);
}
void AMyGameModeServer::StartServer()
{
const auto Version = FString::Printf(TEXT("Server/%s"), *UMyLibrary::GetGameInstance(this)-
>GetGameVersion());
const bool bVerbose = UMyLibrary::GetBoolCommandLineParam("Verbose");
Server = NewObject<UServer>(this);
Server->Start(Version, bVerbose);
}
Контроль ошибок на сервере
● Сервер владеет доверенной версией состояния игрока
● Применяя транзакции из журнала, сервер выполняет
CheckCanExecute на каждой
● После применения журнала к состоянию игрока вычисляется хеш
от него
● Хеши на клиенте и сервере должны совпадать
● В случае ошибки CheckCanExecute или несовпадении хешей
возвращается ошибка, после этого клиент забирает последнее
верное состояние с сервера
Асинхронные транзакции
● Применяются, если на результат выполнения транзакции могут
повлиять события извне (пользовательский ввод)
● Процесс применения разделён на Begin и Commit
● Begin возвращает Promise, который получает результат
выполнения транзакции и затем вызывает метод завершения
Commit
● Между Begin и Commit события ввода добавляются в
PlayerInputEvents в журнале
Асинхронные транзакции
struct ITransactionAsync : ITransaction
{
ITransactionAsync()
: Promise(MakeShared<FTransactionPromise>())
{
}
/** Asynchronously apply the transaction to data model */
virtual FTransactionPromiseRef OnBeginPromise(URootData* RootData, UUserInfoData* UserInfoData)
{
OnBegin(RootData, UserInfoData);
return Promise;
}
virtual bool OnCommit(URootData* RootData, UUserInfoData* UserInfoData) const = 0;
virtual bool OnRestore(URootData* RootData, UUserInfoData* UserInfoData) const = 0;
virtual void OnFail(URootData* RootData, UUserInfoData* UserInfoData) const = 0;
protected:
virtual void OnBegin(URootData* RootData, UUserInfoData* UserInfoData) = 0;
FTransactionPromiseRef Promise;
};
Асинхронные транзакции
class FTransactionPromise
{
public:
void Then(const FResolveDelegate& InOnResolve, const FRejectDelegate&
InOnReject);
/** Set promise result */
void SetResult(bool bSuccess);
};
Планы по улучшению системы
● Избавиться от отдельного Context, перейдя на сериализацию
объекта транзакции
● Шаблонизировать создание объектов транзакции
● Выделить систему в плагин
Спасибо за внимание!
Антон Рассадин
anton@rassadin.net
Telegram: @antonrr
СПАСИБО!
Антон Рассадин
anton@rassadin.net
Telegram: @antonrr
https://rassadin.net

More Related Content

What's hot

Михаил Давыдов — JavaScript: Асинхронность
Михаил Давыдов — JavaScript: АсинхронностьМихаил Давыдов — JavaScript: Асинхронность
Михаил Давыдов — JavaScript: Асинхронность
Yandex
 
Автоматизация тестирования клиентской производительности / Николай Лавлинский...
Автоматизация тестирования клиентской производительности / Николай Лавлинский...Автоматизация тестирования клиентской производительности / Николай Лавлинский...
Автоматизация тестирования клиентской производительности / Николай Лавлинский...
Ontico
 
FPUG Dzyga presentation
FPUG Dzyga presentationFPUG Dzyga presentation
FPUG Dzyga presentation
Ivan Filimonov
 
Использование юнит-тестов для повышения качества разработки
Использование юнит-тестов для повышения качества разработкиИспользование юнит-тестов для повышения качества разработки
Использование юнит-тестов для повышения качества разработки
victor-yastrebov
 
Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...
Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...
Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...
Ontico
 
Практика применения Pinba в Badoo / Денис Карасик (Badoo)
Практика применения Pinba в Badoo / Денис Карасик (Badoo)Практика применения Pinba в Badoo / Денис Карасик (Badoo)
Практика применения Pinba в Badoo / Денис Карасик (Badoo)
Ontico
 

What's hot (20)

Михаил Давыдов - JavaScript. Асинхронность
Михаил Давыдов - JavaScript. АсинхронностьМихаил Давыдов - JavaScript. Асинхронность
Михаил Давыдов - JavaScript. Асинхронность
 
Windows Azure and node js
Windows Azure and node jsWindows Azure and node js
Windows Azure and node js
 
Михаил Давыдов — JavaScript: Асинхронность
Михаил Давыдов — JavaScript: АсинхронностьМихаил Давыдов — JavaScript: Асинхронность
Михаил Давыдов — JavaScript: Асинхронность
 
Mobile Fest#spb 2012
Mobile Fest#spb 2012Mobile Fest#spb 2012
Mobile Fest#spb 2012
 
Многопоточность, работа с сетью (Lecture 12 – multithreading, network)
Многопоточность, работа с сетью (Lecture 12 – multithreading, network)Многопоточность, работа с сетью (Lecture 12 – multithreading, network)
Многопоточность, работа с сетью (Lecture 12 – multithreading, network)
 
"Успеть за 100 миллисекунд: контекстная реклама на Sphinx" Дмитрий Хасанов (...
"Успеть за 100 миллисекунд: контекстная реклама на Sphinx" Дмитрий Хасанов  (..."Успеть за 100 миллисекунд: контекстная реклама на Sphinx" Дмитрий Хасанов  (...
"Успеть за 100 миллисекунд: контекстная реклама на Sphinx" Дмитрий Хасанов (...
 
Автоматизация тестирования клиентской производительности / Николай Лавлинский...
Автоматизация тестирования клиентской производительности / Николай Лавлинский...Автоматизация тестирования клиентской производительности / Николай Лавлинский...
Автоматизация тестирования клиентской производительности / Николай Лавлинский...
 
Библиотеки для передачи данных (Lecture 13 – multithreading, network (libs))
Библиотеки для передачи данных (Lecture 13 – multithreading, network (libs))Библиотеки для передачи данных (Lecture 13 – multithreading, network (libs))
Библиотеки для передачи данных (Lecture 13 – multithreading, network (libs))
 
Android - 11 - Multithreading
Android - 11 - MultithreadingAndroid - 11 - Multithreading
Android - 11 - Multithreading
 
FPUG Dzyga presentation
FPUG Dzyga presentationFPUG Dzyga presentation
FPUG Dzyga presentation
 
Wordpress Cron
Wordpress CronWordpress Cron
Wordpress Cron
 
Hacking PostgreSQL. Локальная память процессов. Контексты памяти.
Hacking PostgreSQL. Локальная память процессов. Контексты памяти.Hacking PostgreSQL. Локальная память процессов. Контексты памяти.
Hacking PostgreSQL. Локальная память процессов. Контексты памяти.
 
Call of Postgres: Advanced Operations (part 3)
Call of Postgres: Advanced Operations (part 3)Call of Postgres: Advanced Operations (part 3)
Call of Postgres: Advanced Operations (part 3)
 
Сергей Перескоков "JS API Яндекс.Карт 2.0: что нового и как это работает"
Сергей Перескоков "JS API Яндекс.Карт 2.0: что нового и как это работает"Сергей Перескоков "JS API Яндекс.Карт 2.0: что нового и как это работает"
Сергей Перескоков "JS API Яндекс.Карт 2.0: что нового и как это работает"
 
Использование юнит-тестов для повышения качества разработки
Использование юнит-тестов для повышения качества разработкиИспользование юнит-тестов для повышения качества разработки
Использование юнит-тестов для повышения качества разработки
 
Киллер-фича языка C# — конструкция async/await
Киллер-фича языка C# — конструкция async/awaitКиллер-фича языка C# — конструкция async/await
Киллер-фича языка C# — конструкция async/await
 
"Пиринговый веб на JavaScript"
"Пиринговый веб на JavaScript""Пиринговый веб на JavaScript"
"Пиринговый веб на JavaScript"
 
Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...
Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...
Девять кругов ада или PostgreSQL Vacuum / Алексей Лесовский (PostgreSQL-Consu...
 
Примеры быстрой разработки API на масштабируемом сервере приложений Impress д...
Примеры быстрой разработки API на масштабируемом сервере приложений Impress д...Примеры быстрой разработки API на масштабируемом сервере приложений Impress д...
Примеры быстрой разработки API на масштабируемом сервере приложений Impress д...
 
Практика применения Pinba в Badoo / Денис Карасик (Badoo)
Практика применения Pinba в Badoo / Денис Карасик (Badoo)Практика применения Pinba в Badoo / Денис Карасик (Badoo)
Практика применения Pinba в Badoo / Денис Карасик (Badoo)
 

Similar to Транзакционный фреймворк для сингловых игр и игр с асинхронным мультиплеером / Антон Рассадин (Pushkin Studio / MY.GAMES)

Java осень 2013 лекция 3
Java осень 2013 лекция 3Java осень 2013 лекция 3
Java осень 2013 лекция 3
Technopark
 
SECON'2014 - Павел Щеваев - Метаданные и автогенерация кода
SECON'2014 - Павел Щеваев - Метаданные и автогенерация кодаSECON'2014 - Павел Щеваев - Метаданные и автогенерация кода
SECON'2014 - Павел Щеваев - Метаданные и автогенерация кода
Конференция разработчиков программного обеспечения SECON'2014
 
Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)
Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)
Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)
7bits
 
CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...
CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...
CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...
CodeFest
 

Similar to Транзакционный фреймворк для сингловых игр и игр с асинхронным мультиплеером / Антон Рассадин (Pushkin Studio / MY.GAMES) (20)

Где кончается react native? / Павел Кондратенко (Rambler&Co)
Где кончается react native? / Павел Кондратенко (Rambler&Co)Где кончается react native? / Павел Кондратенко (Rambler&Co)
Где кончается react native? / Павел Кондратенко (Rambler&Co)
 
Java осень 2013 лекция 3
Java осень 2013 лекция 3Java осень 2013 лекция 3
Java осень 2013 лекция 3
 
SECON'2014 - Павел Щеваев - Метаданные и автогенерация кода
SECON'2014 - Павел Щеваев - Метаданные и автогенерация кодаSECON'2014 - Павел Щеваев - Метаданные и автогенерация кода
SECON'2014 - Павел Щеваев - Метаданные и автогенерация кода
 
Взломать Web-сайт на ASP.NET? Сложно, но можно!
Взломать Web-сайт на ASP.NET? Сложно, но можно!Взломать Web-сайт на ASP.NET? Сложно, но можно!
Взломать Web-сайт на ASP.NET? Сложно, но можно!
 
Silverlight 5
Silverlight 5Silverlight 5
Silverlight 5
 
Async
AsyncAsync
Async
 
Luxoft async.net
Luxoft async.netLuxoft async.net
Luxoft async.net
 
Медиавозможности HTML5. WebRTC
Медиавозможности HTML5. WebRTCМедиавозможности HTML5. WebRTC
Медиавозможности HTML5. WebRTC
 
Aspect Oriented Approach
Aspect Oriented ApproachAspect Oriented Approach
Aspect Oriented Approach
 
The Old New ASP.NET
The Old New ASP.NETThe Old New ASP.NET
The Old New ASP.NET
 
Использование шаблонов и RTTI для конфигурации симулятора флеш-накопителя - Г...
Использование шаблонов и RTTI для конфигурации симулятора флеш-накопителя - Г...Использование шаблонов и RTTI для конфигурации симулятора флеш-накопителя - Г...
Использование шаблонов и RTTI для конфигурации симулятора флеш-накопителя - Г...
 
Вячеслав Смирнов - Инструменты нагрузочного тестирования
Вячеслав Смирнов - Инструменты нагрузочного тестированияВячеслав Смирнов - Инструменты нагрузочного тестирования
Вячеслав Смирнов - Инструменты нагрузочного тестирования
 
Scorex framework
Scorex frameworkScorex framework
Scorex framework
 
SSRS не для dba
SSRS не для dbaSSRS не для dba
SSRS не для dba
 
Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)
Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)
Стажировка-2014, занятие 8. Обзор Sails framework (Node.js)
 
Wild Async .NET world: AID Kit for boy-scouts
Wild Async .NET world: AID Kit for boy-scoutsWild Async .NET world: AID Kit for boy-scouts
Wild Async .NET world: AID Kit for boy-scouts
 
Изоморфные приложения и Python - Виталий Глибин, Huntflow
Изоморфные приложения и Python - Виталий Глибин, HuntflowИзоморфные приложения и Python - Виталий Глибин, Huntflow
Изоморфные приложения и Python - Виталий Глибин, Huntflow
 
CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...
CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...
CodeFest 2012. Сошников Д. — Разработка мобильных приложений на платформе Mic...
 
Lviv MDDay 2014. Андріан Буданцов “Внутрішній світ iOS додатків”
Lviv MDDay 2014. Андріан Буданцов “Внутрішній світ iOS додатків”Lviv MDDay 2014. Андріан Буданцов “Внутрішній світ iOS додатків”
Lviv MDDay 2014. Андріан Буданцов “Внутрішній світ iOS додатків”
 
Windows Phone 7 Game Development
Windows Phone 7 Game DevelopmentWindows Phone 7 Game Development
Windows Phone 7 Game Development
 

More from DevGAMM Conference

More from DevGAMM Conference (20)

The art of small steps, or how to make sound for games in conditions of war /...
The art of small steps, or how to make sound for games in conditions of war /...The art of small steps, or how to make sound for games in conditions of war /...
The art of small steps, or how to make sound for games in conditions of war /...
 
Breaking up with FMOD - Why we ended things and embraced Metasounds / Daniel ...
Breaking up with FMOD - Why we ended things and embraced Metasounds / Daniel ...Breaking up with FMOD - Why we ended things and embraced Metasounds / Daniel ...
Breaking up with FMOD - Why we ended things and embraced Metasounds / Daniel ...
 
How Audio Objects Improve Spatial Accuracy / Mads Maretty Sønderup (Audiokine...
How Audio Objects Improve Spatial Accuracy / Mads Maretty Sønderup (Audiokine...How Audio Objects Improve Spatial Accuracy / Mads Maretty Sønderup (Audiokine...
How Audio Objects Improve Spatial Accuracy / Mads Maretty Sønderup (Audiokine...
 
Why indie developers should consider hyper-casual right now / Igor Gurenyov (...
Why indie developers should consider hyper-casual right now / Igor Gurenyov (...Why indie developers should consider hyper-casual right now / Igor Gurenyov (...
Why indie developers should consider hyper-casual right now / Igor Gurenyov (...
 
AI / ML for Indies / Tyler Coleman (Retora Games)
AI / ML for Indies / Tyler Coleman (Retora Games)AI / ML for Indies / Tyler Coleman (Retora Games)
AI / ML for Indies / Tyler Coleman (Retora Games)
 
Agility is the Key: Power Up Your GameDev Project Management with Agile Pract...
Agility is the Key: Power Up Your GameDev Project Management with Agile Pract...Agility is the Key: Power Up Your GameDev Project Management with Agile Pract...
Agility is the Key: Power Up Your GameDev Project Management with Agile Pract...
 
New PR Tech and AI Tools for 2023: A Game Changer for Outreach / Kirill Perev...
New PR Tech and AI Tools for 2023: A Game Changer for Outreach / Kirill Perev...New PR Tech and AI Tools for 2023: A Game Changer for Outreach / Kirill Perev...
New PR Tech and AI Tools for 2023: A Game Changer for Outreach / Kirill Perev...
 
Playable Ads - Revolutionizing mobile games advertising / Jakub Kukuryk (Popc...
Playable Ads - Revolutionizing mobile games advertising / Jakub Kukuryk (Popc...Playable Ads - Revolutionizing mobile games advertising / Jakub Kukuryk (Popc...
Playable Ads - Revolutionizing mobile games advertising / Jakub Kukuryk (Popc...
 
Creative Collaboration: Managing an Art Team / Nastassia Radzivonava (Glera G...
Creative Collaboration: Managing an Art Team / Nastassia Radzivonava (Glera G...Creative Collaboration: Managing an Art Team / Nastassia Radzivonava (Glera G...
Creative Collaboration: Managing an Art Team / Nastassia Radzivonava (Glera G...
 
From Local to Global: Unleashing the Power of Payments / Jan Kuhlmannn (Xsolla)
From Local to Global: Unleashing the Power of Payments / Jan Kuhlmannn (Xsolla)From Local to Global: Unleashing the Power of Payments / Jan Kuhlmannn (Xsolla)
From Local to Global: Unleashing the Power of Payments / Jan Kuhlmannn (Xsolla)
 
Strategies and case studies to grow LTV in 2023 / Julia Iljuk (Balancy)
Strategies and case studies to grow LTV in 2023 / Julia Iljuk (Balancy)Strategies and case studies to grow LTV in 2023 / Julia Iljuk (Balancy)
Strategies and case studies to grow LTV in 2023 / Julia Iljuk (Balancy)
 
Why is ASO not working in 2023 and how to change it? / Olena Vedmedenko (Keya...
Why is ASO not working in 2023 and how to change it? / Olena Vedmedenko (Keya...Why is ASO not working in 2023 and how to change it? / Olena Vedmedenko (Keya...
Why is ASO not working in 2023 and how to change it? / Olena Vedmedenko (Keya...
 
How to increase wishlists & game sales from China? Growth marketing tactics &...
How to increase wishlists & game sales from China? Growth marketing tactics &...How to increase wishlists & game sales from China? Growth marketing tactics &...
How to increase wishlists & game sales from China? Growth marketing tactics &...
 
Turkish Gaming Industry and HR Insights / Mustafa Mert EFE (Zindhu)
Turkish Gaming Industry and HR Insights / Mustafa Mert EFE (Zindhu)Turkish Gaming Industry and HR Insights / Mustafa Mert EFE (Zindhu)
Turkish Gaming Industry and HR Insights / Mustafa Mert EFE (Zindhu)
 
Building an Awesome Creative Team from Scratch, Capable of Scaling Up / Sasha...
Building an Awesome Creative Team from Scratch, Capable of Scaling Up / Sasha...Building an Awesome Creative Team from Scratch, Capable of Scaling Up / Sasha...
Building an Awesome Creative Team from Scratch, Capable of Scaling Up / Sasha...
 
Seven Reasons Why Your LiveOps Is Not Performing / Alexander Devyaterikov (Be...
Seven Reasons Why Your LiveOps Is Not Performing / Alexander Devyaterikov (Be...Seven Reasons Why Your LiveOps Is Not Performing / Alexander Devyaterikov (Be...
Seven Reasons Why Your LiveOps Is Not Performing / Alexander Devyaterikov (Be...
 
The Power of Game and Music Collaborations: Reaching and Engaging the Masses ...
The Power of Game and Music Collaborations: Reaching and Engaging the Masses ...The Power of Game and Music Collaborations: Reaching and Engaging the Masses ...
The Power of Game and Music Collaborations: Reaching and Engaging the Masses ...
 
Branded Content: How to overcome players' immunity to advertising / Alex Brod...
Branded Content: How to overcome players' immunity to advertising / Alex Brod...Branded Content: How to overcome players' immunity to advertising / Alex Brod...
Branded Content: How to overcome players' immunity to advertising / Alex Brod...
 
Resurrecting Chasm: The Rift - A Source-less Remastering Journey / Gennadii P...
Resurrecting Chasm: The Rift - A Source-less Remastering Journey / Gennadii P...Resurrecting Chasm: The Rift - A Source-less Remastering Journey / Gennadii P...
Resurrecting Chasm: The Rift - A Source-less Remastering Journey / Gennadii P...
 
How NOT to do showcase events: Behind the scenes of Midnight Show / Andrew Ko...
How NOT to do showcase events: Behind the scenes of Midnight Show / Andrew Ko...How NOT to do showcase events: Behind the scenes of Midnight Show / Andrew Ko...
How NOT to do showcase events: Behind the scenes of Midnight Show / Andrew Ko...
 

Транзакционный фреймворк для сингловых игр и игр с асинхронным мультиплеером / Антон Рассадин (Pushkin Studio / MY.GAMES)

  • 1. Транзакционный фреймворк для сингловых игр и игр с асинхронным мультиплеером Антон Рассадин Pushkin Studio / MY.GAMES
  • 2. Зачем фреймворк? Чтобы выделить общие для нескольких проектов части кода в отдельные модули или плагины и переиспользовать.
  • 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); //... }
  • 12. Контекст транзакции struct FTransactionContext { UPROPERTY() FString Type; UPROPERTY() FString Id; UPROPERTY() int32 Amount; /** More transaction data */ };
  • 13. Транзакция struct ITransaction { /** Initialize transaction object from context */ virtual void Init(const FTransactionContext& Context) = 0; /** Perform execution check */ virtual FTransactionCheckResult CheckCanApply(const URootData* RootData) const = 0; /** Apply transaction to data model */ virtual bool Apply(URootData* RootData, UUserInfoData* UserInfoData) const = 0; };
  • 14. Конкретная транзакция struct FTransactionSellItem final : public ITransaction { static FTransactionContext CreateContext(int32 CurrentTime, const FString& ItemId); void Init(const FTransactionContext& InContext) override; FTransactionCheckResult CheckCanApply(const URootData* RootData) const override; bool Apply(URootData* RootData, UUserInfoData* UserInfoData) const override; protected: /** Item to sell */ FString ItemId; };
  • 15. Проверка возможности применить FTransactionCheckResult FTransactionBuyClickerUpgrade::CheckCanApply(const URootData* RootData) const { const auto UserInfo = RootData->GetUserInfo(); const auto ClickerData = UserInfo->GetClickerData(); if (ClickerData->GetLastSimulationTime() < Timestamp) { return FTransactionCheckResult(FString::Printf(TEXT("%s: advance simulation to current time. (%d < %d)"), *_FUNC_LINE, ClickerData->GetLastSimulationTime(), Timestamp)); } const auto ClickerUpgradeInfo = UDataLibrary::GetClickerUpgradeInfo(RootData, ClickerUpgradeType); const auto NextUpgradeData = ClickerUpgradeInfo.NextClickerUpgradeData; if (!NextUpgradeData.IsValid()) { return FTransactionCheckResult(FString::Printf(TEXT("%s: can't upgrade "%s" further, max level"), *_FUNC_LINE, *(Utils::EnumToString(ClickerUpgradeType)))); } return true; }
  • 16. Применение к датамодели bool FTransactionBuyClickerUpgrade::Apply(URootData* RootData, UUserInfoData* UserInfoData) const { const auto ClickerUpgradeInfo = UDataLibrary::GetClickerUpgradeInfo(RootData, ClickerUpgradeType); const auto NextUpgradeData = ClickerUpgradeInfo.NextClickerUpgradeData.Get(); const auto ClickerManager = FClickerManager::Create(UserInfoData, RootData); ClickerManager->InstallClickerUpgrade(NextUpgradeData, Timestamp); ClickerManager->SpendClickerUpgradeParts(NextUpgradeData->GetPrice(), Timestamp); const auto QuestManager = FQuestManager::Create(UserInfoData, RootData); QuestManager->HandleRetroactiveQuestEvent(EQuestType::ClickerUpgrade, Timestamp); return true; }
  • 17. Журнал struct FTransactionJournalEntry { GENERATED_BODY() UPROPERTY() FGuid Id; UPROPERTY() FTransactionContext Context; }; struct FTransactionJournal { /** Array of applied transactions */ UPROPERTY() TArray<FTransactionJournalEntry> Transactions; /** Input events produced by player during async transactions */ UPROPERTY() TArray<FPlayerInputEvent> PlayerInputEvents; }
  • 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); };
  • 21. Синхронизация журнала PsWebServer https://github.com/PushkinStudio/PsWebServer ● Плагин на основе библиотеки CivetWeb ● Журнал сериализуется FJsonObjectConverter
  • 22. Инициализация сервера void UServer::Start(const FString& InVersion, bool bInVerbose) { WebServer = UPsWebServerLibrary::ConstructWebServer(this); WebServer->StartServer(); // Register handlers AddHandler<UServerPingHandler>(TEXT("/api/ping")); AddHandler<UServerShutdownHandler>(TEXT("/api/shutdown")); AddHandler<UServerJournalHandler>(TEXT("/api/journal")); } template <class T> void UServer::AddHandler(const FString& Endpoint) { static_assert(std::is_base_of<UServerHandler, T>::value, "T must be a child of UServerHandler"); const auto ExactEndpoint = Endpoint + TEXT("$"); auto Handler = NewObject<T>(this); Handler->SetServer(this); WebServer->AddHandler(Handler, ExactEndpoint); }
  • 23. Серверный GameMode void AMyGameModeServer::BeginPlay() { InitDataController(); InitTransactionManager(GetDataController()->GetRootData()); GetTransactionManager()->DisableJournal(); /** ... */ const auto OnCompleted = FBattleLevelPreloadedDelegate::CreateUObject(this, &AGameModeServer::StartServer); GetBattleController()->PreloadBattleLevels(OnCompleted); } void AMyGameModeServer::StartServer() { const auto Version = FString::Printf(TEXT("Server/%s"), *UMyLibrary::GetGameInstance(this)- >GetGameVersion()); const bool bVerbose = UMyLibrary::GetBoolCommandLineParam("Verbose"); Server = NewObject<UServer>(this); Server->Start(Version, bVerbose); }
  • 24. Контроль ошибок на сервере ● Сервер владеет доверенной версией состояния игрока ● Применяя транзакции из журнала, сервер выполняет CheckCanExecute на каждой ● После применения журнала к состоянию игрока вычисляется хеш от него ● Хеши на клиенте и сервере должны совпадать ● В случае ошибки CheckCanExecute или несовпадении хешей возвращается ошибка, после этого клиент забирает последнее верное состояние с сервера
  • 25. Асинхронные транзакции ● Применяются, если на результат выполнения транзакции могут повлиять события извне (пользовательский ввод) ● Процесс применения разделён на Begin и Commit ● Begin возвращает Promise, который получает результат выполнения транзакции и затем вызывает метод завершения Commit ● Между Begin и Commit события ввода добавляются в PlayerInputEvents в журнале
  • 26. Асинхронные транзакции struct ITransactionAsync : ITransaction { ITransactionAsync() : Promise(MakeShared<FTransactionPromise>()) { } /** Asynchronously apply the transaction to data model */ virtual FTransactionPromiseRef OnBeginPromise(URootData* RootData, UUserInfoData* UserInfoData) { OnBegin(RootData, UserInfoData); return Promise; } virtual bool OnCommit(URootData* RootData, UUserInfoData* UserInfoData) const = 0; virtual bool OnRestore(URootData* RootData, UUserInfoData* UserInfoData) const = 0; virtual void OnFail(URootData* RootData, UUserInfoData* UserInfoData) const = 0; protected: virtual void OnBegin(URootData* RootData, UUserInfoData* UserInfoData) = 0; FTransactionPromiseRef Promise; };
  • 27. Асинхронные транзакции class FTransactionPromise { public: void Then(const FResolveDelegate& InOnResolve, const FRejectDelegate& InOnReject); /** Set promise result */ void SetResult(bool bSuccess); };
  • 28. Планы по улучшению системы ● Избавиться от отдельного Context, перейдя на сериализацию объекта транзакции ● Шаблонизировать создание объектов транзакции ● Выделить систему в плагин
  • 29. Спасибо за внимание! Антон Рассадин anton@rassadin.net Telegram: @antonrr