Как стать автором
Обновить

Комментарии 16

Статья большая и классная, но мне кажется, то что предлагает ТС это очевиднейшая вещь, тему которой не поднимали потому что она очевидная. Мы переиспользуем логику с незапамятных времен, в чем новость то?

Спасибо за ваш комментарий!
Я уверен, что не только мы используем этот подход, но здесь речь не просто о выделении кода в общую библиотеку, которую подключают и на клиенте, и на сервере.
Это скорее про целостную архитектуру с детерминированным поведением, проверкой хэша состояния и полной синхронизацией команд между клиентом и сервером.
Очевидно, что статья — это по сути описание одного из шаблонов проектирования, но найти подробную информацию по именно такому подходу мне было сложно.
Ближайшее, что я нашёл — статья про сетевую синхронизацию в Age of Empires, где тоже используется детерминированность и проверка через хэш состояния: https://www.gamedeveloper.com/programming/1500-archers-on-a-28-8-network-programming-in-age-of-empires-and-beyond
Также похожие описания есть здесь (хоть и про язык Haxe): https://community.haxe.org/t/sharing-logic-between-client-and-server/923/7, но там концепция только вскользь упоминается.
Думаю, одной из причин является отсутствие устоявшегося общепринятого названия для этого подхода.

Насколько я понял, вы тоже используете похожую архитектуру? Могли бы поделиться опытом? Какой технологический стек у вас? Unity и .NET backend?
Были ли у вас какие-то вызовы или проблемы, которые я не затронул в статье?

Если вещь "очевиднейшая", почему схожее решение успешно продается той же Metaplay? Причем некоторые студии покупали их решение даже по модели revenue share.

Вы точно читали статью?

Почитал про Metaplay — это очень близко к тому, что описано в статье.

https://docs.metaplay.io/introduction/introduction-to-metaplay.html#code-sharing-between-client-and-server

Думаю, для многих команд это может быть хорошим решением, но я стараюсь минимизировать использование облачных сервисов — если такой сервис закроется, можно оказаться в сложной ситуации.

А так спасибо за наводку! Думаю, будет здорово собрать в комментариях примеры использования подхода, описанного в статье.

Это был ответ, что решение не "очевиднейшее". К тому, что компании готовы делиться прибылью из-за того, что не могут/не знают как сделать это самим.

Mеtaplаy - убогое, неповоротливое решение (работаю в компании, где один из проектов на нем). И подсев на него в живом проекте, с него сложно уйти из-за оверинжиниринга и нестандартных решений внутри.

И для студии лучше вложиться в свой сервер. Эта статья - отличное начало для инхаус решения

Мы используем похожий подход в нескольких наших проектах.
Если стоит такая цель проекта, архитектура неплохая, но нужно отметить несколько аспектов:

- Разрабатываешь де факто оффлайн игру, а платишь за нее как за онлайн игру. Код все же несколько сложнее, чем, к примеру, обычная игра с облачными сохранениями. Аналогично платишь за сервера - им нужно выполнять весь код логики.


- Исходящие из этого проблемы - зависимость от интернета, проблемы с соединением. Толстый слой обвязок на каждой команде может быть тяжеленьким (да, да, сериализация-десериализация).


- Полный cheat proof - это преувеличение. Клиент все еще в руках врага. Многие операции (покупки, реклама, любые локальные действия, если они чисто клиентские, что практично) не безопасны. По сути пользователь не может сказать "сохрани мне 1000 деняк", но может прислать "выполни ка команду квест завершен 10000 раз." Конечно команда может быть защищена и проверять состояние игрока, но все дыры не перекроешь, все эдж кейсы не предусмотришь, даже двойные верификации могут оставлять лазейки.


- Никак не поможет если вам нужен реальный мультиплеер. Система синхронна только пока работает связка клиент-сервер. Клиент ведущий, но сервер все валидирует, система надежная, но к ней не подмешать других игроков. Соответственно если нужен мультиплеер - по сути ты делаешь отдельный классический мультиплеер с колбеками и задержками (и их коррекцией, предсказаниями и хаками). А потом, по результатам сессии, говоришь серверу "Я реально победил в этом матче, зуб даю" (поверь мне на слово). Конечно можно использовать всякие валидационные коды и криптозащиту, но все же.

Для многих проектов эта архитектура может быть оверинженерингом и неприятно удивить ваше начальство счетами за сервера. Знайте, с чем работаете.
В остальном модненькая такая.

Так же рекомендую тщательно продумать над хорошим API команд, чтонибудь в духе
logic.Do(new ApplySomeCommand(a, b, c)) и системой событий.
И писать поменьше бойлерплейта. За все архитектуры надо платить, мы натурально пишем по несколько классов на каждый чих (архитектура не моя, поэтому повлиять на API Я не мог)

Существует похожая архитектура синхронизированных детерменированных логических контейнеров. Обычно используется в стратегиях. У нее похожий принцип, но логика другая - клиенты независимо применяют компактные команды и апдейты на систему (карта, юниты и прочее). За счет того, что передаются только команды (действия игрока), такие системы могут пережевывать тысячи юнитов - клиенты отправляют друг другу только команды в духе SelectUnitsInAres(FromXY, ToXY) и SendSelectedUnits(ToXY), а юниты по факту обсчитываются локально - каждый клиент выделяет все 10000 юнитов в области и отправляет их в точку, а потом просто сравнивают хеши состояний - нет ли рассинхрона.

Код все же несколько сложнее, чем, к примеру, обычная игра с облачными сохранениями.

Да-да, я полностью согласен: за всё есть своя цена. Проще, как вы и говорите, использовать обычные облачные сохранения — и для многих проектов это нормальное решение. Я на самом деле это и упоминал, когда во введении говорил про "толстый" клиент.

зависимость от интернета, проблемы с соединением.

Зависимость от интернета тут есть, но какое-то время вы можете накапливать команды и отправлять их на сервер после появления соединения.

 Полный cheat proof - это преувеличение.

По безопасности операций: если мы говорим о покупках игровой валюты через платёжные системы, то мы делали так — команда начисления денег локально срабатывает без проверки чека и добавляет деньги в стейт, а на сервере в обработке этой команды есть дополнительный блок под условной компиляцией. Этот блок делает запрос в платёжную систему для валидации чека. Если валидация не проходит, то и денег игрок не получит.

Другие виды наград обычно игрок получает за участие в онлайн-бою с другими игроками. Тут можно использовать аналогичный подход: если у вас бой происходит на авторитарном сервере, можно сделать так, чтобы он в конце присылал клиенту его результаты с подписью. Это отправлялось бы в SharedLogic, на клиенте это бы сразу давало результат (награду), а на сервере дополнительный код проверял бы подпись результатов, отменяя команду, если результаты были модифицированы. Есть нюансы — например, нужна защита от повторной обработки той же самой команды. Для этого в результатах боя нужен серийный номер боя. В трёх словах сложно описать, но концепция должна быть понятна.

Как вы правильно заметили, очень важно, какой набор команд вы используете. Если есть команда выполнить квест Х — это дырка. Но если у нас команда купить предмет, и система квестов засчитывает, например, 10 покупок предметов как выполнение квеста, то тут накрутить не выйдет. Потому что для покупок нужны деньги, а их накрутка невозможна из-за того, что я описал выше — про результаты онлайн-боя и обработку инаппов.

И тут можно самому выбирать степень защиты в зависимости от ваших требований, популярности проекта, бюджетов и т. п.

Но я безусловно согласен: всегда могут оставаться лазейки. Часть из них вы можете оставить сами для упрощения системы, о части не узнаете, пока вас не начнут взламывать. Но это часто происходит даже в случае "тонких" клиентов с минимумом логики.

архитектура может быть оверинженерингом и неприятно удивить ваше начальство счетами за сервера

По счетам за сервера — даже в проектах с тысячами CCU нам всегда хватало с большим запасом одного выделенного сервера на проект. Если не использовать всякие облачные провайдеры вроде AWS/Amazon, то это примерно 100 долларов в месяц — так что это последнее, о чём бы я переживал. Но, конечно, в зависимости от сложности ваших команд, размера стейта игрока, частоты команд, результаты могут варьироваться в широких пределах.

Синхронизированные логические контейнеры, которые вы упоминаете, как раз, мне кажется, описаны в статье про Age of Empires, о которой я выше в комментариях уже писал: https://www.gamedeveloper.com/programming/1500-archers-on-a-28-8-network-programming-in-age-of-empires-and-beyond
Действительно, есть много концептуально схожих моментов у этой архитектуры с описанной в статье.

Рад слышать ещё одно подтверждение, что предложенная архитектура используется в проектах вашей студии. В этом и была цель статьи — показать тем, кто не знаком с таким подходом, что он существует и успешно применяется во многих компаниях. Я уверен, кому-то из наших коллег это может помочь.

Это скорее для охлаждения ожиданий - архитектура не идеальна, нужны дополнительные телодвижения.
У нас тоже есть валидация для покупок, но некоторые операции могут неприятно удивить.
Безопасность не появляется автоматически от использования этой системы. Скорее она дает широкие возможности для валидации логики.
У нас тоже есть батчинг команд и довольно длительный период работы без интернета, даже хеширование (теоретически команды можно применить на старте следующей сессии - и они будут валидны)
Кроме того, можно наткнуться на "легальные" варианты читов - некую последовательность легальных действий, которые нельзя провернуть в клиенте явно, но можно вызвать командами, и они будут адекватными с точки зрения сервера.
Тут, впрочем, нет никакой разницы с ботами и локальными хаками в онлайн играх.

Совершенно верно, тут нет безопасности "автоматически".
Степень валидации может варьировать от нуля до 100%, причем иногда она намеренно 0%.
Например, при сохранении настроек, которые влияют только на индивидуальный опыт игрока: громкость звука, качество графики, отображение никнеймов игроков и т.п. можно завести просто команду SetInt с полями string Key, int Value. Хранить эти значения в SortedDictionary в UserProfile. Так получится аналог PlayerPrefs, сохраняемый на сервере. Валидации никакой нет - положить туда можно что угодно, но и выгоды от того что ты локально у себя эти настройки поставишь вне предполагаемых значений никакой.
Но с игровой валютой так конечно обычно не стоит поступать )

Большое спасибо за проделанную работу! Контента по этой теме действительно немного, особенно написанного доступно и просто. Ради шутки даже вбил SharedLogic в поиск по Хабру — результат, как говорится, налицо.

Планировал и сам подобный ликбез оформить, но теперь уже просто буду ссылаться на этот материал в смежных работах.

Для улучшения производительности мы в командах ещё применяем батчинг команд, когда несколько команд отправляются на валидацию серверу одним пакетом. Т.к. всё локально и так просчитывается, клиент имеет возможность подождать чуть подольше, чтобы накопить команды. Но при такой реализации появляется больше сложностей с откатами в случае проблем.

p.s. "Вот результаты его выполнения на AMD Ryzen 9 5900X (12 cores) " дважды повторяется

Батчинг команд — это отличная оптимизация! В статье я не стал об этом писать и добавлять это в примеры кода, чтобы оставить всё настолько простым, насколько возможно.
В наших проектах мы тоже используем реализацию с батчингом, но, как правило, она отключена в конфиге — просто потому, что и так всё работает достаточно быстро, а батчинг немного снижает удобство отладки в случае, если есть расхождения в стейтах.

Ещё можно использовать вместо JSON бинарные форматы сериализации (Protobuf, MessagePack etc.) — это тоже может дать ускорение в 5–10 раз, но ценой небольшого усложнения кода и снижения удобства отладки.

json хорошо сжимается gzip-ом. А на сервере можно включить компрессию ответов. Возможно смысла нет от бинарной сериализации

Мы же говорим о снижении нагрузки на CPU сервера, а это его наоборот дополнительно нагрузит. В каких то сценариях это может быть полезно, но максимальный CCU скорее снизится от этого.

Говоря о дополнительной нагрузке, вы используете связку из батчинга команд на основе тикрейта/сердцебиения, вместе с отправкой чистых бинарных данных, мимо конвертации через json/bson? Вместо названия команды, можно единый id отправлять небольшого размера, в подавляющем большинстве хватит uint16 ключа. Если не однобайтового.

По идее, это очень сильно должно помочь с нагрузкой на CPU сервера и уменьшением траффика по сети. Игроки с мобильным интернетом это даже оценят, если речь о некоторых конкретных регионах

Спасибо, было интересно почитать)

Возможно, стоило кокретнее указать что такое решение предназначено только для онлайн синглплеерных игр где мы хотим постоянно что-то продавать типа гача-игр и ферм. Т.к. сравнение хэшей моментально разбивается об асинхронность действий игроков над одним и тем же профилем.

Всем кому было интересно как и мне, я бы посоветовал обратить внимание на SpacetimeDB. Очень красивая идея синхронизовывать стейты множества клиентов и сервера через синхронизацию баз данных нон-стопом. Т.е. клиент даже ничего не знает про сервер, а просто работает с локальной базой данных, обновляя и реагируя на обновления. Мне понравилось что в таком ключе клиент даже не узнает об откате состояния, т.к. для него это просто ещё одно изменение.

Я думаю, что утверждать, будто это решение предназначено только для одиночных игр — не совсем верно. Оно может быть использовано в том числе и для одиночных проектов, но мы как раз применяем его в многопользовательских играх, просто в комбинации с другими решениями, используемыми для непосредственного взаимодействия игроков в бою.

В целом, его удобно использовать для мета-игры: инаппы, гача, инвентарь и т.п. — я об этом пишу в разделах «Взаимодействие в реальном времени» и «Пошаговый режим с хранением состояния».

Спасибо за наводку на SpacetimeDB — выглядит интересно, нужно будет с ней поэкспериментировать.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации