Компания
85,08
рейтинг
4 декабря 2015 в 11:40

Разработка → История одной оптимизации: передача и обработка результатов боя recovery mode

Сегодня я расскажу вам о небольшой части большого проекта — World of Tanks. Многие из вас, наверное, знают World of Tanks со стороны пользователя, я же предлагаю взглянуть на него с точки зрения разработчика. В этой статье речь пойдет об эволюции одного из технических решений проекта, а именно — передаче и обработке результатов боя.
Постбоевая


Под капотом


Для понимания сути рассматриваемой проблемы сначала опишу, как устроены «Танки». Проект представляет собой распределенное клиент-серверное приложение, которое на стороне сервера представлено несколькими разнотипными узлами. Вся физика и внутриигровая логика обсчитываются на сервере, а клиент отвечает за взаимодействие с пользователем — обработку пользовательского ввода и отображение результатов. Упрощенная схема кластера выглядит так:
Схема кластера

За взаимодействие с клиентом отвечают BaseApp-узлы. Они принимают информацию от пользователя по сети и передают её внутрь кластера, а также отсылают пользователю информацию о событиях, произошедших внутри кластера. Все физические взаимодействия, происходящие на арене, просчитываются на CellApp-узлах. Там же собирается информация о событиях, произошедших с танком за время боя: количество выстрелов, попаданий, пробитий и т. д. Один бой может обслуживаться несколькими CellApp-ами, на каждом из которых могут обсчитываться несколько пользователей из разных боев. По окончании боя пакеты статистики по всем танкам отсылаются из CellApp-ов на BaseApp. Он агрегирует пакеты и обрабатывает результаты боя: высчитывает штрафы и награды, проверяет выполнение квестов и выдает медальки, т. е. формирует другие пакеты данных, которые отправляет пользователям. Важно отметить, что CellApp-ы и BaseApp-ы представляют собой изолированные процессы. Некоторые из них могут располагаться на других машинах, но в пределах одного кластера. Так что передача данных между ними происходит через сетевые интерфейсы, а между клиентом и кластером — через интернет.
Для передачи пакетов данных используется протокол с гарантированной доставкой, реализованный поверх UDP. Под капотом, за весь I\O и гарантированную доставку поверх UDP, отвечает библиотека Mercury, которая является частью платформы BigWorld Technology. Фактически мы можем повлиять на данные только перед отправкой – подготовить их таким образом, чтобы оптимизировать затраты на отправку/получение.

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

Начало


Давайте взглянем на процесс подготовки данных.
Большая часть внутриигровой логики World of Tanks написана на Python. Данные, пересылаемые между CellApp и BaseApp, а также данные, отправляемые пользователю по окончании боя, представляют собой dict с разнообразными значениями от простых (вроде целого/дробного числа или значения True/False), до составных — словарей и последовательностей. Ни code-object, ни классы, ни экземпляры пользовательских классов никогда не передаются от одного узла другому. Это связано с требованиями безопасности и производительности.

Для того чтобы было удобнее рассматривать дальнейший материал, приведем пример данных, с которыми мы будем работать далее:
data = {
	"someIntegerValue" : 1,
	"someBoolValue" : True,
	"someListOfIntsValue" : [1, 2, 3],
	"someFloatValue" : 0.5,
	"someDictionaryValue" : {
		"innerVal" : 1,
		"innerVal2" : 2,
		"listOfVals" : [1, 2, 3, 4],
	},
	"2dArray" : [
		[1,   2,  3,  4,  5,  6],
		[7,   8,  9, 10, 11, 12],
		[13, 14, 15, 16, 17, 18],
		[19, 20, 21, 22, 23, 24]
	]
}

Для непосредственной передачи данных между узлами, экземпляр dict необходимо преобразовать в бинарный пакет данных минимального размера. При совершении удаленного вызова движок BigWorld предоставляет возможность прозрачной для программиста конвертации Python-объектов в бинарные данные и обратно с помощью модуля cPikcle.
Пока объем передаваемых данных был небольшим, они перед отправкой просто конвертировались в бинарный формат при помощи модуля cPickle (назовем этот протокол обмена версией 0). Именно в таком виде вышла первая закрытая бета-версия танков 0.1 в далеком 2010 году.

К достоинствам этого метода стоит отнести простоту и достаточную эффективность как в плане быстродействия, так и компактности итогового бинарного представления.
>>> p = cPickle.dumps(data, -1)
>>> len(p)
277


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

Оптимизируем


Шло время, значительно выросли объемы данных и одновременный онлайн, а, следовательно, выросло и количество одновременно завершаемых боев и объем информации передаваемой по окончании боя. Для сокращения трафика мы выкинули строковые ключи из пересылаемых данных и заменили индексами в list. Такая операция не привела к потере данных: зная порядок ключей, восстановить исходный dict легко.

Для проведения операций удаления и восстановления ключей был необходим шаблон, в котором хранились бы все возможные ключи и соответствующие функции:
NAMES = (
	"2dArray",
	"someListOfIntsValue",
	"someDictionaryValue",
	"someFloatValue",
	"someIntegerValue",
	"someBoolValue",
)

INDICES = {x[1] : x[0] for x in enumerate(NAMES)}

def dictToList(indices, d):
	l = [None, ] * len(indices)
	for name, index in indices.iteritems():
	l[index] = d[name]
	return l
	
def listToDict(names, l):
	d = {}
	for x in enumerate(names):
	d[x[1]] = l[x[0]]
	return d

>>> dictToList(INDICES, data)
[[[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12], [13, 14, 15, 16, 17, 18], [19, 20, 21, 22, 23, 24]], [1, 2, 3], {'listOfVals': [1, 2, 3, 4], 'innerVal2': 2, 'innerVal': 1}, 0.5, 1, True]
>>> 
>>> len(cPickle.dumps(dictToList(INDICES, data), -1)
165

Как видно из примера, бинарное представление стало более компактным по сравнению с версией 0. Но за компактность мы расплатились временем, затраченым на предварительную обработку данных и добавлением нового кода, который нужно поддерживать.
Это решение вышло в версии 0.9.5, в самом начале 2015 года. Протокол с преобразованием dict в list, последующим pickle.dumps(data, -1) назовем версией 1.

Оптимизируем дальше


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

1. Для каждого вложенного словаря мы применили такой же подход, как и к главному словарю, — выкинули ключи и преобразовали dict в list. Таким образом, «шаблон», по которому данные преобразуются из dict в list и обратно, стал рекурсивным.

2. Внимательно присмотревшись к данным перед отправкой, мы заметили, что в некоторых случаях последовательность заключена в контейнер типа set или frozenset. Бинарное представление этих контейнеров в протоколе cPickle версии 2 занимает гораздо больше места:
>>> l = list(xrange(3))
>>> cPickle.dumps(set(l), -1)
'\x80\x02c__builtin__\nset\nq\x01]q\x02(K\x00K\x01K\x02e\x85Rq\x03.'
>>> 
>>> cPickle.dumps(frozenset(l), -1)
'\x80\x02c__builtin__\nfrozenset\nq\x01]q\x02(K\x00K\x01K\x02e\x85Rq\x03.'
>>> 
>>> cPickle.dumps(l, -1)
'\x80\x02]q\x01(K\x00K\x01K\x02e.'

Мы сэкономили еще несколько байтов, преобразовав set и frozenset в list перед отправкой. Так как приемной стороне обычно неинтересен конкретный тип последовательности, а важны только данные, такая замена не привела к ошибкам.

3. Довольно часто не для всех ключей в словаре заданы значения. Некоторые из них могут отсутствовать, а другие — не отличаться от значений по умолчанию, которые известны заранее и на передающей, и на принимающей стороне. Также нужно помнить, что значения «по умолчанию» для данных разных типов имеют разное бинарное представление. Достаточно редко, но все же встречаются значения по умолчанию, чуть более сложные, чем просто пустое значение определенного типа. В нашем случае это несколько счетчиков объединенных в одно поле, представленное в виде последовательностей из нулей. В этих случаях значения по умолчанию могут занимать много места в бинарных данных, пересылаемых между узлами. Для того чтобы добиться еще большей экономии, перед отправкой мы заменяем значения по умолчанию на None. В результате во всех случаях бинарное представление станет более компактным или не изменится в длине.
>>> len(cPickle.dumps(None, -1))
4
>>> len(cPickle.dumps((), -1))
4
>>> len(cPickle.dumps([], -1))
4
>>> len(cPickle.dumps({}, -1))
4
>>> len(cPickle.dumps(False, -1))
4
>>> len(cPickle.dumps(0, -1))
5
>>> len(cPickle.dumps(0.0, -1))
12
>>> len(cPickle.dumps([0, 0, 0], -1))
14

Рассматривая примеры, стоит учесть, что cPickle добавляет в бинарный пакет заголовок и терминатор, общий объем которых составляет 3 байта, а реальный объем сериализированных данных равен (X - 3), где X — значение из примера.
Более того, замена значений по умолчанию приносит выгоду и при сжатии бинарных данных zlib-ом. В бинарном представлении list-элементы идут друг за другом без всяких разделителей. Несколько значений по умолчанию подряд, замененных на None, будут представлены в виде последовательности из одинаковых байтов, которые могут быть хорошо заархивированы.

4. Данные архивируются zlib-ом с уровнем компрессии равным 1, т. к. этот уровень позволяет достичь оптимального соотношения степени архивации ко времени работы.

Если свести воедино шаги 1–3, то получится примерно так:
class DictPacker(object):
	def __init__(self, *metaData):
		self._metaData = tuple(metaData)
	# Packs input dataDict into a list.
	def pack(self, dataDict):
		metaData = self._metaData
		l = [None] * len(metaData)
		for index, metaEntry in enumerate(metaData):
		try:
				name, transportType, default, packer = metaEntry
				default = copy.deepcopy(default) # prevents modification of default.
				v = dataDict.get(name, default)
				if v is None:
					pass
				elif v == default:
					v = None
				elif packer is not None:
					v = packer.pack(v)
				elif transportType is not None and type(v) is not transportType:
					v = transportType(v)
					if v == default:
							v = None
				l[index] = v
		except Exception as e:
				LOG_DEBUG_DEV("error while packing:", index, metaEntry, str(e))
		return l

	# Unpacks input dataList into a dict.
	def unpack(self, dataList):
		ret = {}
		for index, meta in enumerate(self._metaData):
			val = dataList[index]
			name, _, default, packer = meta
			default = copy.deepcopy(default) # prevents modification of default.
			if val is None:
				val = default
			elif packer is not None:
				val = packer.unpack(val)
			ret[name] = val
		return ret

PACKER = DictPacker(
	("2dArray", list, 0, None),
	("someListOfIntsValue", list, [], None),
	("someDictionaryValue", dict, {},
	 DictPacker(
		("innerVal", int, 0, None),
		("innerVal2", int, 0, None),
		("listOfVals", list, [], None),
	)
	),
	("someFloatValue", float, 0.0, None),
	("someIntegerValue", int, 0, None),
	("someBoolValue", bool, False, None),
)

>>> PACKER.pack(data)
[[[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12], [13, 14, 15, 16, 17, 18], [19, 20, 21, 22, 23, 24]], [1, 2, 3], [1, 2, [1, 2, 3, 4]], 0.5, 1, True]
>>> len(cPickle.dumps(PACKER.pack(data), -1))
126

В итоге все эти приемы были применены в протоколе версии 2, который вышел в версии 0.9.8, в мае 2015 года.
Да, мы еще больше увеличили время, затрачиваемое на предварительную подготовку, но зато и объем бинарного пакета сократился значительно.

Сравнение результатов


Для того чтобы можно было посмотреть, к чему привело применение вышеописанных приемов на реальных данных, приведем график зависимости размера пакета данных об одном танке, передаваемых с CellApp на BaseApp по окончании боя, в различных версиях от версии.
Размер пакета с информацией о танке, пересылаемый с BaseApp на CellApp

Напомним, что в режиме «Превосходство» версии 0.9.8, игрок может выйти в бой на трех танках и соответственно суммарный объем данных возрастет троекратно.

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

Где 0.9.8u — обработка без сжатия zlib-ом (uncomressed), а 0.9.8c — с применением сжатия (compressed). Время указано в секундах на 10000 итераций.

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

Заключение


В нашем случае критически важным было уменьшить объем передаваемых данных между узлами. Также желательно было сократить время предварительной обработки. Поэтому протокол второй версии стал для нас оптимальным решением. Следующим логичным шагом является вынесение функциональности сериализации в отдельный бинарный модуль, который будет производить все манипуляции самостоятельно, и не будет хранить информацию, описывающую данные в бинарном потоке, как pickle. Это позволит ужать объем данных еще больше, и, возможно, уменьшит время на обработку. Но пока мы работаем с решением, описанным выше.
Автор: @enmk

Комментарии (57)

  • +12
    Вот бы ещё отдел баланса свою тяжкую работу здесь описал, а мы бы с радостью обсуждали в комментариях.
    • +6
      [irony] И отдел по эффективному повышению «надоев» (доната)[/irony]
      • +8
        По этой теме уже кто только не выступал. Извините, увидел комменты про баланс и надои, и не удержался :)
        Заголовок спойлера

        • 0
          Просмотрел видео, половину не понял о.О Нет, суть угадать можно — сделали платную модель, поломали баланс, и потом с ней тоже что-то случилось (неясно — то ли урезали, то ли выпустили новую модель, снова сломавшую баланс, то ли и то и другое…). Но фразы… Вот как можно угадать слово «нерфить»? Я даже не имею представления, производная от чего это — есть только компания игрушек NERF → НЕРФ, так может они плохой пластик производят, и «нерфить», это как делать бракованную партию?

          Больше всего понравилось «начинают вайнить на форуме». Это, наверное, как «начинают бухать на форуме», и мозг живо представляет горестные заголовки «Вздрогнем за почившего Т1000», печальные комментарии «мне будет его не хватать», серый фон, и все ЧЕРНЫМИ БУКВАМИ!
          • –1
            поиграйте в вот и посмотрите видосы на ютубе. Сразу всё поймёте
            • 0
              Да не, трудно потом вылезти. Я не знаю, может другим легче, но меня затягивает даже в намного менее кооперативный Xonotic, где и матч то один длится, казалось бы, лишь 5…10 минут в среднем.

              Наверное у меня модель мышления поломана. Потому что сажусь, к примеру, за диплом, и понимаю, что он очень скучен, неинтересен, и очень хочется поиграть в Xon. Или, к примеру, прихожу домой, и думаю «Сейчас что-нибудь крутое сделаю — вон, Фабрис Беллар и QEMU, и ffmpeg написал, и формулу офигенную открыл, и еще много чего, ну чем я хуже??». И вот, смотрю я на свои идеи, и не испытываю ни малейшей симпатии, они размываются в серую и нудную муть, зато Xonotic — это море веселья прямо сейчас!

              Я сейчас вспомнил Ричарда Фейнмана — он рассказывал, как алкоголь бросил. Позволю себе его процитировать.
              Однажды днем, около половины четвертого, я шел мимо пляжа Копакабаны и
              наткнулся на бар. И тут же, совершенно внезапно, у меня возникло огРОМное и
              сильное желание: «Именно это мне сейчас и нужно; это будет как раз кстати. Я
              с удовольствием выпью что-нибудь прямо сейчас!»

              Я уже почти вошел в бар, и тут мне подумалось: «Стоп! Еще только
              середина дня. Здесь никого нет. Нет никакой причины пить, ведь сейчас не
              может быть никакого общения. Почему же у тебя возникла такая сильная
              потребность выпить?» Тут я испугался.
              Тут я испугался.
          • 0
            в целом, всё правильно поняли, но есть нюансы.
            NERF — это название фирмы производителя оружия, которое стреляет мягкими безопасными снарядами или мячиками. Ну, т.е. оружие, которое не может нанести вреда даже ребенку. Качество пластика там отличное, так что тут не угадали
            whine — ныть, скулить. опять немного мимо))
    • 0
      Если не секрет, что требуется от идеального балансера? И чем плох текущий? Играю как «казуал» с племянником, и особых проблем вроде бы нет.
      • 0
        Описываю свой личный опыт.

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

        Некоторые танки просто ничего не могут сделать с танками на два уровня выше. И это не зависит ни от мастерства, ни от боевой ситуации. Разработчики оправдывают баланс +2 желанием разнообразить количество техники в боях, но это оправдание уже несущественно, количество танков достаточно возросло. А с отменой уровня боёв 12 жизнь восьмёрок вообще в АДъ превратилась. Соответственно, баланс +1 был бы идеален, под него проще и танки балансить.

        Это что касается балансера. А ещё САУ. Тут и говорить нечего. Класс нуждается в перебалансировке, это признают и сами разработчики.
        • 0
          Хм. А что плохого в подобного рода боях? Ведь в половине случаев победа принадлежит союзной команде. Опять-таки, баланс по роли — сложная вещь, очень. Тот же Т-34 можно использовать как в роли флангового дырокола, так и в роли светляка. Роль сильно зависит от экипажа, модулей, используемого вооружения. Балансировать по множеству параметров — долго, и результат не гарантирован.

          Насчет +2 — вопрос тоже спорный, ибо есть исключения. Luchs, ELC и иже с ними демонстрируют, что вражеский танк +3 можно отлично засветить.

          САУ, возможно, дисбалансна, но она дисбалансна в обеих командах.

          P.S. Предлагаю на этом завершить. Оказывается, тема весьма холиварна, и не раз поднималась на танковых ресурсах.
          • –3
            Это не твой акк случайно?
          • 0
            > Luchs, ELC и иже с ними демонстрируют, что вражеский танк +3 можно отлично засветить.

            Ага, ну засветил, и что? Толпа оленей стреляет куда угодно, но только не по твоему засвету — как жить?

            Роль танка, кстати, меняется в зависимости от его вооружения. ELC с топовой пушкой должен использоваться как быстрая, малозаметная и мощная ПТ, а вовсе не как ЛТ.
        • +2
          Я как человек играющий часто без према на 7-8 уровнях могу сказать что меня это не особо напрягает.
          Те с моими 54% я могу играть на 10ках в ноль, но смысла нет, новые танки не купишь. Идеальный уровень без према именно до 8го.
          Я, кстати, играл еще до введения ±2 и тогда на моем Т-34 (который 5й) видел в бою ИС-7. Приходилось думать, читать и выживать.

          Просто поменялся подход что-ли… раньше гораздо больше времени уделялось обучению в кланах и на форумах. Вон на стримах Десертода некоторые после стольких лет до сих пор танк сменить не могут не выходя из компании.

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

          С артой тоже все не так просто. Контингент танков кажется 30+. Это люди с работой, детьми и внимание! деньгами. Им бы поиграть и расслабиться после дня и совершенно не хочется каждый день врываться на средних танках. Эти люди никуда не уйдут, у них не бомбит. Если арту понерфят они сядут на ТД типа бабахи или босса. Там альфа не хуже. И будет в боях по 7-8 бабах или маусов. Будет снова визг что нужно понерфить и их.
          Но скажем так… точность у арты нерфить не будут. И так хуже некуда. А от всего остального будут страдать бумажные танки на которых играет очень много статистов, типа батчата или новых чехов. Там даже прямого не надо и так криты идут.
          Для примера возьмем fv304 (6я арта). Теперь когда попадает к 8м просто от большинства рикошетит или выбивает по 80 урона. При полете снаряда в 2,5с, бешеной перезарядке, дальности стрельбы 600 метров. И обзоре 8к в 430м. Первым огребет статист на бате. Он первый засветился (остальные еще едут), а Т-62 я могу просто не пробить.

          Против них мы имеем статистов и стримеров. Первые будут ныть если сделать ММ по скиллу, ведь кто-то окажется менее статистом, статка будет падать. И тех, кто забил на рандом и катает ГК и укрепы, где арты нет или можно договориться чтобы не брали.
          И стримеров, которым НУЖЕН бтр иначе стрим будет скучным и народ уйдет.
          • +2
            На инсте гейта минусовый хуррик, в бубле, в агре.
          • 0
            Вот круто :) Вроде бы всё по-русски, но половину не понял :)))
      • –1
        www.youtube.com/user/murazortv по ссылке вам все раскажут.
      • 0
        Пусть сделают так, что бы не забрасывало в бои, где на 8 восьмерок в одной команде приходится 3 восьмерки другой команды. Остальные естественно 9 и 10.
        • 0
          А зачем? Танки одного уровня сильно различаются по полезности. Сравните AMX 40 и БТ-7: первый выше уровнем, но в большинстве случаев бесполезен. Второй картонный, без урона, но шустрый и зело полезный.
          Интуитивно кажется, что бои с выровненными по уровню составами будет лучше, но знатоки наверняка приведут немало контрпримеров.
          Погуглил, каждый танк в балансере имеет свой коэффициент полезности. В вашем пример количество компенсируется качеством.
          • 0
            Вот когда вы в составе команды, в которой 8 танков восьмого уровня выкатитесть на команду, где таких танков три, а в топе у них тройка ТТ. Тогда имеет смысл говорить. Особо доставляет, когда выезжаешь в такой бой на каком-нибудь Т-44.
            Если что, на уровнях ниже 8 с этим параметром баланса всё еще куда ни шло.
        • 0
          Пусть сделают так, что бы не забрасывало в бои, где на 8 восьмерок в одной команде приходится 3 восьмерки другой команды. Остальные естественно 9 и 10.

          Почти идеальный вариант для выполнения:
          1. СТ-15 на Т-55А, играя на ст 8 уровня, надеясь на наличие в красной команде трех ПТ (убить надо двух ПТ, одна ПТ — запасная)
          2. ЛТ-15 на Т-55А, играя на ЛТ-8, надеясь на полевую карту.
    • +4
      «Возможно все, но зачем?» (с)
  • +4
    Насколько Я вижу, у вас схема данных фиксирована и хранится как на отправляющей стороне, так и на принимающей. В UDP пакетах передаются только значения (без имён полей и структу). Вы не пробовали в качестве эксперимента использовать google.protobuf вместо cpickle? По-моему, protobuf как раз придуман как формат передачи данных фиксированной структуры. Я тут набросал небольшой пример:
    Описание структуры данных:
    gamedata.ptoro
    package protobuf;
    
    message MatrixRow {
    	repeated int32 cell = 1 [packed=true];
    }
    
    message InnerDict {
    	required int32 innerVal = 1;
    	required int32 innerVal2 = 2;
    	repeated int32 listOfVals = 3 [packed=true];
    }
    
    message GameData {
    	required int32 someIntegerValue =1;
    	required bool someBoolValue = 2;
    	repeated int32 someListOfIntsValue = 3;
    	required float someFloatValue = 4;
    	required InnerDict someDictionaryValue = 5;
    
    	repeated MatrixRow _2dArray = 6;
    }
    


    Серриализация одного объекта:
    test.py
    import gamedata_pb2
    
    obj = gamedata_pb2.GameData()
    obj.someIntegerValue = 1
    obj.someBoolValue = True
    obj.someListOfIntsValue.append(1)
    obj.someListOfIntsValue.append(2)
    obj.someListOfIntsValue.append(3)
    obj.someFloatValue = 0.5
    
    dictValue = obj.someDictionaryValue
    dictValue.innerVal = 1
    dictValue.innerVal2 = 2
    dictValue.listOfVals.append(1)
    dictValue.listOfVals.append(2)
    dictValue.listOfVals.append(3)
    dictValue.listOfVals.append(4)
    
    #Create 2D matrix. Looks ugly but works!
    row1 = obj._2dArray.add()
    row1.cell.append(1)
    row1.cell.append(2)
    row1.cell.append(3)
    row1.cell.append(4)
    row1.cell.append(5)
    row1.cell.append(6)
    
    row2 = obj._2dArray.add()
    row2.cell.append(7)
    row2.cell.append(8)
    row2.cell.append(9)
    row2.cell.append(10)
    row2.cell.append(11)
    row2.cell.append(12)
    
    row3 = obj._2dArray.add()
    row3.cell.append(13)
    row3.cell.append(14)
    row3.cell.append(15)
    row3.cell.append(16)
    row3.cell.append(17)
    row3.cell.append(18)
    
    row4 = obj._2dArray.add()
    row4.cell.append(19)
    row4.cell.append(20)
    row4.cell.append(21)
    row4.cell.append(22)
    row4.cell.append(23)
    row4.cell.append(24)
    
    data = obj.SerializeToString()
    print(len(data))
    


    В результате объект серриализуется в строку размером 67 байт.
    • +5
      Подумал о том же самом, protobuf здесь просто-таки напрашивается
      Статья оставляет отчетливое впечатление очередного изобретения велосипеда
    • +2
      На момент выхода протокола версии 0, биндингов protobuf для питона не существовало.
      Протокол версии 1 был скорее «пожарной мерой», которую было достаточно легко ввести в эксплуатацию.
      Возможно, стоило вместо версии 2, использовать protobuf, но на данный момент у нас порядка 20 промежуточных шаблонов, из которых в различных комбинациях склеивается 7 финальных шаблонов. Я могу быть не прав, но мне кажется, что в данном случае перенести подобную схему на protobuf будет достаточно проблематично.
      • +2
        И да, сейчас protobuf рассматривается как одна из альтернатив 2-ой версии.
        • +2
          Посмотрите ещё на cap'n'proto — он рвёт google protobuf просто на кусочки.
  • 0
    Делюсь свои испытанным решением. В бинарном протоколе реализованы простые типы: int8, int16, int32, int64, float, double, string и список любых типов и любой вложенности. Места занимает минимум. Естественно каждый пакет приходится формировать и парсить вручную (с помощью индивидуального кода для каждого пакета), но от этого больше плюсов нежели минусов, так как есть гибкая возможность в случае изменения формата пакета внести версионные изменения.
    • 0
      эмм bson есть, почему не использовать его, в вашем случае

      bsonspec.org
      • 0
        а есть еще и msgpack msgpack.org
        более компактный чем bson и более простой, чем протобуф
        • 0
          Отвечу здесь. bson и даже msgpack не достаточно компактны. И первый и второй вместе с собственно данными содержат структуру пакета. При пересылке множества пакетов эта информация остается долгое время (если не на протяжении всей жизни приложения) неизменной и вносит существенный оверхед. Вот пример из MessagePack — {«compact»:true,«schema»:0} — 18 байт, и это сообщение еще надо как-то передать по сети, то-есть обернуть хотя бы заголовком с длиной сообщения и типом сообщения. В моем варианте заголовок занимает 4 байта и сами данные — 2 байта. Итог 6 байт пригодных для отправки по сети вместо пускай даже 18+(4)
          • +1
            Protobuf упакует подобный месседж в 4 байта при более изящной реализации которую легче поддерживать :)
        • 0
          Зачем передавать в _каждом_ пакете его структуру, если 1) эта структура уже известна обеим сторонам (и серверу, и клиенту) 2) даже если неизвестна, то особым сообщением передать структуру, а данные гонять без описания структуры
  • 0
    а почему было не взять гугловый protocol buffers?
    • –2
      Может быть потому что это, как и GWT, например, трэш и ад?
      • +5
        И мы конечно же увидим тут аргументированные примеры треша и ада?
        • 0
          лично я треша и ада с протобуфом не наблюдал (очевидно не такие сложные структуры данных), но мои знакомые сильно намучилась с протобуфом
          • +2
            >но мои знакомые сильно намучилась с протобафом
            Не факт, что их мучения связаны именно с реализацией протобафа.
            Использую его уже несколько лет. Никаких проблем не испытывал.
            Все проблемы которые встречал у коллег, были от незнания как его использовать.
          • 0
            радуют такие знакомые
      • 0
        Пастернака читали?
  • 0
    > протокол с гарантированной доставкой, реализованный поверх UDP.

    Для чего такая конструкция нужна? (Почему не TCP ?)
    • +3
      Различия в скорости и способе обработки пакетов.

      TCP:
      -требует установки соединения(на это тратим время)
      -запись/чтение из буфера
      -задержка ожидания заполнения буфера(можно пофиксить флагом NO_DELAY)
      -если какой-то пакет потерялся, то вся очередь стоит пока не доставят потерянный покет/таймаут

      UDP:
      -нет соединения, просто шлем пакет
      -пакет либо пришел весь, либо вообще не пришел(в случае с TCP могут быть фрагменты)
      -длина заголовка меньше

      RUDP:
      -добавляет флаг доставки, очередность и тд.

      С RUDP мы можем организовать сразу несколько очередей(каналов) передачи данных с разными настройками(гарантировать доставку, соблюдать порядок, приоритет) в пределах одного «соединения».
      • 0
        Все верно, мы не можем себе позволить дополнительную задержку на установление TCP-cоединения.
  • –3
    Хотелось бы отметить то, что реально раздражает.

    1) блокировка интерфейса пользователя в стандартном клиенте игры при сетевых проблемах.

    2) арта 9-10 уровней

  • 0
    Вопросы:

    1. Где Ольга Сергеевна? Она в декрете? ;)
    2. За оптимизацию последних патчей спасибо. Ноут стал норм тянуть.
    3. Приходите на конференции, Барышников очень интересно рассказывал тогда 2 часа. Жалко до 45 минут теперь урезали.
    У вас зоопарк технологий и решений просто потрясающий. Про использование того же RabbitMQ и логгера интересно было бы послушать. Многие такое за всю жизнь не видят.
    4. Что нужно знать чтобы устроиться программистом-юниором к вам в Питере? ;)
    5. Реально у вас получить статку по игрокам для Data Science Capstone? Можно конечно и самому, но с ограничениями api получится сдвиг по времени.
    • +2
      1. Не знаю, честно. Я простой разработчик и кто такая Ольга Сергеевна не имею ни малейшего понятия.
      2. Спасибо за доброе слово, и хоть моего кода на клиенте практически нет, приятно слышать что кому-то нравится.
      3. Так вроде бы WG присутствует на конференциях.
      4. Не знаю, я работаю в Минске. Попробуйте порыться на официальном сайте, может быть найдете подходящую вакансию, я попал именно так.
      5. Не знаю, попробую выяснить в понедельник.
      • +1
        Ольга Сергеевна это девушка на тизере, слева )
        https://www.youtube.com/user/wotTV4players
      • 0
        Вы не знаете Ольгу Сергеевну?!
        Дочку знаменитого и легендарного Сергея Буркатовского, он же Serb
    • 0
      > 4. Что нужно знать чтобы устроиться программистом-юниором к вам в Питере? ;)

      Набор приостановлен, насколько мне известно.
  • 0
    Какую реализацию Python используете? GIL не мешает? Какую версию языка?
    • 0
      CPython 2.7, с небольшими изменениями. GIL не мешает — Python-код обрабатывается в одном потоке.
  • 0
    все ваши оптимизации… год назад на ноуте был фпс40 сейчас к 20 со всякими твикерами еле еле дотягивает
    • +1
      Я так понимаю, что статью вы не читали, т.к. описанная в ней оптимизация не имеет никакого отношению к отрисовке сцены боя на клиенте.
  • +1
    Интересная статья. Хотелось бы больше статей на тему архитектуры серверных решений для гейм дева.

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

    Есть мнение, что реалтаймовые игры будут трендом нескольких ближайших лет. Т.е. топ гроссинг будут покорять игры, требовательные к хорошей модели синхронизации клиент-сервера (Шутеры, RTS, ...). Причем стоит ожидать их не только от крупных компаний, но и от небольших компаний-разработчиков игр, от инди разработчиков в том числе. Сейчас самое дешевое решение — использовать синхронизацию типа мастер-клиент в связке с каким-нибудь облачным решением вроде Unity Photon. Что собственно и делают маленькие студии. У такого решения есть недостатки, учитывая, что часто реал-тайм игры — соревновательные игры, требовательные к наличию авторитарной стороны.

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

    Я как раз работаю в одной из таких студий. Правда у них в свое время получилось наработать неплохую кодовую серверную базу, которая покрывает пока что все потребности, но это ненадолго. Понятно, что в процессе жизни студии принимались решения, позволяющие дешево добиваться цели, в итоге получился монолитный C++ сервер, работающий на мощных железках. Из плюсов: весь онлайн (относительно WoT мизерный) можно обслуживать в одном сервере, все работает шустро и оптимизировано. Из минусов: никакого масштабирования, вся бизнес-логика на C++ (с трудом можно найти толковых спецов, а даже если найдешь, то они будут долго пилить свои задачи).

    Мне кажется можно сформировать какой то пул best practice, или может даже доступных для многих решений, которыми смогут пользоваться разработчики.
    • +2
      Ну так есть же доклад об архитектуре WoT
      https://www.youtube.com/watch?v=5y23xezgQKE
      • 0
        Благодарю, не видел этого доклада.
  • 0
    А CellApp это физический хост или некая виртуальная машина? Сколько ресурсов необходимо для расчета одного боя?
    • 0
      CellApp — это отдельный процесс. К сожалению, ответить сколько именно ресурсов необходимо, не представляется возможным. Т.к. нагрузка постоянно меняется и бой может обсчитываться сразу на нескольких машинах внутри кластера.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка