Wolfenstein 3D в EXCEL?

В прошлый раз я рассказал как малой кровью сделать подобие игры «3 в ряд» в рабочей книге Excel с использованием встроенного языка программирования Visual Basic for Applications (VBA).

В закладки
Аудио

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

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

На деле же за это время произошло много событий. Например, коллега заинтересовал меня программированием микроконтроллеров. Я поморгал встроенным светодиодом на WEMOS D1 Mini (на базе ESP8266), написал небольшую программу для подключения платы к домашнему WI‑FI, а потом задумался о перспективах этого направления.

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

Очевидно, в какой-то момент мне стало плохо от осознания своей беспомощности. Для человека, который вроде бы нормально знает VBA и плавает в CSharp (как написать название этого языка без проставления хэштэга?), попытка реализации игры на C/C++ смерти подобна.

Тогда я приступил к поиску решения. Оно оказалось очевидным, но я искал его около месяца. Да, я не очень сообразительный.

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

Гениально, правда?

Примерно в это же время я нашел замечательный фреймворк Monogame для СSharp, написанный на базе XNA. Я не буду рассказывать о своей находке, потому что это уже сделали за меня.

ИТОГ РАЗ: проект консоли заморозить до лучших времён.

ИТОГ ДВА: игре быть (надеюсь, что в ближайшем будущем).

Наконец-то он перешел к делу

Что еще можно ожидать от человека, который занимается странными вещами и тестирует все самые безумные идеи в Excel?

Еще больше насилия над офисным инструментом и устаревшим языком программирования!

Так как игра изначально планировалась к реализации на базе маломощного процессора, то единственным очевидным решением было использовать ретро стиль, который не требует много ресурсов.

Какая игра портирована даже на осциллограф (Нет, это не Скайрим, отстань, Тодд) и не требует по современным меркам практически ничего?

Конечно, Doom (1993). Но графический движок Doom оказался для меня достаточно сложным в освоении, поэтому я обратил внимание на более старое творение id Software — Wolfenstein 3D.

Фактически, Wolfenstein 3D мог бы называться просто Wolfenstein. Приписка «3D» скорее всего появилась исключительно в коммерческих целях.

Графический движок Wolfenstein 3D представляет собой лишь проекцию 2D пространства, по которому перемещается главный герой. При этом пол и потолок располагаются на одном уровне, а стены всегда параллельны осям X и Y. Никакого разнообразия.

Вот так это выглядит (картинка нагло украдена с Вики):

Технология, которая позволяет добиться такого результата, называется Raycasting или «Бросание лучей».

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

​Схематически это выглядит так

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

В этом и заключается суть ядра графического движка Wolfenstein 3D.

А теперь немного извращений

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

Поле для вывода изображения

Так как все данные в Excel хранятся в табличной форме, а размер ячеек таблицы изменяется, то можно представить, что ячейки — это своеобразные пиксели.

Сперва нужно определиться с размером «экрана». Я выбрал такие значения:

  • Высота — 64 ячейки.
  • Ширина — 96 ячеек.

Такое соотношение ячеек не слишком нагружает Excel при отрисовке уровня. При желании эти значения можно изменить, потому что изображение получается уж очень ступенчатым.

Карта уровня

Карта уровня находится на соседнем листе:

Единички символизируют стены. «P» — это игрок.

Переменные и константы

Открываем встроенную IDE (как бы громко это ни звучало) и создаём модуль. Первым делом пишем следующий код:

Option Explicit Option Base 0 'Ширина и высота экрана, поле зрения, размер 'одного блока Const RENDER_H = 64 Const RENDER_W = 96 Const FOV = 75 Const cellHeight = 64 Const cellWidth = 64 'Определяем переменные Sub startRender() Dim plX As Double, plY As Double, plPOV As Double, _ arrMap() As Variant, rngMap As Range, mapHeight As Long, mapWidth As Long, _ arrRender() As Variant 'Определение размера карты mapHeight = Sheets("MAP").Cells(Rows.Count, 1).End(xlUp).Row mapWidth = Application.WorksheetFunction.CountA(Sheets("MAP").Rows(1)) 'Диапазон карты Set rngMap = Sheets("MAP").Range(Sheets("MAP").Cells(1, 1), _ Sheets("MAP").Cells(mapHeight, mapWidth)) arrMap() = rngMap.Value 'Расчёт положения игрока на карте plX = GetPlayerCoord(arrMap(), "X") plY = GetPlayerCoord(arrMap(), "Y") 'То самое направление взгляда, изменение которого мне лень прописывать plPOV = Application.WorksheetFunction.Radians(90) 'Вызываем функцию, которая возвращает массив данных для отрисовки изображения arrRender() = getArrayForRender(plX, plY, plPOV, arrMap()) Call renderImage(arrRender()) End Sub

Функция для определения положения игрока в системе координат выглядит следующим образом (на деле эту функцию можно сократить до нескольких строк, так как длина одного блока стены по X == его длине по Y. Для тестовой версии сойдёт и так):

Function GetPlayerCoord(arrMap() As Variant, strAxis As String) As Double Dim countFD As Long, countSD As Long For countFD = 1 To UBound(arrMap(), 1) For countSD = 1 To UBound(arrMap(), 2) If arrMap(countFD, countSD) = "P" Then If strAxis = "X" Then GetPlayerCoord = (cellWidth / 2) + (cellWidth * countSD) ElseIf strAxis = "Y" Then GetPlayerCoord = (cellHeight / 2) + (cellHeight * countFD) End If End If Next Next End Function

P.S. Я уже почти нажал кнопку, чтобы опубликовать статью, и меня осенило, что можно вообще избавиться от переменной «размер блока» и этой функции. Я уже говорил, что не очень сообразительный?

Ядро движка

Теперь самое интересное. Для начала посмотрите на этот «профессиональный» чертеж:

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

Всего за цикл программа кастует ровно столько лучей, сколько пикселей занимает ширина поля вывода изображения. В нашем случае — 96 «ячеек-пикселей». В прошлом, когда компьютеры были слабые, это бы однозначно замедлило игру. Но мы живём во времена, когда не жалеют ресурсов даже для яиц коня, поэтому отрисовка целых 2 073 600 (1920 * 1080) различных пикселей не должна замедлить процессор. Конечно я говорю не про Excel, который подтормаживает даже при 6144 (96*64) «пикселях».

Первый луч будет «брошен» под углом, который рассчитывается по формуле: направление взгляда — поле обзора/2.

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

Угол последнего луча рассчитывается по формуле: направление взгляда + поле обзора/2.

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

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

Function getArrayForRender(plX As Double, plY As Double, _ plPOV As Double, arrMap() As Variant) As Variant Dim arrRender(0 To 95) As Variant, countRays As Integer, countLength As Double, rayX As Double, rayY As Double, _ rayXMod As Long, rayYMod As Long, FOVAngle As Double, plFOV As Double plFOV = Application.WorksheetFunction.Radians(FOV) For countRays = 0 To 95 Step 1 'Рассчитываем угол каждого последующего луча FOVAngle = (plPOV - plFOV / 2) + (countRays * (plFOV / RENDER_W)) 'Цикл, который перемещает луч вперёд под установленным углом For countLength = 0 To RENDER_H * 20 Step 0.05 'Новые значения X и Y для каждой итерации цикла rayY = plY + countLength * Sin(FOVAngle) rayX = plX + countLength * Cos(FOVAngle) 'Считаем, в каком элементе массива карты находится стена rayYMod = Int(rayY / cellHeight) rayXMod = Int(rayX / cellWidth) 'Если луч встречает стену ("1") If arrMap(rayYMod, rayXMod) = "1" Then 'Записываем в массив высоту каждого столбца arrRender(countRays) = (Int((RENDER_H * 64) / (countLength * Cos(plPOV - FOVAngle)))) Exit For End If Next Next getArrayForRender = arrRender() End Function

Рендеринг изображения

Осталось лишь взять полученный в процессе работы предыдущей функции массив значений и отрисовать поочерёдно каждый столбец нашего «экрана».

Для этого пишем следующий метод:

Sub renderImage(arrRender() As Variant) Dim rngField As Range, countColumns As Integer, countRows As Integer, firstRow As Integer 'Запускаем оптимизацию: отключение обновления экрана stuffM.startOpt Set rngField = Sheets("RENDER").Range(Sheets("RENDER").Cells(2, 2), Sheets("RENDER").Cells(RENDER_H + 1, RENDER_W + 1)) rngField.Interior.Color = RGB(200, 200, 200) For countColumns = 1 To 96 'Каждое нечетное число приводим к четному для более плавной отрисовки If arrRender(countColumns - 1) Mod 2 <> 0 Then arrRender(countColumns - 1) = arrRender(countColumns - 1) End If 'Если столбец выше размера экрана, приводим его к размеру экрана If arrRender(countColumns - 1) > 64 Then arrRender(countColumns - 1) = 64 End If 'Ячейка, с которой начинается отрисовка столбца firstRow = RENDER_H / 2 - arrRender(countColumns - 1) / 2 'Отрисовка столбцов For countRows = firstRow To firstRow + arrRender(countColumns - 1) rngField.Cells(countRows, countColumns).Interior.Color = vbWhite Next Next 'Останавливаем оптимизацию stuffM.stopOpt End Sub

Вот что получается в итоге:

​Красной стрелкой - направление взгляда
Видео, которое демонстрирует процесс рендеринга

В заключение

Конечно, это всего лишь первые шаги к созданию полноценного графического ядра для Wolfestein-подобной игры. Помимо прочего нужно прописать рендеринг текстур, спрайтов, оружия, HUD.

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

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

Код в файле немного отличается от приведённого выше, потому что я правил его на ходу. Избавлялся от мусора и разных тестовых строк.

P.S. Теперь, когда я испытал подобный опыт, тот клон Doom, который сделал в Excel один несчастный программист (с разрушением окружения, многоуровневостью и даже воксельными моделями), уже не кажется таким фантастическим. Конечно путь от простого рендеринга лабиринта до полноценной игры - сложный путь. Он требует огромного количества времени, сил, знаний и навыка усидчивости.

Но сразу же возникает один вопрос. Зачем тратить на это время и силы, если можно двигаться вперед, а к VBA возвращаться лишь для очередного забавного теста технологии, которая противоречит самой сущности Excel?

Материал опубликован пользователем.
Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Александр Милашев", "author_type": "self", "tags": [], "comments": 29, "likes": 84, "favorites": 79, "is_advertisement": false, "subsite_label": "gamedev", "id": 92678, "is_wide": false, "is_ugc": true, "date": "Wed, 15 Jan 2020 14:12:58 +0300", "is_special": false }
0
{ "id": 92678, "author_id": 34713, "diff_limit": 1000, "urls": {"diff":"\/comments\/92678\/get","add":"\/comments\/92678\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/92678"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 64954, "last_count_and_date": null }
29 комментариев
Популярные
По порядку
Написать комментарий...
24

А теперь ждём разработку «Quake 1» под «1С-Предприятие» )))

Ответить
5

Минимальные требования: 1Tb RAM? 

Ответить
24

Wolfenstein 3D в EXCEL?

мсье знает толк в извращениях

Ответить
2

И тем не менее до рендера на SQL ему еще далеко:

https://habr.com/ru/post/435390/

Ответить
12

Ох, обожаю такие статьи! Очень круто!

Ответить
5

Благодарю, я старался)

Ответить
–6

а зачем.

Ответить
22

Потому что некоторым людям интересно решать сложные задачи просто так, их мозгу некомфортно просто деградировать, лёжа на диване.

Ответить
7

их мозгу некомфортно просто деградировать, лёжа на диване

Это не легкое дело, да, не каждый так сможет

Ответить
1

Ну, на самом деле, занятие не лёгкое, если заниматься этим нонстоп длительное время. Мне так кажется. Мой рекорд - 1 день

Ответить
0

Пиздец ну ты и слабак

Ответить
1

Буду тренироваться ещё. Выложу пост о результатах. Уверен, о таком читать интереснее, чем про этот Эксель.

Ответить
0

Надо будет написать гайд "как провести месяц не выходя из дома". Я в этом профи.

Ответить
6

Не ну все, бросаю Unreal Engine и перехожу на Excel)

Ответить
2

А сделайте также дум новый чтобы мне комп апгрейдить не пришлось.

Ответить
6

Зачем? играй на фотоаппарате 

Ответить
0

Если комп способен запустить Doom в Excel, то комп уже и подавно может запустить сотню таких Doom'ов сам по себе.

Ответить
2

Ага, у меня там даже ссылка в конце на эту игру есть) Тут он конечно заморочился

Ответить
2

А есть в Блокноте?

Ответить
0

Шутки шутками, но я думаю и в блокноте такое можно сделать. Вот тут мужик запилил подобное в консоли на C++, и если блокнот использовать для вывода символов вместо консоли, то должно все получиться)

https://youtu.be/xW8skO7MFYw

Ответить
1

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

Ответить
1

Привет,

молодец, хорошая статья вышла, но есть пара возражений по логике кода.
1.Функция определения положения игрока действительно странная. зачем 2 раза лопатить массив карты, чтобы найти сперва X-координату, а затем Y? Это все определяется за один проход, и не важно квадратная ячейка или нет (просто если хочется неквадратный мир - держим отдельно 2 переменных: cellSizeX и cellSizeY). Вобщем, не оправдано ни с т.з.быстродействия, ни с т.з. копактности кода.
2.Процедура рей-трейсера, конечно, достаточно короткая по коду, но жутко нерациональная. Как делать более рационально:
a) создаем переменные дистанции до пересечений луча с горизонтальными линиями сетки карты и текущих координат X и Y места их пересечения. Также создаем переменные шага дистанции и приростов по X и Y между двумя соседними пересечениями. То же самое - для вертикальных пересечений. Всего - 12 переменных (на деле, их немного больше, но это уже совсем детали).
б) вычисляем первое пересечение с горизонтальной и вертикальной линиями сетки (берем остатки от деления на размер клетки положения игрока по оси Х и Y и дальше с помощью синуса и косинуса получаем недостающие координаты, а потом вычисляем инициальные дистанции до этих пересечений) и заполняем соответствующие переменные. Потом, используя размер ячейки, заполняем переменные межклеточных приростов по X и Y и шаги дистанций между 2 соседними клетками по горизонтали и по вертикали.
То есть, фактически, у нас есть 2 луча - к горизонтальным линиям и к вертикальным. И приращивать мы их будем в одном цикле, на конкурентной основе (в каждую итерацию, сравнивая дистанции, пройденные лучами, мы приращиваем тот из них, который "короче", пока один из них не "упрется" в стенку). Чем такой путь лучше (ведь мы трассируем не 1 луч, а сразу 2)? Выигрыш будет в количестве итераций цикла (у меня количество итераций на карте 24х24 будет максимум 23*2 = 46, а в вышеуказанном рейкастере итераций ожидается до 64*20/0,05 =25600(!)). Плюс, учитывая эмпирический характер пересечений в таком рейкастере, на экране, несмотря на "микроскопические" приросты, может появится "гребенка" при приближении к стенам (особенно, если там таки появятся текстуры). Еще один, небольшой выигрыш: в моем случае для приращения позиции лучей мы используем сложения, а не умножения, что "дешевле" по времени.
3.В рендере:
а) "сглаживатель отрисовки" - это костыль, который "лечит" вышеуказанную проблему "гребенки" (причем - за счет снижения и без того низкого разрешения). Он не нужен.
б) использовать свойство .Interior.Color для изменения цветов ячеек - это очень "дорого" и не на всех версиях Экселя отработает без багов. Лучше создать пользовательскую палитру (ту, что из 56 цветов) при помощи ActiveWorkbook.Colors() и потом использовать свойство .Interior.ColorIndex для присвоения цвета по его индексу в палитре.
в) Очень специфический для Экселя "хинт": закрашивать ячейки поштучно через .Cells(,) гораздо медленнее, чем закрасить сразу весь столбец одним цветом через .Range(Cells(,),Cells(,))
г) Чтобы отрисовка шла быстро и плавно, перед началом отрисовки нужно отключать системный рендер экрана (Application.ScreenUpdating = False), а по завершении включать снова (Application.ScreenUpdating = True).

Там много чего еще можно понемножку улучшить даже на таком базовом уровне "вольфенштейна", но если не полениться и дополнить код хотя бы тем, что я описал выше, Вы удивитесь, насколько быстрее и круче оно все у Вас заработает.

Удачи!
Я на itch.io: https://davevsziggy.itch.io/dave-vs-ziggy

Ответить
1

Ааааа, я чувствовал, что этот день настанет! Я из-за Dave vs. Ziggy в т.ч. начал изучать VBA)

Спасибо за советы, вечером изучу вопрос)
P.S. По поводу Application.ScreenUpdating. Я его использовал, только решил не заострять на этом внимание в статье)

Ответить
1

Только, конечно, недостающие приросты по X и Y вычисляются не через синус и косинус (которые используются для вычисления дистанций), а через тангенс (потому что мы ведем вычисления не от вектора, а от нужных нам длин его проекций на оси X и Y).

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

Ответить
1

Просто для справки: оно 3D называется потому что это триквел/ремейк/перезапуск стелсового Castle Wolfenstein из 80-ых. ID сначала хотели сделать 3D ремейк, потом решили сделать более динамичную игру (а мог бы получится первый трёхмерный стелс задолго до Thief). Они и права купили. 

Инфа с английской вики.

Ответить
1

Да, был бы интересный проект. Для того-то времени. Кстати в Wolfenstein 3D тоже есть зачатки(очень очень зачатки) стелса. К солдатам, которые стоят спиной, можно подкрасться

Ответить
1

Ха, сказал же не нужны!

Ответить
1

Знание Excel - уровень Бог!

Ответить

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "Article Branding", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "cfovz", "p2": "glug" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Баннер в ленте на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 16, "label": "Кнопка в шапке мобайл", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "chvjx", "p2": "ftwx" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } }, { "id": 20, "label": "Кнопка в сайдбаре", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "chfbl", "p2": "gnwc" } } } ]