Руководство для разработчиков дополнительного контента RRS
Содержание
- Введение
- Общее представление об RRS как о программном комплексе
- Модули подвижного состава. Быстрый старт
- Установка и настройка среды разработки
- Проект простейшего локомотива: создаем каркас
- Конфигурационный файл подвижной единицы
- Конфигурационный файл поезда
- Тест заготовки модуля в симуляторе
- Обработка нажатий клавиш
- Реализация простейшей тяговой характеристики
- Пользовательские параметры конфигурационного файла
- Основы пошаговой отладки
- Создание и настройка визуальной модели
- Базовые принципы анимации элементов модели
- Промежуточные итоги
- Создание модели настоящего локомотива
- Сборка RRS из исходных текстов
- Windows 10/11 64 bit
- Linux 64 bit
Введение
Если вы читаете эту страницу, то скорее всего решили попробовать себя в роли разработчика моделей подвижного состава для симулятора RRS. Это не может не радовать, и у меня для вас, как водится, две новости: одна — хорошая, а другая — плохая.
Плохая заключается в том, что возможности, предоставляемые симулятором, можно реализовать, умея программировать на языке C++.
Однако, хорошая новость в том, что прикладной интерфейс программирования (API) предоставляемый движком игры, практически ничем не ограничивает разработчика.
Симулятор имеет модульную архитектуру, главной идеей которой является вынесение программного кода, описывающего работу оборудования конкретной единицы подвижного состава (локомотива, вагона и т.п.) в отдельную динамически подключаемую библиотеку (DLL в ОС Windows), давая разработчику возможность реализовать все нюансы его работы.
В какой-то мере, RRS наследует идеи, предложенные в замечательном космическом симуляторе Orbiter, дополнения для которого я разрабатывал в далеком 2007 году. С сожалению, такая архитектура совершенно не характерна для существующих железнодорожных симуляторов.
В данном руководстве мы, шаг за шагом, начиная с элементарных примеров, рассмотрим весь процесс создание модели локомотива. Приобретя некоторый опыт, мы рассмотрим более сложные приемы разработки, владение которыми позволит вам воплотить в виртуальном мире RRS саму смелую свою задумку.
Общее представление об RRS как о программном комплексе
Компоненты симулятора
Симулятор представляет собой не одну программу, а комплекс взаимосвязанных программ. В штатном режиме запускается три процесса:
- launcher — пользовательский интерфейс для настройки поездки и запуска игры
- simulator — физический движок игры, выполняющий моделирование движения подвижного состава
- viewer — графическая подсистема, реализующая функции трехмерной визуализации
За непосредственное взаимодействие с игроком отвечает viewer — он обрабатывает пользовательский ввод с клавиатуры и мыши и отображает на экране поезд и окружающую его обстановку. Viewer является «лицом» игры, но не самым важным её компонентом.
Процесс simulator несет на себе основную нагрузку, выполняя такие функции как: формирование поезда, в соответствии с выбранной пользователем конфигурацией; загрузку и инициализацию модулей подвижных единиц; математическое моделирование движения поезда и работу его систем в реальном масштабе времени; взаимодействие с графической подсистемой — прием от неё управляющих команд и передачу данных о фактическом положении поезда на участке. Именно в этом процессе крутится код модулей DLL подвижного состава, который нам предстоит научится писать.
Обмен информацией между simulator и viewer осуществляется за счет стандартных средств межпроцессного взаимодействия (IPC). В текущей версии симулятора используется общая память (shared memory), в будущих версиях наиболее вероятен переход к взаимодействию через механизм сокетов. На данном этапе можно абстрагироваться от всех этих механизмов — вся необходимая информация распределяется между компонентами симулятора без участия разработчика модулей подвижного состава. Пока важно знать следующее:
- процесс simulator и используемые им библиотеки — это физический движок игры, технологически именуемый TrainEngine 2. Все модули подвижного состава и оборудования работают в контексте этого процесса;
- процесс viewer — графическая подсистема игры, работающая на базе открытого графического движка OpenSceneGraph;
Структура каталогов симулятора на диске
После установки симулятора стандартным способом, в зависимости от того пути, что был указан при установке, на диске возникает каталог RRS со следующей структурой

- bin — исполняемые файлы и библиотеки движка игры;
- cfg — каталог, содержащий конфигурационные файлы;
- data — каталог с ресурсами игры: здесь хранятся модели, текстуры, звуковые файлы и конфиги анимаций;
- docs — документация к симулятору;
- graph — сервисный каталог для хранения данных регистратора параметров движения
- lib — каталог подключаемых модулей движка игры;
- modules — каталог, содержащий модули подвижного состава и оборудования;
- plugin — каталог плагинов, расширяющих функционал движка симулятора;
- routes — каталог с маршрутами;
- screenshots — каталог для хранения скриншотов, создаваемых игрой по нажатию F12;
- sdk — каталог, содержащий заголовочные файлы, необходимые для разработки модулей подвижного состава и оборудования;
- themes — темы оформления интерфейса лаунчера.
Таков краткий перечень каталогов, теперь же перейдем к более подробному описанию каждого из них.
Каталог bin
На первом этапе достаточно иметь общее представление о содержимом этого каталога

Он содержит следующие исполняемые файлы:
- launcher.exe — интерфейс настройки и запуска симулятора («лаунчер»);
- simulator.exe — исполняемый модуль физического движка игры;
- viewer.exe — исполняемый модуль графической подсистемы;
- pathconv.exe — утилита для обработки маршрута ZDSimulator, выполняющая транслитерацию путей к файлам моделей и текстур, а так же приводящая эти пути к платформонезависимому виду.
- profconv.exe — утилита для генерации дополнительных файлов, необходимых для корректной работы симулятора с маршрутами ZDSimulator
Кроме того, каталог содержит массу DLL-библиотек, относящихся как к динамическому, так и графическому движку игры. Содержимое этого каталога не меняется разработчиком, поэтому назначение каждого из файлов мы пока описывать не будем.
Каталог cfg
Крайне важный каталог, через который происходит настройка моделей подвижных единиц и собственно встраивание их в игру.

Он содержит базовые конфигурационные файлы для самого симулятора
- brakepipe.xml — настройки математической модели тормозной магистрали;
- init-data.xml — начальные параметры, задающие расположение поезда по умолчанию и настройки физического движка;
- launcher.xml — настройки лаунчера
- settings.xml — настройки графической подсистемы
- solver.xml — настройки решателя дифференциальных уравнений движения поезда.
Кроме того, здесь имеется ряд подкаталогов, для хранения специфических конфигов
- couplings — конфигурационные файлы моделей сцепных приборов (поглощающих аппаратов автосцепки)
- devices — каталог конфигурационных файлов разнообразного стандартного оборудования, устанавливаемого на подвижной состав: тормозные краны, воздухораспределители, электрические машины и аппараты;
- main-resist — файлы, задающие формулу основного сопротивления движению, согласно Правил Тяговых Расчетов (ПТР), для разных типов подвижного состава;
- trains — каталог конфигурационных файлов поездов, доступных к запуску из меню лаунчера;
- vehicles — каталог конфигурационных файлов модулей подвижных единиц:
Мы очень подробно поговорим о каталога trains/ и vehicles/, когда будем писать пример собственного локомотива. Пока ограничимся обзорным рассмотрением.
Каталог data
Следующий чертовски важный каталог. Он содержит ресурсы игры, относящиеся к подвижному составу — конфиги анимаций, 3D-модели и звуки.

Каждый из этих каталогов содержит папки с именами, совпадающим с именем конфигурационного файла модели единицы подвижного состава в каталоге vehicles/. Каждый каталог соответствует конкретной модели ПС. Например, каталог анимации выглядит в стандартной поставке симулятора так

Папка vl60pk-1543 соответствует электровозу ВЛ60пк-1543 и содержит все xml-конфиги необходимые для анимации элементов электровоза и кабины.

Как видно тут довольно много файлов, каждый из которых настраивает определенную анимацию: подъем токоприемников, вращение колес, поворот рукояток и тублеров в кабине и т.п.
Папка models/ имеет похожую структуру, и, на примере ВЛ60пк имеет следующее содержимое

Здесь содержаться две модели в формате OSGT: модель самого электровоза и модель кабины, а так же папки с текстурами к ним. Папок с текстурами может быть сколько угодно, что дает возможность «перекрашивать» модель и кабину, просто указав путь к текстурам в конфиге подвижной единицы.
Каталог sounds/ содержит сами звуки (семплы) в формате WAV и конфигурационный файл sounds.xml, в котором прописывают настройки воспроизведения каждого семпла.

Каталог lib
Пока он содержит одну единственную DLL — файл rkf5.dll. Это подключаемая динамически библиотека, предоставляющая решатель систем дифференциальных уравнений. Симулятор непрерывно решает систему дифференциальных уравнений движения поезда, используя ресурсы этого решателя.
В настоящий момент используется метод Рунге-Кутты-Фельберга 5-6 порядков (откуда и название rkf5), который дает хорошее быстродействие при достаточной устойчивости решения задачи продольной динамики поезда.
Но никто не говорит, что это единственный решатель, который можно использовать. Разработчик может написать свой собственный решатель и настроить симулятор для работы с ним. Вдруг у вас получится лучше чем у нас?
Каталог modules
Место, где хранятся DLL-модули подвижного состава и оборудования.

DLL лежащие к корне этого каталога — модули стандартного оборудования, которое может быть установлено на любую модель подвижного состава. Например krm395.dll — модуль поездного крана машиниста усл. №395, широко применяемый на подвижном составе колеи 1520 мм. Такое решение дает ряд преимуществ:
- не нужно разрабатывать модель крана самостоятельно (не, ну только если очень хочется) — можно взять готовый модуль, установить его на разрабатываемый локомотив и готово!
- такая унификация дает возможность изменить логику работы крана машиниста без переделки всех дополнений
Пока такого оборудования мало, но в будущем список будет расширяться. Опять таки, модули оборудования могут создаваться сторонними разработчиками, для чего предусмотрен соответствующий API.
Модули подвижных единиц располагаются в соответствующих каталогах, например папка vl60 содержит DLL-модуль данного электровоза, а папка passcar — модуль пассажирского вагона.

Каталог plugins
Функциональность симулятора может расширятся, за счет разработки плагинов, который хранятся в данном каталоге.

Например, симулятор можно научить понимать интересующий вас формат маршрутов. Сейчас RRS использует маршруты от другого симулятора — ZDSimulator, для загрузки которых применяется плагин zds-route-loader.dll. Однако, теоретически можно написать плагин, обрабатывающий другой формат маршрутов и симулятор будет работать с этими маршрутами. Так мы и поступим, когда будет разработан собственный формат маршрутов RRS.
Кроме того, в симуляторе планируется ввод возможности реализовывать собственные аппаратные пульты. Для адаптации к различным протоколам взаимодействия с этими пультами так же может быть написан соответствующий плагин. Плагин modbus.dll будет предназначен для работы с пультами по протоколу шины Modbus.
Каталог routes
Содержит маршруты. Под маршрутом понимается совокупность трехмерных моделей, текстур и конфигурационных файлов, позволяющих осуществить трехмерную визуализацию интересующего участка железной дороги. Кроме того, маршрут содержит информацию о профиле пути на конкретном участке железной дороги, которая используется при расчете физики движения поезда по данному участку.
В каталоге routes располагаются папки с маршрутами

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

В данных заголовках прописаны интерфейсы ко всем классам и функциям, через которые симулятор взаимодействует с модулями подвижного состава и оборудования. При написании модуля эти файлы подключаются к исходному коду модуля.
Таким образом, мы кратко пробежались по структуре катал
Общие принципы устройства игровой физики
Железнодорожный поезд — сложнейший динамический объект. Описанию законов его движения посвящены тысячи к книг. Ежегодно наука пополняется массой новых знаний об этой отрасли. Не всё так просто с описанием поезда и в симуляторе RRS. Готовьтесь, напрягите извилины и постарайтесь вникнуть в суть рассказа — все нижеприведенное крайне важно для качественной разработки дополнений RRS.
Для начала, дадим ряд определений, на которых базируется концепция динамического движка TrainEngine 2 (TE2), на котором построен симулятор.
Единица подвижного состава, подвижная единица (ПЕ) — железнодорожный экипаж, имеющий кузов, ходовую часть и сцепные приборы, представляющий собой минимальную структурную единицу в составе поезда.
Под подвижной единицей в RRS понимается: несамоходный вагон, вагон электро- или дизель-поезда, секция локомотива, автомоториса дрезина и т.п.
Очень важно понимать следующее принципиальное отличие RRS от других железнодорожных симуляторов — в этом симуляторе все подвижные единицы динамически равноправны. То есть вагон ничем не отличается принципиально от локомотива — у него есть динамическая модель, для него задаются характеристики. Разработка вагонов разного типа так же предусматривается концепцией RRS.
Подвижные единицы RRS объединены в поезда.
Поезд — динамический объект, состоящий из нескольких подвижных единиц, соединенных межвагонными связями, имеющих общую тормозную магистраль и линии управления электропневматическим тормозом.
Любой объект, отображаемый в лаунчере RRS является поездом.

Поездом является и одиночный односекционный локомотив, такой как ВЛ60пк — просто этот поезд состоит из всего одной подвижной единицы. Поездом будет и двухсекционный локомотив, например ВЛ80 будет, с точки зрения RRS поездом, состоящим из двух ПЕ (секций). Поезд RRS описывается конфигурационным файлом, расположенным в каталоге trains.
Дадим еще одно важное определение
Межвагонная связь (сцепной прибор, сцепка) — механическое устройство, обеспечивающее соединение подвижных единиц между собой, а так же свягчающее ударные нагрузки и гасящее продольные колебания ПЕ.
В RRS межвагонные связи реализуются специальным DLL-модулем (coupling) который, про просьбе динамического движка игры вычисляет силу взаимодействия подвижных единиц между собой, в зависимости от их взаимного перемещения и его скорости. Определяющей, в свойствах межвагонной связи, является силовая характеристика поглощающего аппарата (ПА) сцепки. На данный момент в RRS реализована упрощенная сцепка (default-coupling) и грузовой упруго-фрикционный поглощающий аппарат (ef-coupling).
Поезд в RRS можно представить в виде следующей расчетной схемы

К каждой ПЕ в поезде приложен ряд сил и моментов:
- T — усилие в межвагонной связи;
- R — сила соновного сопротивления движению, связанная с сопротивлением качению и сопротивлением внешней воздушной среды;
- B — тормозная сила, реализуемая тормозами поезда;
- M — момент приложенный от тягового привода к каждой (!) колесной паре.
- mg — сила тяжести, связанная с профилем пути.
В модели поезда предполагаются следующие допущения (упрощения)
- каждая ПЕ — одномассовое твердое тело, имеющее жесткую связь с вращающимися колесными парами. Вертикальная и поперечная динамика не учитываются. Вписывание ПЕ в кривые учитывается кинематически;
- ПЕ, в каждый момент времени, считается расположенной на одном элементе профиля. Под элементом профиля понимается участок профиля с неизменным в его пределом уклоном;
- колесные пары движутся без проскальзывания.
Итак, какая же магия происходит внутри движка игры при расчете движения поезда? Расчет происходит по следующему алгоритму
- модуль каждой подвижной единицы, на основании положения поезда на профиле и угловой скорости вращения колесных пар рассчитывает тяговые и тормозные моменты, прикладываемые к колесным парам;
- на основании положения ПЕ в пространстве, моментов приложенным к КП, движок игры вычисляет основное сопротивление движению, усилие в межвагонных связях и определяет ускорение каждой ПЕ;
- ускорение каждой ПЕ дважды интегрируется, давая на выходе текущую скорость и положение каждой ПЕ в поезде.
Теперь вы понимаете, почему RRS дает столько свободы разработчику? Потому что симулятор интересуют только моменты, приложенные к колесным парам! Как они будут рассчитаны остается на совести разработчика модуля ПЕ. И он волен сделать этот расчет в полном соответствии с работой систем локомотива и вагона. Ничто не ограничивает его на этом этапе. Этот факт делает RRS уникальным симулятором, с одной стороны, но повышает порог вхождения в него для разработчика, с другой. Но мы с вами знаем, что хорошие вещи просто не делаются, так что смелее и вперед! Дальше будет немного проще.
Именно потому, что симулятор полагается исключительно на законы, открытые сэром Исааком Ньютоном, в нем возможно реализовать и электровоз, и тепловоз, и паровоз, любой тип моторвагонного подвижного состава, путевые и специальные машины. Допускается реализация поездов с разнородным тормозным оборудованием. при этом движок игры будет учитывать эти нюансы в конечном результате симуляции. Не правда ли, заманчивая перспектива для настоящего исследователя железнодорожного транспорта? Все эти функции доступны в игре уже сейчас.
Таков краткий обзор устройства и принципов построения RRS. А теперь, друзья, вперед! Погрузимся в программирование и вместе сделаем свое первое дополнение для
Модули подвижного состава. Быстрый старт
Установка и настройка среды разработки
В силу того, что RRS является свободным проектом, для разработки его программного кода и дополнений к нему используется свободное же программное обеспечение, распространяемое по условиям лицензии GPL, как и сам симулятор.
RRS основывается на кроссплатформенном фреймворке Qt для языка C++, и использует его среду разработки — QtCreator. В качестве компилятора языка C++ для ОС Windows используется проект MinGW — реализация компилятора GCC для Windows. Для разработки под ОС на базе ядра Linux используется нативный GCC.
В рамках данного руководства мы опустим разработку под Linux — не смотря на то что сам симулятор разрабатывается именно под этой операционной системой, вопрос выхода версии под нее вопрос несколько отдаленной перспективы, в силу разнообразия дистрибутивов данной ОС и некоторых организационных моментов. Симулятор прекрасно чувствует себя в линуксе, и работает он там гораздо быстрее чем в Windows, но пока мы отложим разговор об этой системе, тем более что линуксоиды, располагая доступными исходниками могут собрать RRS и самостоятельно. На данном этапе. Думаю мы друг друга поняли, тем более я и сам мечтаю сделать официальную сборку под несколько дистрибутивов линукса.
Прежде чем мы начнем, давайте убедимся что
- у вас установлена и работоспособна крайняя официальная версия RRS;
- вы обладаете базовыми навыками программирования на C++;
- вы внимательны, усидчивы и готовы к штурму новых знаний и навыков.
Итак, нам нужен фреймворк Qt, среда разработки QtCreator и компилятор MinGW. Все это мы получаем, сходив на официальный сайт проекта Qt. Качаем и запускаем инсталлер
Внимание! Я предлагаю качать оффлайн инсталлятор исходя из сугубо практических соображений быстроты установки. Вы же можете выбрать любой другой вариант, предоставляемый на сайте Qt.
Не буду описывать процесс в подробностях, ограничусь лишь основными моментами, критичными для успешного результата.
Во-первых убедимся в верности пути установки

Во-вторых, выберем из весьма значительного списка только необходимые нам компоненты, а именно среду разработки QtCreator, компилятор MinGW 7.3.0 64-bit и сам пакет фреймворка Qt, собранный компилятором MinGW 7.3.0 64-bit. В общем нужно расставить галочки так, как показано на скриншоте.

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

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

Еще я крайне рекомендую обзавестись нормальным текстовым редактором, ориентированным на разработчика. Кроме кода на C++ нам придется писать много XML-конфигов, поэтому хороший текстовый редактор (забудьте про чудовищный Блокнот!) совершенно необходим в хозяйстве. Я рекомендую Notepad++ или Sublime Text 3, которые так же можно получить совершенно бесплатно.
Итак, у нас все готово, чтобы начать работать над своим первым дополнением для RRS.
Проект простейшего локомотива: создаем каркас
Залогом успешной разработки всегда является грамотная организация проекта. Как говорил один из моих учителей: «Программу можно написать в одну строчку и она будет работать. Только разобраться в том, как она работает будет невозможно».
Таким образом, обычно файлы исходного кода организуются в проекты, содержащие сам код и настройки для сборки исполняемых файлов. Не является исключением и QtCreator, имеющий стандартные средства для создания типовых проектов. Мы можем воспользоваться таким мастером, но для более глубокого понимания структуры проекта дополнения создадим пустой проект вручную.
Для начала определимся, что за дополнение мы будем создавать. Пусть это будет локомотив. Однако, на первом этапе мы не будем заботится о его характеристиках и верной логике работы систем — реализуем простейшую выдуманную машину, чтобы понять, за какие рычаги должен дергать разработчик дополнения, чтобы реализовать свои задумки в плане создания более серьезной техники. Мы так и назовем этот локомотив — простейший локомотив.
Создание пустого проекта
Выберем место на диске и создадим там папку с проектом, назовем её simple-loco.

В ней создадим дерево проекта — создадим еще одну папку simple-loco, в которой создадим каталоги include и src. В них будут располагаться, соответственно заголовочные и исходные файлы проекта. Рядом с этим каталогом создаем пустой файл simple-loco.pro, который является сценарием сборки проекта из исходных текстов в QtCreator

Теперь этот pro-файл откроем в QtCreator

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

Лучше, если эти каталоги располагаются уровнем выше исходных текстов, дабы не засорять исходники мусором, генерируемым компилятором в папках build-*. Поэтому мы и создали дерево проекта именно таким образом, чтобы выполнить это условие. Указываем необходимые пути (как показано на скриншоте выше) и жмем кнопку «Настроить проект». После чего мы попадем в основное рабочее пространство проекта — редактор кода

Пишем сценарий сборки проекта
Щеклаем мышкой по файлу simple-loco.pro, он открывается в редакторе, куда мы вбиваем следующий заклинания
# Шаблон проекта - динамическая библиотека (DLL) TEMPLATE = lib # Задаем те библиотеки QT, которые нам пригодятся QT -= gui QT += xml # Имя итогового файла DLL и путь, куда он должен быть помещен после # сборки TARGET = simple-loco DESTDIR = $$(RRS_ROOT)/modules/$$join(TARGET,,,) # Библиотеки симулятора, с которыми компонуется DLL локомотива LIBS += -L$$(RRS_ROOT)/bin -lCfgReader LIBS += -L$$(RRS_ROOT)/bin -lphysics LIBS += -L$$(RRS_ROOT)/bin -lvehicle LIBS += -L$$(RRS_ROOT)/bin -ldevice LIBS += -L$$(RRS_ROOT)/bin -lfilesystem # Путь к необходимым заголовочным файлам INCLUDEPATH += ./include INCLUDEPATH += $$(RRS_ROOT)/sdk/include # Указываем файлы, включаемые в проект HEADERS += $$files(./include/*.h) SOURCES += $$files(./src/*.cpp)
Разберем подробнее этот сценарий. Первая строка с директивой TEMPLATE указывает на тип проекта, в нашем случае это динамическая библиотека (DLL).
Следующие две директивы указывают, какие библиотеки Qt мы включаем, а какие исключаем из нашего проекта. Поскольку нам не нужен никакой графический интерфейс, мы убираем все связанные с ним библиотеки, зато добавляем библиотеки для работы с XML — симулятор и локомотивы для него активно используют этот формат в качестве конфигурационных файлов.
Далее мы указываем имя DLL, которое должно получится после сборки проекта в параметре TARGET, а так же тот путь, по которому следует собрать библиотеку в параметре DESTDIR. DLL назовем simple-loco, а вот для указания пути в каталог модулей симулятора, где должна лежать эта библиотека нам впервые пригодится системная переменная окружения RRS_ROOT, обращение к которой из QtCreator выглядит как конструкция $$(RRS_ROOT). Убедимся, что QtCreator видит эту переменную, заглянув в параметры системного окружения. Для этого жмем на левой панели инструментов кнопку «Проекты» и в появившемся окне открываем вкладку «Системная среда»

Отлично! Переменная есть и в данном случае равна пути C:\RRS (на вашем компьютере будет, возможно, по другому.
Далее, параметр $$join(TARGET,,,) сформирует имя каталога, совпадающее с именем DLL в каталоге модулей, таким образом каталог, по которому будет размещена DLL в рассматриваемом случае будет иметь путь C:\RRS\modules\simple-loco, что нам и требуется.
Следующая группа директив LIBS указывает какие библиотеки симулятора нам нужно компоновать с нашей DLL. Много кода, выполняющего рутинные операции вынесены в библиотеки расположенные в каталоге bin/. Без этих библиотек наша DLL будет неработоспособна. Обычно модуль подвижной единицы использует следующие DLL симулятора
- CfgReader.dll — библиотека для работы с конфигурационными файлами XML;
- physics.dll — библиотека, содержащая физические константы, такие как ускорение свободного падения, нормальное атмосферное давление и т.п., а так же ряд стандартных математических функций, облегчающих жизнь разработчика;
- vehicle.dll — библиотека, реализующая модель подвижной единицы и дающая симулятору интерфейс для взаимодействия с модулями ПЕ;
- device.dll — библиотека, обеспечивающая интерфейс к стандартным модулям оборудования, таким как краны машиниста, резервуары, токоприемники и т.п.;
- filesystem.dll — диспетчер файловой системы, позволяющий быстро получить путь к любому каталогу внутри симулятора, а так же дающий функции для кроссплатформенной работы с путями.
Следующие директивы INCLUDEPATH необходимы для указания компилятору путей поиска заголовочных файлов, необходимых для сборки проекта. Нам нужны собственные заголовочные файлы нашей DLL и заголовочные файлы из папки sdk/include. Указываем пути к ним, применяя все ту же переменную RRS_ROOT.
Две последние директивы HEADERS и SOURCES необходимы, для того чтобы указать QtCreator, какие файлы включены в проект. Приведенная конструкция означает, что в состав проекта входят все файлы в каталоге include, имеющие расширение *.h в качестве заголовочных файлов, и все файлы в каталоге src с расширением *.cpp в качестве файлов исходных текстов.
Таким образом сценарий сборки готов и мы можем идти дальше.
Пишем код локомотива
Нам необходимо добавить два файла в наш проект — заголовочный и файл исходного кода. Щелкаем правой кнопкой по имени проекта слева и в появившемся меню выбираем «Добавить новый», и в получившемся диалоге выбираем тип имя заголовочного файла, а так же его расположение обязательно в папке include нашего проекта



Как видим, файл simple-loco.h появился в дереве проекта. Аналогичным образом добавляем файл simple-loco.cpp, только уже в каталог src


Следует обратить внимание на то, что несмотря на глобальные настройки включения файлов в проект, которые мы применяем в сценарии сборки, QtCreator не понимает свою же директиву $$files() и упрямо включает добавленные через мастер файлы в проект повторно

Это может привести к ошибкам в сборке, поэтому придется вручную привести наш файл pro к изначальному виду

Этому багу уже много лет и разработчики Qt не спешат его исправлять. Так что при применении «глобинга» приходится каждый раз править настройки проекта после добавления очередного класса или модуля в проект.
Итак, отредактируем заголовочный файл проекта, создав класс нашего локомотива
#ifndef SIMPLE_LOCO_H #define SIMPLE_LOCO_H #include "vehicle-api.h" //--------------------------------------------------------------------- // //--------------------------------------------------------------------- class SimpleLoco : public Vehicle { public:
/// Конструктор класса SimpleLoco(QObject *parent = Q_NULLPTR);
/// Деструктор класс ~SimpleLoco(); private: }; #endif // SIMPLE_LOCO_H
Вообще говоря, класс может называться совершенно произвольно — симулятор не знает и никогда не узнает имея этого класса. Симулятор умеет работать только с классом Vehicle, от которого наследуется класс нашего локомотива. Класс Vehicle используется динамическим движком игры для вызова методов класса нашего локомотива. Для создания работоспособного модуля ПЕ нам необходимо явно конструктор и деструктор класса, а так же создать функцию getVehicle(), которая видна из DLL симулятору и служит для создания экземпляра нашей подвижной единице в игре. Реализация методов традиционно выполняется в файле *.cpp
#include "simple-loco.h" SimpleLoco::SimpleLoco(QObject *parent) : Vehicle (parent) { } SimpleLoco::~SimpleLoco() { } GET_VEHICLE(SimpleLoco)
При реализации конструктора в обязательном порядке следует вызвать конструктор базового класса Vehicle — он выполняет много важной работы, сокрытой от разработчика локомотива, но принципиальной для движка симулятора. Конструктор и деструктор не будут выполнять пока никакой работы.
Отдельное внимание следует обратить на наличие макроса GET_VEHCILE(), который принимает имя нашего класса в качестве параметра. Этот макрос создает специальную функцию getVehicle(), которая должна присутствовать в DLL модуля ПЕ, чтобы симулятор смог сконструировать экземпляр этой ПЕ. Без нее мы получим сообщение о неудачной загрузке модуля ПЕ.
Ну вот, теперь выберем тип сборки «Выпуск» (внизу левой панели инструментов) кликнем правой кнопкой на имя проекта в дереве проекта и в появившемся меню нажимаем «Пересобрать».

Зеленая полоска в правом нижнем углу говорит нам о том, что DLL успешно собрана. И правда — заглянем в каталог modules/ и убедимся в этом сами


То есть мы успешно создали модуль DLL для нашего локомотива. Пусть он ничего пока не делает, но он обладает рядом характеристик, таких как масса, количество осей,
Конфигурационный файл подвижной единицы
В прошлый раз мы написали простейший модуль локомотива. Теперь нам необходимо сделать так, чтобы игра увидела этот модуль и позволила выбрать этот локомотив, чтобы мы убедились в его работоспособности. Для этого необходимо написать два конфигурационных файла: для подвижной единицы и для поезда.
Итак, в папке vehicle создадим каталог simple-loco, и поместим в него XML-файл simple-loco.xml такого содержания
<?xml version="1.0" encoding="UTF-8"?>
<Config>
<Vehicle>
<EmptyMass>135000</EmptyMass>
<PayloadMass>0</PayloadMass>
<Length>21.7</Length>
<WheelDiameter>135000</WheelDiameter>
<MainResist>default</MainResist>
<NumAxis>6</NumAxis>
<WheelInertia>2.0</WheelInertia>
</Vehicle>
</Config>
Внимание! Файл должен быть сохранен в кодирувке UTF-8! Обязательно убедитесь в этом, чтобы не испытывать проблем в дальнейшем. Именно поэтому для создания этих конфигов настоятельно рекомендуется нормальный текстовый редактор, а не Блокнот…
Это базовый конфигурационный файл, содержащий только ключевые параметры, характерные для каждого объекта класса «подвижная единица». Рассмотрим смысл этих параметров
- EmptyMass — масса неэкипированного локомотива, а для вагона — масса тары, кг;
- PayloadMass — масса экипировочных материалов для локомотива и полная грузоподъемность для вагона, кг;
- Length — длина по осям автосцепок, м;
- WheelDiameter — диаметр колеса по кругу катания, м;
- MainResist — файл с формулой основного сопротивления движению. О нем мы обязательно поговорим подробнее, а пока осттавим стандартный файл default;
- NumAxis — число осей колесных пар;
- WheelInertia — момент инерции колесной пары относительно оси вращения, кг · м2
Эти параметры читаются симулятором автоматически. Однако, кроме них здесь могут быть другие параметры, как стандартные, так и определяемые самим разработчиком для конкретной модели подвижного состава. Об этом мы тоже поговорим очень подробно, но пока нам надо убедится, что наш конфиг расположен в нужном месте в симуляторе

и двигаться дальше.
Конфигурационный файл поезда
Теперь, для того, чтобы наш локомотив стал доступен в списке выбора лаунчера, необходимо сформировать поезд состоящий из одного нашего локомотива. Для этого в каталоге trains/ создаем файл simple-loco.xml такого содержания
<?xml version="1.0" encoding="UTF-8"?> <Config> <Common> <CouplingModule>default-coupling</CouplingModule> <CabineInVehicle>0</CabineInVehicle> <ChargingPressure>0.5</ChargingPressure> <InitMainResPressure>0.9</InitMainResPressure> <NoAir>0</NoAir> <Title>Простейший локомотив</Title> <Description>Учебный пример простого локомотива</Description> </Common> <Vehicle> <Module>simple-loco</Module> <ModuleConfig>simple-loco</ModuleConfig> <Count>1</Count> <PayloadCoeff>1</PayloadCoeff> </Vehicle> </Config>
Файл содержит два основных типа секций. Секция Common содержит общую информацию о поезде
- CouplingModule — имя модуля сцепного прибора. Позволяет задать тип поглощающего аппарата в сцепках по всему поезду. Доступна разработка собственных модулей сцепок;
- CabineInVehicle — номер подвижной единицы, в которой расположена кабина управления;
- ChargingPressure — зарядное давление тормозной магистрали (ТМ), МПа
- InitMainResPressure — начальное давление в главных резервуарах;
- NoAir — флаг «поезд без воздуха»: если поставить его равным 1, то поезд будет полностью без воздуха и значение давления в ГР не имеет значения. Для любителей откачивать поезд перед поездкой для пущего реализма;
- Title — заголовок, отображаемый в списке поездов лаунчера;
- Description — описание поезда, отображаемое в лаунчере.
Секция Common существует в одном экземпляре. А вот секций Vehicle, которая описывает группу стоящих подряд подвижных единиц может быть несколько. Каждая секция Vehicle содержит следующие параметры
- Module — имя DLL-модуля подвижной единицы;
- ModuleConfig — имя конфига подвижной единицы. Здесь прописывается конфиг, помещенный нами в каталог vehicles/;
- Count — число таких ПЕ, стоящих в поезде подряд;
- PayloadCoeff — уровень загрузки ПЕ.
Вы, возможно, спросите- а что, для одной и той же DLL могут быть несколько конфигов? Да, конечно. Допустим DLL описывает целый класс однотипного подвижного состава, например электровоз ВЛ60 существует в пассажирском и грузовом вариантах, отличающихся тормозным оборудованием и редуктором тягового привода. Зачем писать две DLL (хотя можно поступить и так, никто не запретит) на мало отличающиеся электровозы? Можно настроить параметры DLL-модуля, чтобы получилась нужная модификация электровоза. То же касается и вагонов, в которых степень унификации может быть еще выше. К тому же, в конфиге прописывается и путь к 3D-модели подвижного состава, так что одна и та же DLL может иметь разное визуальное представление в игре.
Параметр Count позволяет поставит подряд не одну, а несколько одинаковых подвижных единиц. Удобен для формирования поездов из однотипных вагонов.
Интересен и параметр PayloadCoeff, изменяющийся от 0.0 до 1.0. Этот параметр определяет уровень загрузки подвижной единицы. Масса подвижной единицы рассчитывается в симуляторе по следующей формуле
mass = empty_mass + payload_coeff * payload_mass
то есть масса ПЕ складывается из массы тары, плюс грузоподъемности, помноженной на уровень загрузки. Это позволяет правильно и точно регулировать загрузку каждой ПЕ в поезде. При чем, если вам вдруг вздумается задать параметр PayloadCoeff, скажем, 10 или 100 — у вас ничего не выйдет. Симулятор автоматически «обрежет» значение так, чтобы оно не превышало 1.
Сохраняем файл, убеждаемся, что он лежит по нужному пути

Теперь все готово к тому, чтобы тестировать наш локомотив.
Тест заготовки модуля в симуляторе
Запускаем лаунчер, выбираем любой из установленных маршрутов, и наш локомотив, который как ни в чем не бывало красуется теперь в списке доступных поездов

Выбираем локомотив, нажимаем «Старт» и….

Вуаля! У нас все получилось. Правда наш локомотив пока что пустышка, ничего не умеет, не имеет внешней визуальной модели и кабины.

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

Мы видим, что наш локомотив катится под уклон, так как, хоть он и пустой, но уже взаимодействует с внешней средой в симуляторе.
Что же, нас можно поздравить — только что мы освоили технику написания DLL-модулей подвижного состава для RRS. Остальное ограничивается только уровнем знаний и фантазией разработчика.
Обработка нажатий клавиш
Симулятор любого транспорта обязан давать игроку возможность этим транспортом управлять. Для управления можно использовать различные интерфейсы ввода данных, самым распространенным в большинстве железнодорожных симуляторов является клавиатура и мышь.
Обработка событий пользовательского ввода в RRS устроена достаточно мудрено из-за того что визуализация и прием событий с устройств ввода работает в отдельном от симуляции движения поезда процессе. Не вдаваясь глубоко в механику данного процесса могу сказать только, что получение состояния клавиатуры реализована достаточно прозрачно для разработчика.
Для начала переопределим в нашем классе SimpleLoco метод keysProcess()
#ifndef SIMPLE_LOCO_H
#define SIMPLE_LOCO_H
#include "vehicle-api.h"
//-----------------------------------------------------------------------
//
//-----------------------------------------------------------------------
class SimpleLoco : public Vehicle
{
public:
/// Конструктор класса
SimpleLoco(QObject *parent = Q_NULLPTR);
/// Деструктор класса
~SimpleLoco();
private:
/// Обработка нажатия клавиш
void keyProcess();
};
#endif // SIMPLE_LOCO_H
В файле simple-loco.cpp описываем реализацию этого метода
#include "simple-loco.h" //------------------------------------------------------------------------ // //------------------------------------------------------------------------ SimpleLoco::SimpleLoco(QObject *parent) : Vehicle (parent) { } //------------------------------------------------------------------------ // //------------------------------------------------------------------------ SimpleLoco::~SimpleLoco() { } //------------------------------------------------------------------------ // //------------------------------------------------------------------------ void SimpleLoco::keyProcess() { } GET_VEHICLE(SimpleLoco)
Метод keyProcess() вызывается симулятором периодически. Реализовав в нем процесс обработки клавиш, можно добиться реакции дополнение на прикладываемые управляющие сигналы. Вопрос только в том, как отслеживать состояние клавиш.
Для этой цели базовый класс Vehicle предоставляет ряд методов
- bool getKeyState(int key) — возвращает состояние клавиши (нажата или отпущена) по переданному в качестве параметра коду клавиши;
- bool isShift() — возвращает истину, если нажат хотя бы один Shift
- bool isControl() — возвращает истину, если нажат хотя бы один Control;
- bool isAlt() — возвращает истину, если нажат хотя бы один Alt
Коды клавиш задаются с помощью перечислителей, определенных в файле sdk/include/key-symbols.h. Остается только определится с тем, что мы в первую очередь реализуем в нашем учебном проекте.
Отставив в сторону реализм, сделаем простейшую операцию — по нажатию клавиш A и D сделаем увеличение и уменьшение, соотвественно, уровня заданного тягового усилия. Для этого заведем в классе переменную, которая будет хранить этот уровень
private:
/// Заданный уровень тягового усилия
double ref_traction_level;
/// Обработка нажатия клавиш
void keyProcess();
};
Обязательно (!) инициализируем эту переменную в конструкторе класса
SimpleLoco::SimpleLoco(QObject *parent) : Vehicle (parent)
, ref_traction_level(0.0)
{
}
C++ это язык в котором очень легко, что называется, «выстрелить себе в ногу». Если в коде существует переменная, значение которой изначально не задано, это может привести к неопределенному поведению программы. Поэтому возьмем за правило инициализировать все вводимые переменные, особенно локальные, как в данном случае.
Теперь мы можем работать с этой переменной, напишем в методе keyProcess() следующий код
void SimpleLoco::keyProcess() { if (getKeyState(KEY_A)) { ref_traction_level += 0.01; } if (getKeyState(KEY_D)) { ref_traction_level -= 0.01; } ref_traction_level = cut(ref_traction_level, 0.0, 1.0);
}
Все очень просто — по нажатию клавиши A заданный уровень тяги увеличивается, по нажатию клавиши D — уменьшается.
Отдельно следует упомянуть вызов функции cut(). Эта функция описана в модуле physics.dll и является шаблонной. Ее назначение — ограничивать подаваемое на вход значение заданными пределами. Прототип этой функции выглядит так
T cut(T x, T min, Tmax)
функция возвращает x, если он находится в пределах от min до max, возвращает min если x меньше минимального значения, и max — если x больше максимального. Таким образом, в приведенном коде мы «обрезаем» значение уровня заданной тяги в пределах от 0 до 1. Ведь если мы не сделаем этого, то при долгом удержании клавиши A уровень может дорасти и до 10, и до 100 и до 1000, что нас не устраивает по понятным причинам.
После компиляции и запуска локомотива все это будет происходить, но каким образом мы сможем отследить правильную работу кода? Познакомимся с ещё одним важным инструментом разработчика — отладочной строкой. Эта строка записывается в стандартную переменную класса Vehicle, названную DebugMsg. Допишем в функцию-обработчик клавиш еще немного кода
void SimpleLoco::keyProcess()
{
if (getKeyState(KEY_A))
{
ref_traction_level += 0.01;
}
if (getKeyState(KEY_D))
{
ref_traction_level -= 0.01;
}
ref_traction_level = cut(ref_traction_level, 0.0, 1.0);
DebugMsg = QString("Зад. тяга: %1")
.arg(ref_traction_level, 4, 'f', 2);
}
Как мы можем увидеть эту отладочную информацию? Для этого запускаем симулятор, жмем F1 и видим внизу экрана эту самую строку, где отображается уровень заданной тяги

Нажимая клавиши мы увидим, что переменная таки действительно изменяется, что не может нас не радовать, ведь мы освоили еще одну базовую возможность API RRS.
Ну так и что, скажете вы, заданная тяга растет, а локомотив не едет. Как добиться того, чтобы он поехал — об этом мы поговорим дальше.
Реализация простейшей тяговой характеристики
Активные движущие силы и способ их задания
В RRS, чтобы заставить локомотив двигаться, необходимо подать на его колесные пары крутящий момент. В классе Vehicle существует специальная переменная Q_a, которая носит название вектор обобщенных активных сил. Этот вектор хранит NumAxis + 1 значений, а если вы помните, то NumAxis определяется в конфиге локомотива как число его осей.
Значение Q_a[0] описывает линейную силу, приложенную к кузову подвижной единицы, а значения с Q_a[1] по Q_a[NumAxis] — как раз таки крутящие моменты, приложенный к колесным парам, с номерами от 1 до NumAxis, от тягового привода.

Для чего нужна линейная сила Q_a[0]. Например, у вас появится желание сделать модель поезда с реактивной тягой (а такой поезд реально испытывался на отечественной железной дороге и кабину его вагона с реактивными двигателями можно увидеть в качестве памятника перед главной проходной Тверского вагоностроительного завода). Вот тогда нам потребуется передача активного тягового усилия через кузов. Через этот параметр можно передавать различные тестовые усилия. В обычных условиях он равен нулю и не используется, но кто знает для каких целей он может понадобится разработчику…
Стоит подать на колесные пары крутящий момент отличный от нуля, и наш локомотив поедет. Попробуем это реализовать.
Для начала переопределим в классе SimpleLoco метод step() вызываемый движком на каждом шаге интегрирования модели поезда. В нем происходит основная работа по вычислению параметров, обеспечивающих движение подвижного состава.
private:
/// Заданный уровень тягового усилия
double ref_traction_level;
/// Обработка нажатия клавиш
void keyProcess();
/// Шаг моделирования систем локомотива
void step(double t, double dt);
};
В качестве параметров этот метод принимает от движка игры текущее время симуляции t и текущий шаг интегрирования по времени dt. Эти параметры необходимы нам для описания процессов, происходящих в подвижном составе, так как все процессы в оборудовании разворачиваются во времени, и движок любезно предоставляет нам все параметры отсчета времени, актуальные на данный момент. Реализуем метод step() написав такой код
void SimpleLoco::step(double t, double dt) { double torque = 5000.0; for (size_t i = 1; i < Q_a.size(); ++i) { Q_a[i] = ref_traction_level * torque; } }
Что мы сделали? Мы задали некоторый постоянный момент torque равный 5000 Н · м, и приложили к каждой колесной паре нашего локомотива, с учетом текущего заданного уровня тяги, которым мы управляем с клавиатуры. Компилируем библиотеку, запускаем симулятор и теперь мы увидим, как наш локомотив, повинуясь командам с клавиатуры берет разбег и начинает движение!

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

Идеальная тяговая характеристика имеет форму гиперболы, и на данном графике вторая половина кривой имеет гиперболическую форму. Почему? Потому, что любой тяговый привод имеет ограниченную мощность — никакой двигатель не в состоянии выдать мощность более той, на которую он рассчитан. Скажем, тяговые двигатели локомотива суммарно обеспечивают мощность равную P_nom. А мощность, как известно, определяется как произведение силы тяги на скорость
P_nom = F · v
Тогда максимальная сила тяги, которую может реализовать локомотив при данной мощности может быть вычислена так
F = P_nom / v
то есть сила тяги обратно пропорциональна скорости. Поэтому, для наиболее полного использования мощности привода все локомотивы имеют гиперболическую или близкую к ней форму тяговой характеристики.
Однако, кроме ограничения по мощности есть еще ограничения по сцеплению колес с рельсами и по электрическим параметрам тяговых машин, что не дает возможность реализовать большую тягу, даже при достаточной мощности привода. Это ограничение (очень условно) можно выразить в виде наклонной прямой, из которой состоит первая половина графика.
Теперь, зная все это, скорректируем код нашего локомотива, чтобы реализовать правильную форму его тяговой характеристики. Введем в наш класс ряд параметров
private: /// Заданный уровень тягового усилия double ref_traction_level; /// Максимальная реализуемая сила тяги, кН double F_max; /// Номинальная сила тяги (соотвествующая продолжительной мощности), кН double F_min; /// Номинальная скорость (соотвествующая часовой мощности), км/ч double V_nom;
Инициализируем эти переменные некими значениями, соответствующими приближенно тяговой характеристики, типичной для реальных локомотивов
SimpleLoco::SimpleLoco(QObject *parent) : Vehicle (parent) , ref_traction_level(0.0) , F_max(450.0) , F_min(350.0) , V_nom(80.0) { }
Теперь реализуем в классе приватный метод, вычисляющий силу тяги в зависимости от скорости
double SimpleLoco::trac_char(double v) { // Переводим номинальную скорость в м/с double v_nom = V_nom / Physics::kmh; double traction_force = 0; // Вычисляем силу тяги, в зависимости от величины // текущей скорости по отношению к номинальной. // Учитываем, что параметры заданной характеристики мы брали в кН, // а движку требуются величины в системе СИ, поэтому домножаем на 1000 // переводя килоньютоны в ньютоны if (abs(v) < v_nom) { traction_force = (F_max + (F_nom - F_max) * abs(v) / v_nom) * 1000.0; } else { traction_force = F_nom * v_nom * 1000.0 / v; } return traction_force; }
Теперь у нас есть функция, позволяющая рассчитать силу тяги, которую может развить локомотив при данной скорости. Но это еще не всё — в методе step() пишем следующий код
void SimpleLoco::step(double t, double dt) { // Вычисляем силу тяги, которую реализует локомотив в данный момент double trac_force = ref_traction_level * trac_char(velocity); // Вычисляем момент, приходящийся на одну колесную пару double torque = trac_force * wheel_diameter / 2.0 / num_axis; // Задаем момент каждой колесной паре for (size_t i = 1; i < Q_a.size(); ++i) { Q_a[i] = torque; } }
Поясню этот код. Во-первых, локомотив выдает тягу не максимальной величины, а в зависимости от того уровня, который мы задали клавишами, поэтому величину, получаемую из тяговой характеристики мы умножаем на этот уровень.
Во-вторых, на колесные пары мы подаем момент, реализуемый приводом этой колесной пары, а тяговая характеристика дает полную тягу локомотива. Поэтому эту полную тягу следует пересчитать в момент приходящийся на одну КП. Для этого всю тягу мы делим на число осей, а потом умножаем на радиус колеса, так как момент приложенный к КП и сила тяги, приходящаяся на эту ось находятся в соотношении
Mкп = Fкп · rколеса
Радиус колеса мы можем вычислить как половина диаметра, используя стандартную переменную wheel_diameter, в которой хранится диаметр колеса, заданный нами через конфиг. Аналогично, число осей локомотива хранится в переменной num_axis.
Теперь, если мы запустим симулятор и дадим тягу, то увидим, как наш локомотив разгоняется, причем довольно шустро. Выведем на экран, через отладочную строку несколько дополнительных параметров, для чего перенесем инициализацию отладочной строки в метод step()
void SimpleLoco::step(double t, double dt) { // Вычисляем силу тяги, которую реализует локомотив в данный момент double trac_force = ref_traction_level * trac_char(velocity); // Вычисляем момент, приходящийся на одну колесную пару double torque = trac_force * wheel_diameter / 2.0 / num_axis; // Задаем момент каждой колесной паре for (size_t i = 1; i < Q_a.size(); ++i) { Q_a[i] = torque; } DebugMsg = QString("Время: %1 с Зад. тяга: %2 Скорость: %3 км/ч Сила тяги: %4 кН") .arg(t, 10, 'f', 1) .arg(ref_traction_level, 4, 'f', 2) .arg(velocity * Physics::kmh, 6, 'f', 2) .arg(trac_force / 1000.0, 6, 'f', 1); }
Теперь внизу, в отладочной строке, можно увидеть, как меняется сила тяги по мере разгона, в точном соответствии с заданной нами характеристикой.
Пользовательские параметры конфигурационного файла
Как вы помните, мы написали много кода, совершенствуя наш тестовый локомотив. При этом мы ввели множество параметров, описывающих его характеристики. Значения этих параметров жестко задаются в коде, и не подлежат изменению без пересборки DLL-модуля. И это очень плохо.
Хорошим тоном для любого разработчика является дать пользователю возможность изменять параметры программы без доступа к её исходному коду.
Такая возможность есть в RRS, и заключается она в том, что в конфиге подвижной единицы можно, наряду со стандартными, описать и собственные параметры. Добавим в конфиг ПЕ vehicles/simple-loco/simple-loco.xml, в секцию Vehicle следующие параметры
<F_max>450.0</F_max> <F_nom>350.0</F_nom> <V_nom>80.0</V_nom>
Как вы уже догадались, это характерные токи тяговой характеристики нашего локомотива. Чтобы прочитать эти параметры, необходимо переопределить в кассе SimpleLoco метод loadConfig()
/// Обработка нажатия клавиш void keyProcess(); /// Шаг моделирования систем локомотива void step(double t, double dt); /// Расчет тяговой характеристики double trac_char(double v); /// Загрузка пользовательских параметров из конфига void loadConfig(QString cfg_path); };
Пишем реализацию данного метода в simple-loco.cpp
void SimpleLoco::loadConfig(QString cfg_path)
{
// Создаем экземпляр "читателя" XML-конфигов
CfgReader cfg;
// Открываем конфигурационный файл по переданному движком пути
if (cfg.load(cfg_path))
{
// Задаем имя секции конфига, из которой будем читать параметры
QString sectionName = "Vehicle";
// Читаем интересующие нас параметы в соотвествующие переменные
cfg.getDouble(sectionName, "F_max", F_max);
cfg.getDouble(sectionName, "F_nom", F_nom);
cfg.getDouble(sectionName, "V_nom", V_nom);
}
}
Вызов этого методы выполняется движком игры, и через параметр cfg_path в него передается абсолютный путь к конфигу ПЕ. Остается только создать специальный объект класса CfgReader, который выполняет отказоустойчивое удобное чтение параметров из XML-конфига. Параметры содержаться в секции Vehicle конфига. Класс CfgReader имеет несколько методов, для получения значений из конфигов
- bool getDouble(QString secName, QString paramName, double ¶m) — получение вещественного параметра paramName из секции secName. Параметр помещается в переменную param, передаваемую по ссылке;
- bool getInt(QString secName, QString paramName, int ¶m) — получение целого параметра;
- bool getString(QString secName, QString paramName, QString ¶m) — получение строкового параметра.
То есть код
cfg.getDouble(sectionName, "F_max", F_max);
читает параметр с именем «F_max», секции «Vehicle» конфига, и помещает значение в переменную F_max. Остальные вызовы действуют аналогичным образом.
То есть теперь, не пересобирая модуль ПЕ, мы сможем менять его параметры через конфиг, в частности настраивать тяговую характеристику в данном случае.
Можете провести теперь несколько экспериментов, меняя параметры в конфиге и наблюдая, как меняются тяговые свойства локомотива.
Основы пошаговой отладки
Программы и библиотеки не всегда работают правильно. И, чаще всего, при разработке, ваш код будет работать неправильно. В практике программирование поиск причин неработоспособности программы или библиотеки называют отладкой.
Ошибки в программах делятся на три типа
- синтаксические
- логические
- ошибки времени выполнения
Синтаксические ошибки связаны с с нарушением синтаксиса языка, чаще всего вылезают при сборке проекта и характерны в основном для начинающих разработчиков. QtCreator оснащен всеми средствами для поиска и нахождения таких ошибок. С опытом количество синтаксических ошибок, допускаемых разработчиком очень быстро падает практически до нуля. Поэтому мы и не будем рассматривать их в данной главе.
Логические ошибки не приводят к ошибкам компиляции, а связаны они с неправильной работой реализуемого алгоритма. Такие ошибки отлавливаются как просмотром кода, с целью неверно реализованных мест, так и пошаговым прогоном проблемного кода в отладчике.
Ошибки времени выполнения — самый сложный вид ошибок. Они могут совершенно не проявлять себя при определенном сочетании параметров программы, и вылезти в самый неподходящий момент, при произвольном изменении параметров работы программы, после перезагрузки системы и т.п. Обычно такие ошибки связаны с наличием в коде не инициализированных переменных, отсутствием проверок на выход за пределы выделенной памяти, отсутствием проверок деления на ноль — список можно продолжать долго.
Ловим логические ошибки: прогоняем алгоритм по шагам
Для отладки DLL, в отличие от программы, требуется дополнительно настроить её проект, а именно указать, какое приложение использует эту DLL и задать параметры его запуска. В нашум случаем модуль ПЕ используется процессом simulator.exe (код ПЕ работает в контексте этого процесса).
Во-первых, нам необходимо собрать отладочную версию нашей DLL, поэтому выбираем внизу панели инструментов конфигурацию сборки «Отладка». Собираем DLL, а затем идем (через ту же левую панель) по пути «Проекты» -> «Запуск». Заполняем поля «Программа», «Параметры командной строки» и «Рабочий каталог», так, как показано на скриншоте ниже

В поле «Программа» указывается путь к файлу simulator.exe, в поле командной строки указываются ключи, с которыми запускается simulator.exe, а именно
- —route — путь к каталогу с маршрутом. В вашем случае может быть указан другой маршрут, в зависимости от того, какой установлен у вас;
- —train-config — имя файла конфига поезда. Нам называется simple-loco.
Лаунчер запускает процесс simulator.exe именно так: определяет путь к выбранному в нем маршруту, определяет имя конфига поезда и подставляет эти значения в ключи процесса simulator.exe.
Теперь предположим, мы хотим пройти по алгоритму, загружающему из конфига пользовательские параметры, то есть по методу loadConfig(). Переходим обратно в редактор кода и ставим точку останова (F9, либо клик по полю с номерами строк) на нужной строке

Красный кружок на 97-й строке означает, что в этом месте программа будет остановлена и управление будет передано в отладчик.
Теперь жмем кнопку «Запуск в отладке» (слева внизу зеленый треугольничек с изображением жучка), дожидаемся запуска процесса (стартует довольно не быстро), и бац — мы остановились в нужном нам месте

Подробнее разберем те возможности, которые предоставляет нам режим отладки в QtCreator

Та строка кода, на которой стоит отладчик обозначается маркером в виде желтой стрелки. Права от окна редактора кода находятся две области, позволяющие просматривать значение видимых в данный момент локальных переменных, а так же окно для просмотра вычисляемых выражений, куда можно добавить значение любой интересующей нас в данный момент переменной.
Снизу, под окном редактора располагается окно так называемого стека вызовов, в котором, снизу вверх, показана последовательность с которой в процессе выполнения, до момента остановки, происходил вызов функций и методов программы и отлаживаемой DLL. Здесь, самой первой строкой (на вершине стека) мы видим метод SimpleLoco::loadConfig() на котором произошел останов. Однако ниже можно увидеть методы, вызов которых предшествовал данному вызову. Абракадабра вида
vehicle!_ZN7Vehicle17loadConfigurationE7QString
несет в себе массу полезной информации. Я специально выделил ее полужирным шрифтом
- vehicle — имя библиотеки, из которой произошел вызов, в данном случае это vehicle.dll, входящая в состав симулятора;
- Vehicle — имя класса, метод которого был вызван: в данном случае это класс Vehicle, базовый класс для SimpleLoco;
- loadConfiguration — имя самого метода класса Vehicle
- QString — тип параметра, переданного в вызванный метод.
Видите, сколько информации мы получили о симуляторе, даже не глядя в его исходный текст? В частности мы знаем теперь, что метод loadConfig() вызывается из метода loadConfiguration() класса Vehicle. А глядя еще ниже по стеку вызовов, мы видим что этот метод, в свою очередь вызывается из метода Train::loadTrain(), из класса Train, расположенного в библиотеке train.dll.
Таким образом, если в отлаживаемый нами метод попали бы неверные данные, мы могли бы с уверенностью отследить путь, по которому эти данные были сформированы и переданы в наш локомотив. Стек вызовов крайне полезная вещь, в чем мы еще многократно убедимся.
В окне просмотра локальных переменных мы видим значение переменной cfg_path, содержащую путь к конфигу нашего локомотива. И так и есть, этот путь C:\RRS\cfg\vehicles\simple-loco\simple-loco.xml.
Нажимая F10 мы можем продолжить выполнение нашего кода построчно, попутно просматривая как изменяются значения переменных, обрабатываемых в методе

В частности, на скиншоте выше мы видим, что после выполнения чтения параметра F_max, в соответствующей ему переменной оказалось то значение, которое перед запуском мы поместили в конфиг. Таким образом мы убеждаемся, что параметры читаются из конфига корректно. Измененные значения переменных подсвечиваются в окнах просмотра красным шрифтом.
То есть в наших руках есть инструмент, позволяющий пройтись по коду DLL и выяснить, почему наш локомотив работает неправильно.
Ловим ошибки времени выполнения
Очень часто бывает так, что библиотека работала себе работала, а потом, после внесения в неё ряда изменений перестала работать, да и ещё и обрушивает программу, вызывающую её. Как с этим бороться, как найти причину?
Давайте внесем критическую ошибку в код нашего локомотива, прямо в конструкторе его класса напишем такой код
SimpleLoco::SimpleLoco(QObject *parent) : Vehicle (parent)
, ref_traction_level(0.0)
, F_max(450.0)
, F_nom(350.0)
, V_nom(80.0)
{
double *a;
a[0] = 10.0;
}
Тот кто хорошо знаком с C++ поймет подвох — мы объявили указатель на массив вещественных чисел, и не выделив память для этого массива пытаемся записать туда число. Соберем DLL в режиме «Выпуск» и запустим симулятор. С вероятность 99% у нас ничего не будет работать, программа вылетит с критической ошибкой, или просто не будет работать. И весь трагизм ситуации заключается в том, что возможно и будет нормально работать — например на разных машинах, в одном случае сим падал у меня сразу же, а в другом нормально работал. Во втором случае этот массив видимо указывает на какую-то доступную программе область памяти, и исключение не происходит. Если в вашем случае все работает, немного измените этот код
double *a = 0;
Ага! Вот мы и приплыли!

Зелена кнопка «Старт» в окне лаунчера означает что упал процесс simulator.exe. Вьювер что-то показывает, но стоим мы явно не на станции Ростов Главный, а в начале маршрута, так как симулятор не передал во вьювер данные о положении поезда. Что же случилось, как выяснить это в случае, когда мы не знаем об ошибке (в данном случае-то понятно)
Запускаем режим отладки DLL, как мы делали в предыдущем параграфе, но не ставим никаких точек останова. Бац!

Segmetation fault — ошибка означающая попытку доступа к недоступной области памяти. Что же, жмем переходим в окно отладчика

Отладчик показывает нам желтой стрелкой ту операцию, которая вызвала падение программы. Ну так и есть — обращение к не выделенной памяти!
Причина падения не всегда может быть такой очевидной — иногда приходится просмотреть стек вызовов, возможна причина сбоя лежит где-то выше того места, где произошел останов. В любом случае, режим запуска отладчике позволяет выяснить причину неработоспособности кода в разы быстрее, чем гадание на кофейной гуще.
Запуск вьювера при отладке
При всех вышеперечисленных возможностях, существует одна проблема — при запуске отладки у нас запускается только процесс simulator.exe. Но в штатном режиме работы существует еще процесс viewer.exe, который, например, обрабатывает ввод команд с клавиатуры. Как быть? Запустить вьювер вручную!
Для начала запускаем DLL в режиме отладки без задания точек останова. При этом у нас стартует процесс simulator.exe.
Переведем вьювер в оконный режим, для чего в файле cfg/stttings.xml выставляем параметр FullScreen в 0

Жмем Win + R, и в появившемся окне вводим

Появляется окно командной строки Windows

Переходим в каталог bin симулятора, введя команду
cd C:\RRS\bin

Теперь запускаем viewer, передав ему в качестве параметров путь к маршруту и имя конфигурации поезда
viewer.exe --route ..\routes\rostov-goryachiy_kluch --train simple-loco
Вьювер должен успешно запуститься и получить положение поезда на участке

Теперь весь симулятор работает в отладочном режиме. Например мы без проблем можем привести локомотив в движение, а затем, перейдя в QtCreator, постaвить точку останова внутри метода step()

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

Переходим в окно вьювера, жмем A — вуаля! Симуляция замерла на обработке этого нажатия

Таким образом, мы имеем возможность управляемого выполнения кода, который пишем. Это дает в наши руки мощнейший инструмент отладки самых сложных алгоритмов. Можно сказать, что теперь мы вооружены до зубов, и скоро сможем приступить к созданию настоящих железнодорожных экипажей.
Создание и настройка визуальной модели
Непорядок в том, что до сих пор наш локомотив — дух бестелесный. Он вроде даже ездит, а внешнего облика у него нет, не говоря уже о кабине. Пришло время дать ему хотя бы внешнюю модель.
Тема разработки 3D-моделей для железнодорожных симуляторов — отдельная большая тема для разговора, и, я уверен, гораздо лучше её раскроет мой коллега Роман Бирюков (Ромыч РЖДУЗ), когда присоединится к написанию документации.
Я буду говорить только о том, каким требованиям должна удовлетворять модель с точки зрения программиста, работающего над DLL-модулем.
Для создания моделей в RRS могут быть использованы в равной степени эффективно как Autodesk 3dmax, так и уверенно набирающий обороты Blender — редактор с открытым исходным кодом. Для меня удобнее Blender, поэтому дальнейший материал я буду излагать с использованием этого редактора. В нем я сделал вот такую простейшую «тушку» нашего локомотива, без претензий на высокую художественность

Модель соответствует габариту подвижного состава колеи 1520 мм, и содержит все необходимые элементы, для тех примеров, которые я собираюсь привести.
Итак, вне зависимости от того редактора, в котором делалась модель, она должна удовлетворять следующим требованиям
- Продольная ось локомотива направлена вдоль оси Y, рабочая кабина находится на положительной половине этой оси;
- Нижние точки кругов катания колесных пар находятся на нулевом уровне;
- Ось Z располагается на равном расстоянии от осей автосцепок локомотива (секции локомотива, вагона и т.п.);
- Все детали, для которых предполагается анимация должны быть выполнены как отдельные объекты и иметь осмысленные имена на латинице, без пробелов.
Вообще, осмысленные имена стоит давать всем объектам в сцене — это золотое правило аккуратной работы. Что касается данной модели, то вот список объектов, из которых она состоит

Body — это кузов локомотива; Bogie-1, Bogie-2 — передняя и задняя тележки, а Wheel-1,…,Wheel-6 — колесные пары.
Каждый элемент, выделенный в отдельный объект может быть в дальнейшем анимирован.
Если модель удовлетворяет этим требованиям, то можно экспортировать её из 3D-редактора. Для Blender существует специальный плагин, который можно скачать тут. Плагин устанавливается стандартным для Blender способом.
Для экспорта модели переходим объектный режим, выделяем все элементы модели, и в меню File -> Export выбираем QSG Model (.osgt).
Для того, чтобы модель была найдена симулятором, в папке data/models создадим каталог с именем, например, simple-loco

и уже в этот каталог помещаем сконвертированную модель и папку с её текстурами. В нашем случает текстуры отсутствуют, поэтому в каталоге будет лежать только модель в формате OSGT

Что дальше? А дальше нам остается прописать в конфиг vehicles/simple-loco/simple-loco.xml еще один параметр
<ExtModelName>simple-loco/simple-loco.osgt</ExtModelName>
то есть указываем путь к модели относительно каталога data/models/. Запускаем симулятор

Модель успешно загрузилась, но… колеса опущены ниже уровня рельс! Почему так происходит.
Это происходит из-за несовпадения начала координат, от которого отстраивается модель в 3D-редакторе с нулевым уровнем в маршрутах, которые, как уже говорилось ранее, без изменения взяты из ZDSimulator.

Плоскость, относительно которой выполняется отсчет высот в ZDS лежит ниже уровня головок рельс, поэтому модель необходимо немного приподнять. Это можно сделать из конфига ПЕ, добавив туда параметр
<ModelShift>0.0 0.0 0.311</ModelShift>
Данный параметр задает вектор, на который необходимо сместить модель относительно начала координат в локальной системе, связанной с траекторией перемещения центра модели. Ось Y этой системы координат всегда направлена по касательной к оси пути, ось Z — вверх, а ось X — вправо. Параметр ModelShift содержит три числовых компонента, разделенных пробелами: первое число указывает смещение вдоль оси X, второе — вдоль оси Y, третье — вдоль оси Z.
Запускаем симулятор снова

Высота h = 0.311 метра подобрана экспериментально исходя из анализа геометрии моделей элементов пути, содержащихся в маршрутах.
Теперь все в порядке, но при попытке поехать мы видим другой непорядок — колесные пары локомотива не вращаются.
О том, как настраиваются анимации моделей в RRS мы поговорим в следующей главе.
Базовые принципы анимации элементов модели
Как уже говорилось выше, любой элемент модели, выполненный в 3D-редакторе как отдельный объект (меш) может быть анимирован в симуляторе.
Такую возможность дает сама структура файла OSGT — он представляет собой не просто геометрическую сетку с текстурными координатами, а целую сцену, в которой хранятся геометрические объекты со своей UV-разверткой, материалы, специальные эффекты на основе систем частиц. Объекты в файле OSGT позиционируются относительно центра сцены через специальные узлы трансформации — и именно воздействие на эти узлы и дает движку RRS возможность выполнять их произвольное перемещение.
RRS, на данный момент поддерживает три вида анимации:
- анимация вращения (AnalogRotation) — обеспечивает вращение элемента относительно заданной оси на заданный угол;
- анимация перемещения (AnalogTranslation) — обеспечивает поступательное перемещение элемента вдоль заданной оси;
- сложная геометрическая анимация (ModelAnimation) — обеспечивает сложную процедурную анимацию группы элементов, обычно кинематически связанных между собой, предусматривает предварительную подготовку такой анимации в 3D-редакторе.
Анимации типа ModelAnimation требуют отдельного разговора, а вот первые два типа рассмотрим сейчас.
Начнем с того, что анимации реализуются на графической стороне игры. Однако многие из них, в частности вращение колес привязаны к физике движения локомотива. А мы помним, что физика и графика у нас два разных процесса. Соответственно должны быть параметры, через которые происходит передача сигналов управления анимацией в графическую часть игры. И такие параметры есть — класс Vehicle, а значит и его потомок SimpleLoco содержит массив из 200 элементов вещественного типа — analogSignal.
Предположим, мы хотим анимировать вращение колесной пар нашего локомотива. Создадим в каталоге data/animation папку с именем конфига нашего локомотива simple-loco.

Мы помним, что дали колесным парам нашего локомотива осмысленные имена: Wheel-1, Wheel-2 и так далее. Вот и в каталоге data/animations/simple-loco создаем файл с именем Wheel-1.xml
Внимание! Имя файла должно совпадать с именем анимируемого элемента модели. В файле пишем следующее содержимое
<?xml version="1.0" encoding="UTF-8"?> <Config> <AnalogRotation> <SignalID>194</SignalID> <Duration>10.0</Duration> <Infinity>1</Infinity> <Axis>-1.0 0.0 0.0</Axis> </AnalogRotation> <KeyPoint> <Param>0.0</Param> <Value>0.0</Value> </KeyPoint> <KeyPoint> <Param>1.0</Param> <Value>360.0</Value> </KeyPoint> </Config>
Секция AnalogRotation содержит общие параметры анимации. Да и само имя, как вы наверное догадались, задает тип анимации — вращение вокруг заданной оси. Разберем смысл каждого параметра этой секции
- SignalID — идентификатор сигнала, то есть номер сигнала в массиве analogSignal в пределах от 0 до 199. Сигнал с указанным номером служит для управления этой анимацией;
- Duration — скорость перемещения (вращение в данном случае) между двумя фиксированными положениями;
- Infinity — если данный параметр равен 0, то движение будет происходить только в пределах, заданных диапазоном ключевых точек. При значении 1 анимация будет продолжаться за пределы заданного диапазона.
- Axis — вектор, задающий направление оси вращения в системе координат модели.
Секция KeyPoint приводит в соответствие значение параметра, читаемого из сигнала с номером SignalID, с углом поворота вокруг оси, задаваемого в градусах. Таких секций должно быть как минимум две: одна задает начальную точку, другая — конечную точку перемещения. Параметры, определяемые этой секцией
- Param — значение сигнала SignalID
- Value — значение угла поворота, град.
В нашем случае мы определяем две ключевые точки: первая говорит о том, что значению сигнала 0 соответствует угол поворота равный 0 градусов; вторая говорит что значению параметра 1.0 соответствует угол поворота 360 градусов. То есть данные ключевые точки описывают полный оборот колеса. Если сигнал принимает значение лежащее между 0 и 1, то угол поворота вычисляется методом линейной интерполяции. Для наших колесных пар как раз и нужно такое бесконечное вращение, так их вращение не ограничивается одним оборотом. А вот, например для анимации стрелки прибора в кабине — наоборот, лучше задать ограниченную набором ключевых точек траекторию поворота.
А если сигнал превысит 1.0? Тогда, при значении Infinity равном 0 анимация прекратится — упрется в конечную точку. Если же Infinity равно 1, то анимация продолжится за счет линейной экстраполяции.
Допустим в начале сигнал имел значение 0, а потом скачком стал равен 0.5, как поведет себя вращение? Оно будет происходить равномерно, со скоростью, пропорциональной параметру Duration вплоть до достижения угла, соответствующего параметру 0.5. Чем выше Duration, тем меньше инертность анимации к изменению параметра сигнала. Однако увлекаться не стоит — для чувствительных анимаций достаточно Duration в пределах от 10 до 50. Таким образом анимация является как бы «следящей», постоянно стремясь держать модель в положении, соответствующем значению сигнала.
Ключевых точек может быть сколь угодно много, что дает возможность анимировать не только линейные повороты, но и, например стрелки приборов с нелинейной шкалой.
Движок игры сканирует папку с анимациями данной единицы подвижного состава, находит все конфиги и ищет в модели объекты с именем, равным имени файла конфига. Затем он приводит в соотвествие этой модели номер сигнала, считанный из конфига и начинает управлять перемещением, в соотвествии со значением сигнала с указанным номером. То есть наша задача подать этот сигнал из нашего DLL-модуля, например это можно сделать в методе step()
void SimpleLoco::step(double t, double dt) { // Вычисляем силу тяги, которую реализует локомотив в данный момент double trac_force = ref_traction_level * trac_char(velocity); // Вычисляем момент, приходящийся на одну колесную пару double torque = trac_force * wheel_diameter / 2.0 / num_axis; // Задаем момент каждой колесной паре for (size_t i = 1; i < Q_a.size(); ++i) { Q_a[i] = torque; }
// Передаем параметр поворота колесной пареanalogSignal[194] = static_cast<float>(wheel_rotation_angle[0] / 2.0 / Physics::PI); DebugMsg = QString("Время: %1 с Зад. тяга: %2 Скорость: %3 км/ч Сила тяги: %4 кН") .arg(t, 10, 'f', 1) .arg(ref_traction_level, 4, 'f', 2) .arg(velocity * Physics::kmh, 6, 'f', 2) .arg(trac_force / 1000.0, 6, 'f', 1); }
Углы поворота колесных пар в RRS считаются движком игры в соответствии с механикой движения ПЕ. Они хранятся в массиве wheel_roattion_angle, который в случае с нашим 6-осным SimpleLoco содержит шесть элементов, нумеруемых от 0 до 5. Углы выражены в радианах. Поскольку в analogSignal передается безразмерный параметр, то есть в случае колес — число совершенных оборотов, то мы делим этот угол на 2Пи, вычисляя это самое число оборотов.
Что же, соберем DLL и проверим что у нас получилось
Ну отлично, одна колесная пара закрутилась! Чтобы это произошло и с остальными нужно создать еще пять конфигов с именами Wheel-2.xml,…,Wheel-5.xml, прописав в них сигналы от 195 до 199 и дописать код в DLL
analogSignal[194] = static_cast<float>(wheel_rotation_angle[0] / 2.0 / Physics::PI);
analogSignal[195] = static_cast<float>(wheel_rotation_angle[1] / 2.0 / Physics::PI);
analogSignal[196] = static_cast<float>(wheel_rotation_angle[2] / 2.0 / Physics::PI);
analogSignal[197] = static_cast<float>(wheel_rotation_angle[3] / 2.0 / Physics::PI);
analogSignal[198] = static_cast<float>(wheel_rotation_angle[4] / 2.0 / Physics::PI);
analogSignal[199] = static_cast<float>(wheel_rotation_angle[5] / 2.0 / Physics::PI);
Теперь будут двигаться все КП
Рассмотренный механизм позволяет не только создать анимированный элемент, но и сделать анимацию физически корректной. Это открывает очень широкие возможности для создания качественных и интерсных моделей подвижного состава.
Промежуточные итоги
Весь вышеприведенный материал образует некий базовый курс, объясняющий главную идею проекта RRS, а также, на простых примерах иллюстрирующий технологию разработки и внедрения подвижного состава в симулятор. Какие выводы можно сделать по этому курсу «молодого бойца»? Как говорил один мой учитель: «В мире нет ничего, состоящего из одних достоинств». Поэтому, возможности симулятора RRS имеют две стороны.
RRS содержит в себе достаточно гибкую систему разработки дополнений, дающую разработчику практически неограниченные возможности в части реализации физики движения подвижного состава. В месте с тем, порог вхождения в эту технологию достаточно высок — требуется владеть навыками программирования на не самом простом (а скорее даже самом сложном из существующих) языке программирования C++. Для реализации качественных дополнений требуется вникать в нюансы работы схем подвижного состава, иметь базовые знания в области математики, механики и электротехники, ведь. Хоть мы и заставили наш пробный локомотив двигаться, но сделали это безотносительно погружения в особенности работы его схемы. Та тяговая характеристика, что была приведена в одной из глав, является результатом работы массы взаимосвязанного оборудования — а мы не учли всего этого.
Но, читатель, понимающий суть вопроса, наверняка догадывается, что в симуляторе, дающем возможность внедрять в логику и физику работы ПС собственные алгоритмы, можно реализовать сколь угодно сложную (в пределах доступной вычислительной мощности) логику работы оборудования. Чтобы было понятно, как реализуется настоящий локомотив, можно взглянуть в исходный код электровоза ВЛ60пк, доступный в пакете исходного кода RRS.
Возможно, кого-то испугает и оттолкнет такой подход. Что же, в мире железнодорожных симуляторов существует масса других решений, имеющих более низкий порог вхождения. Ведь каждый из нас выбирает то, что ему по душе, верно?
Но если вас ничего не пугает, и вы задаете резонный вопрос — а почему документация содержит так мало информации, то я постараюсь на него развернуто ответить, прямо сейчас.
В «курса молодого бойца» мы не рассмотрели и %1 возможностей, предоставляемых API симулятора. Без внимания остался обзор комплекта стандартного оборудования и класс Device, позволяющий это оборудование использовать и создавать. Не рассмотрели мы и общие принципы устройства и работы автоматических тормозов системы Матросова, применяемых на подвижном составе колеи 1520 мм. Не рассказали о нюансах работы силовых схем локомотивов и МВПС различного типа.
Все эти вопросы мы обязательно осветим в дальнейших главах этого курса. И таких глав будет очень много, более того, процесс создания документации никогда не будет окончен, так как RRS — проект развивающийся и ему предстоит еще долгая история.
Создание модели настоящего локомотива
Я долго думал над тем, каким образом писать «продвинутую» документацию разработчика. Были разные мысли по этому поводу, пока в конце-концов я не остановился на следующей идее.
Было бы неплохо создать для симулятора настоящий локомотив, одновременно рассказывая об этом на сайте. Таким образом можно убить сразу двух зайцев — создать новое дополнение и написать хорошую документацию.
Мой выбор пал на вот эту машину

Знаменитый, можно сказать легендарный, и до сих пор актуальный на сети железных дорог России и стран ближнего зарубежья, пассажирский магистральный тепловоз ТЭП70. Тем более, что симпатия к этой машине у меня с детства. Строительством ТЭП70 для RRS мы (без шуток) займемся на страницах документации.
Я не буду углубляться в то, как создать проект локомотива, как компилировать и отлаживать динамическую библиотеку — обо всем этом уже подробно рассказывалось в первой главе документации. Вместо этого мы сосредоточимся на следующем:
- На физических принципах работы локомотива и его систем.
- На способах реализации моделей оборудования в симуляторе.
- На технологии анимации органов управления и измерительных приборов.
Класс Device
Как было описано в первой части документации, основным классом, через который происходит взаимодействие симулятора с моделью подвижной единицы является класс Vehicle, от которого наследуется класс конкретной модели ПЕ. Разрабатывая тепловоз ТЭП70 мы создадим соответствующий класс TEP70, унаследовав его от Vehicle.
Однако, любой подвижной состав, будь то локомотив или вагон, представляет собой совокупность разного рода оборудования: механического, электрического, пневматического. Работа этого оборудования может описываться достаточно сложными математическими соотношениями. Кроме того, различные блоки оборудования взаимодействуют между собой.
Одним из основных принципов RRS является предоставление разработчику полного контроля над разработкой программной части дополнения. А это, в том числе означает, что код, реализующий функции DLL подвижной единицы, можно писать произвольным образом, не придерживаясь каких либо правил. Например, можно реализовать локомотив ТЭП70 в методах класса TEP70, не прибегая к созданию других классов. Однако, такой подход приведет к разбуханию и запутыванию кода, усложнению его отладки и сопровождения.
Очевидно, что выгоднее реализовать каждых элемент оборудования локомотива в виде отдельного класса, реализовав внутри него функции устройства и обеспечить возможность получения состояния и задания параметров, для взаимодействия с другими устройствами. Для этой цели в динамическом движке симулятора предусмотрел класс Device.
Этот класс позволяет реализовать любой динамический объект, описываемый дифференциальными и алгебраическими уравнениями. Он представляет собой абстрактное устройство, состояние которого описывается вектором состояния y.
Интерфейс класса Device можно посмотреть в комплекте SDK, поставляемого с симулятором в файле device.h. Опишем основные методы и свойства этого класса
- virtual void step(double t, double dt) — выполняет один шаг интегрирования дифференциальных уравнений и расчет алгебраических соотношений. По-умолчание решение дифференциальных уравнений выполняется методом Рунге-Кутты 4-го порядка. Метод может быть переопределен в наследнике, например для задания собственного алгоритма решения дифференциальных уравнений.
- void setY(size_t i, double value) — задать значение переменной состояния. Несмотря на то, что внутри классов-наследников возможен прямой доступ к вектору y, рекомендуется пользоваться этим методом, так как он является «дуракозащищенным», исключая выход за границы массива.
- double getY(size_t i) — возвращает значение переменной состояния. Аналогично, рекомендуется для использования, так как исключает выход за границы вектора y.
- virtual void read_config(const QString &path) — выполняет чтение файла конфигурации в формате XML. При этом предполагается что файл находится в каталоге cfg/devices/
- virtual void read_custom_config(const QString &path) — выполняет чтение конфигурационного файла, расположенного по произвольному пути.
- void setControl(QMap<int, bool> keys, control_signals_t control_signals = control_signals_t()) — передает внутрь класса состояние клавиш и массив сигналов, принимаемых от внешних устройств, подключенных к симулятору.
Публичные методы используются для настройки устройства и организации его взаимодействия с другими устройствами. Реализация функционала устройства выполняется разработчиком внутри защищенных методов
- virtual void ode_system(const state_vector_t &Y, state_vector_t &dYdt, double t) = 0 — абстрактный метод, реализующий систему дифференциальных уравнений, описывающих динамику устройства. Является методом обратного вызова, вызывается 4 раза при выполнении метода step(…)
- virtual void preStep(state_vector_t &Y, double t) — вызывается перед выполнением шага интегрирования дифференциальных уравнений. В данном методе реализуются алгебраические соотношения, описывающие работу устройства.
- virtual void postStep(state_vector_t &Y, double t) — вызывается после выполнения шага интегрирования дифференциальных уравнений. В данном методе реализуются алгебраические соотношения, описывающие работу устройства.
- virtual void load_config(CfgReader &cfg) — реализует загрузку параметров из XML-конфига. Данный метод вызывается при вызове методов read_config(…) и read_custom_config(…). При этом выполняется автоматическое открытие файла, его разбор. Доступ к считанным параметрам выполняется через методы класса CfgReader, экземпляр которого передается в load_config(…) по ссылке.
- virtual void stepKeysControl(double t, double dt) — рекомендуется для организации обработки клавиш. Вызывается на каждом шаге симуляции.
- bool getKeyState(int key) const — возвращает состояние клавиши по её коду.
- bool isShift() const — проверка нажатия клавиш Shift.
- bool isControl() const — проверка нажатия клавиш Ctrl.
- bool isAlt() const — проверка нажатия клавиш Alt.
По сути реализация устройства сводится к созданию класса, наследующего от Device и переопределении его защищенных методов. Естественно, для полной ясности необходимо привести конкретный пример реализации.
Будем считать, что мы создали проект DLL нашего тепловоза, как это описано в соответствующей главе. Реализуем для тепловоза первое устройство — контроллер машиниста.
Создание модели контроллера машиниста
Контроллер машиниста — основное устройство, с помощью которого выполняется управление тягой локомотива, задание направление его движения, а так же управление электродинамическим торможением, если локомотив оборудован таковым. На тепловозе ТЭП70 это устройство выглядит так

Чем-то напоминает автомобильный руль, но таковым, конечно же не является. С помощью этого штурвала машинист задает мощность, вырабатываемую дизель-генераторной установкой тепловоза, регулируя развиваемое локомотивом тяговое усилие. Каждому положению штурвала соответствует определенный уровень мощности. Штурвал поворачивает главный вал контроллера. Кроме главного вала имеется так же реверсивный вал, поворачиваемый соответствующей рукояткой (на фото она вытащена из контроллера). Реверсивный вал задает направление движения тепловоза.
Сборка RRS из исходных текстов
Как уже неоднократно было сказано, симулятор является проектом с открытым исходным кодом, распространяемым по лицензии GPL v2.0. Поэтому, для разработчика доступно не только написание дополнений в виде модулей подвижного состава, но и модификация исходного кода самой игры, с целью расширения её возможностей.
Проект RRS является достаточно сложным, и сборка его из исходников — процесс нетривиальный. При этом сама сборка не представляет собой особенных трудностей. Для разъяснения этапов и нюансов сборки симулятора и создана данная страница руководства.
Windows 10/11 64 bit
Все нижеследующее рассчитано на читателя, знакомого с основами программирования, администрирования ОС семейства Windows, а также, имеющего базовые навыки использования систем контроля версий ПО. Автор руководства не несет ответственности за допущенные читателем ошибки. Автор, в данном тексте, не ставит своей целью разъяснение базовых навыков и деталей руководств пользователя ПО, упоминаемого в тексте. Все действия, выполняемые читателем далее, выполняются им на свой страх и риск.
Системные требования
В настоящее время симулятор ориентирован на использование в 64-разрядных версиях операционной системы Windows. Перед началом сборки игры следует убедиться в том, что
- У вас установлена актуальная версия ОС со всеми необходимыми для работы обновлениями
- Установлена актуальная версия драйвера видеокарты, поддерживающая OpenGL
Требования к средствам разработки
Для сборки проекта симулятора требуется установленный фреймворк Qt5 (рекомендуемая версия Qt 5.15.2), собранный 64-разрядным компилятором MinGW (рекомендуемая версия MinGW 8.10 64-bit)
Зависимости сборки
Симулятор использует ряд сторонних библиотек. Все зависимости собраны из исходных текстов компилятором MinGW. Для упрощения процесса подготовки окружения можно воспользоваться прекомпилированными версиями зависимостей, список которых, вместе со ссылками на скачивание, приведен ниже
Наименование | Версия | Назначение | Загрузка |
OpenSceneGraph | 3.6.5 | Графический фреймворк для разработки 3D-графики на базе API OpenGL | СКАЧАТЬ (8,53 Мб) |
OpenAL | 1.1 | Кроссплатформенный API для работы с аудиоданными | СКАЧАТЬ (4,88 Мб) |
FreeType | 2.4.10 | Библиотека для растеризации шрифтов и операций над ними | СКАЧАТЬ (685 Кб) |
zlib | 1.2.11 | Свободная кроссплатформенная библиотека для сжатия данных | СКАЧАТЬ (112 Кб) |
Внимание! Содержимое каждого из приведенных в списке 7-zip архивов распаковывается в отдельный каталог в произвольном, удобном вам месте жесткого диска. Для распаковки архивов требуется установка архиватора 7-zip, получить который можно здесь.
Настройка окружения сборки
После установки вышеприведенных зависимостей необходимо настроить следующие системные переменные окружения
- %FREETYPE_LIBRARY% — содержит путь к библиотеке libfreetype.dll
- %OPENAL_BIN% — содержит путь к каталогу bin/ библиотеки OpenAL
- %OPENAL_INCLUDE% — содержит путь к заголовочным файлам (каталог include/) библиотеки OpenAL
- %OSG_BIN_PATH% — путь к каталогу bin/ библиотеки OpenSceneGraph
- %OSG_INCLUDE_PATH% — путь к заголовочным файлам (каталогу include/) библиотеки OpenSceneGraph
- %OSG_PLUGINS_PATH% — путь к плагинам OpenSceneGraph
- %ZLIB_LIBRARY% — путь к библиотеке libzlib.dll
Для примера, приведу свои значения указанных переменных окружения

Установка ресурсов игры
Симулятор RRS, помимо исходного кода, содержит ряд ресурсов, необходимых для нормальной работы, а именно
- Модели, конфиги анимации и звуки для электровозов ВЛ60пк и ВЛ60к, поставляемых в комплекте с игрой
- Модели, конфиги анимации вагонов, поставляемых в комплекте с игрой
- Маршрут «Испытательный полигон»
- Шрифты, используемые графической подсистемой
Данные ресурсы не хранятся в репозитории исходного кода, в виду их значительного объема. Архив, содержащий всё перечисленное выше, можно скачать здесь.
Следует создать в произвольном месте файловой системы каталог, назвав его, например RRS и распаковать туда содержимое данного архива, так, чтобы получилась следующая структура каталогов

Получение исходного кода симулятора
Исходный код симулятора размещен на ресурсе Github. при работе над проектом используется система контроля версий Git, в связи с чем, для корректной работы с деревом проекта следует установить инструментарий для работы с Git.
В каталоге с ресурсами игры (папки data/ и routes/) вызываем консоль GitBash

В появившемся терминале вводим команду

Начнется процесс клонирования репозитория на вашу локальную систему, который успешно завершается так

В каталоге проекта создан каталог с именем RRS

Переходим в него через терминал Git

Проект содержит зависимые репозитории (сабмодули), которые необходимо так же загрузить.
К ним относятся:
- asound — звуковая подсистема симулятора, использующая OpenAL API
- osgdb_dmd — плагин OenSceneGraph для загрузки 3D-моделей формата DMD (DGLEngine 1.1), используемых в маршрутах
- tcp-connection — сетевой модуль (используется ограниченно, но из зависимостей не исключен)
Инициализируем сабмодули

и обновляем их

Теперь у нас развернуто полное дерево исходников симулятора
Настройка проекта и сборка в QtCreator
Запускаем IDE QtCreator из комплекта фреймворка Qt и открываем проект

Указываем путь к скрипту сборки проекта

Настраиваем сборочные каталоги, аналогично тому, как показано на рисунке (с точностью до конкретного пути к проекту)

Выбираем требуемую конфигурацию сборки

Правой кнопкой мыши на заголовке проекта выбираем пункт «Пересобрать»

После сборки, дерево каталогов проекта выглядит так

При четком выполнении инструкций, приведенных выше, сборка не должна доставить неудобств
Развертывание проекта вместе со средой времени выполнения (runtime)
Успешная сборка не означает получение работоспособного симулятора. Необходим этап развертывания проекта — компоновку программных модулей, файлов конфигурации, ресурсов и библиотек, необходим для работы игры. Симулятор придерживается концепции stand alone, то есть развертывание позволяет получить на целевой системе симулятор, содержащий в своей структуре все необходимое для его работы.
Для начала определим системную переменную окружения RRS_DEV_ROOT, значение которой определяет путь к корневому каталогу, где мы будем разворачивать собранный симулятор. В моем случае она определена так

Для финального развертывания открываем консоль Qt (Пуск -> Qt -> Qt<номер версии> (MinGW<номер версии> 64-bit))

Переходим к каталогу проекта

Переходим в каталог scripts/

Далее, запускаем скрипт развертывания windeploy.bat

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

Если всё прошло успешно, мы получаем базовую сборку симулятора. Запуск выполняем стандартным способом, отправив на выполнение файл bin/launcher.exe

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