Wolfenstein 3D в EXCEL?

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

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

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

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

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

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

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

Wolfenstein 3D в EXCEL?

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

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

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

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

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

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

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

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

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

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

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

Wolfenstein 3D в EXCEL?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Карта уровня

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

Wolfenstein 3D в 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. Я уже почти нажал кнопку, чтобы опубликовать статью, и меня осенило, что можно вообще избавиться от переменной «размер блока» и этой функции. Я уже говорил, что не очень сообразительный?

Ядро движка

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

Wolfenstein 3D в EXCEL?

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

Всего за цикл программа кастует ровно столько лучей, сколько пикселей занимает ширина поля вывода изображения. В нашем случае — 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?

8888
37 комментариев

Комментарий недоступен

25
Ответить

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

5
Ответить

Wolfenstein 3D в EXCEL?


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

25
Ответить

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

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

2
Ответить

Комментарий недоступен

12
Ответить

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

6
Ответить