Не так давно в официальной группе UE4 в vk я спрашивал, какие темы были бы интересны сообществу, чтобы о них рассказать :) Одним из популярных запросов стала работа с сетью на движке.


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


Так что, если вам интересно, как мы делали сеть для нашей Armored Warfare: Assault, добро пожаловать под кат.




Нельзя представить себе Unreal Engine в отрыве от Unreal Tournament, лейтмотивом проходящим через все версии движка. Как следствие, одной из сильных сторон UE4 является мощнейший сетевой инструментарий, интегрированный в движок на базисном уровне. По моей личной оценке, единственный движок, который столь же щепетильно подходил бы к вопросам на том же уровне — это движок Quake 3.


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


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


Как всё устроено


Для начала работы над мультиплеером в UE4 потребуется понимание трех доступных путей коммуникации:


  • Репликация переменных: от сервера на клиент, и не иначе. Клиент ни при каких условиях не может изменить переменную так, чтобы она поменяла свое значение на сервере.
  • RPC: от сервера на клиент.
  • RPC: от клиента на сервер.

Третий путь — единственный, чтобы сообщить какие либо данные с клиента на сервер. Репликация переменных (первый путь) используется для синхронизации состояний экторов между сервером и клиентами. Отправка RPC с сервера на клиент (второй путь) — событийная модель для отправки специфических данных.


Коротко, работает всё так. У каждого реплицируемого эктора есть параметр NetUpdateFrequency, который задаёт, сколько раз в секунду эктор будет проверять своё состояние на предмет «чем бы мне обменяться по сети». По умолчанию этот параметр равен безумным 100.f, что означает: если ваш эктор реплицируется, попытки синхронизации и отправки данных будут каждый тик.


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


Так мы приходим к самому первому правилу: выстави адекватные NetUpdateFrequency для всех реплицируемых экторов.


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


В некоем «усредненном» случае — аркады, MOBA, мобильные игрушки, а также «медленные шутеры» а-ля танки и прочие — частота сетевых обновлений может и должна быть гораздо ниже. У себя в AW: Assault мы используем частоту обновления состояния танка 10 раз в секунду. Я также знаю проекты, которые работают исходя из частоты сетевых обновлений главного персонажа 6-8 раз в секунду.


Прочие объекты — различные «точки захвата», «флаги», «патроны», «игровые состояния» — могут обновляться еще реже. Хороший пример: по умолчанию движковый класс PlayerState реплицируется всего раз в секунду, и это правильно. Если внезапно какое-то изменение в состоянии эктора должно быть доставлено максимально быстро, всегда есть возможность вызвать ForceNetUpdate().


Замечу, что компоненты эктора наследуют его частоту сетевых обновлений, поэтому сразу приходящее на ум «разделение» одного эктора на части с разной частотой обновлений — задача нетривиальная. Точнее так: если какой-то компонент требует иной частоты, чем способен жить эктор в целом, надо аккуратно вырезать его в отдельную сущность. Если же компонент может жить в сети «медленнее», чем его владелец, при этом владелец обновляется не каждый тик — это нормальная ситуация.


Правило второе: reliable RPC должны быть серьезно обоснованы. У нас в отделе ходит шутка, что каждая reliable rpc функция выдаётся «под роспись» руководителя. В каждой шутке доля шутки.


Надо держать в голове, что это дорого. Очень. Накладные расходы на RPC как таковую не так велики, сколько возможные последствия. Обращаться аккуратно, как со склянкой нитроглицерина. Особенно в случае multicast. Самое страшное, что может у вас случиться после того, как вы перестали «телепортироваться» — отключение клиента от сервера из-за переполнения такой штуки, как reliable buffer. Построенная на таких эвентах сетевая архитектура становится гиперчувствительна к пингу и потере пакетов.


Простой пример: вы хотите отправить с сервера на клиент уведомление о каком-то действии. Допустим, вы делаете симулятор алкоголика онлайн, и у вашего персонажа каждый тик регенирует печень, существующая как отдельный компонент. Всем клиентам очень надо знать каждый шаг «лечения», чтобы красиво показать это на экране (multicast, reliable). Каждый тик вы отсылаете RPC LiverHealed(float HealedHeath). Тестируете в редакторе на двух клиентах: красота, все довольны. И вот, живая ситуация: клиент залагал, потеря пакетов, надо отправить все накопившиеся за полсекунды RPC, и вы обнаруживаете, как больной радостно вылетает с сервера.


Очевидно, что как минимум не надо слать RPC каждый тик сервера, если NetUpdateFrequency в разы меньше — это просто застолбит очередь. Надо аккумулировать эти значения и отсылать реже. И еще раз подумать, а точно ли это reliable данные, и если так, нельзя ли обойтись репликацией переменной, а если не так — сделать unreliable. Во многих случаях также стоит подумать, не может ли клиент сам рассчитать, произошло ли событие, на основании имеющихся у него данных об игровом мире (то же самое лечение печени персонажа — это изменение её health'ов за клиентский тик).


Концептуальные ловушки


Или скорее пара вещей, о которых стоит сказать.


  • При занулении переменной — реплицируемого UObject'а, — OnRep не будет вызван и перестанет вызываться на клиенте в дальнейшем. Звучит как баг, и если честно, я не помню, почему оно работает именно так. Достаточно об этом знать и учитывать при разработке. Проверил на синтетическом тесте для UE 4.18: всё работает как надо, так что информация более не актуальна.
  • Вызов клиентской RPC на сервере не выполнит саму функцию на сервере, только отошлёт команду выполнить её на клиенте. Если необходимо выполнить её и на сервере, надо вручную вызвать Implementation.
  • Если эктор обновляется по сети редко (например, PlayerState), дважды с опаской применяйте там reliable RPC. За время между синхронизациями вы легко можете забить буфер, если не будете об этом думать. В идеале, такие специальные сетевые классы должны тикать и обновляться по сети с однопорядковой частотой.

Сетевые трики и приёмы


Направлены как на оптимизацию нагрузки на сеть, так и на оптимизацию нагрузки на CPU.


Глобальные стейты


Кстати, правило третье: если можешь обойтись без репликации экторов — сделай это.


Два примера из нашего AW: Assault, которые реализуют это правило, но по-разному:


  • Разрушаемые деревья. Они некритичны для геймплея, поэтому просто не реплицируются. Каждый клиент уронит своё дерево сам, на основании доступных у него данных. Совершенно не страшно, что реальная картина мира в этом плане разнится между сервером, клиентами и между клиентами.
  • Разрушаемые объекты окружения. Заборы, машины, заграждения. Они напрямую влияют на геймплей, за ними можно «прятаться», пока объект не разрушен. Поэтому требуется, чтобы эти объекты на всех клиентах отражали реальное положение дел. Для синхронизации используется этот самый глобальный стейт.

На втором примере всю концепцию можно описать так:


    /** Array of bit masks for minimization of space used for destructible actors states. Replication handled by OnRep_DestructableMasks method */
    UPROPERTY(ReplicatedUsing=OnRep_DestructableMasks)
    TArray<int32> DestructedActorsMasks;

    /** Handles replication of destructible actors masks */
    UFUNCTION()
    void OnRep_DestructableMasks();

Каждый int32 кодирует состояние целых 32-х объектов. На сцене у нас может быть более тысячи таких объектов. В случае кодирования до 1024 объектов можно было бы обойтись всего двумя int32 (перемножение битовых масок), но мы пока оставили репликацию массива, т.к. даже текущее решение работает, не нагружая сеть. На этапе загрузки карты передача по сети условных сорока интов — не слишком большие данные, а во время боя не так много объектов умирают одновременно. Заботу об оптимальной репликации массива при изменении некоторых его полей берет на себя движок.



Данный подход работает на основании того, что статические объекты имеют четкий порядок загрузки и на сервере, и на клиенте. Их можно закэшировать в массив и реплицировать только индексы измененных объектов. И пускай сейчас реализована лишь бинарная логика (жив/разрушен), такой подход будет оправдан, даже если появятся дополнительные поля (например, «здоровье» и «пробиваемость»), т.к. это однородные экторы, живущие в большом количестве на карте.


Хотите сделать полностью разрушаемый мир из стен? Работайте с ними через такие стейты. Несколько интов в виде битовых масок дадут возможность закодировать тысячи разрушаемых объектов. «Стандартный» же путь через репликацию каждого такого эктора отдельно легко убьет вам как сеть, так и CPU сервера (на проверку кого там надо реплицировать и кому, а кого — нет).


Упаковка данных


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


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


В итоге, нашим кор-программистом был написан прокси-класс для репликации таких состояний. Использование выглядит так:


    RepOwnerFlags
    .Add(&bEnableLockTarget)
    .Add(&bCanMove)
    .Add(&bIsDrowning)
    .Add(&bInWater)
    .Build();

    RepPublicFlags
    .Add(&bIsDying, this, "OnRep_IsDying")
    .Add(&bIsMoving, this, "OnRep_IsMoving")
    .Add(&bIsTurning, this, "OnRep_IsTurning")
    .Add(&bIsInFire)
    .Add(&bIsEngineBurning, this, "OnRep_IsEngineBurning")
    .Add(&bHasMinimapObservers)
    .Build();

RepOwnerFlags и RepPublicFlags — реплицируемые переменные класса, работающие как обёртка над uint64. Сами же переменные стали обычными, не реплицируемыми с точки зрения движка булями:


    /** Notifies of death */
    UFUNCTION()
    void OnRep_IsDying();

    /** Identifies if pawn is in its dying state */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Death)
    bool bIsDying;

Хорошей идеей будет также паковать bool'ы в uint8:1 внутри реплицируемых структур, если их несколько.


И да, аналогичное правило действует и для RPC функций. Отправляешь несколько булей — пакуй их. С флоатами в вектор и int8 (по-возможности) вместо int32 та же ситуация.


Жирные экторы


Легко представить ситуацию, когда стартовая репликация эктора занимает приличное время. Например, данные о броне танка в нашем случае (внутри танк состоит из большого количества кусочков, со своими параметрами, зависящими от прокачки).


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


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


bool AMyActor::ReplicateSubobjects(class UActorChannel **Channel, class FOutBunch **Bunch, FReplicationFlags **RepFlags)
{
    bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

    auto NetDriver = Channel->Connection->GetDriver();
    for (int32 i = 0; i < MyDataArray.Num(); i++)
    {
        // Check for saturation
        if (((Channel->Connection->QueuedBits) + Channel->Connection->SendBuffer.GetNumBits() + Bunch->GetNumBits()) >= 0)
        {
            return WroteSomething;
        }

        auto DataObject = MyDataArray[i];
        if (DataObject != nullptr)
        {
            WroteSomething |= Channel->ReplicateSubobject(DataObject, **Bunch, **RepFlags);
        }
    }

    return WroteSomething;
}

MyDataArray — это те самые данные. Такой подход позволяет избежать «зависания» всей сети при создании эктора по сети.


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


Условие, а не результат


С точки зрения оптимизации сети будет правильным реплицировать клиентам условие, порождающее вычисления, а не конечный результат таких вычислений. Например, информация о точке, куда «смотрит» персонаж, может породить все расчеты о вращении и прицеливании, которые можно просчитать и сгладить на клиенте.


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


Квантизация векторов


Очень редко требуется реплицировать вектор с полной точностью. Поэтому для репликации векторов стоит использовать специальные классы, оптимизированные под это дело: FVector_NetQuantize, FVector_NetQuantize10, FVector_NetQuantize100 и FVector_NetQuantizeNormal.


Из личной практики: точность выше FVector_NetQuantize100 не требовалось ни разу, в абсолютном большинстве для сети используются FVector_NetQuantize и FVector_NetQuantizeNormal.


(Не) использование сетевой релевантности


Если вы не знаете, зачем вам сетевая релевантность для игровых персонажей, лучше её отключите. Настройки «по умолчанию» в движке рассчитаны больше для квейкообразных шутеров (уши UT, о которых я уже говорил). Они не подходят для игр с открытым (визуально) миром, да и случай с «жирными» экторами тоже не очень приятен.


Сервер на основании NetCullDistanceSquared (расстояние в квадрате) между эктором и игроком решает, должен ли этот эктор существовать на конкретном клиенте. Если расстояние между ними больше, эктор будет удален с клиента, если меньше — заново создан на клиенте. Это операция проводится с таймаутом в 5 секунд (значение RelevantTimeout по умолчанию).


Ключевое здесь — заново создан. В случае жирного эктора данный процесс может как забить сеть, так и привести к фризу на время спавна объекта (если его ресурсы успели выгрузиться из памяти). В случае открытого мира это еще и появление посреди локации «из ниоткуда». Если ваши экторы постоянно бегают «на границе» (по-умолчанию она проходит в 150 метрах от вашего персонажа), этот процесс может быть неприятным.


В это же время правильно настроенная релевантность может существенно сэкономить вам траффик и нервы, если геймплей позволяет провести такую настройку. В случае нашего AWA сетевая релевантность не используется никак: в отличие от привычной многим схемы «танк виден на клиенте, только когда обнаружен», у нас вы можете увидеть танк противника через всю карту.


Конечно, управлять релевантностью вы можете не только на основании встроенного механизма, но и переопределив AActor::IsNetRelevantFor(...). Иногда это необходимо не столько из-за соображений оптимизации сети, сколько для защиты от читеров. На примере MOBA: персонажи, скрытые в тумане войны, не должны реплицироваться на клиент, чтобы избежать самых банальных мапхаков. Нет данных — нет конфеток.


Отладка сети


Плейтесты


Играйте. Тестируйте. Проверяйте. Не в редакторе и тепличных условиях, а в реальных — с собранным клиентом и развёрнутым в боевых условиях сервером. Локальный сервак на девелорской машине — плохая идея, особенно если потом вы планируете использовать виртуалки, где обычно ~4000 flops производительности.


И да, каким бы удобным ни было тестирование сети в редакторе, реальная сборка, особенно если речь идет о консолях или мобильных устройствах, это всё равно отдельная вселенная. Поведение клиента в неидеальных условиях будет иным.


Network Profiler


В комплекте с движком идёт прекрасная утилитка: NetworkProfiler


Буквально недавно вот так мы искали причину дисконнекта на одном конкретном танке:



Профайлер имеет свои странности и ограниченно работает на мобильных устройствах, но даёт понимание общей картины ваших сетевых данных.


Наиболее полезен он для выявления подозрительно «жирных» данных и сетевых спайков. Использовать обязательно.


Симуляция плохой сети


Плейтесты могут выявить только очевидные проблемы с сетью. К сожалению, настоящие проблемы начинаются тогда, когда ваш платящий пользователь сидит в далекой деревне, у него есть Wi-Fi-роутер и трехметровая антенна на доме, через которую идёт интернет на этот роутер, и вышка сотовой связи в трех километрах. С пакетами в такой конфигурации может случиться что угодно, при том, что пинг будет достаточно адекватный.


UE4 «из коробки» умеет симулировать различные условия сети, такие как задержку пинга, потерю пакетов или их неправильный порядок. Подробнее об этом написано здесь: Finding Network-based Exploits


В эпиковской статье приводится в пример конфигурирование сети через указание параметров в ini, для тестирования это не слишком удобно. Тем более, что для полноценного тестирования удобнее иметь несколько преcетов и переключаться между ними в runtime, без перезапуска редактора.


Делается это так: в папке UE4/Engine/Binaries/ вы создаете файл, например, network_bad.txt, такого содержания


Net PktLoss=1
Net PktOrder=0
Net PktDup=0
Net PktLag=120
Net PktLagVariance=0
p.netshowcorrections 1

Теперь вы можете прямо в консоли редактора вызвать exec network_bad.txt и применить описанные настройки. Как вы уже поняли, это просто набор консольных команд, «упакованных» в файл.


Мониторинг траффика


Поднимаете дедикейтед. Устраиваете на нём плейтест. Смотрите траффик на отдельно взятом порту. Оцениваете, сколько в среднем траффика на вход и на выход.


Этот пункт весьма очевиден, но почему-то многие им пренебрегают.


Послесловие


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


Наш результат на AW: Assault — возможность играть на 3G сети без каких-либо проблем и значимых лагов. Даже Edge (правда, при стабильном соединении) можно назвать достаточным. На мой взгляд, это весьма достойные цифры для мультиплеера на 16 игроков. Помимо толщины канала, мы также весьма не критичны к пингу, в отличие от многих других игр.


Если вам есть что дополнить, опровергнуть или обсудить — добро пожаловать в комментарии! :)

Комментарии (22)


  1. Flakky
    02.04.2018 18:07
    +1

    Огромное спасибо за статью, подчерпнул для себя пару вещиц, которые не знал)

    Единственное не очень понятно, ловушки касаются только С++ или БП?

    Вызов RPC на сервере не выполнит саму функцию на сервере, только отошлёт команду выполнить её на клиенте. Если необходимо выполнить её и на сервере, надо вручную вызвать Implementation.

    В БП Multicast RPC, например, вызывается и на сервере тоже, будучи вызванном с сервера.

    Кстати, ещё одна ловушка в БП и плюсах:
    OnRep функции от переменных вызываются на клиенте при получении репликации, а на сервере при простом изменении. Но если менять переменную не через Set, а другим способом (например инкремент или SetByRef), то OnRep на сервере не вызовется. В какой-то момент я просидел с этим долго.
    А в плюсах изменение переменной вообще не вызывает OnRep на сервере, будь то инкремент или нет. Нужно после изменения вручную вызывать OnRep.
    Зачастую, конечно, не нужен онреп на самом сервере, но, например, бывает полезно, когда нужно вызывать диспатчер всякий раз, когда меняется значение. При этом нужно сообщить всем, включая сервер.


    1. ufna Автор
      02.04.2018 18:13

      C++, все сетевые вещи строго переношу в код. На уровне прототипа БП нормально использовать для сети, но для продакшна такие вещи в них оставлять опасно.

      Кстати, ещё одна ловушка в БП и плюсах:

      Спасибо за дополнения! :) Еще из веселого — переопределенная в дочернем классе мультикаст функция в блюпринтах будет вызываться дважды, если дёрнуть Parent функцию. Описано здесь, если кому интересно.


  1. SVAY
    02.04.2018 23:26

    Большое спасибо за статью, очень полезная.

    Я еще не очень опытный, и первый раз слышу что-то про упаковку чего либо в int и битовые маски, мне это представляется как массив булеанов в виде числа, но как его разбирать? Особенно по объектам, или реализовывать битовые маски.

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


    1. An70ni
      03.04.2018 08:33

      я так думаю простыми битовыми операциями


    1. ufna Автор
      03.04.2018 11:13

      Как уже было сказано An70ni выше, это обычные битовые операции :) В гугле находятся кучей туториалов по запросу «c++ bit operations», например, вот: www.cprogramming.com/tutorial/bitwise_operators.html


  1. Merkat0r
    03.04.2018 01:30

    Главная проблема сетевого стека Unreal Engine — сетевой стек Unreal Engine. Отсюда и мамкины читеры, которые ваяют их на коленке за полчаса.
    Иногда бывает еще забавнее — к примеру, радар от одной игры работает в другой буквально без допиливания :)


    1. ufna Автор
      03.04.2018 08:33

      Это актуально для любого массового движка, только не имеет никакого отношения к сетевому стеку.

      Есть принципиально два разных направления хаков:
      1. Хак клиента, который вы описали. Мапхаки, воллхаки, эймхаки и иже с ними. Борьба с этим — постоянное противоборство щита и меча. Чем популярнее технология клиента или сама игра — тем больше высококлассных спецов его «разбирают». Читаки пишутся далеко не «мамкиными хакерами» (это потом уже тулзы общего назначения они стараются приладить, но серьёзные читаки это весьма бизнес). Защита — определять по косвенным признакам и банить. Сложно, дорого, не надежно.
      2. Хаки косяков сетевой архитектуры. Например, когда клиент решает откуда и куда он выстрелил, а сервер ему верит. Очевидно, это кривые ручки разработчика игры, давшего клиенту авторитарные права на такие вещи. Защита — бить по ушам разработчика. Архитектура, которая не верит клиенту ни в чем, кроме инпута, полностью устойчива к такому виду хаков.

      Если ваша игра популярна, с хаками из первого пункта все равно бороться самим ;)


      1. Merkat0r
        03.04.2018 10:41
        -4

        Оук… Вы таки уверены, что на самом деле знаете Unreal Engine, сетевой стек Unreal Engine и славу сетевого стека Unreal Engine? :)

        Потому, что сейчас мне показалось что вы обычный кодер, который зачем-то спорит о том о чем вообще даже не догадывается.

        радары и сетевой стек
        image


        1. ufna Автор
          03.04.2018 10:49
          +2

          Очень толсто :) Хочется увидеть ваш увлекательный рассказ на тему сетевого стека на анриале. Порадуйте меня и общественность срывом покровов.


          1. Merkat0r
            03.04.2018 11:13

            Что толсто? Я вот теперь уже абсолютно уверен, что как работает *меч* (и собственно каким должен быть щит) вы понятия не имеете. Собственно, кол-во читеров говорит за себя. Если из современных — PUBG тоже понадеялся на дефолтный стек *трольфейс*

            PS я уже больше 14 лет ковыряю UE с позиции меча, кудаж мне до великого гуру с хабра :)


            1. ufna Автор
              03.04.2018 11:43

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

              P.S. Про PUBG это даже не смешно. Первые версии и спидхаку были подвержены, ну и? Если вы его копали, вы прекрасно знаете какие вещи там архитектурно были косячны со старта, что для беты весьма ожидаемо.


              1. INCorpes
                03.04.2018 17:07

                Спидхак всё еще там ;), а еще и «телепортация» появилась, не знаю насколько успешно банят таких.


  1. DimPal
    03.04.2018 12:06

    В случае кодирования до 1024 объектов можно было бы обойтись всего двумя int32 (перемножение битовых масок)

    Ни кто не подскажет, как 64 битами закодировать 1024 объекта?


    1. Tutanhomon
      03.04.2018 12:10

      Матрица 32х32 :)
      Сам только полчаса назад узнал


    1. ufna Автор
      03.04.2018 12:16

      Кодируется индекс + маска на 32 объекта, т.е. данный метод предполагает не репликацию массива, а отправку RPC. В случае если мир упорядочен разумно блоками в мире (например, персонаж въезжает в кучу объектов, которые лежат в одной маске или «рядом»), потребление сети и CPU минимально.


  1. forrgit
    04.04.2018 13:19

    Так же хотелось бы добавить следующее:
    Если multicast функция помечена как unreliable и имеет входные аргументы, то на клиентах она не вызывается


    1. ufna Автор
      04.04.2018 13:21

      Нет, если у вас такое поведение unreliable функции, то это первый показатель что сеть у вас «забита». UFUNCTION(Client, Unreliable, NetMulticast) отлично доходят даже с большим числом параметров при нормальной загрузке сети (у нас немало вещей ходят на клиенты таким способом :) ).


  1. Andsteadur
    04.04.2018 18:51

    Не могли бы Вы на пальцах расписать каким образом все таки кодируется? Или дать наводку, что почитать по этому поводу? Крутил и так и этак битовые маски и все равно не получается закодировать несколько значений, чтобы потом получилось корректно восстановить


    1. ufna Автор
      04.04.2018 18:51

      Посмотрите www.cprogramming.com/tutorial/bitwise_operators.html, здесь всё очень подробно расписано. Особенно посмотрите пример про машины, в частности:

      int is_in_use(int car_num)
      {
          return in_use & 1<<car_num;
      }


      1. Andsteadur
        04.04.2018 19:15

        Прошу прощения — не к тому комментарию написал.

        Битовые операции это дело ясное. Закодировать в 32хбитной переменной 32 булевых значения проще простого. Я был озадачен тем, как имея 64 бита можно закодировать 1024 значения:

        В случае кодирования до 1024 объектов можно было бы обойтись всего двумя int32


        1. ufna Автор
          05.04.2018 12:52

          Речь идёт о кодировании объектов в RPC. Можно представить это как матрицу 32х32, первый инт — будет задавать к каким строкам применить состояние второго инта. Т.е. это некий аналог «индекс + состояние».


          1. Andsteadur
            05.04.2018 14:52

            Теперь ясно. А я воображал, что состояние 1024 объектов упаковывается какой-то магией в два int32