Создание хитмапов с передвижением игроков на мидкорном проекте: краткий гайд
Зачем нужны хитмапы на мидкорных проектах и как мы их используем, я уже писал в предыдущем материале. А теперь расскажу про техническую часть — как получаем координаты пользователей, как накладываем на карту и что получаем в итоге.
Пойдем прямо по шагам, их будет всего пять.
Шаг первый. Отправляем координаты игрока
Во-первых, мы отправляем координаты с каждым ивентом в игре, например, смерть, чекпоинт или убийство из определенных видом оружия. Во-вторых, чтобы получить более детализированную картину путей игрока, мы также отправляем его местоположение через заданные промежутки времени.
Частота этого события определяется во многом здравым смыслом, но большую роль играет, насколько в итоге увеличится размер базы данных. Мы много экспериментировали, и 5-7 секунд оказались наиболее оптимальными значениями частоты отправки таких логов.
Вот пример события и его параметров в формате json:
{”map”:”MapTutorial”,”frame_rate”:”35”,”x”:”633”,”y”:”-56390”,”graphic settings”:”normal”}
Назвали его scene_performance, оно отправляется каждые 5 секунд в активных сессиях и вместе с координатами x и y отправляет технические параметры, например, FPS и настройки графики.
Шаг второй. Делаем скриншот
Далее у клиентских разработчиков берем скриншот уровня. Пример изображения из нашего мидкорного симулятора езды по бездорожью:
Шаг третий. Сопоставляем масштабы координат и скриншота
Все подготовительные процедуры выполнены, данные собраны, теперь можем приступать непосредственно к анализу пользовательских маршрутов на уровнях.
Сначала сопоставим масштабы координат игрока с размерами и ориентацией скриншота. Или если перефразировать на язык линейной алгебры — необходимо представить (спроецировать) каждый вектор пространство координат игрока в пространстве координат нашего изображения карты.
Поэтому для двумерного декартового евклидового пространства воспользуемся определениями преобразований координат, а также матричных переходов. Нас будут интересовать поворот координат (и иногда отражения), их сжатие/растяжение и параллельный сдвиг.
Реализация таких преобразований на Python выглядит так:
def Normalize(x, y, img_size):
x_out, y_out = x.copy(), y.copy()
shape_x, shape_y = img_size
shift_x = (x_out.min() + x_out.max(right_q))/2
shift_y = (y_out.quantile() + y_out.max())/2
scale_x = abs(1 / (x_out.max() — x_out.min()))*shape_x
scale_y = abs(1 / (y_out.max() — y_out.min()))*shape_y
x_out = (x_out — shift_x)*scale_x
y_out = (y_out — shift_y)*scale_y
return x_out, y_out
def Translate(x, y, shift=None, scale=None, turn=None):
x_out = x.copy()
y_out = y.copy()
if turn is not None:
x_out = x * np.cos(turn) — y * np.sin(turn)
y_out = x * np.sin(turn) + y * np.cos(turn)
if scale is not None:
x_out = x_out * scale[0]
y_out = y_out * scale[1]
if shift is not None:
x_out = x_out + shift[0]
y_out = y_out + shift[1]
return x_out, y_out
Здесь функция Normalize приводит размер облака точек с координатами передвижения игрока к размеру картинки и отцентровывает их друг относительно друга, а функция Translate выполняет преобразование координат игрока — поворот координат, их сжатие/растяжение и параллельный сдвиг.
Функция Normalize упрощает процедуру сопоставления, если мы точно знаем, что игроки находились по краям карты — чтобы мы знали реальные диапазоны координатного поля, в котором могут находится игроки. Часто для этого мы сами заходим в игру и обходим всю карту по периметру. Но если форма карты несимметричная и игроки не могут находится на всей прямоугольной площади скриншота, то функция Normalize может давать ошибочные преобразования. Поэтому при работе с координатами мы сначала применяем функцию Normalize, а потом для её результата используем функцию Translate.
Но даже после всех преобразований могут быть «выбросы» — точки, которые выходят за границы изображения карты. Обычно таких событий не очень много, но их надо удалить из анализа (самые серьезные выбросы лучше убирать до использования описанных выше функций).
Шаг четвертый. Накладываем координаты на карту
После преобразования координат игроков, необходимо наложить их на наш скриншот. Для чтения изображения в Python мы используем библиотеку cv2. Это достаточно популярное расширение для обработки изображений.
Для этого сначала необходимо инициализировать вспомогательную матрицу, которая будет соответствовать исходному изображению, и размеры которой будут сопоставляться с масштабами нашей системы координат. Элементы такой матрицы мы получим из количества появлений игроков в соответствующих точках на координатной плоскости.
Пример кода нахождения такой матрицы:
hit_mtx = numpy.zeros((number_y_cells, number_x_cells))
for indX, indY in zip(x, y):
i = int(indX)
j = int(indY)
hit_mtx[j, i] += 1
Получившуюся матрицу надо привести к формату и размеру нашего изображения. Это можно сделать с помощью функции cv2.resize.
Далее значения матрицы частот появления игроков в конкретной точке мы можем перекодировать в матрицу интенсивности цвета в соответствующей точке (ячейке). Цвет будет тем насыщеннее, чем больше значение соответствующего элемента матрицы, то есть чем чаще в этой точке находились игроки.
При этом надо не забыть, что размерность изображения равняется 3, где последнее измерение отвечает за количество каналов передачи цвета — поэтому полученную нами матрицу необходимо привести к нужному формату дублированием её на каждый из каналов. Это необходимо, чтобы в дальнейшем мы могли наложить нашу тепловую карту на изображение.
Пример построения такой матрицы на Python:
def _MaskOfHitArea(img_shape, hit_area, alpha):
mask = np.zeros(img_shape)
mask[:,:,0] = alpha * numpy.float32(hit_area)
mask[:,:,1] = mask[:,:,0].copy()
mask[:,:,2] = mask[:,:,0].copy()
return mask
Где alpha < 1 отвечает за прозрачность накладываемых друг на друга изображений.
В конечном итоге мы получим финальную тепловую карту путем смешивания исходного изображения img и матрицы интенсивности цветов color_mtx * mask, которая характеризует частоту появления игроков в каждой точке плоскости, по формуле
img * (1 — mask) + mask * color_mtx, где color_mtx задает цвет заливки координат игроков
Соответственно, если на шаге 3 вы выбрали не подходящие преобразования, вы это заметите по получившейся тепловой карте, из-за чего придётся повторить шаги 3 и 4.
Описанный выше алгоритм можно применять к различным видам событий, выделяя их разными цветами. Например, так мы определяли места убийств для разных классов оружия (в этой статье подробнее).
Карта фрагов из снайперских винтовок:
Шаг пятый. Делаем красиво
И наконец, благодаря функции cv2.VideoWriter, можно создавать небольшие видео с динамикой передвижения игроков на карте.
Это можно сделать, последовательно итеративно проходя заданные временные промежутки, где в теле цикла будет реализован алгоритм из четвертого шага (но только не за всё время наблюдений). Количество итераций и будет соответствовать частоте изменения кадров.