Тут довольно сложная ситуация.
Если исходить из того что нужно использовать один и тот же движок на клиенте и на сервере, то возникают следующие проблемы:
1) Если использовать PhysX Unity на клиенте, то мы не можем гарантировать что на сервере будет использоваться та же версия PhysX. Да Unity публикует данные о том какую версию PhysX они используют, но в той версии которую они поставляют с Unity вполне могут быть кастомные изменения для поддержки движка. Либо использовать Unity на сервере, но на мой взгляд это плохое решение.
2) Если не использовать PhysX, то появляется проблема портации C++ на мобильные платформы и интеграция его с Unity. Наш проект запускался одновременно под Android и iOS. Тратить несколько месяцев разработки на такую задачу, когда проект находится на стадии софт-лаунча, довольно рискованно.
К тому же, как я уже говорил, у нас довольно простая физика в игре, все что нам требовалось делать рейкасты и свипкасты.
Не понял ваш комментарий по поводу «вектора изменений».
Физика на клиенте нужна, для расчета предикшена локального игрока, в частности такие вещи как передвижение и ассист в системе прицеливания делаются на основе рейкастов. Более подробнее как работает сетевой код я писал в этой статье habr.com/ru/company/pixonic/blog/415959
Если же вы про то, зачем нам на клиенте хранить историю физики (UnityPhysicsHistorySlice). То в Production-билдах не зачем, вся история храниться в ECS. История физики нужна только для специального режима «локальной симуляции», когда клиент помимо собственно клиентского кода, еще эмулирует сервер. Это довольно сильно ускоряет прототипирование фичей, так как можно разрабатывать игровую логику, без редеплоя сервера.
//Обрабатываем клик левой кнопки мыши
if (Input.GetMouseButtonDown(0))
{
//Берем точку по которой игрок нажал и отправляем всем компонентам уведомление
var hit = GetMouseHit();
Events.PublishAsync("poittogound", new PointOnGroundEventData { Sender = this, Point = hit.point });
}
protected override void OnUpdate()
{
//Передача состояния по позиции агента
if (agent.remainingDistance > agent.stoppingDistance)
{
Events.Publish("agentmoved",
new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity });
}
else
{
Events.Publish("agentmoved",
new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero });
}
}
Массивы
protected override Task[] OnUpdateAsync()
{
//Передача состояния по позиции агента
if (agent.remainingDistance > agent.stoppingDistance)
{
return new Task[] { Events.PublishAsync("agentmoved",
new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) };
}
else
{
return new Task[] { Events.PublishAsync("agentmoved",
new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) };
}
}
И это только для управления одним персонажем. А если в у нас будет в сцене 5-10-20 объектов построенных на этой системе?
Возможно для десктоп платформ это еще может работать в небольших инди проектах, то для мобильных проектов это недопустимо. На проектах которых мне довелось работать, количество аллокаций в кадр было в районе 500 — 600 байт. При больших значениях у вас GC будет вызваться и отрабатывать настолько часто и долго, что добиться стабильных 30 FPS не удасться
1) Нет, все компоненты привязаны к какой-то entity. Я упустил в статье момент создания SpawnAvatarRequest. Он у нас происходит не внутри систем ECS, а при первоночальном создании GameState:
var avatarRequestEntity = gs.WorldState.CreateEntity();
avatarRequestEntity.AddSpawnAvatarRequest(1); // создаем игроков на первом тике мира
2) Да такой механизм нужен и используется. Это по-сути одна из «фичей» ECS. Когда можно динамически добавить новое поведение к уже существующей сущности. На примере нашего шутера игроку в рантайме могут добавляться компоненты: неуязвимости, невидимости, ускорения движения и т.д. Пример кода из системы расчета невидимости:
if (ghost.IsInHidingZone) // если игрок в зоне "невидимости"
{
playerEntity.DelAimable(); // удаляем компонент, который отвечает за то что в игрока можно целится
playerEntity.AddInvisible(); // добавляем компонент невидимости
}
3) Да такие ситуации бывают, вообще ответ на этот вопрос достоин отдельной статьи. О том как работает синхронизация я рассказывал в статье о сетевом коде проекта
Если вкратце, то клиент просто пропускает симуляцию нескольких тиков в случае хиккапа, и берет последние валидные данные с сервера. (игрок при этом видит подвисание).
4) Верно у нас в интерфейсе entity для каждого компонента существуют методы Add*MyComponent*() и Del*MyComponent(). Однако мы используем кодогенерацию в нашей ECS, и вам как программисту нужно лишь написать свой компонент, и указать что он используется в Enitity. Вот пример как выглядит класс Entity в проекте кодогенерации:
public class Entity
{
public Transform Transform;
public Movement Movement;
public CharacterRotation CharacterRotation;
[DontPack]
public AmmoToAdd AmmoToAdd;
[DontPack]
public ShotDamage ShotDamage;
}
Кодогенератор добавить в проект все необходимые методы по созданию, удалению, итерированию и сериализации этого компонента.
В нашей системе тик произойдет в любом случае. Спавн персонажей выполняется только на сервере, а сервер тики не пропускает. Данный пример был взят из реального проекта где используется отложенный по времени спавн персонажа. В целом все таймеры в игре реализованы таким же способом. Записывается время тика на котором должно произойти событие. И условие которое проверят текущее время системы.
Тот же самый метод мы используем в примере HealthPowerUpMovementSystem:
Репозиторий один, солюшина два. Первый — тот, который генерирует Unity, второй — с серверным и общим кодом. Общий код лежит внутри проекта юнити, в папке Assets.
Добавление общего кода происходит через серверный солюшн, а Unity из коробки автоматически подхватывает все новые файлы в свой солюшн.
Для нас основным плюсом в переходе на такую систему было упрощение вливание фичей в develop. (Мы работаем по стандартному gitflow) В системе с 3 репозиториями, когда делается фича нужно было синхронизировать время вливание фичи во всех репозиториях. А так же создавать отдельные pull-requests в каждом репозитории. Для нашей команды это было неудобно.
Как это (пропуск или добавление тика) отражается на поведении объектов, видны ли какие-то артефакты?
Артефакты игроку не видны, если дополнительные тики происходят редко (один дополнительный тик раз в несколько секунд).
Если выполнять ускорение/замедление симуляции чаще, то это проявляется для игрока как ускорение/замедление объектов в игре. Например персонаж начинает двигаться не с постоянной скоростью, а то ускоряясь, то замедляясь.
Еще один вопрос. Решали ли вы вопрос с шарингом общего сетевого кода? Насколько я понимаю, сервер — это standalone решение, тогда может возникнуть желание работать с common частью в и вне юнити.
У нас шарится весь код симуляции (ECS). Изначально у нас было три репозитория, клиент, сервер и сабмодуль шареного кода. Но позже пришли к выводу что команде удобнее работать в одном репозитории, где просто существует папка с шаренным кодом.
Проводили замеры из Москвы до серверов в Амстердаме, на офисной сети WiFi, 4G и 3G.
Из операторов MTC и Билайн.
На 4g в среднем пинг 50 — 60 ms.
3g — ближе к 60-65 ms.
По-мимо того что отметили в комментарии habr.com/company/pixonic/blog/415959/#comment_18845211
Хочу добавить что:
1) Пересылка помимо инпута еще и состояния мира с клиента на сервер влечет за собой значительное увеличение трафика. На мобильных сетях это критично.
2) Что делать когда два клиента шлют противоречивую информацию о мире (например оба стоят в одной позиции)
Основные проблемы с реализацией сетевого кода с которыми мы столкнулись это был объем трафика, потери в сети и производительность симуляции мира на клиенте.
Ваше решение снижает нагрузку на сервер (не нужно симулировать мир с большой частотой), но практика реализации нашего проекта показывает что это не самое узкое место в мультиплеерных играх
500 — это абстрактная цифра с вашего примера. Но да, мы замеряли пинг на различных сетях: в целом ситуация по пингу зависит от того как далеко находятся пользователи от гейм. серверов. Тут нужно найти золотую середину между количеством серверов в регионах и качеством геймплея у пользователей
Ubisoft тогда разделил обработку попаданий по задержкам пользователей — пинг <100 обрабатываем просто, пинг > 100 и < 500 по более сложному алгоритму, пинг >500 отключаем игрока.
Спасибо, поресерчим этот вопрос. У нас грубо говоря сейчас только два кейса пинг <= 500, и пинг >500. Если пинг выше, отключаем игрока, если ниже считаем по алгоритму из статьи
Раз мир синхронизируется целиком, то невозможно фильтровать данные, например как это сделали в Battlefield 4 или 1? Или есть еще куда уменьшать трафик?
Да у нас не используется конкретно этот метод, однако каждому игроку отправляются только те данные, которые необходимы ему для визуализации картинки, и расчета предикшена. Так же у нас применяется дельта-компрессия, если какая-то сущность не изменилась между тиками, данные о ней не отправляются на клиент.
Мы выпустим отдельную статью о всех оптимизациях трафика которые использовали на этом проекте.
Тут дело в том что, если пакет с выстрелом не дошел до сервера вовремя — выстрел на сервере уже не применится. Сервер обрабатывает инпут только для текущего тика. Если на сервере уже наступил тик N, и в этот момент с клиента дошел инпут для тика N-1, сервер просто проигнорирует эти данные.
Для того что бы уменьшить количество потерь при отправке инпутов игрока мы используем механизм скользящего окна, и буфер инпутов на сервере.
Если же потеря пакета все же произошла, произойдет расхождение состояний мира клиента и сервера, и на клиенте отработает механизм согласования (reconciliation).
Да мы используем UPD. В данный момент мы используем Photon SDK (не PUN) для транспортного слоя. Но хотим уйти от этого решения. Оно нас не устраивает по ряду причин.
По-поводу reliable — в Photon SDK поддерживает reliable из коробки. Но по-факту reliable у нас используется только в момент авторизации на гейм. сервере, когда передается большой объем статических данных (конфигурация матча).
Основной гейм.плей использует unreliable unordered udp. Причина в том, что нам просто не нужно гарантировать доставку данных. Т.к. сервер каждый тик шлет обновленное состояние мира. И если если какой-либо пакет потеряется, мы просто пропустим один кадр.
Так же стоит учитывать что на клиенте реализован механизм интерполяции, на случай потерь единичных пакетов.
В быстрых мультиплеерных играх более важна скорость доставки пакетов, чем потери.
Клиент в данных инпута отсылает два времени:
1) Время тика в котором находится он сам (с учетом на сколько он впереди сервера) На диаграмме в статье это тик №20.
2) Время тика в котором клиент видит остальной мир, на диаграмме это тик №10.
На диаграмме, сервер в момент отсылки находится в тике 15.
Когда пакет с данными дойдет до сервера, сервер будет находится в тике №20. Для сервера это станет «настоящим» временем. И сервер применит инпут от клиента подписанный 20м тиком. Т.е. клиент будет совершать выстрел в настоящем времени сервера.
Но для того что бы правильно смоделировать ситуацию того что видел клиент, будет использоваться второе время, пришедшее с клиента. (тик №10)
Резюмируя:
1) Время клиента в инпуте необходимо для определения в какой тик нужно применить инпут.
2) Время остального мира которое видит клиент, необходимо для того что бы правильно рассчитать результаты выстрела.
Да, все верно:
текущее время — это время игрока сделавшего выстрел. Состояние игрока берется на момент выстрела. А весь остальной мир (противники, окружение) берутся из истории, так как их видел игрок. Клиент отсылает на сервер номер тика, в котором находилось все окружение, когда был произведен выстрел.
Идет партия через горы. Мастер:
-Гном, вы оступились и падаете в пропасть. Ваши действия?
Гном:
-Быстро-быстро машу руками.
-о_О кидайте кубик!
(кидает) 20!
-еще раз!
-20!
-ну ка дай сюда кости! (мастер кидает сам) 20! 20!
Мастер, вздыхая:
-Ваша партия с офигевшими лицами смотрит, как гном в полном боевом облачении, махая руками, вылетает из пропасти
Пользуюсь Rider в связке с Unity с октября 2016 года, на OS X. В сравнении с MonoDevelop — небо и земля. Конечно ранние билды довольно часто крашились, но последнее время все работает отлично. Для Unity в Rider есть отдельный плагин, с поддержкой Unity-синтаксиса, подсветкой шейдеров, и дебаггером. Довольно большие проекты (100к-200к строк кода) открывает быстро, тормозов не замечено.
Мы вдохновлялись их идеями при разработке этого проекта
Если исходить из того что нужно использовать один и тот же движок на клиенте и на сервере, то возникают следующие проблемы:
1) Если использовать PhysX Unity на клиенте, то мы не можем гарантировать что на сервере будет использоваться та же версия PhysX. Да Unity публикует данные о том какую версию PhysX они используют, но в той версии которую они поставляют с Unity вполне могут быть кастомные изменения для поддержки движка. Либо использовать Unity на сервере, но на мой взгляд это плохое решение.
2) Если не использовать PhysX, то появляется проблема портации C++ на мобильные платформы и интеграция его с Unity. Наш проект запускался одновременно под Android и iOS. Тратить несколько месяцев разработки на такую задачу, когда проект находится на стадии софт-лаунча, довольно рискованно.
К тому же, как я уже говорил, у нас довольно простая физика в игре, все что нам требовалось делать рейкасты и свипкасты.
Физика на клиенте нужна, для расчета предикшена локального игрока, в частности такие вещи как передвижение и ассист в системе прицеливания делаются на основе рейкастов. Более подробнее как работает сетевой код я писал в этой статье habr.com/ru/company/pixonic/blog/415959
Если же вы про то, зачем нам на клиенте хранить историю физики (UnityPhysicsHistorySlice). То в Production-билдах не зачем, вся история храниться в ECS. История физики нужна только для специального режима «локальной симуляции», когда клиент помимо собственно клиентского кода, еще эмулирует сервер. Это довольно сильно ускоряет прототипирование фичей, так как можно разрабатывать игровую логику, без редеплоя сервера.
Массивы
Также, то что уже сказали до меня: link
И это только для управления одним персонажем. А если в у нас будет в сцене 5-10-20 объектов построенных на этой системе?
Возможно для десктоп платформ это еще может работать в небольших инди проектах, то для мобильных проектов это недопустимо. На проектах которых мне довелось работать, количество аллокаций в кадр было в районе 500 — 600 байт. При больших значениях у вас GC будет вызваться и отрабатывать настолько часто и долго, что добиться стабильных 30 FPS не удасться
2) Да такой механизм нужен и используется. Это по-сути одна из «фичей» ECS. Когда можно динамически добавить новое поведение к уже существующей сущности. На примере нашего шутера игроку в рантайме могут добавляться компоненты: неуязвимости, невидимости, ускорения движения и т.д. Пример кода из системы расчета невидимости:
3) Да такие ситуации бывают, вообще ответ на этот вопрос достоин отдельной статьи. О том как работает синхронизация я рассказывал в статье о сетевом коде проекта
Если вкратце, то клиент просто пропускает симуляцию нескольких тиков в случае хиккапа, и берет последние валидные данные с сервера. (игрок при этом видит подвисание).
4) Верно у нас в интерфейсе entity для каждого компонента существуют методы Add*MyComponent*() и Del*MyComponent(). Однако мы используем кодогенерацию в нашей ECS, и вам как программисту нужно лишь написать свой компонент, и указать что он используется в Enitity. Вот пример как выглядит класс Entity в проекте кодогенерации:
Кодогенератор добавить в проект все необходимые методы по созданию, удалению, итерированию и сериализации этого компонента.
Тот же самый метод мы используем в примере HealthPowerUpMovementSystem:
Добавление общего кода происходит через серверный солюшн, а Unity из коробки автоматически подхватывает все новые файлы в свой солюшн.
Для нас основным плюсом в переходе на такую систему было упрощение вливание фичей в develop. (Мы работаем по стандартному gitflow) В системе с 3 репозиториями, когда делается фича нужно было синхронизировать время вливание фичи во всех репозиториях. А так же создавать отдельные pull-requests в каждом репозитории. Для нашей команды это было неудобно.
Артефакты игроку не видны, если дополнительные тики происходят редко (один дополнительный тик раз в несколько секунд).
Если выполнять ускорение/замедление симуляции чаще, то это проявляется для игрока как ускорение/замедление объектов в игре. Например персонаж начинает двигаться не с постоянной скоростью, а то ускоряясь, то замедляясь.
У нас шарится весь код симуляции (ECS). Изначально у нас было три репозитория, клиент, сервер и сабмодуль шареного кода. Но позже пришли к выводу что команде удобнее работать в одном репозитории, где просто существует папка с шаренным кодом.
Из операторов MTC и Билайн.
На 4g в среднем пинг 50 — 60 ms.
3g — ближе к 60-65 ms.
Хочу добавить что:
1) Пересылка помимо инпута еще и состояния мира с клиента на сервер влечет за собой значительное увеличение трафика. На мобильных сетях это критично.
2) Что делать когда два клиента шлют противоречивую информацию о мире (например оба стоят в одной позиции)
Основные проблемы с реализацией сетевого кода с которыми мы столкнулись это был объем трафика, потери в сети и производительность симуляции мира на клиенте.
Ваше решение снижает нагрузку на сервер (не нужно симулировать мир с большой частотой), но практика реализации нашего проекта показывает что это не самое узкое место в мультиплеерных играх
Спасибо, поресерчим этот вопрос. У нас грубо говоря сейчас только два кейса пинг <= 500, и пинг >500. Если пинг выше, отключаем игрока, если ниже считаем по алгоритму из статьи
Да у нас не используется конкретно этот метод, однако каждому игроку отправляются только те данные, которые необходимы ему для визуализации картинки, и расчета предикшена. Так же у нас применяется дельта-компрессия, если какая-то сущность не изменилась между тиками, данные о ней не отправляются на клиент.
Мы выпустим отдельную статью о всех оптимизациях трафика которые использовали на этом проекте.
Для того что бы уменьшить количество потерь при отправке инпутов игрока мы используем механизм скользящего окна, и буфер инпутов на сервере.
Если же потеря пакета все же произошла, произойдет расхождение состояний мира клиента и сервера, и на клиенте отработает механизм согласования (reconciliation).
По-поводу reliable — в Photon SDK поддерживает reliable из коробки. Но по-факту reliable у нас используется только в момент авторизации на гейм. сервере, когда передается большой объем статических данных (конфигурация матча).
Основной гейм.плей использует unreliable unordered udp. Причина в том, что нам просто не нужно гарантировать доставку данных. Т.к. сервер каждый тик шлет обновленное состояние мира. И если если какой-либо пакет потеряется, мы просто пропустим один кадр.
Так же стоит учитывать что на клиенте реализован механизм интерполяции, на случай потерь единичных пакетов.
В быстрых мультиплеерных играх более важна скорость доставки пакетов, чем потери.
1) Время тика в котором находится он сам (с учетом на сколько он впереди сервера) На диаграмме в статье это тик №20.
2) Время тика в котором клиент видит остальной мир, на диаграмме это тик №10.
На диаграмме, сервер в момент отсылки находится в тике 15.
Когда пакет с данными дойдет до сервера, сервер будет находится в тике №20. Для сервера это станет «настоящим» временем. И сервер применит инпут от клиента подписанный 20м тиком. Т.е. клиент будет совершать выстрел в настоящем времени сервера.
Но для того что бы правильно смоделировать ситуацию того что видел клиент, будет использоваться второе время, пришедшее с клиента. (тик №10)
Резюмируя:
1) Время клиента в инпуте необходимо для определения в какой тик нужно применить инпут.
2) Время остального мира которое видит клиент, необходимо для того что бы правильно рассчитать результаты выстрела.
текущее время — это время игрока сделавшего выстрел. Состояние игрока берется на момент выстрела. А весь остальной мир (противники, окружение) берутся из истории, так как их видел игрок. Клиент отсылает на сервер номер тика, в котором находилось все окружение, когда был произведен выстрел.
Покажем после релиза :)
Идет партия через горы. Мастер:
-Гном, вы оступились и падаете в пропасть. Ваши действия?
Гном:
-Быстро-быстро машу руками.
-о_О кидайте кубик!
(кидает) 20!
-еще раз!
-20!
-ну ка дай сюда кости! (мастер кидает сам) 20! 20!
Мастер, вздыхая:
-Ваша партия с офигевшими лицами смотрит, как гном в полном боевом облачении, махая руками, вылетает из пропасти