Nollopa Serious Macro Scripting. Углубленный уровень.


Serious Macro Scripting. Углубленный уровень

Введение

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

Доступ к спискам доступных действий и событий у сущностей в редакторе скриптов

к оглавлению

В редакторе скриптов есть система подсказок при написании кода.
Для её активации, нужно нажать Ctrl+Space(или правую кнопку мыши). Подсказки контекстно зависимые - разместив курсор в теле функции (между скобками {...}) получим варианты как начать строку.  Выбрав первый вариант Action Object.Function() у нас появится вот такая строчка: 
Action ?.?()
Поставив курсор у первого знака вопроса и опять нажав Ctrl+Space(или правую кнопку мыши) появится список из доступных переменных в скрипте, по дефолту там будет Null и This. Заменим знак вопроса на нужную переменную, например, Entity. Теперь назначим ей сущность. Напомню, что для этого нужно выделить сущность в мире, зажать Ctrl+Alt+Shift и перетащить её на переменную Entity.
Теперь, перейдя ко второму знаку вопроса и нажав на Ctrl+Space мы получим список действий, доступные этой сущности.
С Wait() и On() do {} эта система работает не так удобно, редактор теряется из-за всех этих скобок и не может предложить нужные подстановки. Однако, как только у вас есть хоть что-то, что написано после точки у переменной, то разместив курсор на этом тексте через Ctrl+Space мы получим список событий, доступные этой сущности.

World Info в скриптах

к оглавлению

Для смены музыки на локациях, запуска таймера и получении информации об игроках нужно в скриптах обращаться к сущности World Info.
Чтобы переменной в скрипте назначить эту сущность необходимо выбрать ее в списке сущностей нажав на клавишу N и выделив там её. После, как и с другими объектами - зажать Ctrl+Alt+Shift и перетащить её на переменную.
Лайфхак - на W можно сохранять в быстрый доступ выделенные объекты на уровне - так что, выделите World Info(убедитесь, что только его), нажмите на W и сохраните выделение.

Сущность игрока

к оглавлению

Для вывода на экран сообщений от Нетриксы, фраз игрока или других NPC, а так же для разных воздействий напрямую с игроком нужно обращаться к сущности игрока.
Получить её можно через World Info:
Player=WorldInfo.GetClosestPlayer()
Интересный момент - скрипт не знает, к какому классу относится эта переменная, так как она назначается во время работы игры, а сейчас, в редакторе она пустая. Из-за этого не будут появляться подсказки по событиям и действиям.
Один из способов, как объяснить скрипту, что мы тут имеем в виду - это заранее обозначить класс переменной:
global var CPlayerPuppetEntity Player

Скрипт

Что тут происходит?

Macro Main ?
{
  global var CPlayerPuppetEntity Player
  Wait (0.1 sec)
  Player=WorldInfo.GetClosestPlayer()
  ActionAsync Player.macInflictDamage(15)
  Wait (False)
}

- объявляем глобальную переменную Player
и указываем, что она относится
к классу CPlayerPuppetEntity

- через 0.1 секунду со старта
переменная получает сущность игрока
и игрок получает 15 единиц урона.

Macro Main ?
{
  global var CPlayerPuppetEntity Player
  Wait event(Detector.Activated)
  Player=WorldInfo.GetClosestPlayer()
  ActionAsync Player.macInflictDamage(15)
  Wait (False)
}

- объявляем глобальную переменную Player
и указываем, что она относится
к классу CPlayerPuppetEntity

- после активации детектора
переменная получает сущность игрока
и игрок получает 15 единиц урона.

Ключевые моменты:
Важно, чтобы игрок успел однозначно загрузиться на карту.
Если нужно сразу со старта уровня что то делать с игроком, то необходимо ставить маленькую задержку Wait (0.1 sec).
В случае, если игрок уже загружен и в игровом процессе понадобится к нему обратиться, то задержка не нужна.

Нетрикса, говорящие NPC и Сэм

к оглавлению

Скрипт

Что тут происходит?

Macro Main ?
{
  global var CPlayerPuppetEntity Player
  Wait (0.10 sec)
  Player=WorldInfo.GetAllPlayersInRange()
  Action Player.ShowNetricsaIcon()
  Action Player.SayNetricsa(NettieQuote,"You are the best!",1.50)
  Wait (1.50 sec)
  Action Player.HideNetricsaIcon()
  Action Player.SayPlayer(SamQuote,"Yeah baby!",1.25)
  Action Player.SayPuppet(NPCEntity, NPCQuote,"Bye-bye!",1)
}

- .GetAllPlayersInRange()- если аргументы оставить пустыми,
то получаем всех игроков на карте, полезно для корректной работы
коопа или мультиплеера.
- Wait (1.50 sec) после команд .Say нужен,
чтобы реплики между собой не пересекались.

- .ShowNetricsaIcon(), .HideNetricsaIcon() - команды,
чтобы показать и спрятать иконку с Нетриксой.

Для диалогов используются следующие команды:
.SayNetricsa(), .SayPlayer(), SayPuppet() - для Нетриксы, Сэма и NPC соответственно.
Аргументы для .SayNetricsa(), .SayPlayer() одинаковые, сначала нужно указать звуковой файл, потом в кавычках ("") написать титры, далее указываем длительность отображения титров на экране.
Эти команды не знают о длительности файла, так что нам необходимо кроме того, что указывать в аргументах нужную длительность фразы, так и добавлять Wait() с длительностью фразы, чтобы следующая фраза не прервала предыдущую.
Для SayPuppet() добавляется вначале еще один аргумент - указание на того NPC, который говорит, чтобы звук доносился именно от него.
Тут мы сталкиваемся с новым видом переменной, которая использует ресурс, а не какую либо сущность из уровня.

Добавление ресурсов в скрипт

к оглавлению

Чтобы добавить ресурс в переменную нужно сделать следующее:
- выделить объект Macro, в котором наш скрипт
- в его свойствах раскрыть вкладку Variables
добавить новую переменную, дать ей имя
- в Var value в Implementation выбрать Resource
и наконец, в Resource выбрать нужный ресурс.

Ловим сущность после спавна

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

Скрипт

Что тут происходит?

Macro Main ?
{
  Action Enemy.SpawnSimple()
  Wait (event(Enemy.SpawneeAvailable))
  EnemySpawnee = Enemy.GetLastSpawned()
  global var CLeggedCharacterEntity EnemySpawnee
  Action EnemySpawnee.PlayAnimLoop("Happy_Jump")
}

Заспавнили персонажа

Как только он появился
- передали в переменную
сущность персонажа

определили переменную для удобства

запустили проигрывание
анимации по кругу

В таком варианте скрипта мы можем работать только с единственным заспавненым персонажем. Плюс сам скрипт начинает выглядеть достаточно запутано, а если нам понадобится обратиться не к 1 спавнеру, а к нескольким, то нужен другой подход. 

Скрипт

Что тут происходит?

Macro Main ?
{
  Action Enemy.SpawnSimple()
  Wait (false)
}On (every(event(Enemy.SpawneeAvailable))) do {
  EnemySpawnee = Enemy.GetLastSpawned()
  RunAsync EnemySpawnee.EnemyBehavior  
}

Macro EnemyBehavior CLeggedCharacterEntity
{
  Action This.PlayAnimLoop("Happy_Jump")
}

- Run/RunAsync - команда запуска функции, применительно к конкретной сущности,
 данном примере запускаем функцию EnemyBehavior к EnemySpawnee

- Macro EnemyBehavior CLeggedCharacterEntity - После тела основной функции Main
мы пишем новую функцию.

Обратите внимание, что вместо знака вопроса мы указали класс сущности, к которой эта функция относится.
Теперь переменная This будет вызывать подсказки для CLeggedCharacterEntity. 

Macro Main ?
{
  Action Enemy_01.SpawnSimple()
  Action Enemy_02.SpawnSimple()
  Wait (False)
}On (every(event(Enemy_01.SpawneeAvailable))) do {
  Enemy_Spawnee_1=Enemy_01.GetLastSpawned()
  RunAsync Enemy_Spawnee_1.Enemy_01_Behaviour
}On (every(event(Enemy_02.SpawneeAvailable))) do {
  Enemy_Spawnee_2=Enemy_02.GetLastSpawned()
  RunAsync Enemy_Spawnee_2.Enemy_02_Behaviour

Macro Enemy_01_Behaviour СLeggedCharacterEntity
{
  Wait (False)
} On (event(This.LastPatrolMarkerReached)) do {
  Action This.AttackMelee("Wave")
  Action This.macSetThreatSensitivity("Standard")
}

Macro Enemy_02_Behaviour СLeggedCharacterEntity
{
  Wait (event(This.LastPatrolMarkerReached))
  Action This.macSetThreatSensitivity("Standard")
}

 

В таком варианте скрипта мы можем работать со всеми заспавнеными персонажами.

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

Send event

к оглавлению

Send event Объект.Событие  - это команда, которая отправляет именное событие какой либо переменной. Соответственно через ожидание событий мы можем это услышать.
Это работает как внутри скрипта так и на другие скрипты. Пример будет в следующем разделе. 

Chapter control

к оглавлению

Конечно логику уровня можно сделать полностью в одном скрипте от начала до конца. А можно на каждый спавнер создавать по отдельному скрипту. Можно попробовать сгруппировать все одинаковые события в свои скрипты и тд и тп, и всё это будет работать. Но как сделать “правильнее”?
Что подразумевается под “правильно”? Удобство внесения изменении, а так же логичная структура, чтобы и через пол года открыть проект и не потеряться. Мы можем воспользоваться уже готовой структурой уровней, как это придумали разработчики оригинального Serious Sam 2.
В чем заключается их подход?
Уровень делится на логические главы, с набором связанных событий.
За начало главы отвечает Chapter Info.
Для каждой главы создается свой отдельный скрипт, условием начала которого становится начало главы, и работает он до тех пор, пока глава не закончится.
Что нам дает такой подход?
Во первых, каждый Chapter Info это точка автосохранения для игрока.
Во вторых, в редакторе мы можем начинать уровень с какой угодно выбранной главы, а это значит, что вместе с ней будет запускаться нужный нам набор событий, при этом остальные скрипты не будут стартовать. Это позволяет быстро и изолировано проводить тесты, например как реализована арена или как работает головоломка, не тратя время на прохождение уровня до или на отключение ненужных спавнеров, которые должны стартовать одновременно с другой главой.
Давайте рассмотрим подобный скрипт для одной главы:

Скрипт

Что тут происходит?

Macro Main ?
{
  Wait (event(Chapter001.Started))
  RunAsync This.Chapter
}

Macro Chapter ?
{
  Wait (event(Chapter001.Finished))
} On (every (4.00 sec)) do {
  Action SoundAlarm.PlayOnce()
} On (event(SpawnDetector.Activated)) do {
  Action Enemy.SpawnSimple()
} On (event(FinalDetector.Activated)) do {
  Send event ChapterControl.Chapter001Ended  
}

Можно заметить, что тут две функции
- Main и Chapter.
Так сделано для удобства
- первая функция запускает основные
события, и событием для начала служит начало главы.
А во второй функции
окончанием служит конец этой главы.

Также можно обратить внимание,
что об окончании главы мы
сообщаем не этому скрипту,
а отправляем событие
на ChapterControl - менеджер глав.

Macro Main ?
{
  Wait (False)
} On (event(This.Chapter001Ended)) do {
  Action Chapter002.Start()  
} On (event(This.Chapter002Ended)) do {
  Action Chapter003.Start()  
} On (event(This.Chapter003Ended)) do {
  Action Chapter004.Start()
}

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

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

IF

к оглавлению

Скрипт

Что тут происходит?

Macro Main ?
{
  global var CPlayerPuppetEntity Player
  Wait (False)
} On (event(Detector.Activated)) do {
  Player=wrld.GetClosestPlayer()
  PlayerHeals = Player.macGetHealth()
  If (PlayerHeals <= 50) {
    Action HealsBig.SpawnSimple()
  } else {
    Action HealsSmall.SpawnSimple()
  }  
}

При активации детектора происходит проверка
здоровья игрока - если меньше или равно 50%,
то будет заспавнено большое здоровье,
если больше 50%, то маленькое

FOR

к оглавлению

Скрипт

Что тут происходит?

Macro Main ?
{
  for (i = 0; i < 5; i = i + 1) {
    Action Enemy.PlayAnimWait("Happy_Jump")
    Wait (1 sec)
  }
}

5 раз будет проиграна анимация

Завершение уровня

к оглавлению

Скрипт

Что тут происходит?

Macro Main ?
{
  Wait (event(Detector.Activated))
  Action WorldInfo.macTheEndSequence()  
}

При активации детектора происходит завершение уровня.



Комментарии   

Оставьте комментарий

Мини-профиль

Гость

Вы в группе: Гости
Ваш IP: 216.73.216.166

Сейчас обсуждают

Sanya_228mc1702
duck_Da_quack6917
duck_Da_quack6
Bebralot2282474
duck_Da_quack70
Mindbreak18
Sylvain122
Mindbreak11
JustRick18
geodeutschTV5

МЕМЫ

Файлы

SeriousSasha
139 27 1

Tenshi
7064 1903 82

nikiPlayer
3554 1269 24

rtemiy
1433 417 15

ILoveQuake
2872 761 12

Категории раздела

Croteam [4]
Материалы по студии разработчика игр CroTeam.
Интервью с разработчиками [9]
Интервью с разработчиками, а также их издателями.
Serious Sam 1 [9]
Статьи о Serious Sam 1
Serious Sam 2 [6]
Статьи о Serious Sam 2
Serious Sam HD [4]
Статьи о HD-серии, ремейков классических версий Сэма.
Serious Sam 3: BFE [10]
Статьи о Serious Sam 3: BFE
Уроки по Serious Editor 1 [61]
Уроки по Serious Editor 2 [5]
Самое серьёзное редактирование, на русском!
Уроки по Serious Editor 3 [40]
Уроки по работе с редактором.
Serious Engine [6]
Описание возможностей всех движков компании Croteam
Разное [26]
Всё что не подошло не под одну из вышеупомянутых категорий.
Serious Sam: Revolution [1]
Материалы по SS: Revolution, серьёзном обновлении классических Крутых Сэмов.
Serious Sam 1.05/1.07 SDK [10]
SDK - набор исходников для Serious Sam: The Second Encounter 1.05/1.07.

Видео

Тизер VR-порта Serious Sam Classics TFE/TSE
93 5.0