Как обращаться с сетевым кодом?

10

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

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

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

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

Я предполагаю, что мой вопрос, каков наилучший способ создать игру для лучшей скорости отклика сети? Ясно, что клиент должен посылать пользовательский ввод как можно скорее на сервер, чтобы я мог сделать так, чтобы код net-send приходил сразу после цикла обработки событий, как внутри цикла игры. Есть ли в этом смысл?

Стивен Лу
источник

Ответы:

13

Игнорировать отзывчивость. В локальной сети пинг незначительный. В интернете 60-100 мс - это благословение. Молитесь богам отставания, что вы не получите шипы> 3K. Ваше программное обеспечение должно работать с очень небольшим количеством обновлений в секунду, чтобы это стало проблемой. Если вы снимаете со скоростью 25 обновлений в секунду, то у вас есть максимальное время 40 мс между тем, как вы получаете пакет и воздействуете на него. И это однопоточный корпус ...

Разработайте свою систему для гибкости и правильности. Вот моя идея о том, как подключить сетевую подсистему к игровому коду: обмен сообщениями. Решением многих проблем может стать «обмен сообщениями». Я думаю, что обмен сообщениями вылечил рак у лабораторных крыс однажды. Обмен сообщениями экономит мне 200 долларов или больше на страховке автомобиля. А если серьезно, обмен сообщениями - это, вероятно, лучший способ присоединить любую подсистему к игровому коду, сохраняя при этом две независимые подсистемы.

Используйте обмен сообщениями для любого взаимодействия между сетевой подсистемой и игровым движком, а также между любыми двумя подсистемами. Межсистемный обмен сообщениями может быть простым как блок данных, передаваемых указателем с помощью std :: list.

Просто имейте очередь исходящих сообщений и ссылку на игровой движок в сетевой подсистеме. Игра может сбрасывать сообщения, которые она хочет отправить в исходящую очередь, и автоматически отправлять их, или, возможно, при вызове некоторой функции, например, flushMessages (). Если игровой движок имеет одну большую общую очередь сообщений, то все подсистемы, которые должны были отправлять сообщения (логика, искусственный интеллект, физика, сеть и т. Д.), Могут выгружать в него сообщения, где основной игровой цикл может затем прочитать все сообщения. и затем действовать соответственно.

Я бы сказал, что запуск сокетов в другом потоке - это нормально, хотя и не обязательно. Единственная проблема с этим дизайном состоит в том, что он обычно асинхронный (вы точно не знаете, когда отправляются пакеты), и это может затруднить отладку, а проблемы, связанные с синхронизацией, появляются / исчезают случайным образом. Тем не менее, если все сделано правильно, ни один из них не должен быть проблемой.

С более высокого уровня, я бы сказал, отделить сеть от самого игрового движка. Движок игры не заботится о сокетах или буферах, он заботится о событиях. События - это такие вещи, как «Игрок X выстрелил», «Произошел взрыв в игре T». Они могут быть интерпретированы игровым движком напрямую. Откуда они генерируются (сценарий, действия клиента, ИИ-плеер и т. Д.), Не имеет значения.

Если вы рассматриваете свою сетевую подсистему как средство отправки / получения событий, то вы получаете ряд преимуществ по сравнению с простым вызовом recv () в сокете.

Вы можете оптимизировать полосу пропускания, например, взяв 50 маленьких сообщений (длиной от 1 до 32 байт) и заставить подсистему сети упаковать их в один большой пакет и отправить его. Может быть, это может сжать их перед отправкой, если это было большое дело. С другой стороны, код может снова распаковать / распаковать большой пакет в 50 отдельных событий, чтобы игровой движок мог их прочитать. Это все может происходить прозрачно.

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

PatrickB
источник
Вы упомянули, что используете простой std :: list или что-то подобное для передачи сообщений. Это может быть темой для StackOverflow, но правда ли, что все потоки совместно используют одно и то же адресное пространство, и пока я не даю нескольким потокам завинчивать память, принадлежащую моей очереди одновременно, я должен быть в порядке? Я могу просто выделить данные для очереди в куче, как обычно, и просто использовать некоторые мьютексы в ней?
Стивен Лу
Да, это правильно. Мьютекс, защищающий все вызовы std :: list.
PatrickB
Спасибо за ответ! До сих пор я добился большого прогресса в своих подпрограммах потоков. Это такое прекрасное чувство - иметь свой собственный игровой движок!
Стивен Лу
4
Это чувство исчезнет. Тем не менее, большие медные, которые вы получаете, остаются с вами.
ChrisE
@ StevenLu Несколько [крайне] поздно, но я хочу отметить, что предотвращение одновременного использования потоков с памятью может быть чрезвычайно трудным, в зависимости от того, как вы пытаетесь это сделать, и насколько вы хотите быть эффективными. Если бы вы делали это сегодня, я бы указал на одну из множества отличных реализаций параллельной очереди с открытым исходным кодом, поэтому вам не нужно изобретать сложное колесо.
Фонд Моники Иск
4

Мне нужно (по крайней мере) иметь отдельный поток для обработки сетевых сокетов

Нет, ты не

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

Не обязательно имеет значение. Когда обновляется ваша логика? Нет смысла выводить данные из сети, если вы пока ничего с этим не можете сделать. Точно так же нет смысла отвечать, если вам пока нечего сказать.

например, он может отправить обратно пакет ACK сразу после получения обновления состояния от сервера.

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

Для большинства сетевых игр вполне возможно иметь такой игровой цикл:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

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

Kylotan
источник
7
Отключение обновления от рендеринга не только настоятельно рекомендуется, но и требуется, если вы хотите не дерьмовый движок.
AttackingHobo
Хотя большинство людей не делают движков, и, вероятно, большинство игр все еще не разделяют их. Согласование для передачи значения прошедшего времени в функцию обновления работает приемлемо в большинстве случаев.
Kylotan
2

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

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

DeadMG
источник
0

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

Ваши игровые сущности общаются с сетевой подсистемой (NSS). NSS объединяет сообщения, ACK и т. Д. И отправляет несколько (возможно, один) UDP-пакетов оптимального размера (обычно ~ 1500 байт). NSS эмулирует пакеты, каналы, приоритеты, повторную отправку и т. Д., Отправляя только отдельные пакеты UDP.

Прочитайте учебник по играм или просто используйте ENet, который реализует многие идеи Гленна Фидлера.

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

deft_code
источник
0

Не полностью «игнорировать отзывчивость». Добавление дополнительной задержки в 40 мс к уже задержанным пакетам мало что дает. Если вы добавляете пару кадров (со скоростью 60 кадров в секунду), то вы задерживаете обработку позиции, обновляя еще пару кадров. Лучше быстро принимать пакеты и обрабатывать их, чтобы повысить точность моделирования.

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

Джастин
источник