Новости/News | | Туториалы/Tutorials | | Форум/Forum | | ЧаВо/FAQ | | Патчи/Patches | | Файлы/Downloads | | Ссылки/Links | | Авторы/About |

Replication De-Obfuscation

Содержание

Введение

Спросите любого более или менее знающего UnrealScript кодера какая самая сложная часть языка, и он без колебаний ответит: репликация (или, возможно, Передвижение игрока (PlayerMovement) или Искусственный интеллект (AI) :). Я сам был таким, и все еще иногда я встречаюсь с некоторыми проблемами, но я думаю, что я достаточно знаю, чтобы написать документацию по этому вопросу. Множество попыток и собственных ошибок помогли мне добраться в самые недра репликации, не без помощи людей, имеющих доступ к исходникам движка. Я попытался собрать все в одном документе. Тем не менее, я периодически получаю все больше и больше информации, которая помогает мне глубже понять репликацию. Так как я не имею достаточно времени на личные разъяснения, я попытаюсь как можно подробнее познакомить Вас с этим вопросом в данном "сетевом" документе. Я надеюсь, что у меня получится рассмотреть понятие репликации глубже, нежели в документации, доступной на сайте EpicGames: http://unreal.epicgames.com/Network.htm и http://unreal.epicgames.com/UTMods.html. Настоятельно рекомендую прочитать их сперва - это поможет Вам легче воспринять этот туториал. В процессе написания туториала, я полагал, что Вы уже имеете опыт программирования на UnrealScript, Вы пытались разобрать репликацию собственными силами, Вы достаточно умны, Вы умеете читать, и Вы думаете, что мои шутки очень остроумные.

Для начала, необходимо уяснить, что Unreal Tournament использует клиент-серверную архитектуру. Клиент является всего на всего беспомощным терминалом, который посылает серверу данные о передвижении игрока, прыгает ли он, стреляет ли он и пр. Также он отображает пользователю то, что передал ему сервер. Информацию клиента необходимо постоянно сопоставлять с информацией, существующей на сервере, из-за возможных лагов, времени пинга и других факторов. Поэтому, для того, чтобы клиент имел более или менее реальное представление мира, он обладает логикой симулирования того, что предположительно должен переслать ему сервер. Клиент знает законы физики, и если что-то передвигается по прямой линии, значит, это что-то и будет дальше передвигаться с такой же скоростью. Также, если что-то падает с высоты, то скорее всего оно будет и дальше падать с ускорением под действием сил гравитации. Этот беспомощный терминал предполагает, как будет развиваться дальнейший игровой процесс, и, таким образам, может предоставить Вам более или менее реальное положение вещей. На другом конце соединения находится сервер, который один обладает достоверной информацией о том, что происходит в игре. Если сервер думает, что Вас убили, значит, Вас действительно убили. Прямо как Ваши учителя: они всегда правы, не смотря на то, что у Вас есть веские доказательства обратного (наподобие того, как Вы уверены, что увернулись от ракеты). Клиент получает информацию от сервера (о том, где находятся другие люди, в кого попали и пр.) и посылает другую информацию назад серверу (о том, куда хочет переместиться клиент, куда он стреляет и т.д.). В клиент-серверной архитектуре, клиент "общается" только с сервером, сервер же, в свою очередь, общается со всеми клиентами (людьми подсоединенными к серверу).

Сервер как авторитет

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

Хорошее описание истории развития сетевых архитектур в играх Вы можете найти в начале этого документа: http://unreal.epicgames.com/Network.htm. Сходите прочитайте раздел History этого туториала, а затем вернитесь сюда. Настоящая модель является клиент-серверной, в которой клиент обладает примерным представлением игрового процесса с целью минимизировать эффект лага. В текущей версии UT клиент оповещает сервер обо всех нажатиях клавиш. В каком направлении смотрит пользователь, в каком направлении он перемещается, стреляет ли он нормальным огнем или альтернативным. Сервер затем использует эти данные для вычисления информации об игроке, и передает эту информации другим клиентам. По большому счету все так и выглядит. ZeroPing же выдвинул смелую идею дать клиентам больше контроля. Он позволяет клиентам самим решать попали в них или нет. Теоретически это звучит великолепно. Пользователь сам решает, что происходит, и если он попал в другого игрока, значит, он действительно в него попал. К несчастью, здесь есть некоторые проблемы.

В таких играх как UT и Q3A, многие люди пытаются читить. Люди же создающие игру должны предотвращать такие ситуации, не давая возможности этого делать. В таких играх клиент ответственен за столь малую часть логики всей игры, что просто не остается никакой возможности читинга. Клиент отсылает свои данные серверу, и только сервер решает, попали ли в игрока или нет. Читы абсолютно ограничены в своих возможностях и все что они могут делать, только модифицировать направление движения игрока и решать читить или не читить (to cheat or not to cheat :) Тем не менее, когда Вы имеете нечто наподобие ZeroPing Вы, тем самым, позволяете клиентам решать жизненно важные проблемы, например, попали ли Вы во врага или нет. Чит же может претендовать на то, что игрок попал сразу во всех игроков, и внести смятение в игру, убивая каждого, кто попадает в поле зрения. Иногда даже может дойти до абсурда в виде многократного убийства одного и того же игрока, в том случае если он неудачно появляется рядом с читером. Моды подобные ZeroPing все же остаются модами, и, соответственно, не обладают столь притягательной силой как сама игра. Поэтому, моды меньше подвержены чит-взлому. Но это не делает их защищенными от читов. Если бы изначально игра имела такой же подход, как и ZeroPing, то через неделю после релиза игры все было бы забито читами. Вы можете понять сопротивление игровых компаний к подобного рода подходу. Это не из-за синдрома "мы-до-этого-не-догадались", как многие думают, а как следствие борьбы с читерами.

Добавлю только, что автор мода ZeroPing скорее всего догадывался об этом. Поэтому он спрятал свой код и сделал невозможным читинг. Таким образом, вся безопасность мода упиралась в так называемый obfuscation подход (скрытие кода, создание трудно поддающегося восприятию, запутанного кода). Этого было достаточно на некоторое время, но позже стали доступны декомпилеры кода. Хоть я лично и не работал ни с одним из них, я знаю о существовании нескольких. Поэтому такой подход не может обеспечивать безопасность долгое время.

Обзор репликации

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

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

Сначала UT сервер производит для каждого клиента следующее:

  1. Запускает все события (такие, как tick, timer, hitwall и т.д.)
  2. Пробегает по каждому из подсоединенных клиентов
  3. Создает список релевантных актеров для каждого из клиентов
  4. Для каждого актера реплицирует переменные которые необходимо реплицировать
  5. Реплицирует все вызовы функций, которые необходимо реплицировать для данного клиента в том случае если вызов этой функции был произведен предыдущий раз при выполнении всех серверных функций

Что при этом делают клиенты:

  1. Получает данные от сервера и обновляет их локальные копии
  2. Запускает все реплицированные вызовы исходящие от сервера
  3. Запускает все симулированные функции (такие как tick, timer, hitwall и т.д.) и обеспечивает локальную обработку игрока
  4. Узнает какие репликационные вызовы исходят от клиента и что при этом должно быть реплицировано серверу, и отсылает эти данные

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

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

Релевантные актеры

Сперва необходимо определить какие актеры являются релевантными для данного клиента. Клиент, являясь беспомощным терминалом, должен знать обо всем, что ему необходимо отображать на экране. Он должен знать о других игроках, оружии на уровне, видны ли какие либо снаряды, виден ли флаг, аптечки и пр. Все эти актеры представляют собой релевантный набор актеров. Каким же образом создаются релевантные наборы актеров? Если актер не является релевантным, то никакая репликация или магия не заставит информацию об этом актере быть переданной клиенту. Этот актер должен быть релевантным для того, чтобы информация о нем была передана клиенту. Для Unreal / Unreal Tournament метод определения релевантности актера выглядит следующим образом (использован фрагмент документа с официальной TechPage):

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

  1. Если актер является bNetTemporary, то он не проходит первый этап.
  2. Если актер не является bStatic и bNoDelete, он проходит первый этап.
  3. Если актер является актером LevelInfo, он проходит первый этап.
  4. Если актер является временным сетевым актером (определяется с помощью bNetTemporary) и он уже был послан клиенту, то он не проходит первый этап. Такого типа актеры будут подробно описаны позже.
  5. Если роль (Role) актера равна значению ROLE_None, он не проходит первый этап.
  6. Если актер является bAlwaysRelevant и время прошедшее с последней проверки его на релевантность меньше, чем 1/NetUpdateFrequency (будет описано позже) секунд, то он не проходит первый этап. Это означает, что сервер обновляет его локальные копии на клиенте NetUpdateFrequency раз в секунду. Значения NetUpdateFrequency для разных классов: Actor: 100, ZoneInfo: 4, GameReplicationInfo: 4, PlayerReplicationInfo: 2, or Inventory: 8.
  7. Если актер является bNetOptional и он имеет не нулевое значение LifeSpan (означает, что он не живет вечно) и с времени его "рождения" уже прошло 150 ms, то он проходит первый этап. Любые попытки реплицировать bNetOptional актера после 150 ms его жизни (он может быть исключен из списка реплицируемых актеров по вине ограничения пропускной способности или по другим причинам) не приведут ни к какому результату.
  8. Если время с последней проверки на релевантность актера больше, чем 1/~NetUpdateFrequency секунд, он проходит первый этап (это сделано из соображений более тонкой оптимизации NetUpdateFrequency, зависит от типа актера. Вычисления будут произведены только в том случае, если актер прошел через сеть правил, указанных выше).
  9. Актер не проходит первый этап.

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

  1. Если актер является bAlwaysRelevant, то он является релевантным.
  2. Если актер принадлежит клиенту или принадлежит актеру, которого мы видим (выявляется посредством PlayerCalcView), то он является релевантным.
  3. Если актер является клиентом или актером которого мы видим (выявляется посредством PlayerCalcView), то он является релевантным.
  4. Если его Instigator-ом является клиент, то он является релевантным.
  5. Если актер имеет звуковое сопровождение и он находится в радиусе слышимости клиента, то он является релевантным.
  6. Если актер является оружием, принадлежащим видимому персонажу (Pawn), то он является релевантным.
  7. Если актер является bHidden и не является bBlockPlayers и не имеет звукового сопровождения, то он не является релевантным.
  8. Если актер не является релевантным (судя по вышеизложенным правилам), но при этом любое из этих правил сделало его релевантным в течение оставшихся RelevantTimeout секунд (по умолчанию 5 секунд, настраивается в UnrealTournament.ini: [IpDrv.TcpNetDriver]), то он становится релевантным.
  9. Актер не является релевантным.

Эти правила, хоть и выглядят сложными, но тем не менее абсолютно точно описывают как UT узнает, что является релевантным для каждого из клиентов. Зачастую, эти проверки не относятся к сути того, что Вы делаете, но знать из не помешает. Подумайте над ними, и Вы поймете их суть. Если что-то не является видимым, значит клиенту, скорее всего, не нужно знать о нем. Если это игрок на другой стороне CTF-LavaGiant, какую пользу может извлечь из него клиент? Или если это невидимый навигационный указатель (pathnode) , который используется ботами, для клиента опять же он бесполезен. Пересылать данные о такого рода актерах было пустой тратой пропускной способности крошечных 28.8Kb модемов.

На практике это выглядит следующим образом: всегда устанавливайте bAlwaysRelevant актера равным true, если он является хранилищем данных, которые необходимо знать клиенту, наподобие PlayerReplicationInfo или GameReplicationInfo. Актеры типа PlayerReplicationInfo, которые содержат информацию об игроке, всегда являются релевантными. Это позволяет клиенту узнать текущий счет в игре и данные о других игроках на уровне (например, его имя, счет, пинг, потерю пакетов данных и пр.), невзирая на то, является ли данный игрок релевантным по отношению к клиенту или нет. Подобного рода актеры являются носителем всей необходимой информации, занимая при этом не большой объем. Это позволяет более разумно использовать пропускную способность модемов с целью репликации этих данных. Не существенная же для клиента информация (положения игроков, их скорость, оружие, анимация и пр.) не реплицируется, когда это не необходимо (репликация этих данных происходит в коде самих игроков (Pawn/PlayerPawn), которые не всегда являются релевантными). Надеюсь, Вы еще живы. Это очень важная вещь, которую нужно усвоить о репликации в Unreal. Где расположить необходимые переменные и как заставить их реплицироваться. Если Вам нужно, чтобы какая-то важная информация была передана клиенту, то лучший способ сделать это, наследуя класс ReplicationInfo и использовать его для расположения необходимых переменных. Информация же являющаяся актуальной только в случае если актер виден, лучше всего расположить непосредственно в самих актерах, релевантность которых меняется со временем.

Короче говоря, если клиентам необходимо что-то знать, они будут это знать. Весь инвентарь является релевантным по отношению не только к клиенту, но и ко всем окружающим игрокам. Все летящие снаряды и эффекты оружия, которые видны, являются релевантными. Все, что может как-то повлиять на течение игры клиента, будет реплицировано. Это включает в себя, вещи, которые могут блокировать движения игрока или актер со звуковым сопровождением. Для избежания "рождения" новых актеров в сетевой игре, UT не сразу удаляет актера из набора релевантных актеров, когда он пропадает с поля зрения. Вместо этого, UT дает ему несколько секунд, перед тем как посчитать его действительно не релевантным. Да, и UT еще берет во внимание тот факт, что игрок может смотреть "глазами" другого актера, например, как в случае с управляемым снарядом Redemeer, или глазами друга по команде, когда Вы нажимаете цифры на правой клавиатуре.

Сетевые приоритеты (NetPriorities)

В то время как описание не представляет особого труда, выяснить, почему же происходят некоторые вещи намного сложней. Unreal Tournament назначает различные приоритеты (NetPriority) для различных типов актеров. NetPriority, который обычно назначается в диапазоне от 1 до 3, определяет приоритет данного актера в общем потоке данных, пересылаемых клиенту, в рамках пропускной способности. Актеры, сперва, сортируются исходя из их приоритетов (учитывается, насколько близко они находятся к клиенту и т.д.). Актеры являющиеся bNetOptional отсылаются в конец списка. Поэтому актеры с более высоким приоритетом пересылаются клиенту с первым "эшелоном", а когда пропускная способность "насыщается", то все остальные актеры остаются ждать своей очереди (UT запоминает, какие актеры не были реплицированы, отсюда и задержка bNetOptional актеров - если они не успеют реплицироваться за 150 ms, UT просто избавляется от них. Смотри условия релевантности указанные выше).

Ниже расположен практически весь перечень приоритетов в UT:

Боты3.0
Игроки3.0
Управляемый Redemeer3.0
Муверы (лифты, двери и пр.)2.7
Снаряды (Projectiles)2.5
Каркасы (Carcasses)2.5
Другие персонажи (спектаторы, титаны, налийские кролики и пр.)2.0
Основные визуальные эффекты (вспышки, кровь, дым)2.0
Простые эффекты (гильзы)1.4
Инвентарь1.4 или 2.5
Все остальное1.0

Эта таблица показывает, каким образом сервер определяет, что информация об игроке (будь то бот или реальный игрок) более важна, чем, например, гильза от патрона. И этот подход имеет смысл. Движение управляемого Redeemer довольно непредсказуемо, т.к. он контролируется игроком, и, в связи с этим, ему необходим более высокий приоритет. Обычные снаряды подчиняются игровой физике, т.е. им назначается стандартные физические модели: PHYS_Falling, PHYS_Pojectile и пр. Таким образом, их движение достаточно предсказуемо. Поэтому значение их NetPriority не большое по сравнению с NetPriority игрока. Тем не менее, снаряды являются важным аспектом игрового процесса (разве справедливо когда в Вас попадает ракета, которую Вы не видели? :) Как следствие, они также обладают достаточно высоким приоритетом. То же самое касается каркасов, т.к. их анимация (когда они разлетаются в воздухе и падают на землю) не менее важна (не сколько для игрового процесса, а для удовлетворения чувств играющего, когда он кого-то убивает). Используйте эти примеры в качестве инструкции при создании Ваших собственных актеров, требующих различные значения приоритетов.

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

Вообще, за время каждого tick-а, все релевантные актеры (по отношению к определенному клиенту), собирают в один массив и сортируют по приоритету. Затем сервер запускает процесс репликации, начиная с актеров наивысшего приоритета, и продолжая вниз по списку. UT приостанавливает процесс репликации, в случае если пропускная способность исчерпана. Вот почему следует грамотно назначать приоритеты. Таким образом, если на экране находится сразу слишком много актеров или Вы имеете дело с 28.8K коннектом, UT будет знать, как их сортировать и кому отдать предпочтение при ограниченной пропускной способности. Если актер не был реплицирован, все его данные, которые нужно было передать клиенту, не забываются. Они будут добавлены в список при следующей репликации (при следующем tick-е), получив тем самым еще одну попытку. Необходимо, также, иметь в виду, что увеличение, например, в два раза значений NetPriority у всех актеров, не повлечет за собой увеличение скорости репликации, т.к. значение NetPriority имеет смысл, только при сравнении со значениями приоритетов других актеров. Учитывая, что 3.0 - это максимальное значение приоритета, нет никакой разницы между актерами с NetPriority равным 4.0 и 400.0, т.к. они оба попадут в начала списка репликации.

Roles и RemoteRoles. Введение

Теперь самое время пояснить цели различных ролей (Roles) в Unreal. Роли просто определяют, какую роль играет данный актер в процессе репликации. Например, игрок должен быть реплицирован совсем по другому, нежели, скажем, коробка амуниции, лежащая на земле, или летящий снаряд, подчиняющийся определенным физическим законам. На сервере роль ROLE_Authority означает, что актер находится на авторитетном сервере. Его удаленная роль (RemoteRole) будет иметь одно из значений, указанных ниже. На клиенте, роль и удаленная роль поменяются друг с другом местами. При этом, удаленная роль будет ROLE_Authority, а роль будет соответствовать одному из нижеприведенных типов ролей. Давайте рассмотрим все типы ролей (которые должны быть установлены в дефолтных свойствах у RemoteRole) в порядке увеличения подвластного им контроля:

Основы репликации

Для каждого актера, являющегося релевантным по отношению к клиенту, серверу необходимо знать какой набор данных должен быть передан клиенту об этом актере. Самое очевидное правило - пересылать переменные, которые изменились. К несчастью, это одно правило уже превышает возможности пропускной способности модема. Поэтому, серверу необходимо уменьшить поток данных, отбросив ненужные. Это делается с помощью специальных выражений репликации (replication statement). Эти выражения запускаются для каждого из клиентов, для каждого актера, являющегося релевантным по отношению к данному клиенту. Только когда получено множество всех переменных всех необходимых актеров, происходит передача данных. Такое множество уже реально "уместить" в пропускную способность обычного модема. Но как клиент может обходится без всей информации об игре? Если Вы остановитесь на этом моменте и подумаете, то поймете, что это все объяснимо. Нужно ли, например, все переменные, имеющие отношение к искусственному интеллекту бота, пересылать клиенту? Ответ: нет. Нужен только конечный результат, который, по сути, состоит из тех же переменных, что и у обычного игрового персонажа. Также, в зависимости от удаленной роли (RemoteRole), репликации могут подвергнуться абсолютно разные переменные. Некоторые роли заставляют реплицироваться координаты актера, периодическое обновление которых вызывает некое подобие его движения, в то время как другие роли реплицируют физику, а не координаты. В итоге создается реальная симуляция движения актера.

Релевантность не рекурсивна

Нужно иметь в виду одну важную вещь, которую легко упустить. Релевантность не передается вследствие реплицированных ссылок на объект. Рассмотрим пример: актер А содержит ссылку на актер В посредством переменной С, и актер А реплицирует переменную С (указывающую на актер В). При этом актер А является релевантным, а актер В - нет (не паникуйте - здесь всего 3 буквы). В этом примере, актер А является релевантным, и поэтому его переменная С будет реплицирована клиенту. Что же знает клиент? Клиент знает об актере В, т.к. в актере А есть ссылки на него, но актер В не является релевантным. Все переменные актера В не подвергаются репликации. Изначально я думал, что реплицированные переменные заставляют объекты, на которые они ссылаются также становиться релевантными, но это не так. Актер может стать релевантным только исходя из вышеуказанных правил, которые не обеспечивают проверку актера на референс со стороны других релевантных актеров.

Проверки репликации

Если Вы попытаетесь посчитать количество клиентов на сервере, затем количество всех их релевантных актеров и количество репликационных проверок переменных у этих актеров, Вы получите огромное число. Поэтому при написании репликационных выражений необходимо соблюдать осторожность. Так как каждая проверка выполняется на сервере, Вы должны учитывать его процессорные ограничения. Не вызывайте функции из выражений репликации. Более того, старайтесь создавать проверки как можно проще, ограничиваясь только условиями. И последнее, у Epic-ов есть некоторые проблемы, возникающие в случае, если проверка репликации происходит слишком долго. В первую очередь, это касается таких важных актеров как Actor или Inventory, которые оба реплицируют огромное количество переменных. Для облегчения работы сервера, Epic-и переместили процесс проверки в нативный код. Поэтому, Вы можете встретить ключевое слово "nativereplication" в некоторых Engine классах. Любые переменные этих классов будут проходить проверку в нативном коде репликации, тем самым, делая репликационные выражения, написанные на UnrealScript, бесполезными для данного класса (их можно использовать, разве что, в качестве документации для UnrealScript кодера). Любые переменные, которые не определены в классе с нативной репликацией (определены в любом классе или подклассе с не нативной репликацией) реплицируются при помощи выражений, написанных на UnrealScript. Но, не смотря на то, что нативная репликация производит проверки на C++, репликационные проверки на UnrealScript все же остаются верными и являются удобным способом выявления проблем репликации и путей их обхода.

Специальные репликационные переменные

При написании условий репликации, Вы должны избегать некоторых вещей. Во время репликации инициализируются несколько полезных переменных, помогающих выяснить нужно ли актера реплицировать или нет. Эти переменные объявлены в классе Actor:

NetUpdateFrequency

NetUpdateFrequency - еще одна очень полезная переменная для репликации. Она указывает релевантным актерам (если актер не является релевантным, то эта переменная не имеет значения) что они должны быть релевантными NetUpdateFrequency раз в секунду. это используется в том случае если изменение данных происходит часто, но Вы желаете только периодического их обновления. Например, PlayerReplicationInfo содержит много данных о клиенте. Такие данные, как пинги игроков, могут меняться довольно часто, и отслеживать их клиенту с такой же частотой не имеет смысла. Но, т.к. PlayerReplicationInfo имеет значение NetUpdateFrequency равное 2, то эти данные обновляются только два раза в секунду, что благотворно сказывается на пропускной способности клиента. Ниже приведен список значений NetUpdateFrequency для всех классов в UT (Hz = количество раз в секунду):

Actor100Hz
ZoneInfo4Hz
GameReplicationInfo4Hz
PlayerReplicationInfo2Hz
Inventory8Hz

Reliable против Unreliable

Еще один вид оптимизации в написании сетевого кода заключается в различии между Reliable и Unreliable данными. В случае с переменными, все данные являются Reliable. Они все гарантированно достигнут клиента, даже если будут переданы в неверном порядке (класс Pawn, тем не менее, имеет код, предотвращающий такие "прыжки" данных, но, несмотря на это, подобный эффект все же может появиться. Например, если прыгать игроком назад и вперед во время лагающей игры, то положение игрока иногда обновляется не в верном порядке). Функции существуют в двух формах: Reliable and Unreliable. Reliable функция, как и Reliable данные, гарантированно достигают клиента. Unreliable функция, в свою очередь, также передается клиенту, но гарантии, что она достигнет клиента, нет. Что-то, что пересылает сообщение клиенту, например, BroadcastMessage или BroadcastLocalizedMessage, является Reliable для того, чтобы убедится, что клиент получит это сообщение. Функции, отвечающие за клиентские обновления серверных данных о положении клиента, также должны быть Reliable. Так как клиент пересылает запросы на обновления данных каждый тик (в функции ServerMove, если Вам интересно), то не имеет значения, достигли ли все они сервера или нет. Вследствие того, что время каждого обновления запоминается, сервер знает каким данным нужно уделить внимание, т.к. они устарели или задержались (эффект лага). Таким образом, сервер застрахован на получение обновлений от клиента, не требуя одновременной передачи сразу всех необходимых данных. Reliable функция периодически пересылается по сети, до тех пор, пока клиент не уведомит сервер о ее получении. Это может привести к задержке на секунду или более, если он попытается переслать функцию один или два раза. Поэтому, если Вы не хотите "убить" коннект Reliable обновлениями о том, куда смотрит клиент и куда он двигается, Unreliable решение будет самым оптимальным в этой ситуации, т.к. серверу необходимо знать "большинство" из них для корректной работы.

Написание выражений репликации

Вы должны знать несколько вещей при написании Ваших собственных выражений репликации. Первое, Вы не можете перегружать выражения репликации. Если оно существует в родительском классе, обеспечивая репликацию Location, Velocity, каких-либо функций или других переменных, которым Вы хотите изменить условия... Вы этого сделать не можете. Вместо этого, Вы должны создать шаблон актера, удовлетворяющий поставленные требования, обеспечив, таким образом, репликацию необходимых функций и данных. Обычно, Вы можете достигнуть этого установкой значений нескольких переменных (Вы не можете менять значение "const", т.к. они инициализируются в нативном коде), или "поиграв" с удаленной ролью актера (RemoteRole), до тех пор, пока не добьетесь нужного результата. Тем не менее, не делайте этого слепо, если Вы, конечно, не уверены в успехе. Вместо этого, читайте дальше и Вы поймете (надеюсь, если я делаю все правильно :) что точно представляет собой каждая из ролей (Roles), для чего она предназначена, и какие побочные эффекты они могут вызывать. Следующее, старайтесь создавать выражения репликации как можно проще. Мы уже обсуждали важность того, что бы избегать написания слишком сложного кода, и это правило здесь не исключение. Второе, используйте bNet* переменные если это возможно. Такие переменные, как bNetInitial, bNetOwner, bSimulatedPawn довольно полезные и "бесплатные" по отношению к CPU. Для них не требуются дополнительные мощи CPU, т.к. они устанавливаются за Вас автоматически. Другая, не менее важная вещь, позволяющая управлять репликацией данных, является Role и RemoteRole. Условие "if (Role == ROLE_Authority)" заставит переменную или функцию реплицироваться от сервера (на котором Role равна ROLE_Authority) к клиенту. Условие же, наподобие "if (Role < ROLE_Authority)", наоборот, заставит переменную реплицироваться от клиента (на котором Role менее "мощная", чем ROLE_Authority) к серверу. Также, выражения репликации могут содержать проверку на то, является ли Role/RemoteRole равной SimulatedProxy или DumbProxy. Лучшее место, где можно найти примеры написания выражений репликации с еще более замысловатыми условиями, это Engine классы, в частности классы Actor, Rawn и PlayerPawn. Я думаю, не стоит упоминать о том, что не следует изменять значения в выражениях репликации. Например, указание i++ в выражении репликации, повлечет за собой непредсказуемые изменения переменной i во время ее репликации, что совсем не желательно. И, наконец, последний совет, сервер проверяет всю входящую информацию на легитимность. Т.е., если клиент пересылает какие либо данные (переменную или функцию) серверу, сервер временно меняет местами значения Role and RemoteRole и "оценивает" выражение репликации для выяснения, имеет ли машина на другом конце соединения истинные причины для пересылки этих данных. Если проверка дала отрицательный результат, то от этих данных (переменной или функции) избавляются. Это означает, что при написании выражений репликации, Вы должны убедиться, что сервер также располагает всеми необходимыми данными для выполнения проверок репликации. В противном случае, они никогда не будут обработаны или приняты сервером.

Репликация переменных

Переменные являются одним из типов данных, которые могут подвергаться репликации. Другой тип - вызовы функций (RPC, если так понятней). Далее будет описано, как данные передаются от сервера к клиенту и наоборот. В этом вопросе существует множество маленьких ситуаций, при которых работа сервера усложняется (исходя из своей специфики) по сравнению с приведенным ранее описанием. Как выше упоминалось, все переменные являются Reliable, поэтому дискуссия на тему Reliable против Unreliable здесь не имеет смысла.

Актеры вновь релевантные (Spawning)

Когда актер становится релевантным по отношению к клиенту, он "рождается" (spawn) на клиентской машине, со всеми своими дефолтными значениями (значения по умолчанию, указываются в defaultproperties). Если сервер изменил значения сразу же после "рождения" актера (перед проверкой на релевантность), то эти переменные пересылаются клиенту в качестве "дополнения", с условием, конечно, что эти переменные описаны в выражениях репликации. Сервер будет пересылать любые значения переменных, если они отличаются от своих дефолтных значений. Но все может пойти наперекор, в случае, если сервер "думает", что на клиенте установлены другие дефолтные значения, чем они есть на самом деле. Изначально, дефолтные значения на клиенте и на сервере совпадают. Если же дефолтные значения изменились на сервере, то они, при этом, клиенту переданы не будут. Вы должны изменять их в симулятивной (simulated) функции для того, чтобы изменения произошли также и на клиенте. Но что же произойдет, если актер станет не релевантным? В этом случае произойдет плохая вещь. Когда актер, затем, станет снова релевантным, то он, заново "родившись", будет иметь дефолтные значения, отличные от дефолтных значений на сервере. Во время очередной проверки, на то, какие переменные необходимо реплицировать, сервер увидит, что значение переменной не отличается от ее дефолтного значения, и, следовательно, ее реплицировать не нужно. Клиент, при этом, никогда не получит необходимые данные о "свежем" актере и будет обладать "старыми" дефолтными значениями. Такая неоднозначность значений может привести к странному поведению актера.

В качестве примера, рассмотрим следующую ситуацию: в некотором, основанном на классах, моде, программист (на этот раз не я ;-) устанавливает обе переменные Mesh и Default.Mesh равными playerclass, который выбрал игрок. Игрок, при этом, периодически появляется и исчезает из поля зрения (соответственно, периодически становится релевантным и не релевантным с целью экономии пропускной способности). Обычно, игроки являются релевантными, только когда Вы можете их видеть и еще приблизительно 3 секунды, после того, как они пропадают из поля зрения. Предположим, теперь, что в это время игрок изменяет playerclass своего персонажа. Или изменяет его во время старта игры, когда она еще не началась и все игроки невидимы и, соответственно, являются не релевантными. Когда же они станут релевантными по отношению к клиенту, вследствие того, что они попали в поле его видимости, или когда только началась игра и они "родились", значение Mesh == Default.Mesh, поэтому переменная Mesh не подвергается репликации. Скин (skin), при этом, продолжает нормально реплицироваться. Что же видит клиент? Он видит игрока с исходным (оригинальным) мешем и не соответствующим ему скином, который, возможно, был вообще создан для совсем другой модели. Как я и говорил, странное поведение, которое трудно отследить. Для того, чтобы избежать подобных ситуаций, не изменяйте дефолтные значений, если, конечно, Вы не знаете что делаете. Простая установка Default.Mesh, только потому, что Вам кажется, что это правильно, приведет только к проблемам. В этом случае, Default.Mesh вообще не нужен, и если убрать его переопределение, все будет отлично работать.

Время репликации

Данные реплицируются только в конце каждого тика (tick). Это означает, что если переменная изменяет за время тика свое значение несколько раз, то только последнее ее значение будет передано по сети. Если же значение переменной несколько раз изменилось во время тика, а затем снова приняла исходное значение, то сервер не "засечет" изменение переменной и, соответственно, не будет подвергать ее репликации.

Клиент-Сервер репликация

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

Репликация только когда необходимо

Оптимизация пропускной способности также затрагивает и репликацию переменных. Переменная реплицируются только в том случае, если сервер "думает", что клиент имеет неправильное ее значение. Это обычно происходит, когда сервер изменяет значение какой либо переменной, и он уверен, что клиент не знает об этом (клиент знает об изменениях, если они были произведены в simulated функции, вызванной на клиентской стороне. Технически, это означает, что такие переменные реплицировать нет необходимости. Подробнее такие ситуации будут рассмотрены ниже.. :) По сути, код может изменять переменные на клиентской стороне, и, при этом, они не будут обновляться сервером, до тех пор, пока их серверные значения также не подвергнутся изменениям. Например, я использовал такой подход при написании статического LOD еще во времена Unreal, до того, как Epic предоставила свой LOD реального времени. Клиент узнавал насколько далеко находится другой игрок и исходя из этого переключался на модель с соответствующей детализацией. Такой подход позволял отрисовывать больше моделей одновременно на экране, не сказываясь, при этом, на производительности. В мультиплеере, клиент также может изменять меш на другие LOD меши, и это не вызовет никаких проблем. Так как сервер никогда не видит изменения своего, серверного меша (если это конечно dedicated сервер), клиент может изменять LOD меш как ему заблагорассудится. Если сервер все же изменит меш, клиент получит данные о нем, переданные сервером, и заменит его опять. Добавлю, что моя реализация даже изменяла интенсивность применения LOD в зависимости от framerate ;)

Апроксимация данных репликации

Еще один прием оптимизации заключается в том, как Unreal пересылает векторы (vectors), ротаторы (rotators) и массивы (arrays) через интернет. Эти правила хорошо описаны в документе http://unreal.epicgames.com/Network.htm на официальной Tech странице Epic, поэтому я просто скопировал их сюда:

Примеры репликации переменных

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

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

В классе PlayerPawn:

reliable if ( Role < ROLE_Authority )
        Password, bReadyToPlay;

Здесь показано, где Ваш пароль пересылается серверу при попытке присоединиться к нему, или, при попытке войти в систему как администратор сервера. Серверу нужен пароль, для того чтобы его проверить, поэтому пароль должен быть передан серверу. Давайте проведем проверку вышеуказанного условия на обеих машинах: на сервере и на клиенте. Сервер: на сервере Role == ROLE_Authority. Поэтому, при проверке условия легко понять, что его результат равен false. Следовательно, переменная Password не реплицируется с серверной стороны. Это означает, что сервер не пересылает эту переменную на другой конец соединения. Клиент: т.к. на клиенте Role не равна ROLE_Authority (помните, что Role равна ROLE_Authority только на сервере), проверка условия дает положительный результат (true). Учитывая то, что репликация с клиентской стороны работает только для PlayerPawn, переменная Password Вашего PlayerPawn будет передана серверу (понятно, что нет смысла пересылать чужие пароли ;). Здесь также продемонстрировано как происходит репликация переменной bReadyToPlay, которая используется в играх Tournament стиля, где все должны нажать старт, прежде чем начнется игра. Заметьте, что пересылаемые переменные принадлежат классу Вашего PlayerPawn, т.к. клиент может реплицировать только переменные, принадлежащие PlayerPawn актеру игрока.

unreliable if( Role == ROLE_Authority )
        Owner, Role, RemoteRole;

Этим выражением мы обеспечиваем гарантию того, что "владелец" объекта всегда реплицируется клиенту. Не забывайте, что все переменные актера-"владельца" при этом не подвергаются репликации (из-за не рекурсивности релевантности), но сам актер - реплицируется. Это означает, что простое сравнение "if (PlayerPawn == Owner)" будет отлично работать, потому что ссылка на переменную Owner реплицируется клиенту. Также, помните, что unreliable и reliable в UT не имеют значения при репликации переменных, т.к. они одинаково обрабатываются. Когда-то, во времена первого Unreal, они влияли на репликацию переменных, но сейчас это уже не имеет значение. Так что не дайте себя этим запутать.

Репликация Role и RemoteRole может, сперва, показаться странным, т.к. по идее они должны иметь противоположные значения на клиенте. Тем не менее, существует нативный код, который отвечает за переключение этих значений при репликации. На клиенте, можно уверенно сказать, что эти переменные поменяются местами, т.е. RemoteRole станет равной ROLE_Authority. Вы должно быть удивлены, зачем они вообще реплицируются, да еще и в первых рядах. В то время как клиент никогда не использует в коде эти переменные, есть одно место, которое легко упустить из виду, но где они очень важны: выражения репликации. Когда клиент оценивает, нужно ли ему реплицировать вызов функции, или какую-либо PlayerPawn переменную серверу, клиент нуждается в точных значениях Role и RemoteRole. Без них клиент не может принять решение о том, что ему нужно пересылать серверу. И из-за того, что сервер проверяет валидность реплицированных данных, клиент должен иметь последние их копии для удачной репликации данных.

unreliable if( bNetOwner && Role == ROLE_Authority )
        bNetOwner, Inventory;

Inventory является "головой" связанного списка, который содержит весь инвентарь актера (обычно Inventory используется для Pawn и PlayerPawn). Мы хотим, чтобы инвентарь реплицировался полностью клиенту, чтобы клиент знал что он имеет в инвентаре, для того чтобы отображать соответствующую информацию на HUD игрока и пр. (согласитесь, это имеет смысл, верно?). Просто указав реплицироваться "голову" списка (в котором каждый элемент списка указывает на следующий) не достаточно. Каждый актер, помимо всего, должен быть релевантным (это удовлетворяется условием, что данный актер принадлежит текущему PlayerPawn), и каждый указатель на следующий элемент списка также должен реплицироваться. Учитывая тот факт, что Inventory является подклассом класса Actor (если проследить всю цепочку наследования), класс Inventory также обладает переменной Inventory, которая подвергается репликации. Именно поэтому, каждый клиент может полностью видеть свой инвентарь на клиентской стороне. Так как мне совсем нет надобности знать содержимое чужого инвентаря (текущее оружие и эффект ShieldBelt реплицируются по иным причинам), Inventory реплицируется только клиенту, который является его "владельцем".

Другая переменная, указанная в приведенном выражении репликации - bNetOwner. Учитывая, что эта переменная инициализируется из нативного кода, как на сервере, так и на клиенте, нет никакого смысла ее реплицировать. Можно, конечно, заявить что ее репликация сказывается на пропускную способность, но эффект этот не значителен. Возможно, ее уберут в следующих патчах.

unreliable if( DrawType == DT_Mesh && Role == ROLE_Authority )
        Mesh, PrePivot, bMeshEnviroMap, Skin, MultiSkins, Fatness, AmbientGlow, ScaleGlow, bUnlit;

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

Теперь перейдем к более сложным выражениям репликации...

unreliable if( RemoteRole == ROLE_SimulatedProxy )
        Base;

Здесь мы имеем выражение репликации немного посложнее, не затрагивающее Role == ROLE_Authority. Мы видим, что данная Base переменная (которая устанавливается с помощью SetBase) реплицируется только в том случае, если актер является симулированным (Simulated Proxy). Если Вы установите для актера значение DumbProxy, то Вы не увидите обновление Base на клиенте, вместо этого мы получим "дерганные" обновления координат (Location) которые сервер пересылает клиенту (более подробно об этом далее). Поэтому, если Вы используете SetBase, убедитесь сперва, что актер является SimulatedProxy, и, если он не является SimulatedProxy, используйте что-то отличное от SetBase :) Или, если Вам необходимы оба SetBase и SimulatedProxy, Base не будет при этом реплицироваться. Вам необходимо будет придумать какой-то обходной маневр, возможно, использовать симулятивную функцию (simulated function) для установки Base, которая отработает на клиенте (опять, более подробно читайте далее). На этом этапе у Вас могут возникнуть проблемы, когда актер получает вызов симулятивной функции, в то время когда он не является релевантным. Это приведет к тому, что эта функция так никогда и не будет вызвана на клиенте, и затем, когда актер станет, наконец-то, релевантным он не будет иметь установленной Base (т.к. функция ее установки вызвана не была). В результате будут происходить всякие странные вещи, которым обычно дают статус багов. Как вариант, Вы можете "прикрепить" (attach) актера (и то к чему он прикреплен) при помощи bAlwaysRelevant, таким образом симулятивная функция в любых случаях будет вызвана, но это может существенно повлиять на пропускную способность :)

unreliable if( RemoteRole == ROLE_SimulatedProxy && Physics == PHYS_Rotating && bNetInitial )
        bFixedRotationDir, bRotateToDesired, RotationRate, DesiredRotation;

Это выражение все еще относительно простое, но уже посложнее. В нем повторяется логика рассмотренного ранее выражения, т.е. сервер пересылает данные клиенту, только если актер на клиенте является Simulated Proxy. Тем не менее, в силу того, что эти переменные применимы только для PHYS_Rotating физики, нет необходимости реплицировать их клиенту, который не использует в данный момент эту физику. И последнее, очень важное условие: bNetInitial. Это означает, что эти переменные будут переданы клиенту, только при первой репликации (т.е. когда они реплицируются первый раз).

unreliable if( bSimFall || (RemoteRole == ROLE_SimulatedProxy && bNetInitial && !bSimulatedPawn) )
        Physics, Acceleration, bBounce;

В данном примере реплицируются очень интересные переменные. Мы видим, что если bSimFall имеет значение true, это приведет к репликации переменной Physics. Это используется, когда игрок выкидывает оружие из инвентаря. Когда оружие "извлекается" из инвентаря, ему необходимо сменить физику с PHYS_None (это значение оружие имеет, находясь в инвентаре) на PHYS_Falling на время полета оружия до земли. Затем снова установить Physics равной PHYS_None когда оружие достигнет земли. Все эти изменения происходят путем установки соответствующего значения bSimFall в важных моментах жизни Unreal Tournament оружия. bSimFall становится равной true когда оружие выкидывают для того чтобы засечь изменения, и не меняет своего значения до тех пор пока не дотронется до земли. На земле физика снова меняется. После того, как оружие оказалось на земле, и поменяла свое значение физики, bSimFall снова становится равной false, поэтому никаких дальнейших обновлений физики не происходит. Рассматривая вторую часть выражения, мы видим, что Physics реплицируется, только если актер является SimulatedProxy, это первая его репликация и он не является симулятивным pawn-ом (simulated pawn). У симулятивных pawn-ов значение bSimulatedPawn установлено равным true если это Pawn с RemoteRole равной ROLE_SimulatedProxy (кто бы мог подумать? :) Это означает, что любые изменения физики у SimulatedProxy актера, который при этом не является Pawn-ом перед репликацией самого актера будет реплицирована клиенту. Значения Physics никогда не реплицируются для Pawn-ов. Вернее, код их физики прошит в скриптах для того чтобы они все время были "прижаты" к земле. Говоря простым языком, когда Pawn хочет прыгнуть, сервер просто устанавливает ему вертикальное ускорение, и Pawn становится способным перемещаться вверх. Это ускорение затем реплицируется клиенту (описано ниже). Клиент проверяет, является ли Pawn игроком (т.е. ботом или игроком), может ли он летать или нет (устанавливается с помощью bCanFly переменной класса Pawn), и не находится ли игрок в зоне воды (т.к. в этой зоне действуют другие силы гравитации и другая физика). По сути, физика для Pawn никогда не реплицируется клиентам, это только так кажется. Если Вы попытаетесь создать альтернативную физическую модель для PlayerPawn, то сначала убедитесь, что bCanFly установлено равным true для того, чтобы нативный код не применял физику падения для игрока, когда он находится на стене или в воздухе. Затем Вы должны описать альтернативный способ передвижения игрока.

unreliable if( !bCarriedItem
                        && (bNetInitial || bSimulatedPawn || RemoteRole < ROLE_SimulatedProxy)
                        && Role == ROLE_Authority )
        Location;

Здесь мы видим другую очень важную переменную, Location, и ее репликацию. Как видно из выражения, Location не реплицируется для предметов, которые в данный момент являются "носимыми". Это очень полезно в случае с инвентарем. Когда игрок носит с собой инвентарь, нет никакого смысла обновлять Location, т.к. он нигде не используется. Репликация координат для всего инвентаря вызовет большую нагрузку на пропускную способность. Посмотрим на следующую часть выражения. Location реплицируется в том случае, если этот актер в первый раз подвергается репликации (в случае если другие части выражения дают результат true). Это очень полезно, т.к. каждый Pawn спавнится в разных местах при входе в игру, также, например, ракеты должны знать свой исходный Location при их "рождении" из RocketLauncher-а и т.д. Location пересылается также актерам, которые являются bSimulatedPawn. Это позволяет корректировать любые ошибки, возникающие при репликации Pawn. Так как Pawn может изменять свое направление, а клиент не всегда может об этом знать (из-за лагов и пр.), обновление Location единственный путь, при котором Location на клиенте может быть правильным. Стоит заметить, что эта техника не используется для обновления Location управляемого Вами игрока во время возникновения лагов. Когда это происходит, Вы являетесь AutonomousProxy, и специальная функция в PlayerPawn берет на себя ответственность за обновление (функция ClientAdjustPosition). Например, PHYS_Walking не похожа на PHYS_Projectile, у которой не трудно с большой точностью предугадать положение. PHYS_Walking просто означает "держите его прижатым к земле". Единственные клиентские апдейты (за исключением Location) затрагивают только Velocity, которая приводит к ошибкам движения игрока через некоторое время. Location при этом используется в качестве корректирующего фактора, обеспечивая тем самым незначительные отличия положения клиента от того, где ожидает его увидеть игрок, одновременно продолжая реплицировать Velocity (скорость) для того, чтобы клиент мог предугадывать движение между очередными апдейтами со стороны сервера. Другое условие репликации Location, это проверка RemoteRole: является ли она меньше, чем ROLE_SimulatedProxy, по сути является ли она ROLE_DumbProxy, т.к. ROLE_None предотвращает любую репликация на первом этапе проверки на релевантность. DumbProxy получает периодические апдейты от сервера каждые несколько тиков, создавая тем самым "прыгающий" эффект при игре по сети. Это было проблемой для шайбы в хоккейном моде, который я однажды пытался сделать, к сожалению, на то время я не знал выхода из этой ситуации :) SimulatedProxy не будут обновлять Location, т.к. он будет предсказываться с помощью Velocity и текущей физической модели. DumbProxies никоим образом не симулируются на клиентской стороне для достижения эффекта плавности, они просто получают апдейты Location. Более подробно я объясню это после рассмотрения всех примеров репликации переменных.

unreliable if( !bCarriedItem
                        && (DrawType == DT_Mesh || DrawType == DT_Brush)
                        && (bNetInitial || bSimulatedPawn || RemoteRole < ROLE_SimulatedProxy)
                        && Role == ROLE_Authority )
        Rotation;

Здесь мы встречаем очередную не менее важную переменную, Rotation. В выражении репликации также встречается проверка на !bCarriedItem по тем же самым соображениям, что были описаны в предыдущем примере. Rotation реплицируется только в случае, если актер является мешем или брашем. В силу того, что спрайты всегда повернуты к нам "лицом", то репликация их Rotation бесполезна. bNetInitial опять же заставляет реплицироваться эту переменную, только в случае, если актер подвергается репликации в первый раз (при условии, что другие части выражения являются true), например, ракета должна знать направление куда ей смотреть когда она находится в воздухе. Ее скорость определяет направление движения, но направление, куда "смотрит" ракета, которое, несомненно, является важным аспектом, определяет Rotation. Rotation также реплицируется и для симулятивных Pawn (для каждого игрока или бота, кроме игрока, которым управляете Вы). Таким образом, Вы можете определить, куда смотрит другой игрок. ViewRotation (которая определяет, куда смотрит клиент) пересылается серверу с помощью ServerMove, где она транслируется в Rotation, затем этот rotation реплицируется всем остальным клиентам. И, наконец, если актер является DumbProxy (единственная RemoteRole удовлетворяющая условия репликации, т.е. которая меньше SimulatedProxy), то он также получает обновления Rotation. Для апдейтов Rotation нет необходимости интерполяции между последними обновлениями, как того требует обновления Location. Лаг Location намного заметнее при быстром движении игрока, но Вы реально никогда не заметите лага Rotation. Более того, невозможно предсказывать и предугадывать Rotation. Он зависит исключительно от движения мыши игрока.

unreliable if( bSimFall
                        || ((RemoteRole == ROLE_SimulatedProxy && (bNetInitial || bSimulatedPawn)) || bIsMover) )
        Velocity;

Здесь мы встречаем последнюю, очень важную переменную в совокупности со сложным выражением репликации. Velocity реплицируется актерам с установленной bSimFall (используется при выкидывании оружия из инвентаря). Простой установки PHYS_Falling для этого недостаточно. Актер должен знать какая у него исходная скорость в тот момент, когда его выбрасывает Pawn. bNetInitial в этой ситуации работать не будет, т.к. это не первая репликация этого актера. Этот актер ранее существовал в виде Weapon (оружия) в течении некоторого времени. В этом случае обновление происходит для установки исходной скорости в ситуациях, когда актер был выброшен Pawn-ом. Продолжая рассмотрение выражения репликации, мы видим, что SimulatedProxy получают значение Velocity при первой их репликации, например, в случае с только что "рожденными" ракетами, гранатами, Shock Projectiles и пр. SimulatedProxy продолжает получать апдейты Velocity если он является симулятивным Pawn, т.к. все Pawn нуждаются в "правильном" значении Velocity для более или менее точного предсказания их движения локально. И наконец, для муверов также реплицируется значение Velocity для того, чтобы клиент мог предсказывать его движения. В старые времена первого Unreal, Velocity для муверов не реплицировался, вместо этого, клиент получал периодические обновления Location, т.к. мувер являлся DumbProxy. В результате, получались дерганные движения муверов при игре по сети. Это было исправлено в Unreal 224 версии и выше.

unreliable if( DrawType == DT_Mesh
                        && ((RemoteRole <= ROLE_SimulatedProxy && (!bNetOwner || !bClientAnim)) || bDemoRecording) )
        AnimSequence, SimAnim, AnimMinRate, bAnimNotify;

Здесь мы встречаем еще несколько переменных, которые имеют отношение к анимации. Эти переменные реплицируеются только в том случае, если данный актер является мешем. Если Вы записываете демку, то анимация всегда пересылается. Иначе, выражение проверяет следующее, довольно запутанное, условие. Если актер является DumbProxy и игрок не владеет этим объектом и переменная bClientAnim не установлена, тогда происходит репликация данных. Рассмотрим пример с оружием: Вы видите анимацию оружия на клиентской стороне, поэтому серверу нет смысла пересылать анимацию клиентам. Это происходит потому, что для всего оружия в Unreal Tournament установлено bClientAnim равным true, указывая тем самым, что за анимацию данного объекта отвечает сам клиент. Если анимация не обрабатывается клиентом и актер является DumbProxy, тогда сервер перешлет эти данные клиенту. SimulatedProxies, включая Pawn, ракеты и другие снаряды, являются симулятивными, и, поэтому не получают анимацию от сервера. Вместо этого, клиент сам берет на себя ответственность за их анимацию с помощью предугадывания. В случае с Pawn, это происходит внутри движка (оставим это для другого туториала ;) и Вам не о чем беспокоиться.

Role и RemoteRole

Надеюсь, последние довольно запутанные выражения и переменные помогли Вам глубже понять концепцию репликации, а также разницу между различными ролями. Не смотря на это, я бы хотел немного вернуться назад, акцентируя внимание на dumb, simulated и autonomous прокси для того, чтобы дать более ясную картину, когда следует использовать каждую из них.

ROLE_Authority - опять же, это роль (Role) для всех актеров на сервере, и удаленная роль (RemoteRole) для всех актеров на клиенте. Говоря простым языком, эта роль означает, что текущая машина - авторитет над этим актером. Это единственное значение, которое может иметь переменная Role на серверной стороне. Остальные значения используются только для указания значений RemoteRole на сервере.

ROLE_None - по сути, означает: этот актер никогда не будет релевантным. Это предотвращает проверку условий репликации для этого актера, и соответственно, этот актер не играет роли в выражениях репликации.

ROLE_DumbProxy - это значение используется для простых актеров, не требующих реального клиент-серверного предсказания. Такие актеры не нуждаются в прогнозировании физики, но, тем не менее, они являются релевантными по отношению к клиенту, что приводит к репликации их переменных. Это значение используется для всех актеров инвентаря (Inventory), когда они расположены на земле. Это не требует применения более "плотного" по отношению к пропускной способности SimulatedProxy, но все же позволяет видеть все необходимое для правильного отображения актера. Если Вы попытаетесь привести в движение этот актер, то помните, что клиент получает только периодические обновления этого актера, обычно каждые пол секунды при среднем коннекте. Такая ситуация приводит к появлению эффекта "рывков" в случае, если Вы используете DumbProxy для актеров с определенными значениями физики, т.к., реально, эта роль предназначалась не для этого. С другой стороны, эта роль очень полезна для обновления координат актера, которому необходимо изменить местоположение на уровне при "респавне" флага и т.д. Эта роль, также, отвечает и за обновления анимации, передаваемые от сервера клиенту, кроме, конечно, тех случаев, когда клиент специально этого не запретил. Короче говоря, актер, для которого установлено это значение роли, является "насильно подпитываемыми" данными с сервера. Эта роль, прежде всего, предназначена для не движимых актеров (за исключением, "прыжков" актеров, например, его транспортация или респавн), но, не смотря на это, являющихся релевантными.
Некоторые особенности: с этой ролью связаны особенности, которые могут существенно отразиться на планировании сетевого кода. Для DumbProxy актеров не вызывается некоторый набор событий на клиентской стороне. А именно: симулированные Tick(), Timer(), код состояний (State) и расчеты физики не производятся на клиентской машине DumbProxy актера. Предполагается, что эти актеры должны получать все данные по сети, и, следовательно, вызов этих событий не производится. Одно исключение из этого правила: расчет физики для такого актера все же производится на клиентской стороне, но только при значении физики PHYS_Falling, что позволяет выкинутому оружию "правильно" падать в сетевой игре. Во время моих ранних попыток создания оружия в туториале (в конце документа), я попытался создать "клиентскую" физику, которая вобрала в себя обновления Location и Rotation, и обновления эти передавались при помощи DumbProxy роли. Но клиент, к несчастью, не производил мои вычисления физики, основанные, главный образом, на Tick(), и мою идею пришлось выкинуть в окно :)

ROLE_AutonomousProxy - эта роль используется для актеров, которыми управляет непосредственно сам клиент. В большинстве случаев ее следует применять для PlayerPawn. Причем это значение RemoteRole должно быть указано только для одного конкретного игрока, для всех же остальных игроков в качестве роли следует указать SimulatedProxy. Игрок, для которого указана AutonomousProxy, является игроком, который представляет собой локальный PlayerPawn. При указании AutonomousProxy, этот актер будет являться SimulatedProxy для всех, кроме владельца или instigator-а. Это гарантирует, что Ваш PlayerPawn будет SimulatedProxy для всех кроме Вас самих. Обратите внимание, что этот же подход используется для управляемого Redeemer (Guided Redeemer). Если сервер будет указывать, куда полетел управляемый Вами Redeemer, то сами понимаете, что такой подход неуместен. AutonomousProxy не просто "работает" на Вас, он отличает Вас от других игроков, которые видят Ваш Redeemer. Как раз это и необходимо, чтобы он действовал для Вас по другому, по сравнению с остальными игроками.

ROLE_SimulatedProxy - здесь следует рассмотреть два варианта этой роли. Действие ее зависит от того, является ли актер Pawn или нет. Рассмотрим отдельно каждый из вариантов.
ROLE_SimulatedProxy (для не Pawn) - в этом случае, роль представляет собой противоположность DumbProxy. В следствие ее использования, ожидается полное предсказывание актера на клиентской стороне, исходя из первоначальных данных. Это подобие экстраполяции графика, где текущая физика является его точками, и имеется некоторые исходные данные, относительно которых и происходит эта экстраполяция. ROLE_SimulatedProxy дает исходное значение Location, Rotation и Physics. Затем происходят обновления значений Velocity по мере их изменения. Эти переменные позволяют производить полное предсказание актера клиентской стороной, предполагая, что вследствие этого будут производиться предсказания физики. В случае с PHYS_Falling и PHYS_Projectile этот подход оправдывает себя. При таких физиках возникновение отклонений невозможно (если, конечно, целеустремленно не "поиграться" с переменными физики, например, bBounce и пр., которые являются очень важными для прогнозирования). Может быть, я объясню подробнее Physics в другом туториале. Может быть :) И, наконец, для этой роли происходит обновление анимации, предполагая, что клиент не является "хозяином" актера и для него не указано использование клиентской анимации. Подводя итог, можно сказать, что эта роль предназначена для актеров, которые беспрепятственно могут предсказывать необходимые данные, находясь на клиенте.
ROLE_SimulatedProxy (для Pawn) - при комбинации это роли с Pawn создается Simulated Proxy другого сорта, который действует иначе, нежели описанный выше. С точки зрения репликации, Pawn-ы являются одними из самых сложных актеров, вследствие того, что, не смотря на попытки предсказывать действия актеров, нужно также иметь в виду, что им свойственна их непредсказуемость. Такого рода поведение невозможно указать для других актеров, кроме написания своего подкласса Pawn, поэтому не думайте, что Вам удастся добиться этого, например, для класса снарядов. SimulatedPawn вобрал в себя все лучшее от DumbProxy и SimulatedProxy. Для него производится обновление Velocity, что позвляет делать предсказания актера на клиентской стороне, в то же самое время происходит обновления Location со стороны сервера для "переопределения" точного положения игрока. Вы, возможно, удивляетесь, почему тогда Pawn не "дергается", когда происходит обновление Location. Объясняю: для Pawn существует нативный код, который заставляет его плавно перемещаться к месту, указанному сервером, в случае, если текущее положение игрока находится далеко от его реального положения. Такой подход позволяет с одной стороны воспроизводить похожую картину реальных движений игрока и, с другой стороны, придерживаться точных координат серверной версии. Все Pawn-ы, не являющиеся игроками, при этом, будут производить прогнозирование исходя только из значений Velocity. Физика для этих актеров учитываться не будет. Это происходит из-за того, что такие Pawn могут обладать PHYS_spider или другой физикой, которые не укладываются в рамки только хождение/падение предсказаний. Игроки и боты - единственные могут обладать специальным клиентским механизмом предсказания для хождение/падение физики. Эта физика искусственная, работа которой заключается в учете значения гравитации текущей зоны для игрока, в случае, если он находится не на земле. Это можно обойти, установив значения bCanFly, которая "замораживает" клиентскую физику, но, тем не менее, должна учитываться в коде, т.к. физика для Pawn не подвергается репликации. В зонах воды, предсказание актеров производится исключительно по Velocity, что является вполне достаточным для такого рода зон, где значение гравитации мало. Актер по любому будет получать обновления положения, поэтому это не играет столь важную роль, даже при долгом движении. В конце концов, актер, также, получает обновления Rotation. Так как Rotation (которая напрямую зависит от ViewRotation, или, говоря просто - от направления, куда смотрит игрок) может радикально меняться, никакие предсказания для этой переменной не производятся. Вместо этого, Rotation игрока просто изменяется на новое значение по мере поступления информации от клиента, который им управляет. Анимация ведет себя также, как и для не Pawn актеров.

Репликация функций, симуляция

Во время игры по сети, функции могут быть вызваны на сервере, на клиенте или на обоих сразу. Иногда нужно первое, иногда другое. Репликация функции и симуляция позволяет достичь этих целей. Не смотря на то, что сначала создается впечатление, будто бы речь идет о двух различных аспектах, они, на самом деле, являются связанными. Я буду обсуждать их вместе, а затем объясню их различия, для того, чтобы Вы имели представление о каждом из них в отдельности.

Внутри, Unreal движок имеет очень простую систему для обработки функций. Он вызывает функцию, которая определяет, может ли движок вызвать UnrealScript функцию на этой машине. Если ответ положительный, то производится вызов функции, если отрицательный - вызов функции не производится. Это все, что репликация и симуляция влечет за собой, единственное дополнение к этой системе - источник UnrealScript симуляции, событий (рассматриваются ниже) и сама репликация функций (большинство которой Вы уже знаете).

Правила Rep/Sim функций

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

  1. Если функция статическая (static), она может быть вызвана на этой машине.
  2. Если это одиночная игра (standalone server), функция может быть вызвана на этой машине.
  3. Если это не реплицированая функция, то переходите к пункту 10.

(остальное относится только к реплицированным функциям, согласно с условием 3)

  1. Если это сервер, и на нем нет родительского PlayerPawn для этого актера (в любой точке родительской иерархии), или если игрок не соединен (игрок находится на listen сервере), то вызов функции производится локально.
  2. Если условия репликации функции не найдено на текущей машине (это означает, что либо функция была сюда реплицирована, либо выражение репликации является недействительным), то переходите к пункту 10.

(остальное относится только к реплицированным функциям, для которых были указаны условия репликации и они являются валидными для репликации)

  1. Если это unreliable функция и коннект (сервер-клиент или клиент-сервер) перегружен, то движок "прикидывается" что функция была вызвана удаленно (например, не вызывает ее локально).
  2. Реплицирует функцию, и не вызывает ее локально.

Последний пункт используется только в случае, если он был вызван выше.

  1. Если это сервер, то функция может быть вызвана.
  2. Если это клиент в виде AutonomousProxy, то функция может быть вызвана локально.
  3. Если это клиент в виде DumbProxy или SimulatedProxy (но не AutonomousProxy), и функция является симулированной, то она может быть вызвана локально.

Хоть это и выглядит сложно, но, тем не менее, это точно описывает в деталях как Unreal управляет функциями, а также всю природу RPC (вызов удаленных процедур). Надеюсь, в нескольких следующих параграфах я смогу прояснить схему, сведя ее к простым правилам.

Подтверждение правил Rep/Sim функций

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

  1. Принадлежать сетевому PlayerPawn в любой точке своей родительской иерархии.
  2. Она должна быть reliable функцией, либо unrealiable, но с достаточной пропускной способность сети на данный момент.

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

  1. Функция является статической (static).
  2. Функция симулированная (simulated).
  3. Это функция AutonomousProxy актера (локальный PlayerPawn на клиенте или клиентский GuidedWarhead).

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

Резюме Rep/Sim функций

Учитывая эти правила, я попытаюсь подвести конечный итог.

На сервере, может быть вызвана любая функция, если это, конечно, не реплицированная (клиент-сервер) функция, которая "встретила" указанные выше условия. Поэтому можно быть уверенным, что все функции будут беспрепятственно вызываться на сервере, кроме тех случаев, когда Вы специально не скажете им этого не делать.

На клиенте, каждая функция имеет свой источник. Источником может служить сервер-клиент реплицированная функция, exec функция или событие (event). События, о которых я пока умалчивал, на самом деле, являются очень простыми. Событие (event) - это любая функция, которая вызывается из нативного кода. События выполняются как на сервере, так и на клиенте. Сервер не говорит напрямую клиенту вызывать событие, но клиент сам знает, когда следует вызывать это событие, исходя из состояния игры (контакт актеров, удар в стену, тик и пр.). Если событие является симулированным, то оно все равно может быть источником вызова функции на клиентской стороне. Источник может вызывать любые другие функции, если, конечно, они прошли клиентские условия, необходимые для их запуска, затем эти функции, в свою очередь, опять могут вызывать любые другие функции и т.д.

Что произойдет, если функция была вызвана, но запуск ее не состоялся по причине того, что, либо она была реплицирована на другой конец соединения, либо не удовлетворила клиентских условия для запуска. В этом случае, во-первых, все будет так, как будто эта функция вовсе и не запускалась. Значение, возвращаемое такой функцией, будет всегда равно 0, "" или None, в зависимости от типа возвращаемой переменной. Все внешние (out) параметры будут возвращены в таком виде, в каком они были в нее переданы. Надежда на получение конечного результата этих функций является источником многих AccessNone, которые дают о себе знать только на клиенте. Exec функции (функции, которые могут быть "забиты" на клавиши), естественно, обрабатываются на клиенте. Не забудьте сделать их симулированными (если, конечно, функция не принадлежит AutonomousProxy актеру, например, подклассу PlayerPawn). В некоторых случаях, Вы, возможно, захотите реплицировать эти функции на сервер, как это сделано в случае с Server-Administrator командами (множество которых определены в PlayerPawn). Exec функции могут быть реплицированы серверу, как и любые другие функции.

Спавнинг в симулированной функции

Рассмотрим еще одну интересную особенность, которую невозможно сразу выявить. Например, из-за того, что актер должен принадлежать PlayerPawn в какой-либо точке родительской иерархии, функция может быть реплицирована только одному игроку в игре (нет, указание в качестве хозяина игрока, другого игрока не сработает :) Широковещательной репликации не существует. Вы можете заставить это работать при помощи симулированных функций, но в этом случае никакие данные не смогут быть переданы между сервером и клиентом (так как обе эти функции имеют свои локальные источники, и существуют только на локальных машинах). Либо Вы можете использовать реплицированные переменные, которые могут и будут реплицироваться всем клиентам, подразумевая, что они являются релевантными по отношению к клиенту.

Если рождение (spawn) происходит в симулированной функции (симулированные функции будут рассмотрены далее), то, в итоге, Вы получите несколько копий только что "рожденного" актера: на сервере и на каждом из клиентов, на котором был произведен вызов функции. Серверная версия, обычно, потом реплицируется клиенту, поэтому клиент в результате будет видеть две версии актера. Это повлечет за собой странное поведение актера, когда обе его версии перестанут быть синхронными. В этом случае клиентская версия легко может быть перепутана с "официальной" версией, из-за того, что по некоторым причинам она не получила обновлений своих переменных.

Если Вам все же необходимо "родить" объект в симулированной функции (существует достаточно причин, чтобы это делать, например, создание чисто клиентских эффектов и пр.), то Вы должны учесть все вышесказанное. Если Вы пытаетесь создать эффект на клиентской стороне, то убедитесь, что сразу после создания, для этого актера будет установлена RemoteRole равная ROLE_None. Так как проверка на релевантность происходит только в промежутках между тиками, то можно быть уверенным, что когда это проверка будет производиться, актер не будет являться релевантным, и, следовательно, не будет реплицирован. Таким образом, сервер и клиент будет иметь по одной локальной копии эффекта, который "отыграет" свое и затем будет уничтожен. Такой подход не применим к долговременным (постоянным) эффектам, так как симулированные функции вызываются не на всех клиентах. Она будет вызвана только на клиентах, содержащих актера, в котором и определена эта функция. Это означает, что если вызов симулированной функции на клиенте не был произведен (потому что на клиенте в тот момент актер не был релевантным), то эффект не будет виден даже после того, как Вы к нему якобы подойдете и он "будет" в поле зрения. Поэтому такой подход стоит использовать только для временных эффектов, которые недолго "живут" и сами себя уничтожают.

Все же, если Вы создаете что-либо долгосрочное или требующее репликации данных с сервера (например, точка прицела), то Вам не следует "рождать" этого актера на клиенте. Обычно, несанкционированное создание актера можно предотвратить проверкой на условие if(Level.NetMode != NM_Client) перед непосредственно самим "рождением". Таким образом, созданный актер затем становится релевантным (учитывая условия релевантности), и его переменные будут реплицированы клиенту. Если же актер был "рожден" и на клиентской стороне, то, в итоге, Вы получите две версии актера, одна из которых будет просто "отсиживаться" без соответствующей информации от сервера.

Оптимизация параметров сетевых функций

Во время репликации функций, Unreal оптимизирует передаваемые ей параметры, при помощи нескольких техник. При передаче векторов (который состоит из трех float переменных), компоненты по отдельности округляются до целых чисел, теряя, таким образом, свои дробные части, а также любые значения не вписывающиеся в границы от -32768 до 32767. Обычная практика, умножать значения вектора перед их передачей, сделав их отскалированными целыми числами, а затем разделить эти числа на тот же множитель для возврата к исходным значениям, которые будут иметь, в итоге, незначительные отклонения. В ротаторах значения разбиваются в пределах от 0 до 255, где промежуток от 128 и выше отвечает за отрицательные значения компонентов ротатора. Затем они округляются до целых чисел и пересылаются по сети. Позже их значения восстанавливаются (за исключением того, что они остаются в виде положительных величин, т.к. они эквиваленты своим отрицательным дубликатам на другом конце соединения). Один подход, который может быть применен для векторов и ротаторов - это передача X/Y/Z или Pitch/Yaw/Roll значений структуры как отдельные float переменные. Таким образом, Вы сможете полностью сохранить точность их значений. И, наконец, planes (или плоскости, которые редко используются) реплицирует свои значения в виде округленных целых чисел.

Симулированные состояния

Другие, много раз пока нами обходимые, особенности представляют собой симулированные состояния. Вы, возможно, уже видели код с использованием состояний (state), а также код, который существует вне функций, но в состояниях, обычно помеченных метками (lable). Такой код, в обычных условиях, выполняется в одиночку на сервере. Но, отметив состояние ключевым словом simulated, Вы, таким образом, можете создавать симулированные состояния. Но имейте в виду, что смена состояний, по умолчанию, не реплицируется и не пересылается клиенту. Тем не менее, если gotostate вызвано из функции на клиенте (либо через репликацию, либо через симуляцию), то клиент будет в курсе о новом состоянии. Симуляция, наследованная от PlayerPawn дает возможность всем своим функциям контролировать состояния, но для других классов необходимо иметь симулированные функции, которые вызываются из симулированного источника, а также цепочку вызовов функций для того, чтобы знать об изменениях состояний. Настоящий UT код делает использование симулированных состояний бессмысленным, но я включил сюда это описание с целью законченности туториала, и на случай, если Вам это когда-либо понадобится.

NetModes и их использование

В объекте Level, который указывает на актера LevelInfo и является доступным из любого объекта, находятся переменная NetMode. Эта переменная используется для описания позиции данного экземпляра UT во всей сетевой схеме. Ниже приведены все возможные значения этой переменной:

NetMode, обычно, не играют столь важную роль, т.к. с помощью ролей (Role) и удаленных ролей (RemoteRole) можно выкрутиться в любой ситуации. Тем не менее, если Вы хотите выполнить какой либо код на клиенте, но не желаете, при этом, его реплицировать, использование NetMode очень облегчит Вам задачу. Например, код выполняется на клиенте посредством цепочки симуляции. Если симуляция выполнена, то Вы затем можете поставить условие 'if (Level.Netmode == NM_Client) {', и код внутри этого блока будет выполнен только на клиенте или на множестве клиентов сразу, если вызов симулированной функции был для них также произведен. Соединив это условие с проверкой на Owner, Вы можете написать блок кода, который будет выполняться только на одном, определенном клиенте. Одна вещь, которая немного все запутывает, это когда Вы хотите убедиться, что Ваш код будет запускаться при одиночной игре, а также для игрока, находящегося на listen сервере. По этой причине, Вам часто будет встречаться следующий код: 'if (Level.NetMode != NM_DedicatedServer) {', как раз то что и необходимо. Другие пример: "рождение" не релевантных джибсов только в том случае, если Вы находитесь не на выделенном сервере (с целью экономии ресурсов CPU). Пример отсюда же: "рождение" джибс-головы. Голова при взрыве должна двигаться в случайном направлении, но в соответствии с движением головы на сервере. Если Вы придадите голове на клиенте случайное значение скорости (Velocity), то по пришествии очередной порции данных с сервера, движение головы будет изменено прямо на лету. Вместо этого, каркас головы не "рождается" на клиенте, вместо этого, сетевой код создает голову и придает ей такую же случайную скорость, как и на сервере. В итоге все движения будут синхронными. Дальнейшее использование оставляю на Вашу фантазию :)

Специальные свойства для исключительных вещей

Использование bNetOptional

Как Вы помните, мы уже не раз говорили об bNetOptional переменной. Теперь, самое время пояснить, для чего она предназначена и где ее следует использовать. Как было сказано ранее, актеры с bNetOptional добавляются в самый конец списка актеров. В результате, их могут обойти вниманием в случае, если пропускная способность переполнена. Если для них остается место, то такие актеры все же подвергаются репликации. На репликацию они имеют 150 мс прежде чем о них забудут окончательно и, соответственно, клиент никогда их не увидит. Для чего это может быть использовано? В большинстве случаев, Вы будете использовать такой подход для временных эффектов, которые реплицируются клиенту, и доживают там самостоятельно свою жизнь. Хороший пример: дымок от пули при попадании в стену. Если пропускная способность сильно нагружена, например, вследствие ожесточенной перестрелки или из-за лага, то эффекта дыма так и не появится. Скорее всего, Вы даже не обратите на это внимания. bNetOptional актеры, также, не будут реплицироваться после исходной попытки их репликации. Их условия для репликации проверяются только один раз, при первом запуске. Такие актеры должны действовать совершенно самостоятельно, без какого либо взаимодействия с сервером. Это уменьшает нагрузку на пропускную способность. Нет никакого смысла, если сервер будет продолжать проверки на релевантность для этих актеров, напрягая, тем самым, и без того обычно перегруженную сеть. Тем не менее, bNetOptional не нужно использовать для всех эффектов подряд. Спрайт взрыва или анимацию взрыва ракет, должны увидеть все. Легко, в пылу сражения, упустить из виду маленький дымок от пули, но огромный взрыв - это уже слишком. Используйте bNetOptional для маленьких, не имеющих жизненно важного смысла актеров, для актеров которые не необходимы. В UnrealTournament эта переменная используется для декалей, фрагментов (это такие маленькие кусочки стены отлетающие при попадании), гильз от пуль, облачка дыма, рикошета (желтые искры от стен) и эффектов при респавне. Обратите внимание, на то, что если в свойствах по умолчанию переменная LifeSpan установлена равной 0, то этот актер будет обрабатываться как любой другой. Если же для актера установлено не нулевое значение LifeSpan, и длина его жизни уже перевалила за 150 мс, то он никогда не будет реплицирован. Любые запросы на уничтожение этого объекта будут реплицироваться клиенту, не смотря на то, в каком периоде своей жизни он находится, т.к. UT будет всегда пытаться убедиться, что клиент уничтожил то, что уничтожил сервер.

Использование bNetTemporary

Раз уж мы здесь, то стоит упомянуть и про bNetTemporary. bNetTemporary актеры предназначены для проведения "отрывистой" репликации, как только кто-то укажет ее. Такие актеры реплицируются сразу после их создания и все. Дальнейшая связь по отношению к клиентским bNetTemporary актерам с сервером не производится. Тем не менее, bNetTemporary актер вовсе не обязательно должен быть в буквальном смысле временным, слово temporary (временный) в данном случае относится только к его репликации, которая производится только один раз. Это используется во многих ситуациях. В UT, все декали и снаряды (projectiles) являются bNetTemporary по умолчанию. Декаль реплицируется всем клиентам, по отношению к которым, она была релевантна во время "рождения", затем связь ее с сервером прекращается, что положительно влияет на нагрузку пропускной способности. Это означает, что если Вы пришли на место баталии, свидетелем которой Вы не были, то Вы не заметите здесь ни одной декали. Они были "временно" реплицированы, поэтому Вы никогда не увидите декаль, если не застали ее во время своей одноразовой репликации. В игре Half-Life декали не были bNetTemporary, поэтому они реплицировались каждому клиенту, в случае, если они попадали в поле его зрения. Это послужило причиной множества лагов для всех игроков, т.к. во время игры "рождается" большое количество декалей и спрей-рисунков. bNetTemporary актеры должны уничтожаться из симулированного кода на клиентской стороне по той причине, что серверный запрос на уничтожение таких объектов не реплицируется клиентам, вследствие их bNetTemporary статуса. Это нужно производить тогда, когда декали утрачивают свою актуальность. Они должны уничтожать себя сами, исходя из клиентских данных, т.к. сервер, в данном случае, ничего о них не может сказать клиенту. Большинство снарядов (projectiles) используют переменную bNetTemporary. Взять, к примеру, лезвия от RazorJack. При репликации, сервер передает начальную скорость клиенту, этого вполне достаточно, чтобы клиент, в дальнейшем, мог самостоятельно ими управлять. Так как движение лезвий определенно и очень предсказуемо, то клиент может без посторонней помощи управлять ими со 100% точностью. Стены, от которых они отлетают на сервере... это такие же, эквивалентные стены, что и на клиенте. Вследствие их экстремальной предсказуемости, серверу достаточно только произвести одноразовую репликацию, а затем ими вплотную займется сам клиент. По этой причине, снаряды являются bNetTemporary. Проблемы могут появиться, когда игрок заходит в большую комнату, повсюду заполненую летающими лезвиями. По той причине, что для них указано bNetTemporary, локальный игрок не будет ничего знать об этих лезвиях. Поэтому, заходя в якобы пустую комнату, он может запросто лишиться головы. Но, т.к. люди не играют с FOV равным 360, то такие ситуации, скорее всего, спишут на то, что лезвие прилетело со спины и он его просто не заметил. В результате никто на это не обращает внимания. Некоторые снаряды, например, управляемый Redeemer или же обычный снаряд Redeemer (который может быть сбит во время полета) не может быть bNetTemporary по той причине, что сервер должен иметь возможность передавать клиенту обновления их данных. Зеленые "сопли" от BioRifle также должны быть видны на полу, когда Вы заходите в комнату (т.к. они являются довольно долгоживущими минами), поэтому и они не могут быть bNetTemporary. Существуют еще примеры, почему снаряды не могут являться bNetTemporary, но, я думаю, что идею Вы уже уловили.

Особое поведение PlaySound и PlayOwnedSound

На это поведение я обратил свое внимание, когда кто-то пытался выяснить, почему в его моде не работал звук, несмотря на то, что он отлично работал в самом UT. После просмотра скриптов, я не смог найти, как же вообще работает звук в UT. Для него не было указано ни репликации, ни симуляции. В конце концов, выяснилось, что звук имеет свои скрытые механизмы работы, зависящие от того, каким образом был вызван этот звук. Игра узнает громкость звука исходя из расстояния, препятствий и пр. UT, при этом, пытается определить "правильный" и грамотный путь передачи и распространения звука, в зависимости от того, как он был вызван. Для начала, разберем работу PlaySound. Эта функция может быть вызвана в двух контекстах:

Из симулированной функции или из реплицированной функции
В этом случае, звук будет воспроизведен во многих местах. Природа симуляция такова, что любой игрок, который видит актера, ответственного за звук, получает симулированные события. В результате, каждый клиент будет воспроизводить звук самостоятельно, вследствие симуляции. В случае реплицированной функции (которая запускается на клиенте), UT считает, что клиент знает, что он делает, и что он желает воспроизвести этот звук только на клиенте. Вы, должно быть, удивлены, зачем нужно производить проверку реплицированных клиенту функций, если проверка на симуляцию уже была произведена (т.к. все реплицируемые клиенту функции должны быть симулированными). Но если Вы помните, то PlayerPawn, имеющий роль ROLE_AutonomousProxy, может запускать реплицированные функции, которые не объявлены симулированными. Поэтому проверка реплицированных функций как раз и предназначена для таких ситуаций. В общем, такое локальное воспроизведение звука и есть то, чего Вы желаете добиться, вызывая PlaySound из симулированных функций.

Из не симулированной функции или из не реплицированной функции
При вызове PlaySound в таком контексте, UT производит некоторую дополнительную работу. В этих ситуациях, PlaySound вызывается на серверной стороне. Это не очень практично в реальной игре, т.к. звук никто не услышит. Вы можете подумать, что вызов достаточно только реплицировать, но Вы забываете, что репликация функций не может быть широковещательной, в результате звук услышит только владелец актера, у которого и был вызван PlaySound метод. Симуляция тоже не поможет, т.к., по сути, она ничего не передает по сети. Вместо этого, UT производит некоторого рода "магию". В классе Pawn определена функция ClientHearSound:

native simulated event ClientHearSound (
        actor Actor,
        int Id,
        sound S,
        vector SoundLocation,
        vector Parameters
);

Функция ClientHearSound реплицируется от сервера к клиенту, в соответствии с выражением репликации в классе Pawn. Учитывая, что вызов такой функции может быть передан только владельцу, нас это вполне устраивает, т.к. в результате вызова Pawn.ClientHearSound(someactor...) на сервере, звук будет услышан этим Pawn на клиенте, таким образом, как будто звук исходил по направлению от someactor. Вся магия заключается в том, что UT удачно превращает вызов функции PlaySound в широковещательную репликацию функции ClientHearSound. Когда происходит вызов PlaySound, она пробегает по всем, расположенным рядом, актерам, которые должны услышать издаваемый звук, и реплицирует каждому из них вызов функции ClientHearSound. Таким образом, каждый клиент который должен услышать этот звук, услышит его. Определение актера, являющегося источником звука, производится по первому параметру, передаваемому в метод ClientHearSound.

Довольно ловкий выход из ситуации для функции PlaySound, т.к. для большинства случаев, функция работает, в принципе, так как и "положено". В случае же, если она не работает, как следует, то, при выяснении неполадок, Вы должны учитывать ее дуализм, и выяснить, каким образом следует ее вызывать (или использовать вызовы своих функций, отвечающих за репликацию звука) для корректной работы.

Другое, интересное поведение можно наблюдать у функции PlayOwnedSound. Цель этой функции - воспроизвести звук у всех кроме владельца. Сперва, это может показаться очень странным, но эта техника используется в нескольких местах. В UT это используется, в основном, для оружия. Когда клиент стреляет из какого либо оружия, он должен сразу это прочувствовать. Поэтому для него, на клиентской стороне, воспроизводится анимация, а также он слышит звук выстрела. Тем не менее, клиент сам не может заставить звук воспроизводиться на других машинах. Все, что происходит локально, это воспроизведение звука оружием сразу после его выстрела. Затем клиент сообщает серверу, что был произведен выстрел, далее, сервер вызывает функцию PlayOwnedSound, в результате чего этот звук слышат все клиенты. По понятным соображения, Вы не захотите, чтобы этот звук был также воспроизведен на клиенте, который произвел выстрел, т.к. для него этот звук уже прозвучал. Надеюсь, что этот пример наглядно показал работу функции PlayOwnedSound, в противном случае, если Вы не удовлетворены, обратитесь к "оружейным" UT скриптам. Одно маленькое замечание: когда UT пытается определить "хозяина" звука, он проверяет только самого актера (для выяснения, является ли он Pawn или PlayerPawn), а также проверяет прямого владельца актера. При этом просмотр иерархии вверх не происходит, как это делают другие функции.

ClientPlaySound и ClientReliablePlaySound

Это простой пример, но тем не менее, заслуживающий внимания. ClientPlaySound является unreliable функцией, которая воспроизводит звук на клиенте. В результате вызова PlayerPawn.ClientPlaySound(...) происходит локальное воспроизведение звука на машине. Такой звук является "бездомным", т.е. он воспроизводится таким образом, будто он исходит из головы (а не рядом, напротив или др.). При вызове этой функции на клиенте, она гарантированно будет запущена, т.к. в этом случае, репликация не будет в этом участвовать (это будет симулированная функция, которая вызвана из другой симулированной функции...). Когда же вызов функции происходит со стороны сервера, тот нет никаких гарантий, что она достигнет другого конца соединения. Что делать в случае, если Вы хотите быть уверенным, что звук будет воспроизведен на клиенте? Конечно, используйте функцию ClientReliablePlaySound. Она является reliable функцией, что дает гарантии достижения и воспроизведения звука клиентом. Здесь я приведу код функции ClientReliablePlaySound, для того, чтобы Вы могли понять, как она работает:

simulated function ClientReliablePlaySound ( 
        sound ASound,
        optional bool bInterrupt,
        optional bool bVolumeControl
)
{
        ClientPlaySound(ASound, bInterrupt, bVolumeControl);
}

Как Вы можете видеть, reliable определение функции в классе PlayerPawn дает гарантии ClientReliablePlaySound, что она будет запущена на клиенте. Как только это произойдет, из нее последует вызов функции ClientPlaySound, который уже будет вызван локально, и, следовательно, звук будет беспрепятственно воспроизведен.

Когда вещи работают неправильно

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

Выясните, что происходит на самом деле

Вам нужно изолировать и выделить проблему. Знать точно, что происходит. Что реплицируется, а что нет. Потом, убедится, что это всего лишь предположения, и, не смотря ни на что, на них полагаться нельзя. Учитывая все то, что происходит во время репликации, самое легкое - это переложить всю ответственность на то, что происходит на скрытом от нас, нативном уровне. Но после прочтения данного туториала, Вы, фактически, сможете решать все проблемы самостоятельно. Я думаю, что в этом документе я затронул все нативные аспекты репликации. Поэтому документ получился таким объемным. Я помню, когда я начинал программировать и встречался с проблемами репликации, то мне так не хватало подобного документа. Я думаю (уверен, что так думают и остальные), что лучше иметь слишком подробное описание, нежели слишком скудное. В любом случае, теперь, когда я имею доступ к исходникам движка, я точно знаю, как работают все эти вещи. Я смог выяснить природу некоторых проблем с сетевой игрой, даже тех, которые возникали не по моей вине. Я говорю это к тому, что знания, как все работает на скрытом уровне, позволяют легко решать проблемы, с которыми Вы столкнулись. Вам не понадобится для этого идти путем проб и ошибок для выяснения места, где код дает "сбой". Просто разобрав и продумав природу проблемы, а также анализируя UnrealScript код, Вы можете выйти на верный путь ее решения.

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

Проверьте выражения репликации

После того, как Вы выяснили, что именно не было передано по сети, проверьте выражения репликации. В 98% случаев, Вы сможете выяснить, почему не произошла передача данных, подробно исследовав выражение репликации. Ранее, в этом туториале, в разделе репликации переменных, я приводил много примеров по выражениям репликации у актеров. Прочитайте этот раздел для ознакомления с наиболее часто используемыми выражениями. Продумывая каждое условие репликации в отдельности, может помочь Вам выявить проблемную ситуацию. Во многих выражениях репликации присутствует переменная bNetInitial, которая отвечает за то, чтобы репликация была произведена только один раз, когда текущий актер впервые становится релевантным. Это приводит к тому, что данные не будут реплицироваться для этого актера в течении всей последующей игры, репликация, иногда, даже не будет произведена, если этот актер пропал из виду, а затем снова появился и, следовательно, снова стал релевантным. Также, ознакомьтесь с описанием репликации и симуляции функций, для того, чтобы определить, все ли условия удовлетворяет Ваша функция для ее передачи по сети, и прошла ли она проверку на клиентской стороне.

Один UnrealScript программист, с которым я разговаривал, столкнулся со следующей проблемой: у него не производилась репликация Velocity для гранат после их выстрела. Но это происходило в результате того, что для них было указано bNetInitial, и, следовательно, никаких последующих обновлений скорости (Velocity) не производилось. Поэкспериментировав с переменными, он выяснил, что удаленная роль равная ROLE_DumbProxy решает все его проблемы, т.к., во всяком случае, гранаты начали двигаться. Очень трудно было предугадать проблему при тестировании игры по LAN и даже при коннекте к собственному серверу, по причине того, что пинг был довольно маленький и сеть обладала высокой пропускной способностью. Но могу Вас уверить, что игроки, сидящие на модемах сразу бы заметили эту проблему :) Вспомните различные свойства ROLE_DumbProxy и ROLE_SimulatedProxy, а также их побочные эффекты, особенно по отношению к репликации. Я сталкивался с несколькими случаями, в которых именно это и являлось причиной странного поведения. Проблема в его подходе заключалась в том, что он исходил из получения координат (Location) от сервера, но в реальной игре по сети такой подход никогда не будет работать, т.к. игроки не смогут получить обновлений координат. В этом случае, гранаты даже не покидали инвентарь. ROLE_SimulatedProxy являлся лучшим решение его проблемы, за исключением того факта, что гранаты не должны быть bNetInitial во время выстрела. Вместо этого, все что нужно было ему сделать, это "родить" нового актера, и указать ему все необходимые свойства "выкинутого" предмета. Это удовлетворяет условия для репликации переменных у ROLE_SimulatedProxy актера, и, следовательно, гранаты будут корректно выкидываться, корректно предсказываться и т.д.

Логи - Ваши друзья

Если это не помогло, то попытайтесь выявить проблему при помощи логов. Если у Вас запущено две копии UT, т.е. сервер и клиент, то генерируются два типа логов. Один - серверный лог, другой клиентский (хех :) Если Вы не уверены в том, что функция запускается на клиенте, то включите в нее лог. Затем проверьте, появилась ли соответствующая запись в клиентском логе. Если да, то, значит, функция отработала на клиенте, и причина странного поведения заключается не в ней. Если записи лога не было, то функция не выполняется на клиенте. Вы, возможно, будете уверены, что это не так, т.к. она является единственным объяснением странного поведения. Но логи не врут. Очень часто встречаются такие ситуации, когда функция отработала на серверной стороне, изменив, при этом, некоторые переменные, которые, затем, были переданы клиенту, что наводит на ложные выводы о том, что функция все же отработала на клиенте. Как я и говорил ранее, такое поведение может легко ускользнуть из виду при тестировании по LAN, но оно даст о себе знать в реальных условиях. Я расскажу, как тестировать вещи в реальных условиях чуть позже. Логи - Ваши друзья, и Вы обязаны познакомиться с ними поближе. Большинство UnrealTournament программистов очень плотно с ними знакомы, и если они уже сталкивались с проблемами репликации, то, можно с уверенностью сказать, что именно с помощью логов они пытались понять, что же происходит. Тем не менее, использование в совокупности техники логов со знанием, что действительно происходит, повысит процент того, что проблема будет найдена и успешно устранена. Я опишу некоторые исследования на этот счет в конце документа. Вы сможете увидеть конкретно, что происходит, по мере создания каждой части исследования, надеюсь, что примеры из реальной жизни помогут Вам прояснить полную картину.

Тестирование в реальных условиях

Тестирование сетевого мода без сети

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

Windows 9x

На платформах Windows 9x, UT клиент, по умолчанию, поглощает 100% CPU ресурсов. Поэтому все сторонние программы, такие как ICQ, mIRC или любые другие, которые Вы можете запустить, нужно закрыть, чтобы они не "воровали" часть ресурсов под себя и UT работал в полном приоритете. Это означает, что если Вы запустите клиент и сервер на одной машине, серверу будет явно не хватать мощности CPU, а UT клиент будет испытывать лаги и потери пакетов. Даже если Вы хотели бы специально получить подобный эффект, это неверный способ его реализации. К счастью, кое-кто уже позаботился об этом и написал простенькую программку для тестирования UT клиента и сервера одновременно. Изначально, эта программка предназначалась для решения подобных дилемм на QuakeWorld серверах, но она отлично подходит и для нашей ситуации.

Рассмотрим ее работу. Для начала скачайте ее: priority.zip и распакуйте куда-нибудь, например, в директорию UnrealTournament\System. Затем запустите сервер. Лучше всего запускать его из командной строки при помощи "ucc server". Основной формат строки для запуска Вашего мода выглядит следующим образом:

ucc server mapname.unr?game=packagename.classname?packagename1.classname1,packagename2.classname2

Вот еще несколько примеров командной строки для запуска сервера:

Теперь необходимо указать программе priority дать UT серверу больший приоритет. Вы можете ознакомиться с прилагаемым к программе readme файлом, если Вы желаете узнать какие параметры за что отвечают. Но в нашем случае, для тестирования UT клиента/сервера, просто передайте на выполнение команду:

priority -title ucc

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

unrealtournament 127.0.0.1

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

Windows NT/2000

Для платформ на базе Windows NT/2000 эта проблема отпадает сама собой, т.к. NT поддерживает "настоящую" многопоточность, и использование priority не понадобится.

Симуляция лагов и потерь пакетов

Тестирование мода вышеприведенным способом решает только часть проблемы. Этот способ дает возможность выяснить, запускается ли мультиплеер или нет. Тем не менее, если мод прошел этот тест удачно, это еще не говорит о том, что он будет работать в реальной сети. При тестировании работы мода по сети, Ваша машина производила соединение к самой себе, причем с очень быстрым коннектом. Тестирование мода по LAN имеет тот же "недостаток", т.е. Вы не получаете эффект игры людей по модему, на сервере, который находится на другом полушарии. К счастью, в UT включены параметры командной строки, которые помогут Вам добиться подобного эффекта. Это: PktLoss, PktOrder, PktLag и PktDup. Эти параметры влияют только на исходящие данные, поэтому, для симуляции реального лага, Вам необходимо будет указать их при запуске и сервера, и клиента. Если Вы откроете UnrealTournament.ini, то Вы можете заметить следующие параметры: SimPacketLoss и SimLatency. Они использовались давным-давно, в далекой-далекой галактике. Теперь они абсолютно бесполезны, и Вы можете вообще их удалить из ini файла.

Реальный способ получить лаг - это применить параметры командной строки при запуске UT. Я затрону и приведу описания всех этих параметров в этом документе. Для того, чтобы воспользоваться их услугами, Вам необходимо запустить UT следующим образом:

unrealtournament.exe server.ip.addr?PktLoss=4?PktLag=400

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

ucc server mapname.unr?PktLoss=10?PktLag=600

В результате будет создан очень "плохой" сервер с ужасными условиями работы... возможно, даже, слишком ужасными (если Вы играете при пинге 600 и 10% потере пакетов, то мне Вас чисто по-человечески жаль :) Перейдем, к описанию, собственно, переменных. PktLoss будет работать всегда. Остальные три параметра: PktOrder, PktLag и PktDup являются взаимоисключающими, т.е. если в командную строку будут переданы несколько параметров одновременно, то они будут проверяться в той последовательности, в какой они были переданы. Например, если Вы передадите PktLoss, PktLag и PktOrder, то будут использованы только параметры PktOrder и PktLoss. PktOrder имеет более высокий приоритет, нежели PktLag, а PktLoss используется в любом случае.

PktLoss

Начнем с PktLoss. Этот параметр влияет на процентное содержание пересылаемых пакетов, а именно устанавливает процентное содержание их потерь при передаче. Если для этой переменной было указано значение 10, то при передаче данных клиенту будет потеряно 10% пакетов. Любая потеря пакетов, обычно, в реальных условиях, очень сказывается на качестве связи, в результате, многие люди жалуются на любые, превышающие 0%, потери. При тестировании, это значение не должно быть сильно высоким, в пределах от 0 до 10. Установите для него следующее значение: PktLoss=5

PktOrder

Далее рассмотрим PktOrder. Он определяет, пересылать ли пакеты в смешанном порядке. Это обычный логический флаг, т.е. эта опция, в отличие от PktLoss, может быть либо включена, либо выключена. Если флаг установлен, то Вы, тем самым, предотвращаете использование параметров PktLag и PktDup (они описаны ниже). PktOrder работает следующим образом: каждый раз перед пересылкой пакета, он заносится в список "рассылки". Затем UT проходит по этому списку и отсылает половину данных из него. Не смотря на то, что выбор и не происходит случайным образом, это не играет существенной роли. Цель все равно достигнута: пакеты пересылаются в смешанном порядке, т.к. каждый из них может находиться в списке "рассылки" неопределенное количество времени. Установите для PktOrder следующее значение: PktOrder=1

PktLag

Если Вы хотите отложить пересылку пакета и симулировать лаг, то параметр PktLag как раз то, что Вам нужно. Эта переменная исключает работу параметра PktDup, а сама зависит от состояния переменной PktOrder (флаг PktOrder должен быть выключен). PktLag задает значение в миллисекундах. Пересылка всех пакетов будет задержана на время, указанное этой переменной. Установите для PktLag следующее значение: PktLag=450

PktDup

Если Вам необходимо имитация повторной пересылки пакетов, то воспользуйтесь услугами PktDup. Этот параметр работает только в случае отключения переменных PktOrder и PktLag. PktDup задает процентное содержание пакетов, передача которых будет дублироваться. Каждый пересылаемый пакет имеет шанс быть передан дважды. Все остальные пакеты будут передаваться в единственном экземпляре. Установите для PktDup следующее значение: PktDup=20

Использование параметров

Теперь, когда Вы знаете за что отвечает каждый из параметров, я приведу примеры их использования. Обратите внимание, что эти параметры распространяются только на исходящие пакеты. Поэтому, если Вы установите PktLag равным 500 на сервере, то клиент испытает лаг продолжительностью 500 мс в промежутке между тем что передает сервер и что видит клиент. Клиентские пакеты, передаваемые серверу, при этом, будут пересылаться с обычной скоростью, например, у меня это происходит примерно за 40 мс. Для того, чтобы имитировать реальный лаг, Вам понадобится установить параметр PktLag как на сервере, так и на клиенте. Если Вы помните, пинг - это время, необходимое для того, чтобы данные совершили полный цикл, т.е. были переданы от клиента серверу и затем обратно клиенту. Для симуляции пинга равного 500, лучше всего установить 250 PktLag на обеих машинах. Все клиенты, присоединенные к серверу, будут испытывать лаг продолжительностью 250 мс, не смотря на их локальные настройки. Затем клиент сам будет добавлять дополнительный лаг, указанный PktLag переменной, в итоге Вы получите реальный лаг длительностью 500 мс.

Из всех приведенных выше параметров, Вы чаще всего будете пользоваться переменными PktLoss и PktLag. Остальные параметры были предназначены больше для самих Epic, для тестирования их собственного сетевого кода, тестирования, проходившего в экстремальных условиях, которые в реальной жизни, как правило, не возникают. Я сомневаюсь, что Вам потребуется использование PktDup или PktOrder, кроме тех случаев, когда Вы не сможете воссоздать проблемы, возникающие у игроков, которые Вам о них сообщают.

Заключение

Вот и все. Надеюсь, теперь Вы знаете о репликации больше, нежели Вы знали ранее. Я допускаю, что все это может Вам показаться абсолютно ошеломляющим и запутанным. Но по мере написания Вами различных модов и тестирования этих принципов, Вы ближе познакомитесь с репликацией и Вы начнете понимать, что происходит на самом деле. Скорее всего, Вам необходимо будет прочитать этот документ несколько раз, для закрепления, прежде чем Ваши знания улетучатся. Если Вам некоторые разделы покажутся слишком запутанными, даже после того, как Вы прочитали это несколько раз, Вы можете связаться со мной по почте: mongo@planetunreal.com. Не ждите от меня персональной помощи, т.к. я могу только себе представить бесконечное количество писем от новичков, которые, не уловив основ, застряли в самом начале. Это всегда практически неизбежно. Конечно, все эти люди, возможно, даже не дочитав до конца документ, все равно будут слать мне письма, поэтому я предупреждаю таких людей заранее :) Я буду прочитываться все письма, но только если в них будут какие либо полезные вещи и предложения, стоящие того, чтобы включить их описание в туториал, или указывающие на неточности или слишком запутанные аспекты, описание которых встречается в документе. В этом случае, я подкорректирую этот туториал и выложу его обновленную версию.

Когда что-то не работает, не перекладывайте всю вину на нативный код репликации. Я думаю, что, в этом документе, я удачно набросал схему всего скрытого поведения репликации. Если ошибка и возникнет, то, как бы Вам неприятно было это слышать, это будет ошибка в Вашем коде. Доверьтесь мне и моему опыту :) Проверьте выражения репликации, убедитесь, что логика в порядке, проследите за поведением кода, как на сервере, так и на клиенте, выясните, что происходит на самом деле. Убедитесь в том, что необходимые для репликации данные, действительно реплицируются или симулируются, используя способы, описанные в этом документе. И как я говорил ранее: лог лог лог лог. Используйте логи повсеместно в Вашем коде, они покажут Вам, что происходит во время выполнения скрипта. После того, как Вы проверили все директивы кода, единственная, реальная возможность убедится в том, что все работает именно так, как Вы это запланировали - это использовать логи, и по ним анализировать, что где происходит. Помня все это, я уверен, что Вам будет сопутствовать успех во всех "сетевых" начинаниях.

Удачи!

Специальные сетевые хитрости (исследования) - требуется описание

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

Ознакомьтесь основательно с вышеприведенными разделами документа: специальные вещи, когда вещи работают неправильно, тестирование в реальных условиях, серверные пакеты, флаги пакетов, нативные функции симулируются автоматически (?), изменение состояний только видны в PlayerPawn, если они не были изменены из симулированной функции (что вызывает проблемы с вновь релевантными актерами), "оконная" информация реплицируется клиенту раньше, чем остальные данные, исследования:

Репликация оружия и его работа по сети. Описание (где?)
Другие хитрости:

Автор

Mike "Mongo" Lambert
mongo@planetunreal.com
Pipeline Productions
Перевод сделан 32_Pistoleta

Хостинг от uCoz