Что такое скрипты? Это маленькие подпрограммы, на которых можно создавать различные явления игровового мира, в народе называемые "фитчами"(feature - свойство). В данном уроке постараюсь объяснить, как написать простой (и не очень глючный) скрипт для какого-нибудь вагона. Сразу скажу, что я смог его только откомпилировать, но в самой игре не смотрел (видюха не поддерживает openGL). Кое-что из объектно-ореентированного программирования я уже писал на railunion'e, остальное лучше почитать в любой книге по объектно-ориентированному языку программирования (лучше семейства Си - C++, C#, Java). Будем считать, что кое-что вы из этого знаете. Итак, допустим, нам требуется сделать вагон, который будет менять разъединённые шланги тормозной магистрали на соединённые, если он хвостовой - дополнительно устанавливать диск хвоста поезда, и при проследовании моста будет издавать дополнительный звук, в снежный день будет пылить снегом.
Начнём с того, что внимательно поищем зацепки, с помощью которых мы сможем ухватиться для обнаружения и обработки того или иного события. Пока предположу, что у вас trainz не ниже Классика (в 2006 одной из вещей нет).
Мы скачали с files.auran.com архив TRS2006-API.zip , и распаковали содержащийся в нём index.chm . Хотя по применимости к 2009 он несколько устарел, всё новое можете посмотреть на сайте trainzdev.com либо почитав базовые скрипты в вашей директории trainz (папка scripts).
Открываем в index.chm раздел Class API Reference, в нём подраздел с таким же названием. Видим длинный список всех базовых классов, которые описаны в скриптах любой trainz 2006 . Нам потребуется класс Vehicle (Вагон), все его родительские классы, а также некоторы другие, которые я укажу позже. Советую перед этим поставить любой электронный cловарь (либо переводчик), так как описание на английском.
Начнём с самой первой функции, которая стоит в оглавлении. В данном случае её можно назвать самой главной. Это
public void Init (void)
(расшифрую - публичная(её может вызвать кто-угодно), не возвращающая ничего, и не нимеющая аргументов функция "иницализация" )
по ссылке находим её описание - Called by Trainz to initialize this Vehicle. (вызывается движком трейнз для инициализации объекта Вагон).
Т.Е. если в эту функцию чего-нибудь добавить, то при инициализации вагона она будет вызвана, а наше добавление выполнится. Инициализация вагона (да и вообще почти всех объектов класса MeshObject и его подклассов, в т.ч. Vehicle) происходит всего 1 раз - при постановке их на карту либо при открытии карты(за исключением кабины). При входе в быстрый машинист переинициализации НЕ ПРОИСХОДИТ, поэтому этот переход надо писать отдельно.
Но там написано замечание - If overriding this method to perform your own custom vehicle initialization, always call back to this parent method via the inherited keyword.
Перевожу - "если вы делаете свой собственный скрипт, вы обязаны вызвать родительский метод с помощью тега inherited". Это значит, что лучше написать своё описание в своём собственном скрипте так
public void Init (void)
{
inherited(void);
//наши добавления
}
Значит это следующее - когда движок вызовет Init вагона с нашим собственным классом, то он в начале выполнит все инструкции класса-родителя (в данном случае vehicle), а затем выполнит все наши инструкции.
С тем, кто будет наш скрипт "запускать", разобрались. Смотрим дальше.
следующая функция public native Train GetMyTrain (void) .
Но в описании её написали как public native Train Vehicle.GetMyTrain(void) . Зачем же нужна эта часть "Vehicle." ?
А вот зачем. Мы вызываем функцию некого объекта. А объект, чья функкция вызывается, тоже надо указать, и это указание позволяет нам управлять как функциями нашего объекта, так и функциями других объектов(если они публичные). Вызов функции нашего объекта вообще-то записывается me.GetMyTrain(void); , но для простоты это же можно записать как GetMyTrain(void); Различия между этими 2 записями никакой нет, но лично я предпочитаю 1 - сразу понятно, над каким объектом проводится функция (т.е. над нами же)
Она нам тоже понадобится. Как мы видим, она возвращает объект класса Поезд, которому принадлежит этот вагон ( Gets the Train this vehicle belongs to.). Но о этом попозже.
Далее идёт public native bool GetDirectionRelativeToTrain (void) . Очнь полезная функция. Определяет, где перед вагона и где его зад относительно поезда. Понадобится нам, когда будем определять, какая шланга у нас соединена, а какая - нет.
(заодно и поставим диск конца поезда)
Полезна и public native bool IsOnBridge (void) - определяет, находится ли наш вагон на мосту.
Прокручиваем АПИ далее, и видим табличку с заголовом Vehicle object messages . Перевод описания системы сообщений есть на трейнсиме (ссылку можете найти на railunion'e), и в данном случае нам понадобится 2 и 3
"Vehicle","Coupled" посылаемое от сцепляемого вагона всем остальным объектам.
"Vehicle" "Decoupled" посылаемое от одного из 2 расцепляемых вагонов всем остальным объектам. В 2006 она плохо работала, в последующих её исправили (об этом я и сказал в самом начале), но я сделаю "заплатку" для работы в 2006.
А теперь смотрим родительские классы. Из класса Trackside нам пока ничего не понадобится, из MapObject - тоже.
А вот из MeshObject кое-что нам понадобится. А именно
public native MeshObject MeshObject.SetFXAttachment(string effectName, Asset asset)
Как вы поняли, именно этой функцией мы будем цеплять мешь. А так как объект меши нам не нужен(честно, об этом объекте я узнал когда писал эту статью ), то можем его никому не возвращать. То есть записи
MeshObject MO= (cast<MeshObject>me).SetFXAttachment("front_couple ",myScepkaAsset); //так тут мы явно преобразовали наш объект из нашего класса в класс MeshObject
MeshObject MO= me.SetFXAttachment("front_couple",myScepkaAsset);
MeshObject MO= SetFXAttachment("front_couple",myScepkaAsset);
me.SetFXAttachment("front_couple",myScepkaAsset);
SetFXAttachment("front_couple",myScepkaAsset);
(cast<MeshObject>me).SetFXAttachment("front_couple ",myScepkaAsset);
для данного случая абсолютно идентичны.
Ну а чтобы отцепить мешь, можно провести
SetFXAttachment("front_couple",null);
где под null понимается указатель на несуществующий объект (тоесть мы именно такой несуществующий объект цепляем вместо старого, в результате на месте сцепки у нас ничего нет). Кстати, 2004 не умела такое делать - там надо было цеплять только существующую мешь (там для имитации полной отцепки новая мешь должна была быть невидимой)
Как вы догадались, string effectName - это имя эффекта, прописанное в конфиге (в примере - довольно распространённое имя "front_couple", используемое в оснащённых скриптами AlexEF'a локах).
Далее нам потребуется класс Asset, который представляет ссылку на "дополнение"(мы его называем допом, куидом(что кстати неверно)), которое установлено в CMP (а не объект, стоящий на карте).
Но в начале мы хотя бы должны знать, какие именно "дополнения" нам потребуются. Для этого лучше всего использовать kuid-table нашего же вагона. Так как эта table ("таблица") находится вне скрипта, то её содержимое можно менять, а значит скрипт можно будет ставить вагонам с разными сцепками.
Чтобы её получить, нам надо посмотреть ещё более общий родитель класса Vehicle - класс TrainzGameObject .
В нём всего 2 функции, одна из которых нам нужна - это public Asset TrainzGameObject.GetAsset(void) С помощью этой функции мы получим "дополнение" нашего же вагона. Например так
Только учитываем, что все пересенные и ссылки, которыми могут пользоваться другие функции нашего скрипта, должны быть написаны вне функций (глобальные переменные).
Осталось посмотреть ещё 4 класса. Первым будет класс Train , из которого нам понадобится 2 функции -
public native Vehicle[] Train.GetVehicles (void) - получаем массив ссылок на все вагоны нашего поезда в порядке следования от головы, и с нумерацией, начинающейся с 0.
Чтобы наш вагон нашёл этот массив вагонов нужно сделать
замечу, что работает криво, звуки красивыми и безглючными получаются не всегда, ну и ладно.
Зато можно воспроизвести звук с увеличенной в volume раз громкостью, которая будет постоянна на расстоянии до minDistance метров от точки привязки attachmentPoint, а затем линейно затухнет до расстояния maxDistance метров. Воспроизводиться она GameObject'е (GameObject - класс-отец TrainzGameObject , и, получается, прадед Vehicle ), а звуки будут браться из asset , причём путь к ним из папки допа будет указан в строке filename .
Ну и дошли мы наконец до GameObject'а, который позволяет работать с системой сообщений. Нам потребуются 3 функции
работает очень хитро. Когда его применяют к объекту, он, если посылается сообщение к объекту target с полями string major, string minor то он ищет в коде функцию со аргументом Message, имя которой написано в строке handler, и вызывает её. (аргументом становится то самое посланное сообщение)
например, ловля сообщения "Train","NotifyPantographs" от поезда всем объектам (в том числе и нашему вагону) делается так
void TPhandler(Message Msg)
{
if(Msg.src==/*кто же это нам сообщение послал о токоприёмниках? */)
{
//делаем то-то
}
//другая обработка
}
public void Init(void)
{
inherited();
//что-то нужное
AddHandler(me,"Train","NotifyPantographs","TPhandl er");
//что-то нужное
}
Есть ещё одна функция, public native void GameObject.Sniff(GameObject target, string major, string minor, bool state)
которая может заставлять объект пересылать нам сообщение предназначенное ему (причём в сообщениии будет указан не он а первоисточник)
ну и было бы не честно не рассказать о системе потоков, которая, на мой взгляд, являестся главным преимуществом Trainz Game Script .
Мы можем объявить функцию потоком. Поток - это отдельный код, который может выполняться одновременно с другими процессами, текущими в игре. Правда, для этого его надо усыпить. Для этого придумана функция public native void GameObject.Sleep ( float seconds ) которая заставляет поток засыпать на seconds секунд.
описание такое
thread void MyPotok()
{ //что-то делается
(cast<GameObject>me).Sleep(10); //полная запись сна на 10 сек
//ещё что-то делается
Sleep(1.1); //спим 1.1 секунду краткая запись
//ещё что-то делается
}
Только теперь мы можем приступить к написанию скрипта. Кое-что скопирую из AlexEf овского
Идея по такая - устанавливаем на наш вагон хендлер, который будет обрабатывать сцепку нашего вагона
и ещё один хендлер обрабатывающий расцепку вагона. Так как о расцепке вообще сообщает только
один из расцепляемых вагонов, будем проверять и их сообщения о расцепке.
Для снежной пыли сделаем отдельный поток, а для звука моста - специальный поток с проверкой "запуск не более одного"
Последний раз редактировалось TRam_; 21.10.2009 в 01:15.
include "Vehicle.gs"
class NewRussianVehicle isclass Vehicle
{
Vehicle inFront;
Vehicle inBack;
Asset SA3_coupled, SA3_uncouped, Disk,MyAsset1;
float currentVelocity=0;
bool we_are_on_brige=false;
void SetCoupler(int pos, bool direction)
{
//// устонавливает соотвецтвующий тип сцепки для переда и зада вагона
//// в зависимости от положения и направления вагона в составе
//// public void SetCouplerType(int pos, bool direction);
//// pos :
//// 0 - одиночный
//// 1 - Первый
//// 2 - В центре
//// 3 - Последний
////
//// durection :
//// true - направление вагона соотвецтвует направлению состава
//// false - направление вагона не соотвецтвует направлению состава
if (pos == 0) //// вагон одиночный
{
SetFXAttachment ("front_couple", SA3_uncouped);
SetFXAttachment ("back_couple", SA3_uncouped);
SetFXAttachment ("front_disk", null);
SetFXAttachment ("back_disk", null);
}
else if (pos == 1) //// вагон в начале состава
{
if (direction)
{
SetFXAttachment ("front_couple", SA3_uncouped);
SetFXAttachment ("back_couple", SA3_coupled);
SetFXAttachment ("front_disk", null);
SetFXAttachment ("back_disk", null);
}
else
{
SetFXAttachment ("front_couple", SA3_coupled);
SetFXAttachment ("back_couple", SA3_uncouped);
SetFXAttachment ("front_disk", null);
SetFXAttachment ("back_disk", null);
}
}
else if (pos == 2) //// вагон в центре состава
{
SetFXAttachment ("front_couple", SA3_coupled);
SetFXAttachment ("back_couple", SA3_coupled);
SetFXAttachment ("front_disk", null);
SetFXAttachment ("back_disk", null);
}
else if (pos == 3) //// вагон в конце состава
{
if (direction)
{
SetFXAttachment ("front_couple", SA3_coupled);
SetFXAttachment ("back_couple", SA3_uncouped);
SetFXAttachment ("front_disk", null);
SetFXAttachment ("back_disk", Disk);
}
else
{
SetFXAttachment ("front_couple", SA3_uncouped);
SetFXAttachment ("back_couple", SA3_coupled);
SetFXAttachment ("front_disk", Disk);
SetFXAttachment ("back_disk", null);
}
}
}
int GetMyNumber(Vehicle[] TrainVehiclesArray)
{
int i=0,ArraySize = TrainVehiclesArray.size();
Vehicle MyVeh=(cast<Vehicle>me);
while(i<ArraySize)
{
if(TrainVehiclesArray[i]==MyVeh)
return i;
i++;
}
return 0;
}
void MyPosition(void)
{
Train MyTrain=me.GetMyTrain();
if(MyTrain!=null)
{
Vehicle[] TrainVehiclesArray = MyTrain.GetVehicles();
int a=me.GetMyNumber(TrainVehiclesArray);
int size_of_train=TrainVehiclesArray.size();
bool direction = (cast<Vehicle>me).GetDirectionRelativeToTrain();
if(size_of_train==1) //вагон одиночный
{
inFront=null;
inBack=null;
SetCoupler(0,direction);
}
else if(a==0) //// вагонов больше одного, этот вагон находиться первым в составе
{
inFront=null;
inBack=TrainVehiclesArray[1];
SetCoupler(1,direction);
}
else if(a<(size_of_train-1)) //// вагонов больше одного, этот вагон находиться в центре состава
{
inFront=TrainVehiclesArray[a-1];
inBack=TrainVehiclesArray[a+1];
SetCoupler(2,direction);
}
else //// вагонов больше одного, этот вагон находиться в конце состава
{
inFront=TrainVehiclesArray[size_of_train-2];
inBack=null;
SetCoupler(3,direction);
}
}
}
void CoupleHandler(Message msg)
{
if(msg.src==me) //мы сцепились
{
me.MyPosition();
}
}
void DecoupleHandler(Message msg)
{
if(msg.src==me or msg.src==inFront or msg.src==inBack) //мы, либо соседний вагон, расцепилсись
{
me.MyPosition();
}
}
thread void SoundLooper()
{
// звук должен быть всего 1, значит, если во время работы одного этого потока вдруг был запущен другой
// то этот другой работать не должен.
if(!we_are_on_brige)
{
we_are_on_brige=true;
while(currentVelocity>20 and me.IsOnBridge()==true)
{
me.Sleep(World.PlaySound(MyAsset1,"brigesound.wav",1.0,14.0,1000.0,me,"a.bog0"));
}
we_are_on_brige=false;
}
}
thread void VehicleLooper()
{
me.Sleep(0.6); //ждём инициализации
Train train1=me.GetMyTrain();
while(train1!=null) //бесконечный цикл
{
train1=me.GetMyTrain();
if(train1!=null)
{
currentVelocity=Math.Fabs(train1.GetVelocity())*3.6; //переводим из м/с в км/ч
if(me.IsOnBridge()==true)
{
SoundLooper();
}
if(currentVelocity>15 and World.GetWeatherType()>4)
PostMessage(me,"pfx","+0+1",0);
else
PostMessage(me,"pfx","-0-1",0);
if(World.GetTrainzVersion()<=2.6) //заплатка на 2006 версию трейнз
{
me.PostMessage(me,"Vehicle","Decoupled",0);//заставляем наш вагон проверить, не отцепили ли от него другие вагоны
}
}
else
currentVelocity=0;
me.Sleep(10);
}
}
public void Init(void)
{
inherited();
//Ищем в Kuid-Table сцепки
//SA3_coupled - сомкнутоя сцепка
//SA3_uncouped - разомкнутоя сцепка
//Disk - диск хвоста поезда
MyAsset1=me.GetAsset();
Disk = MyAsset1.FindAsset("Disk");
SA3_coupled = MyAsset1.FindAsset("SA3-coupled");
SA3_uncouped = MyAsset1.FindAsset("SA3-uncoupled");
PostMessage(me,"pfx","-0-1",0.9); //с некоторой задержкой отключаем снежную пыль
me.AddHandler(me,"Vehicle","Coupled","CoupleHandler");
me.AddHandler(me,"Vehicle","Decoupled","DecoupleHandler");
me.PostMessage(me,"Vehicle","Coupled",0.8); //изображаем, как будто вагон наш сцепился :)
//задержка нужна для того, чтобы модель успела прогрузиться
me.VehicleLooper();
}
};
TRam_, вопрос один закрался. Использование в SetCoupler else if'ов вместо switch case'ов как-нибудь аргументированно? (исключая аргумент "мне так захотелось")
Это все прекрасно, но можно сделать скрипт вагона универсальный для всех новых создаваемых вагонов.
Скажем так:
1. Свет включался и выключался по разному и с помощью кнопки.
2. Сцепка.
3. Аним двери.
4. Пассажиры.
5. Ну и так далее.
И указать где и какие точки привязки разместить.
Чтобы разработчикам вагонов просто прописать данный скрипт и все.
Этого так не хватает. Вон Женя делает Шексну, вместо того чтоб отвлекать одного из скриптеров, взял бы и скачал такой универсальный скрипт и вставил его в вагон и все будут довольны.
Я прекрасно понимаю, что щас скажите а сделай сам, ну зачем так грубо, яже предлогаю дело.
Уважаемый Владимир Tram_ не рассмотрите мое предложение сделать данный скрипт в виде урока, не описывать скрипт, а просто дать такой и показать где что надо для него расставить.
наверно создаваемых пассажирских вагонов (тот что описан выше - для грузовых)
со светом один вопрос. Движок трейнз не умеет делать тени в кабине, но зато скрипт умеет изменять ячркость освещения полигон в ней. Поэтому мне сейчас не хватает ни информации по зависимости освещённости от времени суток и погоды. (а так как трейнз не запускается, на "ощупь" сделать эту зависимость физически не смогу. Может кто-нибудь с фотометром 2 графика освещённости от времени сделает?(пасмурная/ясно) ). Ещё хуже со звуками - это единственная вещь в трс, которая требует очень продолжительных плясок с бубнами, а плясать с бубнами без информации о результате - верх безумия.
Для шексаны я уже делаю то, что в состоянии проверить только по коду, да и то очень медленно. Кстати, у этого вагона есть несколько индивидуальных особенностей, так что везде его поставить нельзя будет...
2. Сцепка.
всм в режима свободной камеры дёргать ручку расцепки? можно... (если о обычной сцепке, то можно и сейчас скрипт AlexEF'а прикрутить, если на старой модели сделать точки привязки)
а по поводу подключения скрипта самый надёжный способ - внимательно посмотреть конфиг, и поставраться делать все меши и дымы такими же, как и там.
А для поиска точек привязки звука открываем скрипт в блокноте, затем правка - найти
"a.
пробежав поиском по скрипту получаем все точки, которые там требуются, тем более что их название является либо транслитным, либо английским названием звука...
Последний раз редактировалось TRam_; 23.10.2009 в 14:49.