Trainz - Россия

[Новости][Статьи] Как писать скрипты для Trainz. часть 1



Дополнительные файлы к статье
Lesson1.cdp (18.5 kb) - пакет с учебным сценарием.
Lesson1.gs (5 kB) - исходный текст сценария
SP3S1DCC.gs (27.7 kB) - исходный текст сценария Highland Valley (DCC)
WriteScript1.rar (90.6 kB) - архив с текстом статьи для печати в формате MS Word 97

Как писать скрипты для Trainz
часть 2.


Учебный сценарий No1 (Lesson 1)

Ничего так не помогает разобраться, как конкретный пример, который можно “пощупать руками”. :)

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

Для учебного сценария я выбрал карту, которая указана в примере файла config.txt - City and Country USA. Эта карта выбрана во многом потому, что на ней в самом Trainz сделано несколько сценариев, поэтому присутствует достаточное количество маркеров и триггеров. То есть, мне не хотелось делать свою карту, чтобы меньше пришлось скачивать, да и время сэкономим. Можно, конечно, было сделать что-нибудь примитивное, но в этом случае сценарий будет плохо смотреться. А так мы получим ещё один рабочий сценарий для Trainz.

Сценарий начинается с создания поезда игрока, который находится на главной станции. Задача игрока доехать до станции Blue Sky соблюдая скоростной режим движения. После того, как поезд игрока покидает станцию, в той же точке создаётся второй поезд, который тоже отправляется на станцию Blue Sky, но по встречному маршруту. Вторым поедом управляет автопилот, то есть, он будет автоматически останавливаться на красных сигналах светофоров. На станции Blue Sky оба поезда встречаются. После того, как первый поезд прибывает на станцию, он открывает путь для движения второго поезда. Тот, в свою очередь, открывает путь для движения первого поезда. После отправления со станции Blue Sky первый поезд возвращается на главную станцию на тот же путь, с которого отправлялся, а второй поезд приходит через некоторое время на соседний путь.

Да, всё это делается для Trainz Railroad Simulator 2004 (TRS2004), в том числе все ссылки идут на хелп, который с ним поставляется. При этом большая часть должна работать и в старом Trainz, начиная с версии 1.3, но никаких гарантий по этому поводу я давать не буду. Посмотрел лишь, что синтаксис базового языка Auran GameScript не изменился, и компилятор gs.exe, который идёт в комплекте с UTC выдаёт точно такой же файл с описанием языка. Но поскольку в UTC не было хелпа по классам, процедурам и функциям, то ничего по поводу совместимости сказать без проверки нельзя. Так что кому интересно – проверяйте сами.

Полный текст сценария см. в файле Lesson1.gs, откомпилированная версия со всеми необходимыми картинками в файле lesson1.cdp.

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

include "trainz.gs"

include "navigate.gs"

include "train.gs"

include "turntable.gs"

include "junction.gs"

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

game class MyLesson1 isclass Scenario

{

//тут текст программы

}

Это объявление главного класса нашей программы. Имя у него MyLesson1, и именно это имя мы указываем в файле config.txt в строчке scriptlibrary Lesson1. Данный класс наследуется от класса Scenario, который в свою очередь унаследован от самого верхнего класса GameObject (в Auran GameScript базовый класс называется GameObject, а не Object как в Delphi или C++).

У класса Scenario имеются следующие методы:

AddHandler(GameObject target, string major, string minor, string handler) GameObject [private]

Exception(string reason) GameObject [private]

Extension GameObject [private]

GetId() GameObject [private]

GetName() GameObject [private]

Load(string data) Scenario [inline]

Monitor() Scenario [inline]

PostMessage(GameObject dst, string major, string minor, float seconds) GameObject [private]

Save() Scenario [inline]

SendMessage(GameObject dst, string major, string minor) GameObject [private]

Sleep(float seconds) GameObject [private]

Sniff(GameObject target, string major, string minor, bool state) GameObject [private]

TrainBadCouple(int vehicleId) Scenario [inline]

TrainCollided(int trainId) Scenario [inline]

TrainDerailed(int trainId) Scenario [inline]

TrainOverAdvisorySpeed(int trainId) Scenario [inline]

TrainOverSpeed(int trainId) Scenario [inline]

TrainSpeedingFine() Scenario [inline]

Поскольку это вводная статья, то я не буду подробно расписывать все методы всех классов, а опишу только самые основные. Остальное смотрите в документации.

Метод Load(string data) вызывается перед вызовом функции main(), которая запускает основной поток сценария, и служит для подготовки сценария к разным вариантам запуска в зависимости от состояния, которое было сохранено пользователем. Желающие посмотреть пример работы с методом Load смотрите файл SP3S1DCC.gs, который я в своё время скачал с сайта Auran, и по которому учился сам. В моём примере текст метода следующий:

bool Load(string data)

{

if(!World.LoadMap(World.FindKUID("city_and_country_usa")))

{

Interface.Log("Error loading scenario map");

return false;

}

Interface.AdjustScore(1000);

return true;

}

Основное его назначение - загрузить нужную карту, а также установить начальное значение счёта, равное 1000. Если при загрузке карты происходит ошибка, то метод Load, как видно из текста, возвращает ложь (false), и Trainz завершает выполнение сценария. В принципе, внутри метода Load можно выполнить всю подготовку к работе сценария, включая создание составов и подготовку начальных маршрутов. Но при этом в методе Load нельзя вызывать методы Sleep() и Wait(), а также, естественно, любые методы, которые связаны с ожиданием какого-либо события.

Соответственно, метод string Save() сохраняет текущее состояние сценария как текстовую строку. Перекрываем метод Save и присваиваем переменной return значение строки, соответствующее точке сохранения. Эту строку мы потом получим в методе Load, когда пользователь загрузит сохранённый сценарий.

Да, я пока не проверял, как это теперь работает в TRS2004, в котором сохранять сценарий можно в любое время. Работу описанных выше функций Load и Save я смотрел в примере SP3S1DCC.gs, который был написан для UTC.

Метод TrainOverSpeed(int trainId) вызывается всякий раз, когда какой-либо поезд начинает ускоряться. При этом вы получаете номер поезда, который начал ускоряться.

Метод TrainOverAdvisorySpeed(int trainId) вызывается всякий раз, когда какой-либо поезд начинает превышать рекомендуемую скорость.

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

В моём примере содержится следующий текст:

void TrainSpeedingFine()

{

Interface.AdjustScore(-10);

}

То есть, при превышении скорости движения любым поездом от текущего счёта будет отниматься по 10 очков каждую секунду. При этом можно установить так называемый “floating speed limit”, то есть диапазон превышения лимита скорости с помощью метода Train::SetFloatingLimit(float delta,bool enable). При этом delta задает допустимое превышение, а второй параметр enable показывает, учитывать ли FloatingLimit устанавливаемую для данного поезда (enable=true), либо будут использоваться установки по умолчанию (enable=false). Да, это метод поезда, то есть в программе его нужно вызывать к тому поезду, для которого мы хотим установить новый допуск (а вот какой стоит по умолчанию - мне выяснить не удалось). В примере этот метод вызывается после создания состава consist1:

consist1.SetFloatingLimit(Train.KPH_TO_MPS * 10, true);

При этом хочется обратить внимание на то, что в Trainz скорость по умолчанию нужно указывать в метрах в секунду (meters per second – MPS). Поэтому для преобразования из км/ч в м/c нужно сделать умножение на специальную константу, объявленную в модуле Train - Train.KPH_TO_MPS * 10. Обратите внимание на использование констант, которые объявлены в других модулях, для которых обязательно нужно указать имя модуля с точкой перед именем константы.

Метод TrainDerailed(int trainId) вызывается в том случае, когда поезд TrainId сходит с рельс. В моём примере при этом сценарий завершается вызовом World.EndScenario(10).

Метод TrainCollided(int trainId) вызывается в случае различных аварий.

Метод TrainBadCouple(int vehicleId) вызывается в тех случаях, когда сцепка вагонов происходит на скорости больше 7 км/ч.

Ну и наконец thread void Monitor() – это основной поток, который нужно запустить для начала работы сценария. Он просчитывает состояние всех объектов и он же вызывает описанные выше методы, возникающие при движении поездов.

Теперь разберём создание поездов.

В начале описания класса MyLesson1 имеется строка:

Train consist1; //поезд игрока

Это объявление новой переменой – экземпляра класса Train.

Далее следует строка:

KUID[] consist1Spec = new KUID[0];

Это объявление массива элементов типа KUID. Это описание кодов локомотива и вагонов, которые будут в составе. Массивы в Auran GameScript, насколько я понял, являются динамическими и автоматически растут при присвоении значения элементу с соответствующим номером.

Соответственно, следующие строки заполняют структуру consist1Spec:

consist1Spec[0] = World.FindKUID("atsf_chair");

consist1Spec[1] = consist1Spec[0];

consist1Spec[2] = consist1Spec[0];

consist1Spec[3] = consist1Spec[0];

consist1Spec[4] = World.FindKUID("atsf_baggage");

consist1Spec[5] = World.FindKUID("f7_sfred");

При этом если мы хотим иметь в составе одинаковые вагоны, то можно получить KUID только один раз, а потом использовать его там, где нужно. Ещё одно замечание по поводу определения KUID. На вход метода World.FindKUID() мы даем ту же строку, которую перед этим написали в файле config.txt. При этом нам возвращается именно тот KUID, который мы прописали этой строке в config.txt. То есть, если вы напишете совсем несуществующий KUID, то Trainz вас обругает в момент загрузки сценария и запускать его не будет. А вот если вы случайно напишете в config.txt существующий KUID от другого вагона или локомотива, то его и получите в сценарии.

Собственно создание состава выполняется методом CreateTrain, который объявлен в классе World:

consist1 = World.CreateTrain(consist1Spec,"PLATFORM1",true);

Первый параметр – массив KUID, который содержит перечень кодов вагонов и локомотива. Второй – маркер, на который нужно поставить поезд. При этом функция объявлена в двух вариантах. В первом случае, как в примере, мы задаём имя маркера, а во втором должны передать экземпляр класса TrackMark. Третий параметр задаёт направление, в котором будет установлен состав на маркере. Если true, как в примере, то по направлению маркера, если false, то в обратном направлении.

Да, если вы надумаете удалить поезд, то вызывайте World.DeleteTrain(Train train).

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

По этому я в приведённом примере поставил локомотив в конец состава. Но если больше ничего не делать, то он будет прицеплен кабиной к вагону, то есть, локомотив необходимо развернуть в обратную сторону. Для этого использована команда:

consist1.GetVehicles()[5].Reverse();

Метод GetVehicles() возвращает массив имеющихся в составе вагонов и локомотивов. [5] – обращение к нужному элементу массива. Метод Reverse() объявлен в классе Vechicle и разворачивает вагон или локомотив.

О строке consist1.SetFloatingLimit(Train.KPH_TO_MPS * 10, true) выше уже написано.

Строка:

consist1.GetFrontmostLocomotive().SetHasDriver(true);

делает следующее. Метод GetFrontmostLocomotive возвращает первый локомотив в составе (которых может быть несколько). А метод SetHasDriver(true) указывает, что в локомотиве будет машинист, если мы установили true, и убирает машиниста, если мы установим false. При этом в составе может быть только один машинист. Если в составе уже был машинист, то он при вызове этого метода со значением true будет удалён.

Строка World.SetCamera(consist1.GetVehicles()[5],World.CAMERA_INTERNAL); устанавливает камеру (вид пользователя). В данном случае игрок окажется внутри локомотива, что задаётся константой World.CAMERA_INTERNAL. Кроме этого можно указать CAMERA_EXTERNAL, при этом камера окажется снаружи поезда. При этом, правда желательно выставить угол камеры так, чтобы она была направлена по ходу поезда. Это делается методом:

World.SetCameraAngle(int yaw, int pitch, float radius)

yaw - горизонтальный угол в градусах от 0 до 360. 0 – направление на север, вращение по часовой стрелке

pitch – вертикальный угол в градусах от –90 до +90. 0 – камера смотрит горизонтально. Отрицательный угол – камера поворачивается вниз, положительный – вверх.

Radius – расстояние до объекта в метрах. При этом учтите, что базовая точка, от которой идёт расчёт, обычно находится на уровне земли. То есть, если задать расстояние 0, то игрок окажется под колёсами поезда. Кстати, кому-то это может и понадобиться.

В некоторых сценариях встречается эффект движения камеры. Делается это через ту же функцию SetCameraAngle. Просто нужно вызывать её с определённым интервалом, постепенно приближаясь к тем значениям, которые будут в конце движения камеры. Для задания пауз используется системная функция Sleep(float time), которой нужно передать время задержки в секундах.

Кстати, если вам в каком-то месте потребуется дождаться какого-то события, и вы решите сделать это через цикл while, то внутри цикла нужно ОБЯЗАТЕЛЬНО ВСТАВИТЬ Sleep!!! Иначе ваш сценарий просто остановится, поскольку во время действия Sleep(), а также других функций, связанных с ожиданием событий, Trainz обсчитывает все остальные потоки в игре. Я это проверил на собственном опыте.

Теперь переходим к подготовке маршрута движения нашего поезда. В Trainz по умолчанию все стрелки получают имя JunctionXXX, где XXX – порядковый номер стрелки. Это имя можно (и по моему мнению НУЖНО) изменить на более осмысленное. В используемой карте практически все стрелки переименованы, и их имена я выяснял с помощью редактора. При написании сценариев я очень рекомендую нарисовать подробную схему путей с указанием положения и имён всех стрелок, триггеров, маркеров и светофоров.

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

Стрелки на карте переключаются с помощью метода:

Navigate.LockJunction(string junction, int direction, bool manualControl)

junction – имя стрелки, которую переключаем;

direction – направление переключения, которое может быть:

DIRECTION_LEFT = 0 – стрелка налево;

DIRECTION_FORWARD = 1 – стрелка вперёд (по центру, если стрелка на три направления);

DIRECTION_RIGHT = 2 - стрелка направо;

DIRECTION_BACKWARD = -1 – стрелка назад (честно говоря, не понял, зачем это);

DIRECTION_NONE = 3 – направление не определено;

manualControl – определяет, можно ли данную стрелку игроку переключать вручную.

В качестве примера строка из учебного сценария:

Navigate.LockJunction("CENTRAL",Junction.DIRECTION_RIGHT,JunctionHand);

которая переключает стрелку "CENTRAL" направо и устанавливает ручной режим в соответствии со значением переменной JunctionHand, которая объявлена в начале модуля. Я специально завёл логическую переменную, с помощью которой можно быстро разрешить или запретить игроку переключение стрелок в сценарии. Дело в том, что во время отладки удобнее, когда все стрелки можно переключить, поскольку частенько путаешься с направлением. Но если вы потом захотите запретить игроку во время сценария переключать стрелки, то, если не будет переменной типа JunctionHand, придётся править текст программы.

Класс Navigate и встроенный объект Navigate содержит набор методов, которые позволяют работать с собственно железной дорогой. То есть, со стрелками (junction), светофорами (signal) и триггерами (trigger). Особенно интересны функции OnJunction и OnTrigger, которые ожидают заданного события на стрелке или триггере. Рассмотрим их более подробно.

Метод Navigate.OnJunction(GameObject runningThread, Train train, string junctionName, int stage) вызывается тогда, когда мы хотим дождаться события на стрелке, которая задаётся её именем в junctionName. При этом параметр stage указывает какое событие нас интересует и может принимать следующие значения:

Navigate.JUNCTION_ENTER = 0 – поезд вошёл в зону стрелки;

Navigate.JUNCTION_INNERENTER = 1 – поезд вошёл на стрелку;

Navigate.JUNCTION_STOPPED = 2 – поезд остановился на стрелке;

Navigate.JUNCTION_INNERLEAVE = 3 - поезд покинул стрелку;

Navigate.JUNCTION_LEAVE = 4 – поезд покинул зону стрелки.

Стрелки в Trainz, как и триггеры, имеют область действия. Но, в отличие от триггеров, у стрелок радиус области действия всегда равен 150 метрам. Поэтому для входа и выхода объявлены два типа констант – первый для зоны действия стрелки, а второй, с приставкой INNER, для самой точки стрелки.

Остальные параметры, которые нужно указать, это runningThread – поток, который вызывает метод OnJunction, тут мы указываем me, что аналогично указанию self в языках Object Pascal или C++, то есть, это ссылка на самого себя. Второй параметр – train, который указывает на тот поезд, событие от которого мы ожидаем.

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

Пример из нашего учебного сценария:

Navigate.OnJunction(me,consist1,"CENTRAL",Navigate.JUNCTION_LEAVE);

Эта строка ожидает выхода первого состава consist1 из зоны действия стрелки (JUNCTION_LEAVE), которая имеет имя "CENTRAL". После этого в сценарии запускается поток для управления вторым поездом (об этом чуть ниже).

Хочется сразу сказать о замеченных мной несоответствиях между документацией и реальной работой Trainz. Я столкнулся с двумя неприятными глюками.

Во-первых, на самом деле Trainz игнорирует параметр train, и возвращение из функции происходит, если какой-либо поезд выполнит указанное событие.

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

Второй метод, который также ожидает событий, это метод Navigate.OnTrigger(GameObject runningThread, Train train, string triggerName, int stage). Назначение параметров и действие метода аналогично методу OnJunction. Единственное отличие в том, что событий, которые возникают на триггере, меньше, чем на стрелке:

Navigate.TRIGGER_ENTER=0 – поезд вошёл в область триггера;

Navigate.TRIGGER_STOPPED=1 – поезд остановился в области триггера;

Navigate.TRIGGER_LEAVE=2 – поезд покинул область триггера.

Пример из учебного сценария:

Navigate.OnTrigger(me,consist1,"StopBlueSky",Navigate.TRIGGER_ENTER);

Сообщения игроку выводятся с помощью метода:

Interface.SetObjective("Let's go!","play.tga");

где первый параметр – собственно сообщение, а второй – имя файла с пиктограммой, который выводится в окне сообщения слева. Этот файл может быть bmp или tga и должен находиться в каталоге сценария (можно ли использовать указание пути к другим каталогам, я не проверял).

Кроме этого в классе Interface объявлено ещё несколько методов, которые можно использовать для вывода сообщений, в том числе Interface.Print(string message), но подробное их описание выходит за рамки вводной статьи. Желающие могут посмотреть сами в хелпе.

И напоследок о создании и использовании нескольких потоков в программе.

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

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

thread void Train2(void)

{

Navigate.LockJunction("CENTRAL",Junction.DIRECTION_LEFT,JunctionHand);

Navigate.LockJunction("CENTRAL 03",Junction.DIRECTION_LEFT,JunctionHand);

Navigate.LockJunction("CENTRAL 04",Junction.DIRECTION_LEFT,JunctionHand);

Navigate.LockJunction("MAIN YARD STH",Junction.DIRECTION_LEFT,JunctionHand);

Navigate.LockJunction("MAIN YARD WEST",Junction.DIRECTION_RIGHT,JunctionHand);

Navigate.LockJunction("CENTRAL STATION",Junction.DIRECTION_LEFT,JunctionHand);

Navigate.LockJunction("Steel Mills",Junction.DIRECTION_RIGHT,JunctionHand);

consist2 = World.CreateTrain(consist1Spec,"PLATFORM1",true);

consist2.GetVehicles()[5].Reverse();

consist2.SetFloatingLimit(Train.KPH_TO_MPS * 10, true);

consist2.Turnaround(); //меняем текущее направление поезда на противоположное

consist2.SetAutopilotMode(Train.CONTROL_AUTOPILOT); //включаем автопилот - поезд начинает движение

Navigate.OnJunction(me,consist2,"BLUE SKY NORTH",Navigate.JUNCTION_INNERENTER);

Navigate.OnJunction(me,consist2,"BLUE SKY NORTH",Navigate.JUNCTION_LEAVE);

//второй поезд ушёл со станции - отправляем первый

Navigate.LockJunction("BLUE SKY STH",Junction.DIRECTION_LEFT,JunctionHand);

Interface.SetObjective("Let's go!","play.tga");

}

его назначение – создание второго состава и управление его поведением. При этом я хочу обратить внимание на тот факт, что многопоточность в Trainz имитируется, а не реализована на уровне прерывания и возобновления потока команд, как это сделано в последних версиях Windows. Поэтому прерывание выполнения команд данного потока и передача управления другим потокам происходит только при вызове методов Sleep(), OnJunction и OnTrigger. Если же мы выполняем большое количество внутренних команд, например, создаём состав из большого количества разных вагонов, то выполнение всех остальных потоков будет на это время приостановлено. Во время игры это выглядит как “зависание программы”, поскольку все остальные процессы в игре на это время останавливаются. Учитывайте это при разработке сценариев. Именно по этой причине я в своём сценарии WoodTrain все составы создаю в самом начале программы.

Ну вот, думаю, что для вводной статьи более чем достаточно. Надеюсь, что прочитав её, вы сможете написать свой не очень сложный сценарий.

С уважением,
Дмитрий Мыльников

4 Апреля 2004 года



[Новости][Статьи] Как писать скрипты для Trainz. часть 1


Хостинг от uCoz
Хостинг от uCoz