Перейти к содержанию

Простой декоративный актор

Актор — это объект внутриигрового мира, например, предмет, враг, декорация (в том числе неосязаемая, как дымок от пули) или сам игрок. Так, все акторы, видимые на скриншоте на рис. 1, обведены красными овалами:

Рисунок 1. Акторы обведены красным

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

Итак, после статьи 2.1 проект создан, файл кода ZScript добавлен. Наш файл ZScript сейчас чист, и если мы попробуем запустить такой пустой проект, то никаких видимых изменений не произойдёт. Чтобы что-то поменялось, нужно сообщить движку, что именно мы хотим; а хотим мы объявить новый, собственный объект игрового мира — нового актора.

Объявление актора

В ZScript объявление нового актора записывается по следующему шаблону:

class Название_Актора: Actor {

}

На месте Названия_Актора должно стоять, собственно, внутреннее название актора — по нему движок будет понимать, что речь идёт именно о нём, и сможет создавать его копии. У названия есть свои правила:

  • Должно состоять только из символов латинского алфавита, символа нижнего подчёркивания _ и цифр. Никаких пробелов, кириллицы, точек, кавычек и всего остального! В шаблоне выше русские слова даны только для упрощения восприятия!
  • Первым символом в названии не может стоять цифра.
  • Желательно не пересекаться с существующими названиями.

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

class ImpulseFloorLamp: Actor {

}
В первой строчке мы сообщаем сообщаем движку, что создаём новый Actor с названием ImpulseFloorLamp ("Импульсная напольная лампа", почему бы и нет) — эта часть называется заголовком актора. Затем, с помощью скобок { }, объявляем начало и конец тела актора — всё, что находится внутри тела, будет связано с актором.

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

В Slade написанный код должен выглядеть примерно так, как на рис. 2:

Рисунок 2. Открытый файл ZScript в редакторе кода Slade

(Далее интерфейс редактора Slade остаётся таким же, так что, кроме самых важных моментов, больше показываться не будет.)

Объявление нескольких акторов

Теперь движок, если его запустить вместе с проектом, будет знать о нашем акторе! Важно понимать, что на данный момент актор просто "абстрактно существует" в движке — он не появится просто так в мире, и даже если его призвать вручную, то не будет делать ровным счётом ничего. Однако теперь нам известен шаблон объявления. А значит, можем создавать другие заготовки, которые впоследствии, когда-нибудь, будут населять новые цифровые вселенные.

Конечно, файл кода можно отредактировать в любой момент, необязательно добавлять всё сразу. Но ниже, для примера, к объявленной ранее импульсной лампе добавлены ещё два актора, под креативными названиями MyActor2 и Lorem_Ipsum.

class ImpulseFloorLamp: Actor {

}


class MyActor2: Actor {

}

class Lorem_Ipsum: Actor {

}

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


В целом объявлений акторов может быть сколь угодно много, однако пока вернёмся только к коду напольной импульсной лампы.

Замена других акторов нашим

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

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

Для того, чтобы заменить все акторы на карте своими, необходимо в заголовке после слова Actor дописать replaces Название_Заменяемого. Тогда на месте Названия_Заменяемого должно стоять название того актора, который будет заменён нашим по всему уровню:

class ImpulseFloorLamp: Actor replaces Zombieman {

}
В этом объявлении все зомби-рядовые (внутреннее название которых — Zombieman) заменяются на наш актор ImpulseFloorLamp. Пока что будем пользоваться таким способом.

Запуск! Промежуточный результат

Итак, первая проверка модификации.

Напоминание

Не забывайте сохранять проект!

Запустить проект можно, например, мышкой перетащив сохранённый *.pk3 или директорию на икноку UZDoom (или другого подходящего порта, как GZDoom, LZDoom, VkDoom и QZDoom). Вообще, для Doom есть множество лаунчеров, облегчающих добавление модификаций.

Также можно использовать командную строку и запустить uzdoom -file Путь из-под директории с движком. Если, например, использовать один из параллельных форков движка (положим, LZDoom) и назвать файл проекта "Simple_decorative_actor.pk3", то запускать нужно lzdoom -file Simple_decorative_actor.pk3.

Не запускается?

Если при запуске проекта сообщается об ошибках в коде — вероятнее всего, были нарушены правила синтаксиса языка программирования, из-за чего движок просто не понимает, что от него требуется. Можно поразбираться, всё ли сделано по шаблону, не забыта ли какая-нибудь скобка. И, конечно, пока есть готовые примеры, их всегда можно скопировать...

Запускаем, и... внезапно, не видим ничего. Настолько ничего, что исчезли даже оба врага, извечно стоявших здесь! Хотя можно убедиться, что остальные объекты на уровне, кроме всех зомби-рядовых, остались на своих местах.

Рисунок 3. Первый запуск мода

На самом деле всё в порядке, актор объявлен, движок о нём знает, и "физически" в игровом мире на месте всех Zombieman он действительно появился — но по умолчанию актор не имеет спрайтов (картинок), и на экране попросту не отображается...

Чтобы его увидеть, необходимо связать с ним нужные спрайты.

Отображение. Спрайты и анимации

Все спрайты и анимации акторов задаются стейтами — минимальными состояниями актора: то, как он в текущий момент должен отображаться и действовать. Каждый стейт включает в себя:

  1. Внешний вид актора, то есть спрайт и фрейм (кадр спрайта);
  2. Длительность стейта (в тактах игры, 1/35 секунды) — задержка перед следующим стейтом;
  3. Действие для исполнения;
  4. Параметры стейта.

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

Если объединить несколько стейтов подряд, получится анимация, или последовательность стейтов. В начале последовательности стейтов обычно ставится метка стейта — читаемое название этой последовательности с двоеточием в конце. Все появляющиеся в мире акторы начинают проигрывать стейты от метки Spawn: (в свободном переводе — "Создание" или "Появление").

Стейты актора задаются через блок стейтов, в коде записывается как States { }. Так как это тоже логический блок, то пишется он тоже с операторными скобками, как и class …, но уже внутри актора:

class ImpulseFloorLamp: Actor replaces Zombieman {
    States {

    }
}
Если бы States { } был расположен вне актора, движок не мог бы понять, с чем именно ему нужно связать блок стейтов.

Итак, мы создаём техническую напольную лампу, поэтому можно взять уже готовые спрайты из Doom с названиями файлов TLP2A0, TLP2B0, TLP2C0 и TLP2D0. Они уже существуют в DOOM.WAD и DOOM2.WAD, поэтому никаких дополнительных ресурсов подключать не требуется — но при желании, разумеется, эксперименты и добавление собственных спрайтов только приветствуется!

Рисунок 4. Все кадры спрайта одной из напольных ламп Doom

Напомним [пока статьи нет, TODO], что в спрайтах первые четыре символа и будут названием спрайта, а последующие пары символов — это фрейм и поворот спрайта. Нумерация фреймов начинается с латинской буквы "A", затем "B", "C" и так далее. Если на позиции поворота указан ноль, как в нашем случае, то объект всегда будет повёрнут к игроку одной стороной. Так, в упомянутом выше файле спрайта TLP2D0 название спрайта будет "TLP2" (сокращение от англ. "Tech Lamp 2", "Техническая лампа 2"), конкретный фрейм — "D" (четвёртый), и этот спрайт будет всегда повёрнут в камеру игрока.

Правила записи стейтов

В двух словах о конвертации из Decorate

Если Вам уже известен Decorate, то синтаксис стейтов в общем случае в ZScript такой же. Достаточно только в конце каждой строки, кроме меток стейтов вроде Spawn:, добавить символ окончания инструкции — точку с запятой.

Стейты предмета Berserk на Decorate:

States
{
Spawn:
    PSTR A -1 Bright
    Stop
Pickup:
    TNT1 A 0 A_GiveInventory("PowerStrength")
    TNT1 A 0 HealThing(100, 0)
    TNT1 A 0 A_SelectWeapon("Fist")
    Stop
}
Аналогичная запись на ZScript (добавляются ; в конце):
States
{
Spawn:
    PSTR A -1 Bright;
    Stop;
Pickup:
    TNT1 A 0 A_GiveInventory("PowerStrength");
    TNT1 A 0 HealThing(100, 0);
    TNT1 A 0 A_SelectWeapon("Fist");
    Stop;
}

Внутри блока стейтов сами стейты записываются по следующему правилу, которое на первый взгляд кажется страшным:

СПРАЙТ ФРЕЙМ ЗАДЕРЖКА ПАРАМЕТРЫ ДЕЙСТВИЕ;
Однако обязательны из них только первые три позиции — название спрайта, фрейм в нём, длительность. И точка с запятой в конце: по ней движок понимает, где кончается инструкция (или, в данном случае, стейт).
СПРАЙТ ФРЕЙМ ЗАДЕРЖКА;
Так, благодаря уже готовым ресурсам из Doom, нашим спрайтом будет "TLP2", а фреймами — "A", "B", "C" и "D" (см. рис. 4 выше). Задержка — ту, которую мы установим сами для этого кадра анимации; например, "35" будет ровно одной секундой, так как движок работает с частотой 35 тактов/сек., а задержка "7" — это 7/35, то есть 0.2 секунды. Количество стейтов в акторе не ограничивается, как и использование в разных местах анимации одного и того же фрейма.
TLP2 A 35;
TLP2 B 7;
TLP2 D 105;

Стейты сменяют друг друга последовательно, сверху вниз — то есть по умолчанию следующим стейтом будет тот, который располагается в коде сразу за этим. Можно принудительно устанавливать другой следующий стейт, но об этом чуть позже.

Попробуем добавить анимацию разрабатываемому актору. Метки стейтов, в том числе и начальная Spawn:, обозначаются с двоеточием в конце.

class ImpulseFloorLamp: Actor replaces Zombieman {
    States {
    Spawn:
        TLP2 A 5;
        TLP2 B 5;
        TLP2 C 5;
        TLP2 D 5;
        Loop;
    }
}
В этом примере анимация состоит из всех четырёх фреймов спрайта "TLP2", все задержки равны 5 (то есть 5/35 секунды, или примерно 0.14 на каждый кадр анимации). Обратите внимание на инструкцию Loop; вместо очередного стейта. По шагам:

  • Первым стейтом будет "TLP2 A 5", так как он стоит сразу после метки Spawn:,
  • …Он сменяется на "TLP2 B 5",
  • …Потом на "TLP2 C 5",
  • …Затем на "TLP2 D 5",
  • …В конце встречается инструкция Loop;, означающая «Перейди на начало текущей именованной последовательности стейтов». Поэтому стейт "TLP2 D 5" переходит на Spawn:, то есть вновь на исходный "TLP2 A 5",
  • …Который вскоре снова сменяется на "TLP2 B 5", и так бесконечно, по кругу.

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

Запуск проекта

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

Можно уже заняться дизайном, добавить кадры анимации. У нас же импульсная лампа — значит, явно покажем этот импульс! Например, так:

class ImpulseFloorLamp: Actor replaces Zombieman {
    States {
    Spawn:
        TLP2 C 30;
        TLP2 D 5;
        TLP2 A 4;
        TLP2 D 1;
        TLP2 A 2;
        TLP2 B 5;
        Loop;
    }
}

Ещё удобной деталью является запись нескольких кадров одинаковой длительности. Так, в блоке кода ниже на самом деле четыре стейта: "TLP2 A" с задержкой "30", затем "TLP2 B", "TLP2 C" и "TLP2 D" с задержкой "4". В конце опять зацикливание на "TLP2 A".

class ImpulseFloorLamp: Actor replaces Zombieman {
    States {
    Spawn:
        TLP2 A 30;
        TLP2 BCD 4;
        Loop;
    }
}

Нулевые задержки

Задержка "0" (как в записи TLP2 A 0;) в стейтах применяется, но необходимо следить, чтобы в коде не было непрерываемых последовательностей, состоящих только из нулевых задержек. Во многих версиях движков автопрерывания стейтов не существует, и такие циклы будут намертво вешать игру.

Забавно, но при экспериментах можно заметить, что спрайты "TLP2 B" и "TLP2 D" из ресурсов Doom различаются буквально двумя-тремя пикселями в нижней части лампы.

Что ж, стейты — крайне важная тема для актора (в конце концов, без них его даже видно не будет). Многим подробнее они будут рассмотрены в статье через одну, "Простой актор-противник". А пока попробуем запустить...

Запуск-2! Промежуточный результат

При запуске Doom 2 MAP01 видна странная картина, как на рис. 5. Все зомби-рядовые заменились на написанные нами лампы (да, пока что они тёмные):

Рисунок 5. Запуск после установки спрайтов

Можно подойти к ним, попробовать потрогать — но есть одно огромное "но": лампа получилась призрачная. Помимо того, что она тёмная, так ещё и неосязаемая!

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

Свойства и флаги актора

В двух словах о конвертации из Decorate

При переносе с Decorate все свойства должны быть обёрнуты в блок Default { } в том же виде, что и для блока States { }. В конце каждой строчки нужно поставить точку с запятой.

Пример конвертации. Свойства предмета Berserk на Decorate:

+COUNTITEM
+INVENTORY.ALWAYSPICKUP
+INVENTORY.ISHEALTH
Inventory.PickupMessage "$GOTBERSERK"
Inventory.PickupSound "misc/p_pkup"
При переносе на ZScript добавляется блок Default { } (в начало Default {, в конец оставшаяся закрывающая операторная скобка), и в конце всех строк ставится ";":
Default
{
    +COUNTITEM;
    +INVENTORY.ALWAYSPICKUP;
    +INVENTORY.ISHEALTH;
    Inventory.PickupMessage "$GOTBERSERK";
    Inventory.PickupSound "misc/p_pkup";
}
(Вообще, в конце строчек с флагами — и только с флагами! — точку с запятой можно не добавлять. Но лучше, как минимум поначалу, всё же писать её везде, просто чтобы лишний раз не путаться в синтаксисе.)

Как было сказано в конце прошлого параграфа, свойства и флаги актора позволяют тонко настроить взаимодействие с игровым миром. Какую применять физику к актору, каким способом его отрисовывать, какой звук должен воспроизводиться, когда актор видит игрока... Свойства и флаги могут меняться прямо во время игры (можно вспомнить, что актор какодемона при смерти перестаёт парить в воздухе и падает на землю — в этот момент у него снимается флаг "в полёте"). Но также у них обязано быть какое-то значение при появлении в мире, значение по умолчанию! Блок свойств актора позволяет как раз задать такие значения.

Собственно, блок свойств по умолчанию буквально и называется "по умолчанию" — "Default". Равно как и блок стейтов, это тоже логический блок, поэтому у него тоже есть операторные скобки, и он также записывается внутри определённого актора. Соответственно, обозначается он как Default { }, и внутри операторных скобок будут построчно записываться все свойства/флаги, устанавливаемые актору при появлении в мире. Обычно принято располагать его ближе к началу актора:

class ImpulseFloorLamp: Actor replaces Zombieman {
    Default {

    }

    States {
    Spawn:
        TLP2 A 30;
        TLP2 BCD 4;
        Loop;
    }
}

Начнём с флагов.

Флаги

Флаг — это единственное значение "да" или "нет", других вариантов у него нет. Если флаг установлен в "да", говорится, что флаг установлен или взведён. Если в "нет" — что флаг убран или сброшен. Есть придание, что такое название он получил потому, что в реальности издали сразу видно, реет ли яркий флаг на флагштоке или его там нет в принципе.

В блоке свойств, чтобы по умолчанию установить актору тот или иной флаг, необходимо написать +НАЗВАНИЕ_ФЛАГА; (плюс, затем флаг, затем точка с запятой):

+SOLID;
+SHOOTABLE;
+DROPOFF;
+TELESTOMP;
+WINDTHRUST;
Так, к примеру, в первой строке производится установка флага SOLID, который отвечает за наличие непроходимого хитбокса актора — то есть будут ли с ним физически сталкиваться другие акторы. В пятой устанавливается флаг WINDTHRUST, который позволяет ветру на уровнях, как в Heretic, сдувать этого актора.

Чтобы сбросить флаг — наоборот, -НАЗВАНИЕ_ФЛАГА; (в начале минус):

-SOLID;
-FLOAT;
-CANPUSHWALLS;
-FLOORCLIP;
Аналогично, в первой строчке убирается непроходимость хитбокса актора; во второй строчке, с флагом FLOAT, у актора убирается возможность полёта.

Полный список всех флагов есть на соответственной странице ZDoom Wiki. По умолчанию все флаги актора сброшены, то есть пока, при создании актора "с нуля", снятие флагов не нужно (такой сброс потребуется в будущем, при создании наследованных акторов). Внутри блока свойств допустимо и устанавливать, и сбрасывать флаги в любом порядке:

+SOLID;
-SHOOTABLE;
-PICKUP;
+WINDTHRUST;
+FLOAT;

В контексте нашей лампы предлагается установить как минимум два флага: непроходимость актора и его постоянное свечение, это флаги SOLID и BRIGHT, соответственно.

Default {
    +SOLID;
    +BRIGHT;
}

Свойства

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

Каждое свойство записывается на новой строке в следующем формате:

Название_свойства Значение;
В конце снова точка с запятой. Например:
Gravity 0.5;
Height 32;
Health 200;
Таким образом устанавливается:

  • Гравитация вполовину ниже обычной (по умолчанию — "1.0");
  • Физическая высота в 32 игровых единиц. Для сравнения: у игрока 56, у кибердемона 110;
  • 200 пунктов здоровья.

Полный список свойств можно найти на соответственной странице ZDoom Wiki. По умолчанию часть свойств уже установлена, их можно найти в определении актора.

Для минимально рабочей импульсной лампы будет достаточно всего двух свойств: физической высоты хитбокса и его радиуса, это свойства Height и Radius.

Default {
    Radius 8;
    Height 48;
}
Фактическая ширина актора — это диаметр, то есть Radius, умноженный на два. В нашем случае лампа будет достаточно стройной по хитбоксу, оригинальные лампы из Doom в два раза толще.

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

Свечение ламп

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

Результат

В итоге должен получиться примерно такой код:

class ImpulseFloorLamp: Actor replaces Zombieman {
    Default {
        +SOLID;
        +BRIGHT;
        Radius 8;
        Height 48;
    }

    States {
    Spawn:
        TLP2 A 30;
        TLP2 BCD 4;
        Loop;
    }
}


Призывать наш актор в произвольном месте можно с помощью команды консоли "summon Название" (консоль открывается по нажатию на тильду ~, на русской раскладке это буква Ё):

summon ImpulseFloorLamp

Рисунок 6. Пять личных импульсных ламп


С этим актором уже можно экспериментировать — задавать разные свойства, менять спрайты и кадры анимации, или не подменять существующих акторов (убрать replaces Название). Следующая статья будет про синтаксис, семантику, ошибки и комментарии — про важные понятия, которые сильно пригодятся в будущем.

И уже за ней, в статье "Простой актор-противник", основное внимание будет уделено поведению актора и воздействию на игровой мир, изменению стейтов и вызову функций.