Привет, Хабр! В этой статье мы разберемся, как тепловые карты (heatmaps) могут стать мощным инструментом для анализа поведения игроков и оптимизации дизайна игровых уровней. Тепловые карты позволяют выявить скрытые закономерности в том, как игроки взаимодействуют с игрой: где они часто погибают, какие пути выбирают, и какие зоны карты остаются неизведанными. С помощью этих данных разработчики могут принимать обоснованные решения, улучшая баланс и увлекательность уровней. В этой статье мы создадим тепловую карту с использованием Python и рассмотрим, как это помогает в практике разработки игр.
Аналитика в играх в первую очередь сосредоточена на удержании игроков, монетизации и пользовательском опыте. Важной частью анализа игр на ПК и консолях является улучшение левел дизайна. Игроки часто неправильно интерпретируют дизайн уровня и используют его таким образом, что это создает худший (как минимум не лучший) опыт как для игрока, что плохо как для него самого, так и для разраюотчика. Плохой пользовательский опыт может привести к неудовлетворенности, фрустрации и оттоку игроков.
Что такое тепловая карта или хитмап?
Когда анализируются пространственные данные, такие как траектории движения игроков, частотность игровых смертей или места размещения предметов, один из лучших способов извлечь инсайты — это построить тепловую карту (heatmap).
Предположим, у вас есть данные о движении в двух или трех измерениях. Эти данные снимаются каждые N секунд и фиксируют координаты игрока X и Y (а иногда и Z). Как только вы получите эти данные от команды разработчиков, тестировщиков или фокус-групп, тепловая карта может предоставить массу полезной информации.
Тепловые карты активно используются в крупных играх для оптимизации карт и балансировки игрового процесса. Например, в игре Overwatch тепловые карты помогали разработчикам понять, какие участки карты игроки используют наиболее часто, где происходят интенсивные сражения и какие зоны остаются незадействованными. Эти данные позволяли улучшить баланс карт, а также корректировать места для стратегического размещения объектов и препятствий. В результате игры становились более увлекательными и динамичными, что способствовало лучшему восприятию уровня и повышению удовлетворенности игроков.
В зависимости от типа игры можно ответить на следующие вопросы:
Какие участки уровня должны быть игнорируемыми игроками, но почему-то ими используются?
Где игроки обычно прячутся от босса, чтобы избежать его атак?
Существует ли паттерн в том, как игроки движутся по уровню, и соответствует ли это задуманному дизайну?
Есть ли объекты, которые не подбираются просто потому, что их никто не может найти?
Есть ли места, которые игроки злоупотребляют для убийства других игроков (PvP)?
Есть ли слабые места, где игроки постоянно погибают?
Пример
Для простоты рассмотрим двумерные графики. Данные — это координаты Unreal от нескольких игроков, проходивших этот уровень.
Давайте воспользуемся hist2d от Matplotlib и histplot от Seaborn в качестве отправной точки.
Histplot
Если мы посмотрим на значения плотности, то увидим, что минимальная плотность в этой сетке размером bins_number * bins_number равна 0, а максимальная плотность превышает 700. Имейте в виду, что если мы изменим количество ячеек (bin number), плотности в каждой "ячейке" будут различаться.
bins = 100
sns.histplot(data=pos_data, x="pos_x", y="pos_y",
bins=bins, cbar=True, pmax=1)
Здесь мы видим поле действия и некоторые тропинки, а также место с высокой плотностью в самом центре
Hist2d
plt.hist2d(pos_data.pos_x, pos_data.pos_y, bins = bins)
Потенциальные проблемы
Эти непрерывные цветовые карты трудно воспринимаются. Обычно люди плохо различают мелкие оттенки, и на этих графиках видно лишь три цвета: один для низких значений, один для средних и один для высоких значений. Все нюансы легко теряются, особенно для людей с дефектами зрения.
Паттерны в данных не всегда очевидны. На графике histplot видно много областей с низкой плотностью, но истинные паттерны едва заметны. На графике hist2d ситуация немного лучше, так как яркие цвета лучше контрастируют с темно-фиолетовым фоном, но поле действия теряется.
Если вы создаете тепловую карту для себя, можете использовать решение, которое вам подходит. Однако если вы собираетесь поделиться результатами с другими заинтересованными сторонами, стоит подумать о том, как они воспримут информацию, чтобы она была понятной.
Как улучшить тепловую карту
-
Используйте дискретные цветовые карты
Люди не всегда хорошо различают оттенки. Лучше выбрать ограниченную палитру с яркими различиями между цветами.
-
Попробуйте палитру от холодного к теплому
Палитры от холодного к теплому работают лучше для пространственных тепловых карт, так как плотность только увеличивается, а не уменьшается. Если вы используете монохромную палитру, вы можете эффективно показать около трех уровней плотности.
-
Проверьте увеличение яркости
Если цвет воспринимается неправильно, важно, чтобы изменения в яркости были различимыми, особенно для людей с нарушениями цветового восприятия.
Как использовать кастомную цветовую карту
Многие библиотеки по умолчанию используют минимальные и максимальные значения в наборе данных плотности и линейно отображают их на цветовую палитру. Однако это может привести к потере деталей в некоторых областях.
Что должно быть на тепловой карте
Места на уровне, которые используются в целом
Места, которые используются довольно часто, формируя пути
Места, где происходит действие
Места с экстремальной плотностью
Что можно исключить из тепловой карты
Места, где плотность близка к нулю. Если из всех игроков только один дошел до точки A, мы обычно считаем, что «игрокам трудно найти точку A». Таким образом, плотность на пути к точке A не будет нулевой, но она будет мизерной.
Пример Matplotlib
1. Создайте пользовательскую палитру
В палитре 11 разных цветов — от бело‑холодно-голубого до красно-черного. Несмотря на то, что эти цвета не выглядят слишком жизнерадостными или хипповыми, они были подобраны таким образом, чтобы менять оттенок и яркость в заметных пределах.
Если палитра десатурирована, она выглядит следующим образом:
multicolor = ["#ffffff", "#e2eeff", "#c3d8f9", "#80bdff", "#42b0f5",
"#a0e242", "#d1cb1c", "#a06a20", "#872a10", "#630d02", "#000000"]
2. Сделайте количество ячеек переменным
В зависимости от ваших данных и целей необходимо выбрать соответствующий уровень детализации. Для уровней игры обычно достаточно квадратного метра. Большие ячейки помогут вам выделить более крупные участки интереса, в то время как меньшие ячейки помогут выявить часто используемые или перегруженные места.
3. Создайте собственный метод отображения
3.1 Вычисление плотностей
Так как нам нужно больше деталей в определённых местах, одним из способов создания карты плотности является использование персентилей*. Сначала мы вычисляем двумерную гистограмму и сохраняем значения плотности.
* Персентиль — это статистическая мера, которая делит набор данных на 100 равных частей, каждая из которых представляет собой 1% от общего числа данных. Таким образом, персентиль показывает, насколько данные расположены по сравнению с остальными.
densities,_,_ = np.histogram2d(pos_data.pos_x, pos_data.pos_y, bins=bins)
3.2 Выберите персентели
Обычно тепловая карта в 2D накладывается на изображение реального уровня. В зависимости от того, какие вопросы мы хотим решить, иногда не стоит перегружать тепловую карту цветом для мест, куда игроки не ходят или посещают очень редко. В то же время мы хотим выделить пути и места, где игроки ходят больше всего.
Один из способов достижения этого — создать линейный набор персентилей с двумя плотными точками в начале и в конце.
Первый персентиль будет выявлять области с очень низкой плотностью, затем мы можем пройти через каждый 10%-ный интервал до 99-го персентиля. Значения между 99-м и 100-м персентилем будут указывать на чрезвычайно плотные участки.
percentiles = [0,1,10,20,30,40,50,60,70,80,99,100]
Затем мы находим границы для нашей цветовой карты. Если мы возьмем набор данных плотности в его исходном виде, существует вероятность, что многие значения будут равны 0, потому что даже если наш уровень не является прямоугольной сеткой, такая сетка создается при вычислении двумерной гистограммы. Эти «ячейки» могут не быть на самом уровне, но они образуются при пересечении координат и заполняются нулями.
Чтобы избежать этого, можно использовать уникальные значения из набора данных плотности.
bounds = []
for i in percentiles:
bounds.append(np.percentile(np.unique(densities),i))
Создайте карту цветов и постройте гистограмму
cmaplist = multicolor
cmap = mpl.colors.LinearSegmentedColormap.from_list(
'Custom cmap',
cmaplist,
len(percentiles))
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
plt.hist2d(pos_data.pos_x,
pos_data.pos_y,
bins = bins,
norm = norm,
cmap = cmap)
cbar = plt.colorbar(ticks = bounds)
plt.show()
Результат будет выглядеть вот так:
Весь код:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
multicolor = ["#ffffff","#e2eeff", "#c3d8f9","#80bdff", "#42b0f5",
"#a0e242", "#d1cb1c", "#a06a20", "#872a10", "#630d02", "#000000"]
bins = 100
densities,_,_ = np.histogram2d(pos_data.pos_x, pos_data.pos_y, bins=bins)
percentiles = [0,1,10,20,30,40,50,60,70,80,99,100]
bounds = []
for i in percentiles:
bounds.append(np.percentile(np.unique(densities),i))
cmaplist = multicolor
cmap = mpl.colors.LinearSegmentedColormap.from_list(
'Custom cmap',
cmaplist,
len(percentiles))
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
plt.hist2d(pos_data.pos_x,
pos_data.pos_y,
bins = bins,
norm = norm,
cmap = cmap)
cbar = plt.colorbar(ticks = bounds)
plt.show()
Фильтрация и сглаживание данных для улучшения тепловых карт
Когда мы строим тепловую карту на основе данных, важно учитывать, что исходные данные могут содержать шум, что приведет к появлению незначительных или искаженных участков на карте. Одним из способов борьбы с этим является использование алгоритмов фильтрации и сглаживания, чтобы исключить редкие или малозначительные значения, которые могут «загрязнять» карту и затруднять анализ. Например, если игроки посетили определенную точку лишь один раз, это не обязательно указывает на важность этого места — скорее, это случайность, которую стоит исключить. Для этого можно применить фильтрацию по плотности, исключив участки с плотностью ниже определенного порога, или сглаживание данных, чтобы уменьшить влияние отдельных выбросов.
Пример фильтрации по плотности
Одним из способов фильтрации данных является исключение точек с низкой плотностью, которые могут быть вызваны редкими событиями. Для этого можно установить минимальный порог плотности, ниже которого данные будут игнорироваться. Например:
import numpy as np
# Пример данных плотности
densities = np.array([[0, 1, 0, 5], [0, 0, 0, 3], [0, 4, 2, 0]])
# Устанавливаем порог плотности
threshold = 2
# Фильтруем данные, исключая все значения ниже порога
filtered_densities = np.where(densities < threshold, 0, densities)
print(filtered_densities)
Этот код исключает все данные с плотностью ниже 2, устанавливая их в 0. Это позволяет убрать малозначительные данные и сфокусироваться на реальных паттернах.
Пример сглаживания данных
Для устранения «шума» можно применить сглаживание с помощью среднего или медианного фильтра. Один из подходов — это использование движущегося окна. Например, для 2D-гистограммы с плотностью можно применить фильтрацию с использованием гауссового фильтра, который плавно распределяет значения плотности по окружающим точкам.
from scipy.ndimage import gaussian_filter
# Применение гауссового фильтра для сглаживания
smoothed_densities = gaussian_filter(filtered_densities, sigma=1)
print(smoothed_densities)
В этом примере применяется Гауссов фильтр с параметром sigma
, который контролирует степень сглаживания. Чем больше значение sigma
, тем сильнее сглаживаются данные.
Гауссов фильтр работает на основе следующей формулы:
Используя такую фильтрацию, мы можем уменьшить влияние шумных данных и сделать тепловую карту более четкой и информативной.
Кроме того, можно использовать медианный фильтр, который заменяет значение в каждой ячейке на медиану значений в его окрестности. Это особенно полезно для устранения выбросов, которые могут сильно искажать тепловую карту.
from scipy.ndimage import median_filter
# Применение медианного фильтра
median_smoothed = median_filter(filtered_densities, size=3)
print(median_smoothed)
Этот код применяет медианный фильтр с размером окна 3, что помогает уменьшить влияние отдельных выбросов в данных.
Заключение
Тепловая карта — это не просто инструмент визуализации, а мощный аналитический механизм, который может значительно улучшить качество игрового уровня. Разработчики могут использовать этот метод для улучшения баланса, повышения интереса к различным участкам карты и исключения избыточных или малозначительных зон. Важно помнить, что нет универсальных решений: наилучший подход всегда зависит от контекста и целей проекта. Размещение тепловых карт на изображении уровня, а также фильтрация и сглаживание данных позволяют делать их более информативными и удобными для восприятия. Если вы только начинаете работать с такими инструментами, это руководство станет отличной отправной точкой для дальнейших экспериментов и анализа.
Источники:
A Heatmap guide for game level analysis by Daria Rodionova
Статья поддерживается командой Serverspace.
Serverspace — провайдер облачных сервисов, предоставляющий в аренду виртуальные серверы с ОС Linux и Windows в 8 дата-центрах: Россия, Беларусь, Казахстан, Нидерланды, Турция, США, Канада и Бразилия. Для построения ИТ-инфраструктуры провайдер также предлагает: создание сетей, шлюзов, бэкапы, сервисы CDN, DNS, объектное хранилище S3.
IT-инфраструктура | Удвоение первого платежа по коду HABR