Trainz Railroad Simulator 2004. Руководство по созданию пользовательских ресурсов

Сценарий для электростанции
[Электростанция]

Введение

Сценарий для электростанции относительно маленький и простой. Все, что он, в сущности, делает - разрешает выгрузку угля в очередь coal_in, чтобы этот уголь мог быть использован процессом coal_consumer. Сценарий также управляет включением/выключением атмосферных эффектов так, чтобы они были синхронизированы с рабочиме процессом, а также делает возможным установку в окне HTML скорости расхода угля и начальной емкости очереди при редактировании свойств электростанции в Топографе.

Сценарий - это файл .gs с исходным текстом, расположенный в том же каталоге, что и сам ресурс. Таким образом, Trainz известно, где найти сценарий. Игра будет искать две следующих записи в файле config.txt ресурса:

script "powerstation"
class "PowerStation"

В нашем случае файл сценария - powerstation.gs, поэтому параметр script установлен в powerstation (имя файла с исходным текстом без расширения .gs). Параметр class равен "PowerStation". Это имя класса сценария в файле powerstation.gs.

Замечание:
В TRS2004 нет необходимости компилировать файлы сценария вручную, поскольку Trainz обрабатывает их автоматически при запуске. Но вы можете вызвать компилятор до запуска игры, чтобы разобраться с возможными синтаксическими ошибками до использования ресурса. Смотрите раздел Сценарии, там приведена информация по ручной компиляции и советы по отладке.
Исходный код powerstation.gs минус реализация самих функций (их мы изучим позже) выглядит так:

include "GenericIndustry.gs"


//
// Сценарий для электростанции довольно прост.  Все, что он делает - выгружает уголь из медленно движущихся
// хопперов и включает/выключает атмосферные эффекты при необходимости.  Кроме того, 
// можно изменить скорость расхода угля станцией, используя реализованные 
// методы PropertyObject.
//
// Процесс coal_consumer работает, пока есть уголь для потребления (а это зависит
// от выгрузки угля, поскольку в настройках по умолчанию входная очередь угля в начале пуста).
//
class PowerStation isclass GenericIndustry
{
  ProductQueue coalInQueue;       // Входная очередь угля.
  bool scriptletEnabled = true;   // Флаг "Сценарий включен/отключен".

  // Отслеживаем, подвезли ли уголь, затребованный в путевом листе.
  int coalWBRemain = 0;


  //
  // Методы GenericIndustry
  //

  // Показывает текущее состояние различных очередей во всплывающем окне браузера.
  thread void ViewDetails(void) {}

  // Возвращает ложь, чтобы показать, что команды загрузки этим предприятием не выполняются.
  bool HandleTrainLoadCommand(Train train) {}

  // Возвращает истину, чтобы показать, что триггер позволяет разгрузку/погрузку во время движения вагона.
  bool TriggerSupportsMovingLoad(string triggerName) {}

  // Выгружает уголь из хоппера в угольную кучу (входную очередь).
  void PerformMovingLoad(Vehicle vehicle, string triggerName) {}


  //
  // Методы PowerStation
  //

  // Главный поток, отслеживающий процесс потребления угля и включающий/выключающий атмосферные эффекты 
  // при выгрузке.
  thread void CoalMain(void) {}

  // Метод инициализации игрового объекта, вызываемый Trainz, когда предприятие создается в первый раз.
  // Отвечает за инициализацию внутренних данных и запуск потоков предприятия.
  public void Init(void)
  {
    inherited();

    coalInQueue = GetQueue("coal_in");
    
    AddAssetToIndustryProductInfo("coal", "coal_in", "coal_consumer", true);

    CoalMain();
  }


  //
  // Методы Industry
  //
  
  // Получает требования этой электростанции (запрос на пополнение угля) для использования в путевых листах.
  public Requirement[] GetRequirements(void) {}

  // Предоставляет информацию, чтобы название этой электростанции появилось в
  // подменю 'Отправляйся к >' меню.
  public void AppendDriverDestinations(string[] destNames, string[] destTracks) {}

};

Все классы сценария предприятия должны быть унаследованы от класса Industry. Но в нашем случае класс PowerStation унаследован от GenericIndustry, общего универсального класса, унаследованного от Industry. Функциональность, предоставленная GenericIndustry подходит для многих предприятий, поэтому при ее использовании сценарий предприятия можно подготовить быстрее, чем в случае кодирования всего с нуля.

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

Замечание:
Для каждой электростанции, размещенной на маршруте, создается отдельный экземпляр PowerStation, управляющий конкретным объектом-предприятием. Вот почему работающее предприятие считается игровым объектом.
Внутренние переменные
В начале объявления этого класса указано несколько переменных-членов класса.coalInQueue ProductQueue используется для доступа к очереди coal_in. Чтобы можно было легко узнать состояние электростанции, используется флаг scriptletEnabled. Две важных переменных, о которых стоит упомянуть, унаследовано от GenericIndustry. Это браузер info и контроллер поезда на предприятии itc.

Метод Init()
У многих сценариев есть метод Init(). Trainz вызывает этот метод, если он есть, при создании игрового объекта. Программист должен позаботиться здесь о любой инициалиализации, требующейся его коду. Обычно в нее входит инициализация всех переменных и, возможно, запуск потока, чтобы игровой объект мог обрабатывать сообщения.

В нашем примере coalInQueue инициализируется вызовом метода GetQueue() (унаследованного от MapObject через Industry). Методу передается имя требуеющей очереди грузов. Обратите внимание, как аргументы, переданные в GetQueue() соответствуют очереди coal_in, определенной в файле config.txt предприятия.

coalAsset присваивается ссылка на уголь. Для этого получается ссылка Asset на объект-предприятие с помощью метода GetAsset(), а затем используется Asset::FindAsset() для извлечения груза "уголь". Причина, по которой уголь извлекается из объекта-электростанции в том, что он находится в таблице КИДов электростанции kuid-table (определенной в config.txt).

Метод Init() родительского класса (GenericIndustry::Init()) вызывается явно с помощью ключевого слова inherited. Таким образом инициализируются и свойства GenericIndustry.

Делается это из следующих соображений: когда PowerStation.Init() вызывается игрой, перекрытый GenericIndustry::Init() вызван не будет. Поэтому, если он не будет здесь вызван явно, важные задачи инициализации GenericIndustry выполнены не будет, что вызовет серьезные проблемы для объекта PowerStation, который в высшей степени зависит от свойств GenericIndustry's. Поэтому очень важно вызывать родительский метод Init() (если таковой есть), чтобы процесс инициализации прошел по всей иерархии классов.

Замечание:
GenericIndustry::Init() запускает поток GenericIndustryMain(), который будет обрабатывать почти все сообщения для этого предприятия. Как это работает, станет более ясно по ходу изучения.
Наконец после того, как все объекты проинициализированы и вызван GenericIndustry::Init(), запускается поток CoalMain(). Заводить такой поток или даже использовать именно это имя метода необязательно, это просто рекомендованный способ создания сценарий для игрового объекта с главным потоком обработки сообщений.


Главный поток электростанции

Поток CoalMain() обновляет состояние флага scripletEnabled и синхронизирует атмосферные эффекты с процессом coal_consumer. Остальную функциональность - обнаружению по срабатыванию триггеров подъезжающих вагоны и обработку запросов на получение информации о предприятии выполняет поток GenericIndustry::GenericIndustryMain().

Поток GenericIndustryMain(), запущенный в методе Init() явным вызовом GenericIndustry::Init(), работает параллельно с CoalMain(). Таким образом, у нас имеется два потока. Хотя потоки в общем случае надо сводить к минимуму и запускать их только в случае необходимости, работа двух потоков в предприятии - это небольшая цена за удобство применения расширяемого полезного класса вроде GenericIndustry.

  //
  // Главный поток, следящий за процессом потребления угля и включающий/выключающий атмосферные эффекты
  // при разгрузке.
  //
  thread void CoalMain(void)
  {
    Message msg;

    wait()
    {
      // Правило электростанциии послало сообщение, показывающее, что станция запущена, поэтому обновим флаг " и
      // убедимся, что процесс coal_consumer включен.
      on "Scriptlet-Enabled", "1":
      {
        if (!scriptletEnabled)
        {
          scriptletEnabled = true;
          SetProcessEnabled("coal_consumer", true);
        }
        continue;
      }
      // Правило электростанциии послало сообщение, показывающее, что станция остановлена, поэтому обновим флаг " и
      // убедимся, что процесс coal_consumer отключен.
      on "Scriptlet-Enabled", "0":
      {
        if (scriptletEnabled)
        {
          scriptletEnabled = false;
          SetProcessEnabled("coal_consumer", false);
        }
        continue;
      }

      // включить атмосферные эффекты при запуске процесс потребления угля
      on "Process-Start", "coal_consumer":
      {
        SendMessage(me, "pfx", "+0+1+2+3+4+5+6");
        continue;
      }
      // отключить атмосферные эффекты, когда процесс потребления угля остановлен
      on "Process-Stop", "coal_consumer":
      {
        SendMessage(me, "pfx", "-0-1-2-3-4-5-6");
        continue;
      }
    }
  }

Поток CoalMain() входит в оператор wait() и остается в нем неопределенное время, ожидая сообщений. Диспетчер Trainz будет посылать разнообразные сообщения нашему объекту-предприятию и оператор wait() будет обрабатывать некоторые из них.

Сообщения Scriplet
Сообщения с главным типом "Scriplet-Enabled" посылаются Правилом электростанции, чтобы сообщить предприятию о наличии/отсутствии электроэнергии. Поскольку наше предприятие и есть электростанция сообщения "Scriplet-Enabled" нам не нужны.

Сообщения процесса предприятия
Когда запускается процесс на предприятии, предприятию посылается сообщение ("Process-Start", "<имя процесса>"). В этом примере при получении этого сообщения включаются все семь атмосферных эффектов для предприятия. Эффекты заключаются в выбросах черного дыма от горящего угля и пара от градирен. Это создает впечатление работающей электростанции.

Обратное происходит при получении сообщения "ProcessStop". Это сообщение означает, что в очереди coalInQueue не осталось угля и поэтому процесс coal_consumer остановлен. Отключением атмосферных эффектов мы заставляем электростанцию выглядеть остановленной и простаивающей.

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

Методы GenericIndustry

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

ViewDetails
Если пользователь в режиме Машиниста щелкнет правой кнопкой мыши на игровом объекте - таком, как предприятие или вагон, появится меню с пунктом Информация.... Если вызвать этот пункт меню, объекту будет отослано сообщение типа ("MapObject", "View-Details"). GenericIndustryMain() вызовет ViewDetails() при получении такого сообщения. GenericIndustry::ViewDetails() - это заглушка, поэтому программист должен предоставить свою собственную реализацию, передающую информацию браузеру.


Выше: Щелкните правой кнопкой на электростанции, чтобы открылось меню и нажмите View Details.

Замечание:
Запрос пользователя на информацию не обязательно должен обрабатываться, но удобно, когда пользователь может увидеть информацию по текущему состоянию предприятия.
  // 
  // Показывает статистику по всем очередям во всплывающем окне браузера.
  //
  thread void ViewDetails(void)
  {
    if (!info)
    {
      info = Constructors.NewBrowser();
      
      info.LoadHTMLString(HTMLWindow.GetCompleteIndustryViewDetailsHTMLCode(me, scriptletEnabled));
      info.SetWindowRect(100, 80, 500, 545);
    }
  }

Этот метод компилирует HTML код, чтобы показать, работает ли предприятие или нет, а также сколько угля осталось в очереди. Код HTML предоставляется служебным методом HTMLWindow::GetCompleteIndustryViewDetailsHTMLCode(). Он может запросить информацию у нужного предприятия.

Предпологается, что будет использоваться браузер info из GenericIndustry. Тогда GenericIndustryMain() сможет установить info в ноль, поскольку класс отслеживает сообщение типа ("Browser-Closed", "") где источник сообщения = info.


Выше: Код HTML, возвращаемый описанным методом, при показе в браузере.

Отказ машинистам в загрузке
Метод HandleTrainLoadCommand() вызывается GenericIndustry::HandleTrain(), чтобы провести поезд через предприятие, когда им управляет машинист, выполняющий команду Загрузить.

  //
  // Возвращает ложь, чтобы показать, что команды загрузки этим предприятием не обслуживаются.
  //
  bool HandleTrainLoadCommand(Train train)
  {
    return false;
  }

Поскольку электростанция может только разгружать вагоны, ее метод HandleTrainLoadCommand() всегда возвращает ложь. Это делается на тот случай, если пользователь скомандует машинисту загрузиться на этом предприятии.

Поддержка разгрузки движущихся вагонов
TriggerSupportsMovingLoad() вызывается GenericIndustryMain(), чтобы определить, поддерживает ли указанный триггер погрузку/разгрузку при движении вагона (или только при его остановке)

  //
  // Возвращает истину, чтобы показать, что триггер позволяет погрузку/разгрузку в движении. 
  //
  bool TriggerSupportsMovingLoad(string triggerName)
  {
    if (itc.IsTrainCommand(vehicle.GetMyTrain(), Industry.UNLOAD_COMMAND))
      return true;

    return false;
  }

Этот метод всегда возвращает истину, поскольку наше предприятие поддерживает погрузку/разгрузку в движении. Если точнее, то разрешается только разгрузка с движущегося вагона, но этот метод не вдается в такие тонкости. Аргумент triggerName не проверяется, поскольку на нашем предприятии есть только один триггер.

Выгрузка угля
PerformMovingLoad() вызывается GenericIndustry::PerformInnerEnterMoving() для погрузки/выгрузки указанного вагона на указанном триггере. Можно безопасно предположить, что vehicle движется над triggerName, поскольку GenericIndustryMain() определяет все движения вагона над внутренними областями всех триггеров предприятия.

Поскольку предприятие поддерживает только разгрузку с движущихся вагонов, этот метод пытается только разгружать vehicle.

  //
  // Выгружает уголь из хоппера в угольную кучу (входную очередь).
  //
  void PerformMovingLoad(Vehicle vehicle, string triggerName)
  {
    float speed = vehicle.GetVelocity();

    if (speed > -5.0f and speed < 5.0f)
    {
      int roomAvailable = coalInQueue.GetQueueSpace();
      LoadingReport report = CreateUnloadingReport(coalInQueue, roomAvailable);
      vehicle.UnloadProduct(report);

      // Отследим изменения в путевом листе
      if (coalWBRemain > 0)
        coalWBRemain = coalWBRemain - report.amount;
    }
  }

Скорость (измеряемая в метрах в секунду) проверяется как положительное и как отрицательное число, поскольку мы не знаем, в каком направлении движется vehicle. Если он идет задом наперед, Vehicle::GetVelocity() вернет отрицательное значение.

Если vehicle движется с безопасной для разгрузки скоростью, делается попытка разгрузить вагон. Для этого вызывается метод Vehicle::UnloadProduct(). LoadingReport, указывающий coalInQueue как очередь для разгрузки и roomAvailable как максимальное количество угля, которое может быть выгружено, передается как один аргумент в UnloadProdct().

Примечание:
На электростанции есть только один триггер, поэтому triggerName не проверяется.

Методы Industry

Чтобы работать более гладко в среде Trainz, сценарий предприятия может реализовать несколько методов класса Industry. Для нашего предприятия определяются методы, добавляющие нашу электростанцию в меню команды Отправляйся к > и генерирующие данные для путевого листа.

Требования
Метод GetRequirements() method вызывается Trainz, чтобы узнать требования предприятия и сгенерировать путевой лист.Должен быть возвращен массив требований, содержащий все данных, необходимые Trainz для генерации путевого листа.

  //
  // Спрашивает требования нашей электростанции (запрос на пополнение угля) для использования в путевом листе.
  //
  public Requirement[] GetRequirements(void)
  {
    Requirement[] ret = new Requirement[0];
    
    if (coalInQueue.GetQueueCount() < 162900 or coalWBRemain > 0) // 3 угольных хоппера
    {
      ResourceRequirement req = new ResourceRequirement();
      
      req.resource = coalInQueue.GetProductFilter().GetProducts()[0];
      
      // Столько мы запросили. Подождем, пока наши требования будут выполнены, 
      // если мы еще не ждем выполнения путевого листа.
      req.amount = 651600;        // 12 угольных хопперов
      if (coalInQueue.GetQueueCount() < 162900 and coalWBRemain == 0)
        coalWBRemain = 651600;

      req.dst = me;
      req.dstQueue = coalInQueue;

      ret[ret.size()] = req;
    }
    
    return ret;
  }

Если в угольной очереди меньше 162,900 единиц или coalWBRemain больше 0, генерируется путевой лист, требующий еще угля в количестве 651,600 единиц (12 полностью загруженных угольных хопперов). Причина для введения переменной coalWBRemain - чтобы путевой лист генерировался только при необходимости (то есть, уголь кончается или предыдущий запрос не был полностью выполнен).


Выше: Путевой лист, сгенерированный Trainz по требованиям, возвращенным нашим методом GetRequireMents().

Меню машиниста
Метод AppendDriverDestinations() вызывается для получения списка путей и их названий для использования в качестве пунктов в подменю команды Отправляйся к для этого предприятия.

  //
  // Дает информацию о пункте выгрузки угля для 
  // подменю меню 'Отправляйся к >' для нашего предприятия.
  //
  public void AppendDriverDestinations(string[] destNames, string[] destTracks)
  {
    destNames[destNames.size()] = "Coal Drop off";
    destTracks[destTracks.size()] = "in_track";
  }

Поскольку у электростанции есть только один путь, in_track, определенный как единственный отрезок пути в файле config.txt предприятия, его имя помещается в конец массива destTracks. Массив destNames array содержит текст для пунктом меню, показываемых игрой пользователю. См. рисунок внизу.

Примечание:
Используя size() в качестве индекса массива, мы ссылаемся на элемент массива сразу же за концом массива. Это расширят наш массив. Индексирование за пределами начального размера массива в языке GS можно выполнять безопасно.

Выше: Меню команды "Отправляйся к..." с именем нашего подъездного пути.


Trainz Railroad Simulator 2004. Руководство по созданию пользовательских ресурсов
Copyright (C) 2002-2003 Auran Developments Pty Ltd. All Rights Reserved.
Hosted by uCoz