|
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
|