UE4 | Инвентарь для Multiplayer #5 | Передача информации между Сервером и Клиентом

Список статей

_mwkwwvinzy5kttn72wk519pddc.jpegВ этой статье мы рассмотрим передачу данных между Сервером и Клиентом в Unreal Engine 4 реализованную на C++. В самом начале, для человека не имеющего профильного образования, это кажется чем-то непостижимо сложным. Несмотря на большое количество примеров и разборов, лично мне было очень непросто сложить целостную картину этого процесса. Но, когда критический объем полученной при прочтении информации и сделанных тестов был достигнут, пришло понимание того, как же это все работает.


В первую очередь, нужно четко понять, что все объекты в игре (за редкими исключениями) могут иметь несколько копий. Оригинал объекта расположен на Сервере. Оригинал есть всегда. Копии же могут существовать на Клиентах, но совсем не обязательно, т.е. их может и не быть. Все значимые события происходят на Сервере, и именно он решает кому об этом знать нужно, а кому нет.

Все что происходит на Клиенте, никому кроме Клиента не ведомо.

Запускаем игру с двумя Клиентами. На сцене кубик.Оригинал этого кубика расположен на Сервере. Он самый важный. Первая копия будет находится на первом Клиенте, а вторая — на втором Клиенте. Если мы сделаем что-то с копией объекта на любом их клиентов, то оригинал не пострадает. Изменение будет сугубо локальным, только для этого Клиента. Если же изменить оригинал, то возможны 2 основных сценария:


  1. Копии клиентов останутся без изменений.
  2. Копии клиентов будут синхронизированы с оригиналом.

Второй значительный момент, который нужно помнить всегда:

Создавать (Spawn) можно только оригинал. Т.е. создать объект на Клиенте нельзя ни при каких условиях.


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


  1. Репликация (Replication).
  2. RPC (Remote Procedure Calls).
  3. TCP.

    Репликация

Первое, что стоит безоговорочно принять:

Репликация — это дорога в одну сторону, и работает только от Сервера к Клиенту.

Второе, что необходимо знать:

Реплицировать можно только объекты или переменные.

И третье, важное условие:

Репликация происходит только тогда, когда произошло изменение на Сервере.


Если в Blueprint мы просто ставим галочки в нужных местах, то С++ все не намного сложнее. Главное не забыть подключить #include «UnrealNetwork.h».

Сначала рассмотрим репликацию объектов.
В конструкторе прописываем:

bReplicates = true;

Если хотим реплицировать движение:

bReplicateMovement = true;

Если нужно реплицировать подключенный компонент:

Component->SetReplicates(true);

С полным описание можно ознакомиться тут.

С репликацией переменных все несколько интереснее.
Начнем с заголовочного файла .h.
Можно просто реплицировать переменную:

UPROPERTY(Replicated)
bool bMyReplicatedVariable;

А можно запустить какую-нибудь функцию на стороне Клиента, если переменная была реплицирована. Неважно, какое именно значения приняла переменная. Важен сам факт ее изменения.

UPROPERTY(ReplicatedUsing = OnRep_MySomeFunction)
TArray MyReplicatedArray;

UFUNCTION()
void OnRep_MySomeFunction();

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

Теперь перейдем к .cpp
Прописываем условия репликации:

void AMySuperActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(AMySuperActor, bMyReplicatedVariable);
    DOREPLIFETIME_CONDITION(AMySuperActor, MyReplicatedArray, COND_OwnerOnly);
}

Первая переменная bMyReplicatedVariable была реплицирована без всякого условия сразу на все Клиенты, тогда как вторая, MyReplicatedArray, была обновлена только для Клиента владельца объекта AMySuperActor, если, конечно, таковой был объявлен.

Полный список возможных условий можно найти тут.



RPC (Remote Procedure Calls)

Данный метод передачи данных, в отличие от репликации работает в обе стороны, но является более затратным. Для его использования точно так же нужно подключать #include «UnrealNetwork.h».

Важная особенность — методом RPC можно передавать переменные.

Сначала нужно сказать, что RPCs бывают с подтверждение получения посылки Reliable и без такового Unreliable. Если в первом случае Отправитель не успокоится, пока не убедится, что посылка доставлена (и будет отправлять ее снова и снова, если нет ответной весточки), то во втором, Отправителю абсолютно все равно, получил кто-то его посылку или нет. Отправил и забыл.
Там где можно — применяем Unreliable. Обычно этот метод подходит для не очень важной информации, либо для часто обновляющихся данных. Там где нельзя, в случае посылки от Сервера к Клиенту, стараемся обойтись репликацией с вызовом функции, как было показано выше.

Итак, для отправки нашей посылки от Клиента на Сервер необходимо прописать три функции:

UFUNCTION(Reliable, Server, WithValidation)
void ServerTestFunction(float MyVariable);

void ServerTestFunction_Implementation(float MyVariable);

bool ServerTestFunction_Validate(float MyVariable);

Reliable — посылка с подтверждением получения.
Server — посылка от Клиента к Серверу.
WithValidation — посылка открывается получателем только при соблюдении условий, описанных в функции bool ServerTestFunction_Validate (float MyVariable). То есть, если функция возвращает true.
ServerTestFunction (float MyVariable) — эту функцию вызывает клиент, если хочет, что-то отправить на сервер. В общем случае, даже не требуется описывать ее в .cpp.
ServerTestFunction_Implementation (float MyVariable) — эта функция будет вызвана непосредственно на Сервере, только если…
ServerTestFunction_Validate (float MyVariable) — данная функция выполняется на Сервере, и если возвращается true, то будет вызвана ServerTestFunction_Implementation (float MyVariable).

Для отправки посылки от Сервера на Клиент, если нас категорически не устраивает использование репликации, по сути меняется только Server на Client:

UFUNCTION(Reliable, Client, WithValidation)

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

UFUNCTION(Reliable, Client, WithValidation)
void ClientTestFunction(float MyVariable);

void ClientTestFunction_Implementation(float MyVariable);

bool ClientTestFunction_Validate(float MyVariable);

В остальном, работает точно так же, как и в предыдущем примере, с учетом того, что на этот раз посылка идет от Сервера к Клиенту.

Существует еще один вариант отправки с Сервера, когда посылка уйдет сразу всем клиентам.

UFUNCTION(Reliable, NetMulticast, WithValidation)
void NetMulticastTestFunction();

void NetMulticastTestFunction_Implementation();

bool NetMulticastTestFunction_Validate();

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



Пример реализации запроса на Сервер
/* Эта функция может выполняться как на Клиенте, так и на Сервере */
void ADreampaxActor::DoSomethingWithOtherActor(ADreampaxOtherActor  *  SomeOtherActor)
{
    /* выполняем проверку, если функция запущена на Клиенте */
    if (Role < ROLE_Authority)
    {
        /* отправляем команду на Сервер, с указателем на объект,
        над которым хотим совершить действие */
        ServerDoSomethingWithOtherActor(SomeOtherActor);
        /* прерываем работу функции, 
        если не хотим выполнять ее на Клиенте */
        return;
    }
    /* попадаем сюда только если функция запущена на сервере */
    SomeOtherActor->Destroy(true);
}

/* Эта функция запускается всегда  на Сервере,
если активирована функция ServerDoSomethingWithOtherActor(SomeOtherActor)
и условие проверки пройдено */
void ADreampaxCharacter::ServerDoSomethingWithOtherActor_Implementation(ADreampaxOtherActor  *  SomeOtherActor)
{
    /* производим запуск функции, но уже гарантированно на стороне сервера */
    DoSomethingWithOtherActor(SomeOtherActor);
}

/* проверка условия на стороне Сервера, можно ли запускать
ServerDoSomethingWithOtherActor_Implementation(ADreampaxOtherActor  *  SomeOtherActor) */
bool  ADreampaxCharacter::ServerDoSomethingWithOtherActor_Validate(ADreampaxOtherActor  *  SomeOtherActor)
{
    /* в данном случае всегда возвращаем true,
    но если необходимо, то можем что-то проверить
    и вернуть fasle. Тогда функция на сервере не запустится */
    return true;
}

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

P.S. Если заметите какие-то неточности или ошибки, пожалуйста пишите в комментариях.

© Habrahabr.ru