Яндекс.Метрика

    gdev

    Сетевой слой движка физики

    Всем привет. Одним игровым проектом на физике box2d стало больше, закончили онлайн-файтинг на физике ragdoll. Хотелось бы сделать некоторые выводы и рассказать об основных технических проблемах, с которыми пришлось столкнуться, и о методах их решения. Статья заинтересует начинающих разработчиков [флеш-]игр с использованием физических движков. Фокус делается на сетевом слое для движка физики.

    Рекомендуем посмотреть блог по теме — http://gafferongames.com/game-physics/. Там есть прекрасное сравнение TCP и UDP применительно к играм.

    Adobe Cirrus


    Изначально в качестве сетевого транспорта для движка физики был выбран Adobe Cirrus, по-другому протокол RTMFP (Real Time Media Flow Protocol). Технология позиционируется как протокол, характеризуемый низкими задержками, топологией p2p, безопасностью и масштабируемостью и наиболее пригодный для разработки приложений реального времени со взаимодействием многих пользователей.
    Для интенсивного в смысле сетевого трафика приложения поддержка p2p показалась большим плюсом. Обещанные низкие пинги были бы очень полезны для онлайн файтинга.

    RTMPF — это протокол поверх UDP. Поддержка UDP для флеш приложений на момент написания статьи (08.09.2011) не введена. Класс DatagramSocket находится на стадии бета-тестирования, и он доступен только из air.

    Ссылаться на RTMPF как на udp неверно. В разных режимах работы RTMFP имеет разные сходства и различия c UDP. Top Krcha обосновывает выбор режима DIRECT_CONNECTIONS для игр (
    www.flashrealtime.com/building-p2p-multiplayer-games-at-adobe-max-2010/). В этом режиме Cirrus обеспечивает гарантированную доставку сообщений, т.е. преимущество малых пингов теряется. Характеристики протокола для разных режимов работы и типов данных можно посмотреть в PDF-слайдах той же презентации. В этой статье под “пингом” понимается время доставки сообщения от одного клиента к другому. На практике этот пинг оказался далеко не малым, часто нестабильным и с высокими лагами.

    Синхронизация объектов физического мира


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

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

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

    Исходя из выбора p2p, была выбрана гибридная схема с владением объектов клиентами и “клиентским предсказанием”. Каждый клиент владеет своим героем. FMS не использовался, следовательно, надо было обеспечить полную синхронизацию мира клиентов через p2p. Каждый клиент моделирует мир полностью, что обеспечивает приемлемую реакцию на ввод пользователя. Во время пакета синхронизации клиент делает поправку состояния героя соперника (за состояние своего героя клиент отвечает сам).

    Пакеты синхронизации


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

    Пересылка ввода пользователя на другой клиент, чтобы на нем использовать принятый ввод для моделирования, не подходит по той же причине — быстрое накопление погрешностей и расхождение объектов на двух клиентах. Необходимо использовать позиционные/угловые данные. После получения пакета состояния targetState от удаленного клиента мы применяем его к локальной копии того объекта, которым владеет удаленный клиент (псевдокод):

    rival.applyState(targetState);
    ...
    void applyState(State targetState) 
    {
        this.x = targetState.x;
        this.y = targetState.y;
    }


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

    void applyState(State targetState)
    {
        this.x = (this.x + targetState.x)*0.5;
        this.y = (this.y + targetState.y)*0.5;
    }


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

    void applyState(State targetState) 
    {
        targetX = (this.x + targetState.x)*0.5;
        targetY = (this.y + targetState.y)*0.5; 
    
        errorX = targetX - this.x;
        errorY = targetY - this.y;
    
        this.applyForce(k*errorX, k*errorY); 
    }


    k — “жесткость“ коррекции. В данном случае корректирующие силы пропорциональны рассогласованию позиций, что придает некоторую пружинистость объектам. Альтернативный вариант с нормализацией (errorX,errorY) перед применением силы показал неудачные результаты — плохо справлялся с большими рассогласованиями.

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

    void applyState(State targetState) 
    {
        targetX = (this.x + targetState.x)*0.5;
        targetY = (this.y + targetState.y)*0.5; 
    
        errorX = targetX - this.x;
        errorY = targetY - this.y;
    
        if(len(errorX,errorY)<MAX_SOFT_CORRECTION_ERROR)
        {
            this.applyForce(k*errorX, k*errorY); 
        }
        else
        {
            this.x = (this.x + targetState.x)*0.5; 
            this.y = (this.y + targetState.y)*0.5;
        }
    }


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

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

    long recentProcessedTimestamp;
    
    void onStateReceived(State state)
    {
         if(state.timestamp <= recentProcessedTimestamp)
        {
              //уже обработан более новый пакет или пакет дублируется 
              return;
        }
        else if(currentLocalTimestamp()-state.timestamp > MAX_PERMISSIBLE_DELAY)
        {
            //пакет слишком старый 
    	return;
        }
        else
        {
           recentProcessedTimestamp = state.timestamp;
           process(state);
        }
    }


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

    Детектирование разрывов и тестирование качества связи


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

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

    Использование TCP/IP


    Cirrus работала нестабильно. Высокие и хаотичные пинги, частые разрывы (в смысле игры), между многими игроками связи не было вообще. Решено было также реализовать синхронизацию с использованием единственной альтернативы, tcp/ip. Тестирование показало, что tcp/ip в большинстве случаев дает меньший пинг и лучше выдерживает его стабильность (отсутствие всплесков по задержкам), чем Cirrus в режиме DIRECT_CONNECTIONS. Пусть все игроки, которые не могут играть через Cirrus, выбирают tcp/ip. Пусть среди всех оставшихся игроков выбор между транспортом делается, исходя из меньшего пинга между двумя клиентами. Тогда, по нашей статистике, только 20-30% всех игроков будут использовать циррус. Пинг измеряется три раза, в виде времени двойного оборота между клиентами, после чего в качестве оценки берется максимальное значение. Cirrus, видимо, далеко не так хороша для игр реального времени, как заверяет Adobe.

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

    Основные выводы

    • При выборе транспорта для игры не надо делать предположений. Если есть критические требования, проведите тестирование всех вариантов;
    • Adobe Cirrus находится на стадии беты, для игр реального времени она показала результаты даже хуже, чем tcp/ip (статистика только для России и СНГ);
    • Adobe Cirrus — это не UDP, она лишь построена на его базе. Внутренние детали протокола не документированы. Можно верить только реальным опытам.

    Ссылка на приложение http://vkontakte.ru/app2316895

    Удачи в разработке игр!