Pull to refresh

Comments 48

А ваше решение не опенсорс случаем?) Хотелось бы поближе посмотреть
Нет, внутреннее решение. Об опенсорсе пока речи не идет.
Некоторые моменты спорны. Мне больше импонирует подход Svelto
Вы имеете ввиду моменты, относящиеся к нашей реализации? Было бы интересно послушать, ибо это наш первый опыт, и мы работаем над тем, чтобы сделать еще круче.
У меня на стыке между ECS, Multiplayer и Unity всегда только один вопрос: Как реализовать 3d физику в игре? Из прошлой статьи я видел, что у вас 2d Volatile Physics. С этим конечно гораздо проще. Как бы вы подошли к 3d физике если бы игра ее требовала?
Точно так же подошли бы: поискали бы готовый движок 3d-физики, который бы мог вписаться в наши техтребования (например, C#, детерминизм/недетерминизм и тд). Или вы имеете ввиду, какие конкретные физические движки мы бы выбрали?
Я пока еще без особой практики в сетевых играх поэтому меня вообще интересует как синхронизируется физика в играх. N раз в секунду синхронизировать все объекты? Это получается очень большой трафик, если физики в игре много. Детерминизм? Тогда движок должен быть детерминированным, а таких я сколько ни гуглил — не получалось найти. 2д движки видел, 3д не видел.
И еще как вы встраиваете физическое состояние в ECS модель? Получается за позицию объекта отвечает и ECS системы и физический движок. Плюс всякие OnTriggerEnter и другие события надо перенести в ECS.
Да, с детерминизмом 3d-физики пока проблемы. Но в нашем случае детерминизм не нужен. У нас клиент симулируют то же самое, что сервер (ну почти то же самое) — prediction, и если есть расхождения, откатывает на последнее валидное состояние с сервера — rollback (Скоро выйдет наша статья про эту часть). Расхождения из-за недетерминизма могут быть, но в наших размахах несущественные.
Мы синхронизируем вcё состояние мира, и это действительно много (у нас было 5Kb). Но мы запилили дельта-компрессию, и теперь трафик небольшой (в среднем 300 байт на стейт).

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

С вводом так. Подписываем ввод номером тика, и шлем по udp пачкой N последних, за счет этого вероятность доставки сильно повышается.
Обратно сервер получает от клиента номер последнего дошедшего состояния, и есть параметр, который говорит, насколько часто надо посылать полный стейт (который тем не менее хитро запакован, с учетом знаний об игре — content-aware compression).

Да, ввод понятно что дублируете:)
Т.е. если я правильно понял, при потере 1 пакета состояния мира, вы не можете применить состояние на клиенте в течение еще 1 RTT?


т.к. клиент должен отправить на сервер nack, и только после этого сервер перепошлет потеряный пакет?

Ну тут только 2 варианта:
1. Клиент симулирует.
2. Интерполяция между дошедшими состояниями. Т.е. если дошли Состояние5 и Состояние7, а Состояния 6 еще нет, но уже нужно, оно получается с помощью интерполяции.
  1. Клиент симулирует и врагов? Т.е. наивная экстрополяция? Мы ведь не знаем ввода других игроков:)
  2. Мы же говорим про дельта-компрессию. т.е. если у вас есть S5 и дошла delta(S6,S7), вы не можете восстановить S7, т.к. у вас нет delta(S5,S6). Или под дельта-компрессией вы подразумеваете что-то другое?
  1. Врагов — нет, только себя.
  2. Да, вы правы, с дельта компрессией все сложнее и интереснее) Я слишком простой пример привел. Я думаю, мы в скором времени еще напишем об этом.
Из моего опыта разработки многопользовательских игр — дельта-компрессия серверного состояния мира (обычно только физического состояния мира — позиции/вектора объектов) хранит baseline (для каждого клиента) относительно которого и создаётся дельта-обновление. Клиент получает дельта-обновление и применяет его для получения нового baseline (=предыдущий baseline+delta) и в случае если этот baseline становится самым свежим — отправляет серверу ACK что теперь он имеет новый baseline (предыдущий baseline+delta). Сервер получив ACK задаёт данному клиенту новый baseline и далее уже относительно него умеет делать дельта-компрессию.
Клиент должен буферизировать baseline (т.к. входящие дельта-обновления будут приходить не для самого свежего baseline ввиду сетевой задержки).
Сервер должен буферизировать один baseline и последующие delta-обновления (чтобы в случае получения ACK на какое-либо дельта-обновление мог сделать baseline+delta и назначить его новым baseline).
Передавать полностью стейт мира (кроме initial при подключении) нужды нет вообще — у клиента будет гарантированно такой же стейт мира за счёт применения дельта-обновлений (для защиты от криворукости в дебаг-версию игры можно добавить baseline checksum в дельта-обновление — чтобы при построении нового baseline на клиенте сверить его с серверным).
Это довольно простой, но очень эффективный подход. Избыточность передаваемых данных крайне небольшая и компенсируется существенным выигрышем за счёт дельта-компрессии в целом.

Да, вот это круто.
Очевидно что будет работать и не ухудшает соединение!
Спасибо:) Держите приглашение на случай если решите статьи писать:)

По связи физ. движка и ECS, в целом это выглядит так — на сервере есть специальные системы для физики, они создают специальные компоненты, к которым привязаны объекты из физической библиотеки. Отдельно в цикле обновляется “мир” физики, вместе с ним обновляются позиции/ориентация объектов, обрабатываются коллизии.

Прям детерменизм нужен только если есть объекты которые очень редко обновляются (и то обычно они редко обновляются т.к. игрок их не видит, а значит и хрен с ним с недетерменизмом).


А так да, много раз в секунду отправляется все состояние мира.

Скажите пожалуйста, а как вы применяете команды (пользовательский ввод) в системах?


Удобно ли пользоваться подходом "все есть компонент" и в случае с "событием" Dead? Или все же есть ощущение что не хватает более callback ориентированного подхода?

В GameState есть сущности с компонентом UserInput от каждого игрока. Есть система, которая проходится по всем компонентам этого типа и обрабатывает ввод.

По поводу событий: в некоторых реализациях ECS («закрытых» пока для внешнего мира, поэтому тут не упомянул) я видел подходы с реализованными событиями. Мне лично понравилось и мы думаем над ними.

Сейчас 2020 год. Они все еще закрытые? Не поделитесь ссылками?
Нашел вот такое на гитхабе.
https://github.com/ancientbuho/entitas-unirx-additions
Это extension для rx. Позволяет подписаться на добавление/удаление/изменение компонента. Пример:
entity.OnComponentAddedAsObservable().Subscribe(dead => do something);

Выглядит больше как сахар, чем подход, правда. Но удобен тем, что можно подписаться сразу на несколько событий (для тех кто знаком с rx). И тут сразу минус, т.к. подписка имеет состояние (запоминает сработавшие события когда ожидаешь много), то при перезагрузке игры из тех же компонент, не сработает. Т.е. ее тоже нужно сериализовать :(

Показалось странным, что Entity умеет вообще всё. Как вы различаете функциональность, например, объекта игрока и какого-нибудь интерактивного объекта (пикабл, дверь и т.д.)? Ведь у них у всех есть и Health, и AddDamage, и весь набор всевозможных компонентов.
И GameState с кучей таблиц разномастных компонентов, наследующих IComponent. Не пробовали собрать их в одну структуру с доступом по типу и id?
Это наша первая реализация, так что не все может быть идеально.
Формально мы не различаем типы сущностей. Для нас сущность может содержать любой компонент, хоть все сразу (но не более одного — особенность реализации). Фактически, системы в коде сами понимают, что это за сущность, по типу компонентов на ней. Например, на двери скорее всего будет компонент Door с ее параметрами и какой-нибудь Destructable, если ее можно разрушать.
По идее ECS все равно, что за объекты у вас в игре, вы сами решаете, каким образом их «обозначить» и нужно ли их как-то различать. Я видел примеры реализаций ECS как с различаемыми типами сущностей (где можно конкретно сказать, что это игрок, это оружие, это препятствие и у них могут быть только «вот эти» компоненты), так и с обобщенной сущностью, где понять, что это за сущность, можно только по ее компонентам (как реализовано у нас).
Кажется, я понял. Меня покоробили широкий функционал в GameState и Entity, завязанный на конкретные классы. Entity заведует и своим ID, и получением своих компонентов, и всей их функциональностью (урон, здоровье, вот это всё). Да, удобно, не спорю, и кодогенерация.
Может, будет удобней добавлять эту функциональность в Entity через методы расширения? Их можно писать/генерировать где-нибудь рядом с компонентом и добавлять или удалять вместе с ним же.
Да, сейчас у нас здоровенный класс Entity, и я думал о том, чтобы разбивать его либо на partial-классы, либо делать методы-расширения (extensions). Руки пока не дошли)
Partial-класс выглядит для меня еще более неказисто. А методы-расширения можно раскидать по namespace (в стиле LINQ) или сборкам, отключать и подключать их в нужных местах. Например, namespace с компонентом Damage и соответствующими методами-расширения для Entity только в том месте, где оно используется, и не пролистывать десятки ненужных методов в автодополнении :)
Подскажите, что за редактор использовался для создания блок-схемы.
Если вы про диаграмму связей, то это стандартные возможности Visual Studio.
Если про схему с ECS, то схему накидал от руки на листочке, а потом попросил дизайнера отрисовать в графическом редакторе)
Скажите, а у вас в ECS все поля по умолчанию интерполируются?
Нет, только те, что помечены специальным атрибутом. На данный момент это в основном положение и ориентация объектов в пространстве.

Забавно кстати что вы это решили включить в ECS:)
Кажется что интерполяция, сериализация и ECS — три разные вещи, а у вас они вместе лежат.


Кстати, у меня вот какой вопрос:
Своего игрока, как известно, надо экстрополировать (ну т.е. client-side prediction).
А вражеских игроков надо интерполировать.


Как у вас это разделение реализовано в концепции систем?


А еще: при выстреле вы наверняка используете Lag Compensation. Для этого надо откатывать во времени назад всех, кроме стрелка. Так что тут аналогичный вопрос. Или вы откатываете только физику, а поля компонент оставляете теми же?


P.S. если кто-то не понимает о чем я тут говорю, тут можно прочитать.

У нас изначально были определенные техтребования к решению, и хотелось, чтобы какие-то фичи уже были реализованы, например, интерполяция. Можно было бы ее напилить «сверху», но мы в итоге сошлись на плотной интеграции — и быстрее реализовать, и легче исправить)

Ок, понятно.
Мы примерно из тех же соображений слили воедино сериализацию и дупликацию объектов:) Правда интерполяция у нас немного в стороне.

Читал ваш перевод статей G. Gambetta и делал схожую реализацию.
Скажите, когда на сервер приходит запоздалый выстрел, берется предыдущий от него стейт мира и на него заново накатываются инпуты + интерполируются позиции/ повороты перевычисляются последующие, вплоть до текущего стейта мира, который рассылается всем? Или какой-то менее расходный по ресурсам метод?
него заново накатываются инпуты

Нет-нет.
1) мир откатывается
2) проверяем, попал ли игрок во врага
3) мир возвращается в исходное состояние
4) попадание применяется (если оно было)


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

Спасибо за ценный совет.

Хотя с первого взгляда мне казалось, что перенакатывать инпут — честнее. Но это ведёт к проблемам (например откат смерти на клиенте)
Да, у нас есть predication и rollback. Об этом я чуть-чуть писал в предыдущей статье, и скоро выйдет новая, где про это будет очень подробно.
Если кратко, на клиенте мы симулируем только локального игрока, остальных не трогаем, берем с сервера (т.е. игрок смотрит на мир в прошлом, а сам в нем в будущем).
Откат во времени есть, работает только на сервере. Подробнее об этом будет в статье.

Да, это мне все понятно. Непонятно только как системы отличают локального игрока и не локального:)


В общем ок, жду следующей статьи.

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

1) Как Ваша реализация решает задачу зависимостей системы от набора компонентов самого Entity? т.е MovementSystem в прямом смысле требует чтобы у Entity были определены компоненты Position & Velosity.
Судя по описанной реализации GameState & Entity Вы вынуждены делать множественные относительно рандомные чтения из памяти только для получения списка Entity, которые обладают необходимым набором компонент для каждой конкретной системы, например:

for entity in entities:  # cache friendly
     if entity.movement and entity.velocity:  # чтение из 2х разных источников в памяти
            process(entity)


2) рассматривали ли Вы исходники реализаций ECS имеющиеся в общем доступе, например, EntityX?
1) Обычно одна система — один компонент, и система проходится по всем компонентам данного типа. Тут все ок. Но когда нужно два и более компонентов, да, чтения рандомные. Обычно система производит поиск компонента, проверяет его на null (существует или нет) и кеширует его на время работы Execute(). На наших малых кол-вах объектов/компонентов, мощностях и CCU нам это ок.
2) Смотрели только те, что указаны в статье. Сейчас немного смотрим другие, как идеи для улучшения нашего фреймворка.

Тоже зацепил этот момент у вас.
Я в своей реализации держу у каждой системы кэш сущностей, по которым ей надо проходить в каждом цикле — а не компонентов.
Список обновляется хэндлером по эвенту ComponentAdded / Removed, в котором сущность проверяется на соответствие сигнатуре компонентов данной конкретной системы.
Выглядит слегка громоздко и накладывает оверхэд на каждое добавление / удаление компонента, зато сильно экономит ресурсы на сложных системах, работающих с несколькими компонентами.
Возможно в идеале надо совмещать оба эти подхода

Спасибо большое за статью! Хочу дополнить и спросить:


| entity ID as integer. Ни в одном из рассматриваемых решений поддержки не было.
По идее можно добавить компонент с единственным полем — айди. Назовем его IdComponent.


| join by ID reference O(N+M) - соответственно связь можно обеспечить по этому айди.
Для ускорения доступа можно завести систему-синглтон, которая будет например класть в словарик entity и отдавать по айди.


Не понял что значит M в O(N+M). N — это все entity в игре, так? Что тогда M?


| reuse component type — возможность использовать один раз написанный тип компонента в разных типах сущностей. Поддерживал только Entitas.


Не понимаю, что значит разные типы сущностей? Не могли бы Вы привести пример?

Не понял что значит M в O(N+M). N — это все entity в игре, так? Что тогда M?

Скорее всего имелось ввиду объединение двух списков из N и M сущностей в один, например при поиске групп по критериям
Спасибо за статьи.
Новичок в юнити, возник вопрос: а как вы построили взаимодействие ECS<>Unity?
ECS у вас в отдельном потоке или основном?
Взаимодействие с юнити конечно же в основном, юнити просто не может взаимодействовать с чем вне основного потока. Однако могут быть реализованы прокси доступы, не могу быть уверен в реальности, так как не видил не одного подобного рабочего примера)

Однако, если брать текущую реализацию unity ecs и jobs то можно построить и на разных потоках, но статья была написана раньше чем юнитеке сделали поадекватнее свой ецс)
Sign up to leave a comment.