№3 Строим тюрьму мышью, и комнаты, которые находятся сами
· Nick
Второй этап завершён, и впервые есть на что посмотреть. Клиент на Godot теперь стоит поверх ядра симуляции, и мышью можно построить одноэтажную тюрьму: класть полы, рисовать стены, ставить двери, отмечать комнаты, сносить всё лишнее. Всё это простые кубы и плоские цвета, людей в ней по-прежнему нет. Но это место, и по нему можно двигать камеру.
Что можно делать сейчас
Вы начинаете новую игру, выбираете размер карты (64, 128 или 256 клеток в стороне) и, при желании, сид, и получаете пустую сетку. Камера ортографическая с фиксированным наклоном. Она поворачивается строго шагами по 90°, приближается к курсору, перемещается клавишами или средней кнопкой и имеет режим вида сверху для строительства. Стены прятали бы комнаты от свободно вращающейся камеры, поэтому она не вращается свободно.
Полы и снос принимают прямоугольник. Стены идут по контуру (или по прямой, если держать Shift). Дверь ставится кликом и выбирает направление по соседним стенам, а повернуть её можно клавишей. Пока вы тянете, область под курсором показывается призраком: зелёные клетки будут построены, красные – пропущены, а подпись сообщает сколько и почему. В мир ничего не отправляется, пока вы не отпустите кнопку, а если ядро говорит, что не изменится ни одна клетка, не отправляется вообще ничего. Правый клик или Escape отменяют.
В модели мира несколько этажей, поэтому есть выбор этажа, а этажи выше текущего скрыты. У комнат свой инструмент, о нём ниже. Отладочный оверлей показывает границы чанков, проходимые клетки, клетку под курсором и панель с таймингами. Весь интерфейс на английском.
Предпросмотр – это настоящий результат
Призрак, который расходится с результатом, хуже, чем отсутствие призрака. Поэтому клиент не гадает, что сделает команда строительства. Ядро умеет ответить «что будет, если выполнить это» для той же области, клетка за клеткой, с причиной для каждой клетки, которую оно отклонит. Проверка, заполняющая предпросмотр, – то же правило, которое команда применяет при настоящем выполнении, так что они не могут разойтись. Клиент только рисует ответ. К ядру он обращается снова, только когда изменилось что-то, влияющее на ответ: инструмент, протянутая область, этаж или сама карта.
Тот же принцип определяет и остальной клиент: он читает состояние мира и отправляет команды, а собственной логики симуляции в нём нет. Пока ядро тикает в главном потоке, а команда от клика применяется в начале следующего кадра, до тика мира. Если время тика начнёт подскакивать, когда мир наполнится людьми, план – вынести ядро в отдельный поток, с измеренным порогом, когда это делать.
Комнаты, которые находятся сами
Я рассмотрел два способа сделать комнаты. В Prison Architect рисуется зона, которая затем становится отдельным объектом со своими клетками. Это легко понять, но в тюрьме это не работает: целый тюремный блок оказался бы одной зоной, а после перестройки стены её приходится перекрашивать. Другой способ – находить замкнутые области автоматически, но тогда нельзя отметить двор, вокруг которого нет стен.
Я выбрал смесь. Хранится только тип комнаты на каждой клетке: камера, столовая, баня, медпункт, комната свиданий, прогулочный двор и так далее. Всё остальное вычисляется по стенам. Пространство – это группа открытых клеток, связанных сторонами, на одном уровне, и оно замкнуто, если не касается края карты или пустого обрыва. Комната – это все клетки одного типа внутри одного пространства. Стены и двери – границы, а тип, нарисованный под стеной, сохраняется, но игнорируется, поэтому возвращается, если стену убрать.
Так получается поведение, которое ощущается правильным без дополнительных указаний. Проведите стену через комнату – получится две комнаты одного типа. Уберите стену – они сольются. Поставьте дверь на место стены – ничего не изменится. Недопустимая комната не удаляется, ведь это было бы противоположностью «без боли»; она показывается красным с причинами: не замкнута, делится с другим типом, слишком мала или нет двери. Можно закрасить прямоугольник, заполнить одним кликом целое замкнутое пространство или очистить его.
Список комнат – производные данные, поэтому он не сохраняется и не входит в хеш мира; сохраняется только тип на каждой клетке. Он перестраивается после команд и после систем, а проверка неизменившегося мира ничего не стоит. На этом этапе изменённый уровень пересчитывается целиком, что занимает около полумиллисекунды на карте 256×256; пересчёт по чанкам может подождать, пока те же механизмы не понадобятся регионам для поиска пути. Правило, которому я его подчиняю: инкрементальная перестройка должна давать тот же результат, что и полная.
Перестраиваем только изменившееся, срез этажей даром
Карта рисуется одной сеткой на чанк на уровень, а не одним объектом на стену. У каждого чанка в ядре есть счётчик версий, и рендерер помнит, какую версию нарисовал в последний раз. Когда мир меняется, в очередь попадают только чанки с более новой версией, и очередь обрабатывается с бюджетом времени на кадр, так что огромное изменение растягивается на несколько кадров, а не замораживает один. Покраска типа комнаты не увеличивает версию карты, поэтому построитель сеток она вообще не будит.
Срез этажей следует той же идее «делать меньше». Этажи выше текущего просто скрываются. Стены текущего этажа сжимаются шейдером до одного метра: один общий материал и одно значение высоты, поэтому переключение этажей или полных стен ничего не перестраивает. Переключение по четырём уровням вызывает ноль перестроек сеток.
Щель в десять сантиметров
Когда я попробовал первую одноэтажную тюрьму на финальной проверке, на уровне выше был пол, и сбоку между верхом стены и плитой над ней проходила тонкая светлая линия. На углу было видно, как пол висит в воздухе. Я подозревал срез этажей, потом z-fighting, потом сглаживание.
Ничего из этого. Пол был одним плоским квадом без толщины, нарисованным на высоте своего уровня плюс десять сантиметров. Стены нижнего уровня доходили ровно до трёх метров. Пол следующего уровня начинался на трёх метрах десяти сантиметрах. Щель шириной десять сантиметров, и сквозь неё была видна освещённая верхняя грань стены. По горизонтали всё совпадало идеально, поэтому заметил я не сразу.
Исправление: пол теперь плита с настоящей толщиной, доходящая вниз до начала своего уровня. Верх остаётся на той же высоте, так что выбор клеток мышью и все оверлеи не изменились. Боковые грани пропускаются там, где пол или стена соприкасаются внутри одного чанка, и рисуются на границах чанков, так что сетка чанка не зависит от соседей. В итоге на торце плиты остаётся тёмная кромка в десять сантиметров. Я посмотрел скриншоты до и после и решил её оставить. Она выглядит как край пола, чем и является.
Выдерживает ли это
Последняя задача этапа – построить мышью одноэтажную тюрьму и посмотреть, не больно ли. По большей части не было. Кроме щели, прогон выявил мелкие досадные вещи: кнопки инструментов не снимали подсветку, когда вы выбирали другой инструмент мышью, а из диалога новой игры нельзя было выйти по Escape. Оба случая исправлены. Меню паузы в списке идей на потом.
Что дальше
Дальше M2, движение и распорядок дня. В мире появятся регионы и достижимость, поиск пути между этажами и поля потоков для толп. У ядра наконец-то появится настоящая работа на каждом тике, так что порог бенчмарка перестанет спать. См. дорожную карту.