Делаем мод на спавн врагов скриптами в Fallout 4 - часть 2 - реализуем спавнер
В этой части проведем оценку локации на соответствие условиям спавна, сделаем сканер для поиска маркеров перехода на уровне вокруг игрока, прошерстим их через цикл, чтобы выбрать подходящий и, наконец, заспавнить на нем первого противника с помощью скрипта.
Предыдущая статья:
Идея работы мода
Для начала продумаем логику мода, что и как он будет делать в игре.
Я хочу, чтобы мод спавнил врагов во враждебной локации после того, как она будет считаться зачищенной. Враги будут спавниться у выхода из локации один раз на локацию, как бы устраивая засаду (как гули в локации Супермарт).
На невраждебной или давно зачищенной локации спавнер срабатывать не будет.
Принцип работы мода
Из вышеописанного следует, что нам нужно:
- Проверить локацию на враждебность
- Проверить спавнили ли мы в ней раньше
- Поймать момент зачистки локации
- Найти маркер перехода (вход) на локацию
- Заспавнить противников на маркере перехода
- Отметить, что на локации уже был спавн
Подходит ли нам локация и был ли в ней спавн мы будем проверять в момент перехода между локациями (через эвент перехода) и хранить в отдельных переменных.
Момент зачистки локации напрямую поймать не выйдет, для этого есть специальный эвент, но он относится к эвентам Alias для каждого конкретного объекта. Мы же делаем универсальный мод и прописывать в нем отдельно каждую локацию (пока) не можем.
Зато у нас есть циклический таймер, и мы можем проверять состояние локации по его истечении. Он длится 20 секунд, этого достаточно, чтобы игрок не заметил задержки внутри игры. Кроме того, локация в Fallout 4 считается зачищенной не со смертью всех врагов, а только со смертью босса подземелья, что может произойти незаметно для игрока посреди боя и еще сильнее сгладить задержку таймера.
Еще мы не хотим, чтобы враги заспавнились у игрока прямо перед носом, а значит, перед спавном нужно убедиться, что игрок достаточно далеко от выхода с локации.
Повторно запускаем свой мод
Для повторного запуска мода нужно открыть редактор Creation Kit, нажать на значок папки на главной панели, опционально отфильтровать название своего мода, выделить его в списке, нажать кнопку Set as Active File (тогда все изменения запишутся в него, а не будут считаться новым модом поверх) и нажать ОК.
Настраиваем проверку локации на возможность спавна
Для начала в скрипте добавим две переменные типа Bool (двоичные), которые имеют всего два состояния: 0 (или false) и 1 (или true), иначе говоря НЕТ/ДА или ВЫКЛ/ВКЛ. Они удобны для хранения простых состояний сущностей и занимают мало памяти.
Используем SpawnerEligibleLoc для определения, подходит ли локация для спавна, а SpawnerAlreadySpawn - для запоминания, что спавн уже состоялся. По умолчанию они равный 0 (false).
Также нам потребуется переменная внутриигровой сущности. Это ключевое слово Keyword с названием LocTypeClearable. Так игровой движок помечает все локации, которые могут быть зачищены. Этой переменной мы приписываем Property и Auto, потому что ее надо связать через окошко Property с конкретной сущностью движка, а Const - потому что она постоянная, всегда будет ссылаться только на один конкретный ключ.
Чтобы не расписывать каждый раз весь код, я буду пропускать неважные в данный момент его части, вставляя три точки в местах разрыва.
Сейчас мы записали переменные в текстовый файл. Чтобы движок их принял, делаем компиляцию, как в прошлой статье (первая картинка оттуда же). Однако, ссылка на ключевое слово сама не проставится, если мы добавляем его через текстовый редактор, переменная будет пуста. Чтобы это исправить, выделяем скрипт и нажимаем Property и снизу прожимаем кнопку Auto-Fill All, она автоматически заполнит все переменные-ссылки, если вы правильно указали их тип и название.
Если же вы решили использовать собственное название, тогда придется делать ссылку на внутриигровой объект вручную (синие метки). Вручную вам придется вводить данные также, если создадите переменную массив со ссылками на множество объектов (об этом в другой раз).
Вернемся к коду. Заполним ранее пустовавшее событие перехода между локациями. Вставим в него логическую конструкцию if - else (если - иначе), она позволяет нам проверять заданные условия и выполнять операции при соответствии им или в противоположном случае.
В коде выше после if (если) мы ссылаемся на встроенную переменную самого эвента akNewLoc, которая хранит ссылку на новую локацию, в которую перешел игрок. После нее мы ставим ".", что значит, что мы собираемся ссылаться на ее внутренние сущности (сущности хранящейся в переменной локации). Далее мы используем встроенную команду HasKeyword, которая проверяет, имеет ли сущность в себе ключ, указанный в скобочках.
Далее мы используем составное условие значком "&&" (и), с помощью которого связываем первое условие со вторым. В нем мы также ссылаемся к хранящейся локации akNewLoc.IsCleared(), используя встроенную в движок команду IsCleared(), которая проверяет, считается ли локация зачищенной в данный момент. Перед ней стоит знак "!" (не), что значит противоположность полученному состоянию.
Иначе говоря, вся эта конструкция логически читается так:
- ЕСЛИ ( новая локация имеет ключ (ЗАЧИЩАЕМАЯ) и НЕ (новая локация ЗАЧИЩЕНА) )
Скобочки после if не обязательны, я их ставлю для лучшей читаемости текста, как и табуляцию.
Проще говоря, мы проверяем является ли эта локация враждебной (если ее можно зачистить, значит, имеется враг для зачистки) и убеждаемся, что она еще не зачищена. В таком случае переменной статуса SpawnerEligibleLoc присваиваем true (т.е. локация подходит под условия спавна).
Если хоть одно из условий не выполняется: локация мирная или уже зачищена, тогда выполняется код, стоящий после else (иначе). В нем мы присваиваем переменной статуса противоположное значение SpawnerEligibleLoc = false.
А в конце вне условий дополнительно прописываем статус "был ли здесь спавн" SpawnerAlreadySpawn = false, что значит, что на локации мы еще никого не спавнили. Ее мы обнуляем (присваиваем false он же 0) при каждом переходе между локациями.
Это может показаться кому-то неинтуитивным, но если перебрать в голове все возможные варианты событий, то вы убедитесь, что это логично.
В самом спорном случае игрок зашел на локу, зачистил ее и покинул до спавна. При возвращении она будет считаться уже зачищенной и спавна не состоится.
Теперь, когда мы знаем статусы локации, пришло время для спанера.
Реализуем спавнер акторов
Добавим еще парочку переменных. Сначала переменную типа ActorBase с названием LvlRaider. Это тип переменной, отличный от Actor. Если Actor хранит в себе конкретного актора/существо, то ActorBase содержит в себе внутриигровой шаблон существа со ссылками на другие шаблоны. Например, для рейдера это значит, что при спавне может появиться рейдер разного пола, с разным лицом и прической.
Вторая переменная типа FormList под названием DES_SpawnMarkersFL - это список. Он может хранить в себе ссылки на самые разные сущности, но конкретно в этом мы будем хранить внутриигровые маркеры. Этот список мало внести в скрипт, нужно создать его в редакторе и там же заполнить.
Список создаем в разделе Miscellanious -> FormList, а внесем в него COCMarkerHeading - это маркер перехода локации, в который помещаются акторы в момент захода на локацию. Он содержит в себе координаты и углы поворота, в которые ставится переместишийся.
Если мы знаем название сущности, но не знаем раздел, в котором она лежит, то можно найти ее через фильтр (не забудьте отжать чекбокс Show Only Active). Для внесения в список просто перетягиваем объект мышкой. Не забываем после правок проводить компиляцию и заполнять ссылки на объекты в Properties скрипта.
Теперь, когда это сделано, напишем отдельную собственную функцию SpawnActors() для спавна и внесем ее вызов в наш таймер. Теперь при каждом истечении таймера скрипт будет пытаться кого-нибудь заспавнить.
Внутри функции вставляем условие со ссылками на статусы локации, что мы проверяем при каждом переходе между локациями. Только если локация подходит для спавна, и спавна на ней еще не было, мы будем производить спавн.
Если условия соблюдены, только тогда мы будем выполнять все остальные вычисления, чтобы не нагружать ПК лишними операциями. Допишем код функции...
Для начала мы создадим внутри функции локальные переменные SpawnMarker и FoundSpawnMarkers, локальные они именно потому что созданы внутри функции и только внутри нее существуют. Как только функция закончит вычисления, память от них освобождается. Это ее экономит, но это также значит, что на эти переменные нельзя ссылаться из других функций и эвентов.
Тип переменных здесь ObjectReference, т.е. ссылка на конкретный существующий в прямо сейчас в игре экземпляр какого-либо объекта - персонажа, локацию, предмет, маркер и т.п. Вторая переменная имеет тип ObjectReference[ ] - скобочки в конце значат, что это массив, т.е. переменная, хранящая в себе не одно значение, а сразу несколько. Для кода это значит, что есть FoundSpawnMarkers[0] (начало всегда с 0), FoundSpawnMarkers[1], FoundSpawnMarkers[2] и т.д., каждый хранит ссылку на отдельный (или один и тот же) объект.
FoundSpawnMarkers мы используем, чтобы в нее складировать все маркеры перехода, которые есть на этой локации, а в SpawnMarker поместим один конкретный, который будем использовать для спавна.
Переменной SpawnMarker мы сразу присваиваем значение none (пусто/никого), а в FoundSpawnMarkers прописываем команду PlayerRef.FindAllReferencesOfType(DES_SpawnMarkersFL as form, 10000). В ней мы берем игрока PlayerRef и используем на нем встроенную в движок функцию поиска объектов определенного типа вокруг FindAllReferencesOfType, в скобках прописываем, что ищем DES_SpawnMarkersFL as form и на каком расстоянии: 10000.
Знание о том, какие бывают встроенные функции и как работают, можно почерпнуть из вики, документации, сети или разбирая, как работают чужие моды и скрипты квестов самой игры.
Запись as form после DES_SpawnMarkersFL означает сообщение коду, в каком формате мы хотим применить переменную в этом месте. Иногда такие приписки необходимо делать для встроенных функций, если они работают только с чем-то конкретным, чем ваша переменная способна стать.
Если это возможно, код поменяет данные в другой формат. Например, переменные типа bool можно перевести в int, ведь bool это либо 0 (false), либо 1 (true), но обратно это может не сработать, если int равен, скажем, 56.
Теперь FoundSpawnMarkers хранит в себе все маркеры COCMarkerHeading вокруг игрока на расстоянии 10000. Нам осталось среди них выбрать один подходящий для спавна (т.е. не слишком близкий к игроку).
Для этой задачи мы используем цикл while (делать что-то пока что-то верно). Он выполняет код внутри, пока выполняется условие в скобках, т.е. пока объявленный выше int i = 0 меньше, чем FoundSpawnMarkers.length.
Т.е. мы собираемся пробежаться по содержимому массива FoundSpawnMarkers по всей его длине .length (встроенная команда, берет из массива количество его составляющих). Для пробега используем переменную i, которую в конце каждого забега увеличиваем на 1 операцией i += 1. Если ее не прописать, то цикл станет бесконечным, а игра зависнет.
Тут может возникнуть путаница. Если мы нашли, например, два маркера, то они будут записаны в FoundSpawnMarkers[0] и в FoundSpawnMarkers[1]. Но длина массива FoundSpawnMarkers .length будет равна 2, ведь элементов 2.
Внутри каждого забега мы объявленной ранее переменной distance присваиваем расстояние от игрока до конкретного найденного маркера из массива PlayerRef.GetDistance(FoundSpawnMarkers[i]) as int. Функция GetDistance получает число в формате float (нецелое), и командой as int мы округляем его до целого значения.
И в конце забега сравниваем это расстояние с заданным нами безопасным в 1000. Если маркер дальше, то записываем его в SpawnMarker.
А после цикла while делаем еще одну проверку. Если мы нашли подходящий маркер, то на маркере SpawnMarker помещаем актора PlaceActorAtMe, который указан в скобочках. И под конец отмечаем, что спавн на локации уже произошел, чтобы он не повторялся в этой же локации при каждом истечении таймера.
Однако, с такой командой может случится баг, если актор заспавнится на неровной поверхности, поэтому добавим еще пару строчек.
Заспавненного актора занесем в локальную переменную а0, чтобы через нее поместить его командой Moveto на маркер в его координаты по осям X и Y (0, 0), и соотнести углы поворота взгляда актора с маркером с помощью abMatchRotation = true. А после этого командой MoveToNearestNavmeshLocation() присоединяем актора к ближайшей точке навмеша - аналог встроенного в движок поиска пути. Без этого актор иногда может заспавниться под углом и ходить, как пришибленный.
LvlRaider сам по себе - это просто ссылка на сущность внутри конструктора. Это шаблон, который после размещения в мире игры превращается в отдельный конкретный экземпляр этой сущности - референс (сокращенно Ref).
Поэтому для действий с конкретным актором мы взаимодействуем не с его шаблоном, а с ним самим (референсом) индивидуально. Для этого при создании и помещаем его в отдельную переменную а0, чтобы через нее иметь к нему прямой доступ.
А вот игрок в игре всегда один, поэтому он и называется сразу PlayerRef, и с ним можно взаимодействовать сразу, используя, например, ту же MoveTo.
Еще одна команда здесь Utility.Wait(0.1) использует встроенные вспомогательные функции движка Utility и вызывает здесь задержку в работе скрипта Wait в 0.1 секунды. Это помогает снять нагрузку со сложных скриптов, давая им немного времени на выполнение вычислений.
Иногда именно такие задержки помогают скриптам корректно работать, успевать выполнять свои расчеты и устраняют фризы в игре во время тяжелых вычислений.
Получилось жирненько. Я постаралась описать принципы работы кода с азов с комментариями даже для тех, кто не разбирается в программировании вовсе. Хотя конкретно здесь не самые простые примеры их реализации.
Сейчас мод спавнит лишь одного актора. Дальше добавим несколько, а также отработаем поддержку МСМ меню и вывод вспомогательных сообщений для отслеживания работы нашей писанины прямо в процессе.