Jump to content

Simstra. Математика.


Recommended Posts

"Наш рассказ пойдёт не о влюблённых..." (c) не Тимас.

 

0. Я начну новый разговор, который уже давно назрел, но который завести было особо негде. Буду рассказывать о том, как Симстра устроена изнутри и почему она устроена так, а не иначе. Этот текст навеян годами программирования. Многие вещи уже достаточно отшлифованы, и о них можно рассказать. Вместе с тем, на сервере в Discord есть канал "Думалка", где публикуются огрызки ещё не сформировавшиеся мыслей.

 

1. Симстра является десктопным приложением. Это означает, что за визуализацию отвечает графическая оболочка. Язык программирования предоставляет доступ к виджетам конкретных элементов управления и обработчикам событий от них. У меня установлена KDE Plazma, но, я думаю, будет работать и в других. Никакой зауми там не используется.

 

2. Для временОго моделирования процессов Симстра использует события таймера. В отличие от симуляций, основанных на 3d-сценах, где следующий фрейм наступает сразу по окончании визуализации предыдущего, здесь длительность фрейма фиксированная и составляет 100мс. Для корректной работы вполне достаточно иметь десять срабатываний таймера в секунду. Соот-но, каждый тик таймера происходит пересчёт всей матмодели симуляции. ВременнЫе процессы (здесь и далее я из буду называть задачами, почему - объясню позже) при каждом тике таймера проверяют, истекло ли отведённое им время. Когда оно истекло - задача выполняет какие-то действия (шаг) и далее переходит на следующий, либо самоликвидируется.

 

3. Симстра уже сейчас имеет огромную объектную модель. Их там уже почти под сотню классов, и это ещё далеко не всё реализовано. Широко используется наследование и полиморфизм, что позволяет избегать написания лишнего кода. Однако, такой подход требует изначальной детальной проработки моделируемых процессов. Далеко не все наследования очевидны с первого раза. Вообще концепция ООП, постигнутая ещё на второй реинкарнации проекта, даёт очень много преимуществ, когда модель сильно разрастается. Главное - грамотно переложить реальные процессы в классы матмодели. Пожалуй, самое сложное в программировании - именно это, а не банальное написание кода понятным компьютеру языком.

 

3. 0. Для хранения как статических данных, так и вводных и симуляций, используется формат xml. Я "заболел" им, увидев всю его мощь, когда разбирал вводные от Stanicar'а. И до сих пор восторгаюсь его удобством. Кроме восторга, такие файлы легко править из под самой IDE. Т.е. открыл проект, а у тебя и все нужные файлы открылись.

 

3. 1. Объекты в Симстра сгруппированы в списки. Раньше использовались динамические массивы, но я таки решил всё унифицировать и перевести всё на списки. Как раз сейчас этим и занимаюсь, посему даже показывать нечего. Меняются названия тегов в файлах, этот процесс не сильно автоматизируется, посему медленно. Кроме того, в одном списке могут находиться объекты разных типов, использующие какой-то общий базовый функционал (общий предок), но развивающие его для своих нужд. Например: есть базовая панель питания с фидерами, есть панель питания с батареей, а также с батареей и с ДГА. Все эти объекты выполняют одну и туже функцию, но имеют разную реакцию на изменение состояний фидеров. В использовании такого подхода и есть самая вкуснотища ООП.

 

3. 2. При возникновении события таймера поочерёдно обрабатываются все списки объектов. Начинается всё с обработки панелей питания, потом идёт всякая мелочёвка, потом смежные работники, путевая модель, СЦБ, ездючины. Порядок обработки списков строго фиксирован, чтобы объекты одного типа всегда обрабатывались раньше или позже объектов другого типа. Вторая реинкарнация проекта зависла как раз на проблеме того, что все объекты были в одном списке и обрабатывались в порядке их создания. Всё-таки лучше быть твёрдо уверенным, что объект уже обработался или, наоборот, ещё имеет старое состояние.

 

3. 3. Пересчёт матмодели симуляции также может вызывать перерисовку информационных окон (стрелки, светофоры, панели питания, инвентарь и т.д.). Это создаёт некоторые неудобства, но лучше так, чем устаревшая информация в окне. Например, из-за частого обновления информации невозможно выделить её и скопировать в буфер обмена. Скорее всего, для этого будет добавлена соответствующая кнопка. Но это не точно.

 

4. По окончании пересчёта всех списков объектов в фрейме происходит обновление данных на аппарате управления. Его картинка рисуется в несколько слоёв из составных элементов. Одни слои отвечают за ячейки индикации, другие - за кнопочки, пломбы и прочую мелочь. С точки зрения симуляции кнопка, пломба, выделение ячейки для двойного нажатия - это такая же ячейка индикации, как и обычная лампочка. Главное - нужную картинку подставить. Все картинки хранятся в одной большой портянке, загружаемой при старте программы и не зависящей от загруженной вводной. По мере добавления новых возможностей портянку также приходится дополнять. Сейчас в ней 1000+ спрайтов. Кроме того, на разных этапах прорисовки аппарата управления добавляются статические картинки, отдельные объекты индикации (амперметры, счётчики, часы) и таблички-аншлаги, а также пиксельным шрифтом рисуются подписи к объектам.

 

5. Обработка событий от пульта происходит элементарно. По координатам распознаётся ячейка, где было совершено нажатие. А у каждой ячейки есть параметр - алгоритм действий. В соответствии с ним как отображается индикация, так и происходит реакция на нажатие мышью. Используются все три кнопки мыши - для нажатий, вытягиваний кнопки, срыва пломбы, надевании/снятии красного колпачка или стопора, выделения кнопки для совместного нажатия.

 

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

 

7. Далее в этой теме я буду рассказывать о каждой из уже написанных матмоделей в отдельности.

Edited by Timas
  • Like 3
  • +1 Rep 5
Link to comment
Share on other sites


"Эх, дороги! Пыль да туман..." (c) не Тимас.

 

Информация в этом сообщении немного устарела и будет обновлена в дальнейшам.

 

0. Сугодня я попытаюсь рассказать о путевой модели Симстры. Путевая модель появилась не сразу. Изначально проект развивался по пути "клеточного пульта": каждая ячейка строго соответствует определённому путевуму элементы, а траектория перемещения ездючины определяется лишь конфигурацией ячеек. Stanicar, Train Director, Train Dixpatcher используют ту же технологию. У неё есть ряд существенных ограничений и неудобств, посему с 2013 года, когда впервые запахло реализмом, я принялся городить собственную путевую модель.

 

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

 

Визуально путевая модель представляет собой однониточный план территории. Для удобства его построения я использовал AutoCAD, но сейчас буду переходить на программы с GPL-лицензированием. Мне совершенно не нужна красивая картинка. Удобство CAD-программ заключается в последовательном построении элементов и автоматическом расчёте их координат. Из готовой модели потом переносятся все элементы в понятный Симстре формат. Автоматизировать этот процесс я не пытался, т.к. мне больше важна логичная последовательность описания элементов, нежели скорость их добавления. На картинке путевой модели текстом проставляются все ID будущих объектов - также в логичной для меня последовательности. Тимасовскую переписывали уже раз десять, и там бы вообще путевую модель пересобрать, а то соседних ID не сыщешь. Но, увы, это путь эволюции разработки, без него никуда!

 

1. 0. Одиночный элемент пути назван субсекцией - TSubSection (потому как термин "секция" используется в модели СЦБ). Каждая субсекция имеет ряд обязательных параметров, описывающих её характеристики: длина, радиус (нулевой для прямого пути), уклон. Также есть ряд необязательных параметров и увязок с другими объектами симулятора, но об этом немного позже. Также каждый объект субсекции имеет два дочерних объекта - конца субсекции - TSubSectionEnd. В них осуществляется привязка к узлам, ограничивающим данную субсекцию, а также к ряду объектов, с которыми непосредственно взаимодействует субсекция (напрмер, кодовый приёмник на стыке блок-участков, светофор в модели СЦБ). В будущем по мере добавления также планируется увязка с односторонними сигнальными знаками. И, самое главное - конец субсекции всегда знает, какой ближайший к ней состав установлен на субсекции. Эта информация нужна для работы кодирования и для будущей модели столкновений ездючин.

 

1. 1.  Субсекции соединяются в узлах путевой модели. Каждый узел - TTrackNode может быть концом пути (тупиком или точкой входа/выхода), обычным стыком в пути или точкой разветвления траекторий движения на разные пути (соответствует острию остряка в реальных условиях). Здесь как раз используется наследование объектов: TTrackNode -> TSimpleTrackNode -> TTrackNodeWithDerailer. Последний явно не описывает третий элемент, а имеет увязку с приводом, управляющим перемещением виртуальных остряков. Кроме того, имеются узлы для увязки с воротами и с переездами. Ворота можно снести, если не предупредить владельца ПНОП о том, что "будите охрану, к вам поехали". А на переезде, само собой, могут возникать ДТП.

 

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

 

1. 2. Подвижные элементы путевого развития представлены двумя объектами (в порядке наследования): TDerailer и TSwitchBlades. Первый описывает сбрасывающий остряк или подвижный сердечник, второй - полноценный комплект остряков. Функционал наследования основан на том, что в первом случае при переведённом положении подвижного элемента происходит сход с рельсов, а во втором - ездючина направляется на ответвление. Движение в пошёрстном направлении происходит одинаково, т.к. в первом объекте ездючина не может выйти "из поля" и встать на рельсы.

 

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

 

1. 3. Кодовый приёмник, как ни странно, тоже стал объектом путевой модели. Так проще. Подробнее о кодировании я расскажу отдельно. Кратно - суть в том, что код распространяется последовательно по субсекциям навстречу ездючинам и приёмникам. А те, в свою очередь, его ловят, анализируют и влияют на другие объекты.

 

1. 4. Ворота - простой объект, используемый только для создания нештатных ситуаций. Никакого другого функционала в них нет. Одначе, через сломанные ворота ездить будет нельзя, посему, пока ветвевладелец их не починит, никаких подач к нему не будет.

 

1. 5. Переезд - ещё один объект путевой модели. Он имеет как увязку по СЦБ, так и по путевой модели. Пока не введено разных типов переездов (обслуживаемые и необслуживаеме, и т.д.), но этот вопрос рассматривается. Скорее всего, он будет решаться на этапе написания вводной Сферическая. Логически увязка переезда и путевой модели представляет собой точку пересечения осей пути и автогужевой дороги. К этой точке и будут привязываться ДТП. Но для работы СЦБ необходимо устройство нескольких рельсовых цепей, посему приходится сразу предусматривать дополнительные узлы. Хуже всего то, что, пока нет движения по перегону, переезд нормально не отладить (переезды в пределах станций и на перегонах имеют разную увязку с СЦБ).

 

1. 6. Односторонние сигнальные знаки ("НТ", "КТ", "С", знаки по управлению токоприёмником) также будут привязаны к концу субсекции. Но писАться это будет на этапе создания "зрения" машиниста.

 

1. 7. Двухсторонние сигнальные знаки (Граница станции, "К", что там ещё...) будут привязаны к узлам, а не к концам субсекций.

 

2. Скоростной режим движения ездючин описывается отдельным списком наборов скоростей (скоростным поездам, пассажирским, грузовым, маневровым). Каждая субсекция имеет увязку с конкретным набором скоростей. Планируется, что при управляемом движении ездючины перед построением кривой скорости будет строиться диаграмма максимальных скоростей, а уже на основании неё - выбираться требуемая скорость следования в каждой отдельной точке. Ест-но, с учётом положения хвоста. Чтобы не было ситуации, когда голова на одном ограничении, хвост на другом, а серединка едет по главному пути с максимальной скоростью. Скорости для движения с отклонением по стрелкам будут задаваться аналогично, с ними проблем не предвидится.

 

3. Пассажирские устройства и грузовые фронты - это отдельные программные объекты. Они не являются напрямую частью путевой модели, но субсекции ссылаются на них при необходимости. При этом понятия остановочного пункта и конкретной платформы разделены: несколько платформ по разным путям могут соответствовать одному остановочному пункту. При движении графиковой ездючины остановка будет засчитана у любой платформы, т.к. в расписании будет указана не сама платформа, а остановочный пункт. А уж куда принимать и по каком пути гнать - это воля конечного пользователя.

 

4. Рельсовые цепи являются уже объектами СЦБ со своим питанием и зависимостями. Но каждая субсекция, имеющая РЦ, ссылается на неё и при занятии-освобождении управляет занятостью РЦ. Соот-но, узел может являться изостыком. Но, поскольку изостык, как программный объект не нужен, то отдельного класса объектов для изостыка не выделено. Хотя во второй реинкарнации проекта они были и реально отрабатывали занятие и освобождение секций.

 

5. Увязка с контактной сетью также будет происходить через субсекции. Я пока не сильно хочу заморачиваться с паденияем напряжения при отдалении от источника питания (для снижения мощности электротяги), но это не сильно страшная тема. Секционирование контактной сети как на станциях стыкования, так и вообще будет выполняться отдельными объектами - фидерными участкми, увязанными с субсекциями.

 

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

 

7. А в следующей серии я подробно расскажу про движение ездючин по путевой модели. И ещё ныне пропущена, но будет написана серия про базовые объекты.

Edited by Timas
  • Like 2
  • +1 Rep 4
Link to comment
Share on other sites


"Камень на камень, кирпич на кирпич..." (c) не Тимас.

 

0. Итак, сегодня на повестке дня базовые объекты. Самые простые в реализации и самые маленькие объекты, из которых собирается Симстра. Их не так уж и много. Большинство из них собраны в отдельном модуле, работа которого не сильно зависит от основной программы. Не сильно - потому как всё-таки обратная отсылка есть. Но пока не очень понятно, останется она или отомрёт. О ней позже.

 

1. Симстра является SDI-приложением. Это значит, что в отдельный момент времени программа работает только с одним документом - файлом вводной или симуляции. Но, в отличие от классических SDI-приложений, главное окно программы не является окном работы с загруженным файлом. В TrainMania было наоборот, там окно с аппаратом управления было главным окном программы. Этот подход себя не оправдял по ряду причин, посему теперь всё раздельно. Выбрали симуляцию, загрузили - вот вам и окна для работы с ней.

 

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

 

3. Базовым типом объекта является TFlaggedObject, унаследованный от классического TObject. Функционал флагового объекта состоит в предоставлении информации об индексе или идентификаторе объекта (поле ID) и в работе с флаговыми данными (поле Flags).

 

3. 0. Идентификатор объекта - это целое положительное число. Нумерация начинается с нуля. Для объектов, сгруппированных в списки, идентификатор является индексом объекта в списке. Большинство списков объектов статичны, но не все. И, соот-но, упорядоченные ID используются не для всех объектов. Ячейки пульта распознаются по координатам, а не по индексам. А список составов сам по себе динамичен, посему ID состава определяется случайно при создании состава. Список задач также днамичен, но работа с ним ещё не переписана. Для списочных объектов, где ID строго упорядочен, на этапе загрузки списка проверяется строгость порядка объектов в списке. При наличии несоответствия загрузка файла прерывается.

 

3. 1. ID объекта в списке может быть изменён. Так, при развороте состава (относительно действующих на него сил) меняется порядок подвижных единиц в нём, чтобы первая по ходу движения ездючина имела ID, равным нулю. Аналогично при развороте конкретной ездючины её передняя и задняя сцепка меняются местами.

 

3. 2. Флаговые данные представляют собой 64-битное поле, каждый бит которого может быть установлен или сброшен. Само поле используется только для чтения и напрямую к нему обращений практически нет. Вся работа производится через ряд методов и функций. Они обеспечивают проверку наличия хотя бы одного бита из заданной маски (GetStateOr), проверку наличия всех битов заданной маски (GetStateAnd), установку битов по маске (SetState), сброс битов по маске (ClearState), ниверсию битов по маске (InverseState), установку или сброс битов по условию (IfThenState), одновременный сброс и установку битов по двум маскам (UpdateState), подсчёт установленных битов по маске (BitsCount), инверсия битов при их неравенстве (ExchangeStates). С точки зрения классического программирования всё это - излишества, но они сильно улучшают читабельность кода. А вместе с константными именами для абсолютно всех используемых флагов читабельность кода повышается вдвое.

 

3. 3. Использование битовых данных пришло ещё с DOS-версии TrainMania'и, и отказываться от него не хочется. Во-первых, такой подход требует меньше места в памяти и в файле при сохранении. Во-вторых, каждый бит не просто указывается числом, а описывается, как константа. Вот пример такого описания для светофора на поле. Здесь префикс sid - указание (для меня), чей флаг используется.

 

Спойлер

{ TSignalDevice - Сигнальные показания }

 

  sidNone = $0000;  {Лампы погашены}
  sidYellow1 = $0001;  {Первый жёлтый огонь - 0} {*}
  sidGreen1 = $0002;  {Первый зелёный огонь - 1} {*}
  sidRed = $0004;  {Красный огонь - 2} {*}
  sidYellow3 = $0008;  {Третий жёлтый огонь - 3} {*}
  sidGreen2 = $0010;  {Второй зелёный огонь - 4} {*}
  sidYellow2 = $0020;  {Второй жёлтый огонь - 5} {*}
  sidWhite1 = $0040;  {Первый белый огонь - 6} {*}
  sidBlue = $0080;  {Синий огонь - 7} {*}
  sidWhite2 = $0100;  {Второй белый огонь - 8} {*}
  sidCallingOn = $0200;  {Пригласительный сигнал - 9} {*} {будет заменён на жёлтую полосу}
  sidGreen80 = $0400;  {Первая зелёная полоса - A} {*}
  sidGreen120 = $0800;  {Вторая зелёная полоса - B} {*}
  sidYellow1Flashing = $1000;  {Первый жёлтый огонь мигает - C} {*}
  sidGreen1Flashing = $2000;  {Первый зелёный огонь мигает - D} {*}
  sidAutoBlock = sidYellow1 or sidGreen1 or sidRed or sidYellow1Flashing or sidGreen1Flashing;  {Автоблокировка}
  sidAll = $3FFF;  {Все лампы} {*}
  sidNextYellow1Flashing = $4000;  {Жёлтый огонь мигает на проходном}
  sidNextGreen1Flashing = $8000;  {Зелёный огонь мигает на проходном}

 

  sidPreHome = $00010000;  {Предвходной в правильном направлении} {*}
  sidPreHomeReversed = $00020000;  {Предвходной в неправильном направлении} {*}
  sidReversedShort = $00040000;  {Короткий блок-участок в неправильном направлении - объединять со следующим} {*}
  sidTransmitReceivedCode = $00080000;  {В развёрнутом положении передавать принятый код без изменений} {*}
  sidInactive = $00100000;  {Закрещен} {*}
  sidTransmitPermitted = $00200000;  {Разрешена трансляция кодов} {*}
  sidRedOff = $00400000;  {Перегорание запрещающего огня}
  sidRedOffFixed = $00800000;  {Перегорание красного огня зафиксировано системой ЧДК}
  sidToneBroken = $01000000;  {Неисправен генератор частоты для ЧДК}
  sidAutoblock4 = $02000000;  {Четырёхзначная автоблокировка} {*}
  sidShuntingBlue = $04000000;  {Синий на маневровом вместо красного} {*}
  sidRoutePointer10 = $08000000;  {Цифровой маршрутный указатель,  иначе - буквенный} {*}
  sidReversed = $10000000;  {Погашен при встречном направлении АБ} {*}
  sidAtStationBorder = $20000000;  {Проходной на границе станции} {*}
  sidCallingOnInSeparateHead = $40000000;  {Пригласительный сигнал в отдельной головке} {*}

 

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

 

3. 5. Звёздочка в описании константы указывает на то, что она может быть установлена на этапе формирования вводной. Если звёздочки нет - константа используется только в процессе симуляции. Иногда при тестировании приходится по сохранённому файлу искать, что же там попало в объект и должно ли оно было туда попасть. Также в некоторых сборках я вывожу для отладки флаговые данные в окна просмотра состояний объектов.

 

3. 6. Кроме того, базовый TFlaggedObject имеет ссылку на владельца (Owner). Это свойство переопределяется в наследуемых объектах, т.к. каждый из наследников может иметь владельца только определённого типа. Указание владельца позволяет двигаться вверх по иерархии объектов. Например, из кабины управления ездючиной получить доступ к концу ездючины, на котором установлена кабина, выше - к самой ездючине, и ещё выше - к составу. Соот-но, Owner каждого типа объекта переопределён для указания типа вышестоящего владельца.

 

3. 7. Для списочных объектов, являющихся элементами главной модели, владелец не определён. Программа имеет ряд переменных, предоставляющих более быстрый доступ к основным объектам модели, нежели обращение через свойства с перепроверкой типов.

 

3. 8. Базовый объект TFlaggedObject также предусматривает возможность отрисовки наследников на канве. Для этого он имеет функцию проверки видимости объекта и собственно процедуру рисования. При этом как проверка, так и рисование может быть абсолютно любым. Напр., для нитки графика движения проверка - это попадание в видимый фрагмент графика, а рисование - отрисовка линии хода, стоянок и цифр минут. А для ключа-жезла проверка - это наличие его в аппарате, а рисование - просто отображение нужного спрайта по заданным координатам.

 

3. 9. Конечно же, базовый объект предоставляет функционал для сохранения самого себя в xml-файл и чтения себя же оттуда. Наследники добавляют в соответствующие процедуры чтение и запись своих данных. Списочные объекты, являющиеся владельцами объектов внутри списка, умеют создавать и загружать объекты из файла со строгим определением их типов. Списочные объекты, которые владеют только ссылками на объекты, читают только идентификаторы объектов. Подробнее о работе списочных объектов будет рассказано в отдельной статье. Там много. Тем более, что она (работа) сейчас как раз переписывается.

 

3. 10. Также заложена основа функционала по формированию списков объектов с их текстовыми подписями для различных диалоговых окон. Но об этом тоже позже, бо тоже переписывается.

 

4. Следующий базовый объект в иерархии - TNamedObject = class (TFlaggedObject). Здесь всё просто: добавляется текстовое наименование объекта (Caption). Соот-но, обновляются методы чтения и записи объекта в файл. Само наименование объекта может быть изменено, но на практике это используется крайне редко.

 

5. Дальше идёт TColoredObject = class(TNamedObject). Он описывает поименованный объект с цветовой характеристикой. Цвет используется в текстовых сообщениях работников в окнах связи, при рисовании ниток графика движения (из объект TTrainCategory) и в других неочевидных местах. Само поле цвета (TextColor) можно изменять. Но, по-моему, изменение цвета уже нигде не используется.

 

6. Ещё дальше: TBlurredColor = class(TColoredObject). Это такая пока секретная штука для рисования грязного подвижного состава. У объекта есть поле Deviation, которое задаёт процент отклонения итогового цвета от заданного. В настоящее время отклонение рассчитывается через преобразование RGB -> HSB, изменение яркости, и обратно в RGB. в дальшейнем планируется также преобразование через светимость.

 

7. TDoublePointedObject = class(TFlaggedObject). Тут всё совсем просто: базовый объект с привязкой к координатам в пешеходной модели. Координаты задаются, как "TDoublePoint = packed record X: Double; Y: Double; end;". Объект ничего не умеет делать со своими координатами и используется только в качестве базового для статических данных.

 

8. Ещё одна вкусняшка - TObjectWithSwitchState = class(TFlaggedObject). Объект предоставляет доступ к полю NeededState, используемому в списках маршрутов, данных об охранных стрелках и в других местах, где необходима информация о требуемом положении стрелки. NeededState описан, как перечисляемый тип состояний стрелок: (ssPlus, ssMinus, ssNothing). В файле xml перечисляемые типы записываются обычными числами, являющимися порядковым (с нуля) номером перечисляемого значения. Соот-но, NeededState="0" - это положение по плюсу.

 

8. 0. Вообще перечисляемый тип TSwitchStates = (ssPlus, ssMinus, ssNothing) используется очень много где. Вся работа со стрелками основана именно на нём: положение остряков, направления движения остряков, положение контактов автопереключателя в стрелочном приводе, положение стрелочного коммутатора на пульте, положение стрелочного макета и т.д. - всё это описывается типом TSwitchStates. По сути - это большой и сложный пример трочиной логики.

 

9. Я описал далеко не все базовые объекты. В следующем выпуске я продолжу. Зачем я это делаю? Чтобы какой-нибудь среднестатистический Артём Владимирович, решивший с горя написать свой симулятор, не просто написал что-то полезное и интересное, а поделился со мной своими мыслями, на которые у меня не хватило мозгов.

  • Like 2
  • Thanks 1
  • +1 Rep 5
Link to comment
Share on other sites


Posted (edited)

"Из чего же, из чего же, из чего же сделаны эти объекты?" (c) не Тимас.

 

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

 

1. TObjectWithLastMessaage = class(TFlaggedObject). Заготовка под общение со смежными работниками по различным средствам связи. TLastMessage описан, как перечисление (lmNone, lmSelector, lmTrainRadio, lmShuntingRadio, lmFaceToFace, lmLoudSpeaking) Я думаю, тут всё понятно: селектор, ПРС, СРС, живьём на посту, громкоговорящая парковая связь. Соот-но, при изменении местоположения работника изменяется и способ общения с ним. Причём способ общения устанавливается не только для самого работника, но и для задач, которые для него в этот момент активны.

 

2. TObjectWithProgramBuild = class(TFlaggedObject). Маленькая "затычка" для проверки соответствия номера сборки. От этого объекта порождены TStaticData, описывающий всю кучу статических данных, и, собст-но, TGame, описывающий симуляцию. И там, и там при загрузке из файла xml необходима проверка номера сборки. Сама идея пришла ещё из TrainMania'и, и сейчас она уже не сильно актуальна. А открытом xml каждый может написать всё, что угодно. Скорее, это для собственного успокоения: в текущий момент я уверен, что работаю с нужным вариантом файла.

 

3. TColorList = class(TFlaggedObject). По сути - обёртка для динамического массива цветов, умеющая сохраняться в xml и читаться оттуда. Используется в качестве набора сигнальных цветов для ламповой и световой индикации на аппарате управления. Также радостно сохраняет и читает 1000 цветовых значений для цветного блокнота (40х25 символов). Такой вот вкусный костыль! Другого применения пока нет.

 

4. TTaskGeneratingObject = class(TNamedObject). Объект, который может создавать задачи и отдавать свой ID в качестве первого параметра данных созданной задачи. Упразднён в сборке 0679, т.к. модель задач будет переделана.

 

5. TPointedObject = class(TTaskGeneratingObject). Достаточно популярный для воспроизводства потомства объект, имеющий привязку к целочисленным координатам. Умеет перемещать себя в абсолютном и относительном исчислении координат. Используется для ячеек аппарата управления, аншлагов, подписей, статических спрайтов и вообще всего того, что надо нарисовать по целочисленным координатам на аппарате управления и на карте участка. Ранее также использовался для рисования сводных спрайтов подвижных единиц, но эта часть проекта временно приостановлена.

 

6. TPultSpriteBasedObject = class(TTaskGeneratingObject). Объект, от которого порождены динаические отрисовки на аппарате управления. Имеет ссылку на отдельно взятый спрайт статической индикации, берёт оттуда координаты. Придуман для того, чтобы при переустройстве аппарата управления при перемещении статического спрайта вся остальная индикация сразу перемещалась за ним. Напр., стрелки и цифры часов следуют за спрайтом часов. Аналогично с цифрами счётчиков, стрелкой амперметра, рукоятками индуктора. Аналогично планируется анимировать коммутатор УКСС-8, поездную радисвязь, телефон, пульт управления ПОНАБом. Только всё это ещё предстоит нарисовать.

 

7. TAlgorithmObject = class(TPointedObject). Тут ещё проще. Свойство Algorithm предоставляет целочисленную переменную, по которой определяется, какой набор действий необходимо выполнить. На этом базируется работа ячеек аппарата управления и рисование подвижных единиц. Есть предположение, что ячейки избавятся от такого способа работы в пользу объектного полиморфизма, но это пока только теория. Потому как есть проверка совместного нажатия двух кнопок, и она как раз основана на ID алгоритма кнопки.

 

8. TLineObject = class(TFlaggedObject) - отображаемая линия. Умеет рисовать себя заданным цветом, но функция, поставляющая цвет, абстрактна и перекрывается в наследниках. Используется при рисовании путей на карте участка и стрелки амперметра. В первом случае цвет вычисляется в зависимости от наличия и состояния секции маршрута, а во втором цвет задаётся статично в объекте амперметра.

 

9. TTalkerFaceSet = class(TFlaggedObject). Описание пары мордочек (ID спрайтов) для диалоговых окон. Умеет выбирать нужную (вправо/влево), больше ничего не умеет. Описания должностей ссылаются на два таких набора: мужской и женский.

 

10. TObjectWithWorker = class(TTaskGeneratingObject). Объект с привязкой к конкретному работнику. Хранилища инвентаря, собственно предметы инвентаря, станции, перегоны и диспетчерские круги. Умеет передаваться от одного работника к другому (метод HandOver) и проверяться на наличие у работника вообще (HasWorker), и конкретно того, от чьего лица которого выполняется симуляция (функция OnMe).

 

11. TObjectWithYard = class(TNamedObject). Ничего лишнего, только увязка со станцией или перегоном.

 

12. Это - наиболее основные базовые и вспомогательные объекты. Скорее всего, я что-нибудь пропустил. По мере добавления новых возможностей в программу их перечень может изменяться. Кроме того, как я уже отметил, есть кандидаты на выбывание. Даже в то время, когда я яростно борюсь с новым интерфейсом, развитие кода происходит весьма динамично. Поэтому я вынужден периодически собирать мысли в кучу и писать разные сказки. Чтобы самому не запутаться!

Edited by Timas
  • Like 1
  • +1 Rep 5
Link to comment
Share on other sites


Posted (edited)

"Клетки, клетки, клетки. Как в метрополитене вагонетки." (c) не Тимас.

 

0. Предыстория данного вопроса и сегодня легко обнаруживается на Королевстве Delphi по ключу "клетчачтые игры". Компонент был обнаружен году в 2010, у автора испрошено разрешение на его доработку под собственные нужды. Сам факт нахождения такого компонента значительно приблизил начало третьей реинкарнации TrainMania'и - Симстры. Во многом архитектура программы была заточена именно под данный компонент. Позже, конечно, работа компонента была детально изучена. Автору огромное спасибо за грамотное описание всего и вся. Многое, между тем, оказалось лишним и невостребованным (движения, анимации и т.д.). Но и появились новые потребности, коих у тов. Григорьева не было.

 

1. Поэтому с переходом на открытый исходный код было решено написать свой аналогичный компонент по образу и подобию. К этому моменту уже достаточно чётко просматривалась будущая структура объектов для него, а также список возможностей всех наследников. На написание собственно нового TCellBoard и его допиливание уже в условиях работы программы ушло порядка трёх месяцев. И сейчас я расскажу, что и как там устроено.

 

2. Итак, собственно TBoard = class(TCustomControl). Основа основ. Первая и достаточно неожиданная особенность, позаимствованная у тов. Григорьева: свойства Width и Height не устанавливают ширину и высоту объекта. Вместо это в конструкторе задаётся количество ячеек по ширине и высоте. Сами же размеры ячеек в TBoard 16х16, но в наследниках могут изменяться (16х26 для Праготрона и 20х28 для ВизИнформа). Компонент не предусматривает установку его в палитру, он используется только при динамическом создании форм. Это есть вторая особенность, и связана она с тесной интеграцией TBoard с менеджером графики и самим пультом. Не скажу, что динамически подгонять размеры на форме достаточно приятно, но это лучше, чем пересобирать всё и вся при каждом изменении, влияющем на работу сетки пульта или табло.

 

2. 0. Компонент TBoard напрямую не используется, хотя и является самодостаточным. Возможно, что где-то в далёком будущем придумается, что от него можно то-то породить. Но пока он является только вспомогательным объектом.

 

2. 1. Для формирования готового сводного отображаемого спрайта в компоненте имеется два Bitmap'а. Недавно я радостно перешёл с нативных Bitmap'ов на объект TRGB32Bitmap, который более быстр в работе. Но об этом будет подробно рассказано в описании менеджера графики. Так что под Bitmap'ом понимаются именно новые объекты. Так вот, два Bitmapа используются для формирования сводного изображения. Первый из них статичен, второй - нет. Первый формируется единожды и содержит фоновое изображение. Для пульта - это всё то, что не меняется в процессе вывода индикации: сетка субблоков, таблички, подписи, маски субблоков, отдельные спрайты типа стола и жезлового аппарата. Программно предусматривается возможность переформирования фонового изображения, но пока это нигде не используется.

 

2. 2. Второй Bitmap является динамическим. При формировании изображения сначала статический фон копируется в этот самый второй Bitmap, а потом уже на нём отрисовывается всё то, что может изменяться: литеры табло или элементы индикации пульта. И только потом уже готовое изображение скармливается канве видимой части TBoard'а. Соот-но, никакого мерцания при прорисовке не возникает.

 

2. 3. Само изображение строится из ячеек. Каждая ячейка - это программный объект TBoardCellBody = class(TObject). Он имеет ссылку на TBoard, а также необходимые для отрисовки параметры: состояние (State), фазу (Phase)) и смещение (Offset). Графика для компонента хранится в менеджере графики в виде нарезанной на плитки большой портянки. Размеры плиток в ней строго соответствуют размерам ячеек. Соот-но, при визуализации каждой ячейки ей передаются координаты на Bitmap'е и собст-но, сама ссылка на Bitmap. И компонент тупо и цинично берёт и рисует в нужном месте нужную плитку из большой портянки. А смещение нужно для того, чтобы не плодить почти одинаковые плитки со смещённым на несколько пикселей изображением. По умолчанию оно нулевое, но в объекте нужные значения пульта загружаются из данных симуляции.

 

2. 4. Для ячеек фона пульта используется наследник от TBoardCellBody - объект TCellBackground. Он имеет ещё один параметр - цвет фона ячейки. По умолчанию этот цвет не используется, но он нужен при установке свойства TBoard.HasBorder в true. В этом случае каждая ячейка перед выводом соотв. плитки рисует по своей площади выпуклую рамку. Цвет фона задаётся в процедуре генерации ячеек фона. А цвета краёв и углов рамки получаются смещением базового цвета через преобразование RGB - HSL - RGB. Благодаря этому немного разноцветные ячейки имеют всегда один и тот же уровень яркости краёв и углов.

 

2. 5. Кроме того, фон пульта может быть изначально залит однородным цветом или же заполнен пёстрыми пикселями.

 

2. 6. Следующий по порядку разработки объект: TLayedBoard = class(TBoard). Здесь, помимо слоя ячеек статической индикации, предусматриваются слои динамической индикации. Их отрисовка осуществляется послойно. Слои и их ячейки создаются в момент генерации сетки пульта или табло. Для пульта в каждую ячейку заносится состояние, фаза и смещение. При отрисовке каждая ячейка работает аналогично: помещает нужную плитку в нужное место с нужным смещением при необходимости. Плитки хранятся в той же портянке, что и для фона. А что на тех плитках изображено, компоненту сетки совершенно безразлично. Ну и да, значение как состояния, так и фазы может быть равно минус единице, в этом случае ничего не рисуется. Это не ошибочное состояние, это именно сообщение о том, что рисовать тут нечего.

 

2. 7. А как же заставить всю эту веселуху рисоваться в нужный момент времени с нужными параметрами? Для этого в модуле работы с сетками пульта и табло имеется специальный таймер. Один на всех. Настроен он на 50мс, этого вполне достаточно. Каждая сетка при создании записывает себя в список объектов в этом таймере. И при удалении, соот-но, оттуда себя выписывает. А таймер при сработке заставляет все имеющиеся в его распоряжении сетки перерисовать себя. Что сетки будут делать при перерисовке - его не касается.

 

3. TPultBoard = class(TLayedBoard). Вод и подкрались мы к пульту. Прямой ссылки на объект пульта в нём нет, т.к. объект пульта глобален и у него есть глобальная же переменная, доступная отовсюду.

 

3. 0. Объект пульта перекрывает методы рисования фона, сводного изображения и метод, вызываемый в общем таймере. При сработке таймера первым делом пульт заставляет шевелиться всю матмодель симулятора. Там огромное количество списков объектов, для которых вызываются метод Run, и внутри матмодели что-то происходит. После этого пульт начинает обновлять параметр Phase в каждой ячейке каждого слоя индикации. Всего слоёв индикации четыре, да пятый, который ушёл в статический слой и уже отрисован на фоне. И вот этот самый Phase всех четырёх слоёв и есть результат проявления разного рода индикации: световые ячейки, кнопки, лампочки, коммутаторы. Каждая ячейка пульта TPult, соответствующая ячейке индикации на компоненте TPultBoard, поставляет свои значения Phase в зависимости от состояния объектов матмодели.

 

3. 1. После отрисовки слоёв индикации рисуются все те объекты, которые не могут быть описаны с помощью ячеек: стрелки амперметров, цифры счётчиков числа нажатий кнопок, индуктор и барабан жезлового аппарата, жезлы и ключи-жезлы. У некоторых из них проверяется условие видимости, о котором я писал, когда рассказывал об общей структуре объектов. И вот только после всего этого сводный спрайт отображения пульта считается готовым и скармливается в метод Paint.

 

3. 2. Для организации прокрутки пульта вправо-влево в окне "Аппарат управления" отдельно ниже отображается горизонтальная полоса прокрутки. TPultBoard имеет ссылку на неё, и при отрисовке себя и при расчёте координат мыши в ячейках (а не в пикселях) учитывается положение ползунка. Сам же пульт имеет обработку колеса мыши, которая заставляет шевелиться полосу прокрутки. А та, в свою очередь, уже двигает саму картинку пульта.

 

3. 3. Кроме того, в зависимости от ячейки, над которой находится мышь, пульт показывает всплывающую подсказку. Текст подсказки и наименования объектов берутся из соответствующих ячеек программного пульта, увязанных с СЦБ и другими матмоделями.

 

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

Edited by Timas
Орфография
  • Like 1
  • Thanks 1
  • +1 Rep 4
Link to comment
Share on other sites


Posted (edited)

"Лети, лети, лепесток..." (c) не Тимас.

 

0. Продолжение рассказа про графику и Праготроны-ВизИнформы.

 

1. 0. Для работы с текстовыми табло создан вспомогательный объект TTextBoard = class(TLayedBoard). Он имеет один слой индикации и универсален для всех типов табло. Для хранения текстовой информации в нём имеется только читаемое свойство BoardString. Эта строка переменной длины (в байтах), но программно поддерживается её размер в UTF-8-символах, равный количеству ячеек индикации. При создании объекта эта строка заполнена пробелами.

 

1. 1. Текстовые табло работают не с абсолютно любыми символами. Мне было интересно сделать именно некоторое подобие оригинальных устройств, в т.ч. и с точки зрения их логики работы. Поэтому как для Праготрона, так и для ВизИнформа есть свой список доступных символов. Кроме того, Праготрон отображает только буквы в верхнем регистре. Для приведения отображаемого текста к используемому ряду символов имеются специальные методы. Также с целью сокращения числа символов объединяются одинаковые по начертанию кириллические и латинские буквы: русское АВСЕНКМОРТХасеорху посимвольно заменяется на латинское ABCEHKMOPTXaceopxy. В ВизИнформе также не было буквы Ч (заменялась на цифру 4), но эту особенность, пришедшую из телеграфного кода МТК-2 (посредством которого табло ВизИнформ и получало текстовую информацию), я опустил.

 

1. 2. Для помещения информации на табло имеется ряд методов, который может быть расширен. Нельзя записать сразу весь текст, но можно поместить его или в нужную строку, или в нужную позицию. При этом, ест-но, всё происходит в UTF-8-символах и их общее количество не изменяется. Можно удалить строку целиком. Также планируется вертикальная прокрутка строк. Есть методы для вставки, удаления и замены отдельного символа, они используются в наследниках, позволяющих редактирование текста.

 

2. 0. Моё любимое детище - TPragotronBoard = class(TTextBoard). Праготрон - это название производителя лепестковых табло. Их, конечно, производят и другие фирмы, но название как-то устоялось. Как ксероксы с памперсами. Каждое знакоместо представляет собой ось с насаженными на неё лепестками. На каждом лепестке с одной стороны изображена половина одной буквы, а с другой - половина следующей. Сборка из лепестков помещается в окошко так, чтобы в нём был виден символ целиком, нарисованный на двух половинках. Из таких знакомест набирается табло произвольного размера.

 

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

 

2. 2. Я не нашёл описания разных модификация Праготрона. Судя по видео, количество лепестков в нём разнится. 39, 40, 52 и т.д. Я добавил кириллицу, и у меня Праготрон умеет отображать 80 символов. Изображения символов сформированы программно с использованием шрифта Noto Mono. Хранятся они в аналогичной пультовым субблокам портянке. Каждая строка - это символ, а фаза - это его состояние перелистывания. При обновлении информации каждый фрейм проверяется, нужный ли символ отображается. И, если нет, значение фазы увеличивается на единицу. Когда фазы одного символа исчерпаны, на единицу увеличивается порядковый номер символа в портянке (State), а фаза обнуляется. И так по кругу. Такой алгоритм обеспечивает написание текста поверх текста. Лепестки будут крутиться, пока не отобразится нужный символ. Ну и для того, чтобы не было сильно страшной синхронизации, введён шанс "заедания" ячейки, при которой сдвиг фазы в текущем фрейме не происходит. Благодаря этому лепестки крутятся вразнобой.

 

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

 

3. 0. TVisInformBoard = class(TTextBoard). ВизИнформ - это блинкерное табло, где изображение строится из отдельных точек. Производились они в Венгрии, применялись на вокзалах, в аэропортах, а сейчас - в качестве маршрутных указателей на городском транспорте. Для индикации отдельной точки, как элемента изображения, используется блинкер - электромеханическое устройство, меняющее положение цветной платины. Блинкер закрыт - обращена чёрная сторона пластины, блинкер открыт - яркая (обычно лаймовая или оранжевая) сторона.

 

3. 1. Визинформ рисует изображения символов из строки BoardString по точкам. Каждому символу соответствует семь байтовых значений, но используются только пять бит в каждом. Соот-но, есть бит в нужном месте - точка рисуется, нет - пропускается. Для рисования точки используется спрайт 4х4 пикселя, края которого снабжены альфа-каналом. Про фльфа-канал и графику будет в отдельной главе этой сказки. Цвет символа может быть любым, но по умолчанию используется традиционный лаймовый.

 

4. TColoredVisInformBoard = class(TVisInformBoard) - это ВизИнформ с цветными символами. Помимо строки UTF-8-символов, он имеет массив такого же размера для цветов знакомест. Есть некоторое количество предопределённых константами цветов, но в теории цвет знакоместа может быть любым. При удалении и вставке символов в редактируемых компонентах цвета знакомест следуют за своими символами. Это была отдельная тема для мыслительного процесса.

 

5. TPultDateTimeVisInformBoard = class(TColoredVisInformBoard). Эта штука берёт свой текст от игрового таймера и больше ничего лишнего не умеет. Используется в окне "Аппарат управления" в качестве часов игрового времени.

 

6. 0. TVisInformBoardText = class(TColoredVisInformBoard) - табло ВизИнформ с возможностью редактирования текста. Имеет указание положения текстового курсора, индикацию режима вставки или замены, а также событие, срабатывающее при изменении данных. Текстовый курсор можно гонять стрелочками и другими клавишами управления курсором. Поддерживается всё, кроме прыжков по словам с зажатым Ctrl. Доступно позиционирование текстового курсора мышью. Также возможна выгрузка текста и цветового массива в программный объект TColoredNoteBook, обеспечивающий работу блокнота в симуляции.

 

6. 1. Блокнот TColoredNoteBook используется в симуляции для временных пометок. Цвет вводимых символов можно выбирать из списка. Предполагается, что он будет полезен для сохранения отдельных номеров вагонов, информации о накоплении на приёмоотправочных путях и грузовых фронтах. В нём также предусмотрена возможность автоматического открытия при загрузке симуляции. Это чтобы сразу увидеть все прежние напоминалки.

 

7. TVisInformBoardDigital = class(TVisInformBoardText). Здесь урезан список допустимых символов. Возможны только цифры. По умолчанию текстовая строка заполняется нулями, а не пробелами. При позиционировании текстового курсора мышью в нужное знакоместо значение цифры в нём увеличивается на единицу. Также возможна прокрутка нужного знакоместа колесом мыши. Удаление символа заменяет его нулём.

 

8. TVisInformBoardNumber = class(TVisInformBoardDigital) - компонент для ввода чисел без ограничения по значению. Может иметь от двух до двенадцати разрядов. Опять же, доступна мышь.

 

9. TVisInformBoardRangedNumber = class(TVisInformBoardNumber) - компонент для ввода чисел с указанием верхнего и нижнего предела. Вводимое число автоматически не корректируется, но при выходе из указанного диапазона изменяется цвет индикации.

 

9. 0. TVisInformBoardVehicleNumber = class(TVisInformBoardNumber) - компонент для ввода номеров подвижного состава с проверкой их корректности по алгоритму Луна. Умеет автоматически корректировать контрольный знак при вводе номера, но эта возможность может быть отключена. Некорректный номер выделяется цветом.

 

9. 1. TVisInformBoardVehicleNumber8 = class(TVisInformBoardVehicleNumber) - компонент для восьмизначных номеров.

 

9. 2. TVisInformBoardVehicleNumber12 = class(TVisInformBoardVehicleNumber) - компонент для 12-значных номеров.

 

10. TVisInformBoardTime = class(TVisInformBoardDigital) - компонент для ввода времени в часах и минутах. Не позволяет ввести некорректное время, автоматически прокручивая изменяемый разряд до ближайшего корректного значения. Если 1959 пытаться превратить в 2959, то на выходе получится 0959, т.к. цифра 2 в первом знакоместе приводит к некорректному значению.

 

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

Edited by Timas
  • Like 2
  • +1 Rep 4
Link to comment
Share on other sites


Posted (edited)

"Если видишь на картине..." (c) не Тимас.

 

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

 

1. При программировании под Windows я столкнулся с дефицитом графических ресурсов: когда в одном цикле надо было отрисовать несколько тысяч спрайтов, программа слетала. Если этот процесс растянуть во времени, то система успевала подготавливать и освобождать ресурсы, и сбоев не было. Но это был один из первых звоночков, что пора что-то менять.

 

2. 0. Долго ли, коротко ли, нашёл я набор объектов TRGB32Bitmap. Это - пользовательская реализация 32-битного битмапа. Из стандартного он умеет проецировать себя на классический TCanvas. Ну и читать себя из bmp-файла. А большего от него ничего не требуется. Код объектов кривущий до ужаса, я бы его раз десять переписал, прежде чем использовать. Не исключаю, что его постигнет участь Григорьевской "сеточки": помучаюсь я с ним, а потом "по мотивам" свой напишу. Но это ближе к рисованию подвижного состава. Сейчас оно меня вполне удовлетворяет.

 

2. 1. Объект TRGB32Bitmap представляет собой прямоугольный массив четырёхбайтных точек. Каждая точка - это четыре канала - цветовые составляющие RGB и альфа. Поддержки альфы в исходниках не было, но прикрутить её не составило труда. Алгоритмы альфа-смешения я ковырял ранее. Из остального TRGB32Bitmap умеет рисовать линии по Брезенхему, прямоугольники, рамки, ну и, ест-но, давать попиксельный доступ к массиву. Ничего лишнего. Есть там ещё какие-то заделы непонятно подо что. Но, судя по тому, как оно находится в версии "ноль с мелочью" уже почти десять лет, самый верный способ - это его переписать. Но не сейчас!

 

2. 2. Самое главное, что TRGB32Bitmap умеет рисовать самого себя на самом себе! Т.е. накидать составную картинку из отдельно нарисованных частей элементарно. Без альфа-канала оно ещё и работает быстро, т.к. используется не попиксельное присваивание, а копирование байтов в памяти. А-ля ScanLines.

 

2. 3. Эксплуатация TRGB32Bitmap в суровых условиях должна показать, что от него ещё требуется, и тогда уже будет принято решение по его модернизации или переписыванию. Не сейчас!

 

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

 

4. Картинки из тарбола выгружаются в TRGB32BitmapList = class(TFlaggedObject). Это - обёртка для типизированного TObjectList, не более. Класс имеет индексированное свойство Items, которое предоставляет доступ к отдельным картинкам. Ничего лишнего.

 

5. TRGB32BitmapTileList = class(TRGB32BitmapList). Это - заготовка для набора "плиточных" изображений - портянок. Помимо списка картинок, он хранит их количество по горизонтали и вертикали. Как оно работает - опишу ниже.

 

6. TBasicSpriteManager = class(TFlaggedObject). Базовый менеджер спрайтов. Содержит в себе TRGB32BitmapList, умеет читаться из тарбола. При чтении как раз и происходит проверка целостности тарбола, хеш передаётся в функцию чтения. Сами файлы в тарболе должны иметь имя, где первые пять символов - это цифры. Они интерпретируются, как индекс списка, по которому надо записать загружаемый спрайт. Список создаётся сразу на заданное количество элементов, но загружаются только те, которые есть. Соот-но, есть функция проверки наличия спрайта по индексу.

 

7. TSpriteManager = class(TBasicSpriteManager). Ничего лишнего. Предоставляет базовому классу размер списка, имя файла тарбола и MD5 для его проверки.

 

8. 0. TTiledSpriteManager = class(TBasicSpriteManager). Почти то же самое, но не совсем. Процедура загрузки спрайтов из тарбола переписана. Помимо собст-но загрузки в родительском классе, она "режет" загруженный файл на плитки и помещает их в вышеописанный TRGB32BitmapTileList. Загруженная сводная портянка при этом удаляется, т.к. целиком в памяти она не нужна.

 

8. 1. Нарезка плиток, ест-но, производится с проверкой размера исходной картинки и плиток. Размеры плиток для каждого спрайта записаны в константы класса, как и имя тарбола и его MD5. При несоответствии размеров (плитки "нарезались" с остатками) выдаётся сообщение об ошибке.

 

8. 2. Ну и самое главное - объект умеет отрисовывать нужную плитку в нужном месте.

 

8. 3. Почему же всё-таки плитки, а не цельная портянка. Ну, во-первых, это не нативный TCanvas, который жрёт графические ресурсы, а просто кусок памяти. Прежних ошибок уже не получить. Во-вторых, практические тесты показали, что одиночную картинку алгоритмы рисуют быстрее, нежели выкусывание фаргмента с проверкой его видимости в спрайте назначения. Ну и более простые алгоритмы, как ни странно, более надёжны!

 

9. Следующим объектом графики является TPixelFont = class(TFlaggedObject). Теперь он используется вместо спрайта пиксельного шрифта для надписай на пульте и аншлагах. Имеет константный массив битовых масок, по которым рисуются отдельные буквы, и массив длин этих букв в столбцах. Также умеет вычислять длину заданного текста и рисовать его заданным уветом в заданной позиции с учётом выравнивания TAlignment.

 

10. Ещё одна вкусняшка: TPixelDot = class(TFlaggedObject). Это спрайт 4х4 с альфа-каналом по краям, который используется для вывода цифр в электронных часах, текста в ВизИнформе и, в будущем, показаний маршрутных указателей. Цвет точки можно задать любой При шаге между точками в три пикселя альфа-канал по краям позволяет им сливаться между собой, что повышает наглядность.

 

11. 0. TGraphicsManager = class(TFlaggedObject) объединяет все вышщеописанные объекты в один глобальный. Кроме того, в нём есть пока не используемый "массив затухания" в 256х256 однобайтных значений, которые вычисляются, как fColorArray1[X, Y] := Round(X * Y / 255); Массив нужен в алгоритме затухания ячеек пульта при разряде батареи и ещё в ряде аналогичных мест.

 

11. 1. Кроме того, объект TGraphicsManager предоставляет ряд вспомогательных функций (посчитать альфа-мешение) для одного цвета и для всего четырёхбайтного пикселя) и процедур (рисование выпуклых рамок для ячеек пульта, аншлагов и табличек).

 

11. 2. В менеджер графики также запихнут менеджер звуков. Звуки вполне удалось победить: библиотека BASS прекрасно завелась под linux, надо было только сразу правильно заводить. Но об этом отдельно!

Edited by Timas
  • Like 1
  • +1 Rep 2
Link to comment
Share on other sites


Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...