Viene descritta in questa pagina l'architettura del programma Train Director. Le informazioni si rivolgono ai programmatori che desiderano contribuire allo sviluppo del programma.

Si ricorda che Train Director è rilasciato seguendo la versione 2 della GNU Public License. Come tale, se si modificano i sorgenti è obbligatorio rendere le modifiche pubbliche in modo che altri sviluppatori possano recepire le modifiche e continuare lo sviluppo.

Si raccomanda inoltre agli sviluppatori di non pubblicare nuove versioni indipendenti del programma, in modo da evitare confusioni ai giocatori su quale versione supporta quale funzionalità. Se mi saranno inviate le modifiche al programma provvederò io a inserirle nell'ultima versione e a pubblicarle sul sito ufficiale.

E' invece consentita, o addirittura incoraggiata, la pubblicazione di versioni su piattaforme diverse da Windows, quali Linux e/o Mac OSX.

Architettura Generale

Il seguente schema a blocchi illustra l'architettura generale del programma:

Oltre ai moduli principali indicati nello schema ci sono altri moduli ausiliari per la gestione di aspetti come gli itinerari e i vari dialoghi con l'utente.

Il seguente schema illustra le principali strutture dati del programma e le loro relazioni:

Descrizione delle Strutture Dati

  • class Track
    La struttura Track descrive un elemento presente sul tracciato. Gran parte degli elementi sono binari nelle loro varie forme e direzioni, ma la stessa struttura è usata anche per descrivere segnali, scambi, icone, e tutti gli altri elementi che il creatore può piazzare sul tracciato tramite l'editor del programma.

    La posizione dell'elemento è indicata dai campi x e y. Tali campi sono coordinate della griglia, e non le coordinate fisiche dello schermo. La conversione da un sistema di coordinate all'altro avviene moltiplicando la coppia x,y per la grandezza di un quadrato della griglia, cioè 9 pixel in modalità normale, o 36 pixel in modalità zoom.

    Tutti gli elementi presenti sul tracciato sono accessibili tramite la lista globale layout. Esistono liste aggiuntive che collegano solo i binari, o solo gli scambi, o solo i segnali. Queste liste sono usate per trovare più velocemente l'elemento cercato in base per esempio alle coordinate o al nome di una stazione o di un segnale.
    Molte funzioni nel file trsim.cpp servono a tale scopo (per esempio findTrack(x, y) o findSignalNamed(name) sono tipiche funzioni che scandiscono tale liste.)
    Gli elementi del tracciato sono caricati e salvati nel file .trk dal modulo loadsave.cpp.

  • class Train
    La struttura Train descrive le caratteristiche di ogni treno presente nell'orario. Ogni treno caricato dal file .sch dal modulo loadsave.cpp viene inserito nella lista schedule, che viene ordinata per orario di ingresso e visualizzata nella finestra orario.

  • class TrainStop
    Ogni treno punta a sua volta una lista di strutture TrainStop. Ognuna delle strutture TrainStop descrive una fermata del treno, principalmente registrando il nome della stazione, l'ora di arrivo prevista e l'ora di partenza prevista. Ci sono poi alcuni campi aggiuntivi che servono a registrare eventuali ritardi durante la simulazione.

    I treni che sono presenti sul tracciato hanno il loro campo position che punta all'elemento di binario corrispondente alla testa del treno. Questo campo viene modificato man mano che il treno procede nel suo percorso. Il percorso stesso è registrato nell'array path.

    Esistono poi vari campi che mantengono la velocità effettiva del treno, la sua velocità massima ed l'ultimo limite di velocità incontrato dal treno, oltre che al prossimo limite di velocità nel percorso (se conosciuto) e il prossimo punto di arresto (se conosciuto). Questi due ultimi campi sono utilizzati per controllare l'accelerazione o la decelerazione del treno.

Organizzazione del Codice

run.cpp

Il modulo principale del programma si chiama run.cpp. Il codice in questo modulo controlla il movimento dei treni in base allo scorrere del tempo simulato. La funzione time_step() simula 1 secondo scandendo la lista dei treni presenti e modificandone lo stato in base a vari fattori. I treni possono avere i seguenti stati:

  • train_READY
    Il treno non è mai entrato nel tracciato. Ad ogni secondo simulato viene controllato se l'orario previsto di ingresso è inferiore all'orario corrente, e se si, si trova il punto di ingresso e si decide se è possibile far entrare il treno nel tracciato. Se tutti i controlli hanno esito positivo, lo stato del treno viene cambiato in train_RUNNING.
  • train_RUNNING
    Il treno è in marcia in un qualche punto del tracciato. Ad ogni secondo simulato viene chiamata la funzione run_train() che aggiusta la velocità e fa avanzare il treno lungo il suo percorso. Il percorso di ogni treno è limitato dal prossimo segnale. Quando il treno ha raggiunto il segnale alla fine del percorso corrente, l'aspetto del segnale viene controllato, e se non è rosso viene calcolato il percorso (blocco) successivo ed il treno viene fatto avanzare ulteriormente. Se invece il segnale è rosso, lo stato del treno viene cambiato in train_WAITING. Se invece il treno ha incontrato una stazione dove ha fermata, lo stato stato del treno viene cambiato in train_STOPPED a meno che la fermata non sia la destinazione finale del treno, nel qual caso lo stato del treno diventa train_ARRIVED. Anche se la fine del percorso corrente è un punto di ingresso/uscita (e se il treno non ha una coda), lo stato del treno viene cambiato in train_ARRIVED.
  • train_STOPPED
    Un treno fermo a una stazione ha lo stato train_STOPPED. Ad ogni secondo simulato il programma controlla se l'orario di partenza previsto è inferiore all'orario corrente, e se si, lo stato del treno viene cambiato in train_RUNNING.
  • train_WAITING
    Un treno fermo ad un segnale ha uno stato train_WAITING. Ad ogni secondo simulato si controlla l'aspetto del segnale per vedere se è cambiato da rosso. Se è ancora rosso si aumentano le penalità Se non è più rosso, si cambia lo stato del treni in train_RUNNING
  • train_ARRIVED
    Un treno che sia uscito completamente dal tracciato, o che sia arrivato alla sua destinazione finale ha lo stato train_ARRIVED. Solo i treni con questo stato possono essere assegnati ad altri treni.

Un treno in movimento segue un percorso che viene deciso la prima volta che il treno impegna il blocco delimitato da due segnali o da un segnale e da un punto di ingresso/uscita.
Tale percorso viene controllato dall'utente tramite la disposizione degli scambi e dei segnali. Quando viene richiesta la disposizione di un segnale a non-rosso, il programma controlla che il blocco a valle del segnale sia libero da impedimenti (come per esempio un altro treno che non sia ancora uscito dal blocco, oppure uno scambio preso di taglio che non consente il passaggio di un treno [non ci sono scambi tallonabili], o un elemento di binario già riservato da un altro itinerario). Se non ci sono impedimenti, il programma cambia l'aspetto del segnale e riserva tutti gli elementi di binario a valle del segnale cambiandone il colore da nero a verde (o bianco nel caso di abilitazione del blocco alla manovra).
Tutto ciò è eseguito dalla funzione findPath0().

Quando un treno arriva al segnale, ed il segnale non è rosso, il treno viene fatto avanzare nel blocco successivo, ed il suo percorso viene registrato nell'array path[]. Man mano che il treno "macina" chilometri, gli elementi vengono rimossi da tale array e vengono ricolorati in nero, liberando cosi' il binario e rendendolo disponibile per la creazione di altri percorsi.

Inoltre, ogni volta che il treno oltrepasas un segnale, viene calcolata la lunghezza del tratto dalla posizione del treno al prossimo limite di velocità e al prossimo punto di fermata (stazione con fermata a orario o prossimo segnale). Queste 2 lunghezze sono usate per il calcolo della curva di frenata o di accelerazione, e vengono ricalcolate anche ogni volta che un treno parte da una stazione o quando si è oltrepassato un binario con indicazione di un nuovo limite di velocità

track.cpp

Il modulo track.cpp si occupa di disegnare i vari tipi di binario sul tracciato in base al loro tipo (binari, scambi, segnali ecc.) e al loro stato (libero, occupato, normale o deviato ecc.).
Laddove il modulo run.cpp si occupa dell'aspetto dinamico della simulazione, il modulo track.cpp si occupa dell'aspetto statico, mantenendo le relazioni tra i vari elementi del tracciato.

Di particolare interesse in questo modulo sono le funzioni track_walkeast/west() e swtch_walkeast/west(). Tali funzioni calcolano il successivo elemento di binario o scambio che può essere raggiunto partendo da un dato elemento lungo una data direzione.

Chiamando ripetutamente una di queste 4 funzioni, la funzione findPath0() è in grado di decidere il percorso che un treno dovrà compiere quando impegna un blocco.

trainsim.cpp

Il modulo trainsim.cpp contiene una varietà di funzioni e può essere considerato l'amministratore generale del sistema. Da questo modulo vengono chiamate moltissime funzioni in base all'operazione che il programma deve eseguire.

Ho deciso fin dall'inizio che le varie operazioni di controllo del programma fossero descritte da stringhe di comandi leggibili. I vari comandi sono quindi controllati dalla funzione trainsim_cmd(), e la corrispondente funzione viene chiamata a seconda del comando da eseguire.

Questo ha avuto un'effetto benefico sull'evoluzione del programma, consentendo l'aggiunta di varie funzionalità avanzate senza dover stravolgere l'architettura di base. Alcune di queste funzionalità sono state l'introduzione delle azioni associate ai pedali, l'esecuzione di comandi dagli script, il possibile salvataggio e ripetizione di una simulazione (non attivato), e la comunicazione con un'altro programma tramite un collegamento socket.

loadsave.cpp

Questo modulo si occupa di salvare e di ricaricare le simulazioni tramite la lettura e scittura dei file .trk, e la lettura dei file .sch e .pth. Si occupa inoltre di salvare i vari report in formato HTML.

Anche in questo caso si è deciso di usare dei formati testo per tali files. Questa decisione ha il vantaggio che facilita la scrittura di programmi aggiuntivi, come per esempio il programma TASPO di Paolo Rosati per il taglia-incolla di porzioni del tracciato (per ovviare alle limitazioni dell'editor contenuto nel programma).

Il file .trk ha un formato criptico, in quanto avevo previsto che potesse essere letto e scritto solo da Train Director. Ciò ha aggravato il compito degli autori di scenari, che comunque sono riusciti a capirne l'organizzazione e a modificarlo indipendentemente dal programma.

I file .sch e .pth sono invece in un formato più accessibile agli umani, in quanto è stato previsto sin dall'inizio che dovessero essere costruiti a mano. La loro struttura è descritta nel manuale utente.

Canvas.cpp e MainFrm.cpp

Questi moduli gestiscono l'interfaccia con l'utente. Il modulo Canvas.cpp gestisce il disegno grafico di bitmap, principalmente convertendo gli elementi presenti nella lista layout e i treni presenti sul tracciato in immagini.

Inoltre si preoccupa di gestire gli eventi che provengono dall'utente, come i click del mouse, e della conversione delle coordinate.

Il modulo MainFrm.cpp si occupa invece dell'aggregazione delle varie finestre in un tutto unico, includendo la gestione delle finestre e dei separatori, la gestione di finestre multiple tramite linguette o di finestre indipendenti dalla finestra (Frame) principale.

Inoltre si occupa della barra del menù in alto, di inviare i comandi quando un menù viene selezionato, e di gestire la barra degli strumenti subito sotto il menù principale (per la visualizzazione dell'ora, delle penalità della velocità di simulazione ecc.)

tdscript.cpp

Questo modulo è responsabile della gestione degli script. In esso sono contenute le funzioni per leggere uno script da una serie di posti come il file orario, o il file dello scenario. Quando uno script è letto in memoria viene convertito in una struttura dati interna in base alle caratteristiche dello script. Le varie funzioni (eventi come OnCleared: e OnUpdate:) vengono isolate e salvate in campi associati all'elemento di binario o al treno a cui si riferisce lo script.

Invece i vari statement vengono collegati dalla funzione ParseStatement() in un tradizionale "Abstract Syntax Tree" (AST), per consentire la veloce identificazione delle porzioni vere e false degli statement if-else, ed il relativo nesting.

La funzione InterpreterData::Execute() viene invece chiamata ogni volta che si verifica un evento significativo associato all'elemento di binario o al treno, come per esempio quando l'utente clicca sull'icona di un segnale ed il segnale ha uno script associato. La funzione attraversa l'AST decidendo in base al tipo dei vari statement cosa fare. Se viene incontrato uno statement if, la funzione chiama la InterpreterData::Evaluate() per controllare se la condizione è verificata, e per decidere se proseguire lungo il ramo "true" o lungo il ramo "false".


Questa pagina è mantenuta da g_caprino@gmail.com
(Togliere il _ prima di inviare la mail.)
Data di creazione: 3 Maggio 2010