2005/08/30

前回の報告はこちら

あらためてVRM4で再現してみた

待ちにまったVRM4版Powered by TOMIX第2号がリリースされました。

念のために補足しておきますが、「鉄道模型シミュレーター」には伝統的に二系統のパッケージがあります。1つはI.MAGiC社独自規格のレール・ストラクチャが収録されたパッケージ。そしてもうひとつは株式会社トミーテックのライセンスを受けたTOMIX規格のリアルNゲージ製品を再現したパッケージです。

フレキシブルレールを使ってリアルNゲージレイアウトを製作するに当たっては特にどちらのパッケージでも条件は同じですが、道床付組み線路を使うのであればPowered by TOMIX版のVRMとTOMIX FineTrackレールの組み合わせが断然便利です。

 

ghostはI.MAGiC社の提灯持ちでもなんでもないので正直に感想を述べますが、VRM4の新機能(夜景対応など)を盛り込んだものと新たに追加されたもの(島式ローカルホームやバリエーションパーツ)以外のストラクチャについて、一部あからさまにVRM3時代からのテクスチャの使い回しと思われるものがあるのはやや遺憾です。特にI.MAGiC規格ストラクチャと並べるとかなり違和感があります。これはV4に始まった話ではないですが、この点、I.MAGiC社には今後のフォローアップを期待したいと思います。

が、それを勘案しても、超ハイクオリティの収録車輌や制御可能な信号機、給電シミュレーション機能など、VRM4第2号は「買い」だと思います。

閑話休題。

以前に、VRM4第0号所収のI.MAGiC規格レールを使って無理矢理拙作第三期レイアウトをVRM再現したことがありましたが、今回改めてTOMIX規格線路でこれをやってみました。

ちなみに、左リンクで提案した地形造成プロセスの改善は(そのままの形ではありませんが)システム4.0.2.0にて達成されました。なかなか使い心地が良いです。I.MAGiC社の健闘に多謝。

<VRM4による拙作スイス風レイアウト再現>

主目的は後述する実験のためだったので作りこみがやや甘いですが、ご注目いただきたいのはVRM4の目立たない新機能「FOV変更」の威力です。

FOVについてはVRM侍所収の拙稿をご参照ください。

<こちらはVRM3による画像>

上掲のVRM4スクリーンショットではFOVを150°に設定しています。下掲の同アングルからのリアルNゲージレイアウト写真と見比べてみてください。

VRM3ではFOV=視野角が固定されていたため、リアルNゲージレイアウトを俯瞰するようにVRMレイアウトを捉えようとすると、視点をレイアウトからかなり離して振り返る必要がありました。この結果、本来リアルNゲージレイアウトを肉眼で捉えた感じとは、どうしても乖離があった点は否めません。

VRM4ではFOVを20°〜200°の範囲で任意に設定できるようになりました。これにより、詳細な解像度を保つ距離からレイアウト全体を俯瞰することが容易になりました。この改善は、地味なものながらリアルNゲージレイアウト設計の、特に「木を見て森を見ず」の問題をVRMの活用によって解決する上で、非常に強力な武器になると考えられます。

「木を見て森を見ず」問題については拙著「鉄道模型シミュレーター レイアウト設計と製作」の第1章にて論じているのでご参照くださいまし。

<こちらはリアルNゲージレイアウト>

TOMIX信号機の動作を再現してみた

ここからが本題です。

VRM4第2号はいろいろと盛りだくさんのパッケージで「コレこそが売りだ!!」と一つの機能を取り上げるのはいささか心苦しいのですが、それでも敢えてピックアップしたいのは、スクリプトによる灯火制御が可能となった信号機の実装です。模型列車の走行に連動して遷移する信号機は、リアルにせよバーチャルにせよ、心躍るものがあります。

以降、リアルNゲージとはあまり関係ない話へ突入します。

が、少し問題が。

VRM4第2号所収のTOMIX信号機(二灯式から五灯式まで揃った大盤振る舞い!!)は「見た目」は製品そのものなのですが、その挙動はユーザーがスクリプトを書いて定義しなければなりません。しかも、以下に述べる2つの問題点から、TOMIX製品の動作を完全再現することは不可能です。

パッケージ所収のスクリプトウィザードを使えばとりあえず動作させることは簡単に出来ます。本稿は、それでは満足できないとおっしゃるマニアックな方のために書きました。

第一に、VRMには「レールに電流を流す」という発想がそもそもありません。TOMIX信号機は列車が信号に対して逆走して来る電流を検知している間は常に赤現示となりますが、そもそも電流方向という概念が存在しないVRMではこれをそのまま再現することは原理的に不可能です。

給電シミュレーターは、トラックプランのショートの可能性を検証するのみであり(これはこれで大いに役立ちます)、ビュワーでの走行には影響を与えません。

第二に、VRM4のセンサーは「編成先頭車の最初の車輪の通過」を検知する仕様になっています。これに対し、TOMIX信号機はそれとともに「編成の最後の車輪の通過」を検知してから信号現示の遷移が始まります。従って、これまた原理的には再現が不可能です。

厳密に言うと、TOMIX信号機には車輪が金属製の場合のみ検知する、という制約があります。

不可能を可能にしてみた

原理的に不可能、原理的に不可能と、ネガティブな発言が続きましたが、わざわざそんなことをあげつらうために文章を書くほど、ghostは暇ではありません。

いや、暇だからこそ(以下略)

確かに原理的には不可能ですが、近似した動作を工夫して再現することは可能です。以下、どのように考え、どうすればそれが実現できるのかを書きます。いきなり現物を示しても良いのですが、ghostは読者諸氏にこの思考過程の面白さを追体験していただきたいと切に願うので、敢えてご不興を覚悟でダラダラ書きます。体調の悪い方は明日読むように。

 

少なくともghostは、コレが脳が耳から出てきそうなくらいに楽しくて楽しくて仕方がないのですが。変ですか、そうですか。

まず、実現すべき動作を整理しましょう。ここでは「結果」だけを重視し、その「原因」はとりあえず棚上げします。既に原因、つまり信号機の動作のトリガとなる事象の再現が不可能であることは明らかですから。

(1) 列車が逆方向から進入して来ることが明らかな場合、赤現示となる。

(2) 列車が正方向から進入すると赤現示となる。

(3) 列車が正方向から通過し終えて一定時間経つと、信号の遷移し始める。減速(YG)→注意(Y)→進行(G)といった具合に。

(4) 信号の遷移の間隔は前回列車が通過してから今回列車が通過するまでの時間によって長くなったり短くなったりする。

(1)には二通りの考え方があり得ます。

編成の進行方向が変化した際、これをリアルにおける電流の逆転とする考え方。もう1つは信号逆進入方向にセンサーを設置し、これが列車を検知した場合に逆進入とみなす方法です。

動作が実際の製品に近いのは前者なのですが、この方法はリバース線構造(まさにここで紹介しているプラン)に対しては無効です。また、この方法を取る場合、編成スクリプト内の方転に連動するメソッドに、レイアウト内に存在するすべての信号オブジェクトを連記しなければならない、という嫌な側面があります。

そこで今回は後者の方法で実現することにしました。むしろこちらの方が実際の鉄道に挙動が近いからです。なお、単にセンサー検知を逆進入と見なしてしまうと正方向から列車が信号を通過してそのセンサーに至った場合にも同じことが起こってしまい、これは不都合です。そこで、センサーを通過した列車の「向き」を知る仕組みが必要となります。

 

(2)は(1)の裏返し、つまり信号機付のレール(またはその直前)にセンサーを設置し、やはり編成の向きを調べて対応すればOKです。

 

(3)は策を講じる必要があります。前述したようにVRM4のセンサーは「列車がさしかかった」ことは検知しますが「列車が通り抜けた」ことは検知してくれません。そこで少し発想を転じることにします。

基本的な戦略はこうです。センサーに列車がさしかかった瞬間、列車の速度を調べます。この後、速度が変化しないとすれば、列車の長ささえわかれば編成全体が通過するのに要する時間がわかるはずです。道のり÷速度=時間、というヤツですね。

速度はGetCurrentSpeed命令で、編成長はGetNumberOfCat命令で得られる車輌数に車輌長を乗算すれば得ることができます。ここから計算される通過に要する時間をSetEventTimerで計ってやれば、列車が通過するタイミングを間接的に知ることができます。これをトリガとすれば、列車が通過し終わった瞬間(またはそれ以降)に何かをする、というスクリプトを書くことが可能になります。

 

厳密には車輌毎の長さの差を考慮すべきですが、わずか数ミリ秒の誤差は無視しても実害ないので、平均的な値として旅客列車であれば20m、二軸貨物であれば14mあたりを採用すれば問題ないでしょう。

(4)は、手間はかかりますがさほど難しくはありません。時間を計るタイマーイベントを定義しておき、一定間隔で加算されるカウンタを前回通過時にリセットし、今回通過した際にその値を知れば、列車が等速走行した場合の次回通過までの所要時間を見積もれます。ただし、初めて列車がセンサーに至った際のカウンタの値は出鱈目ですから、これに対してはなんらかの措置が必要になります。

前段も含め「列車の等速走行」が前提になっているのがややネックですが、それを言ったらTOMIX信号も列車の速度変化には対応しないので同じですね。

スクリプトを書いてみた

この仕組みには登場人物(オブジェクト)が少なくとも4つあります。第一に信号機、第二に信号機に隣接する正方向進入検知センサー、第三に逆方向進入検知センサー、そして最後に走行する編成です。「少なくとも」と書いたのは、編成は1つとは限らない(あらためて詳述します)からです。

以下のサンプルスクリプトはアップデータ4.0.2.2を適用した状態で動作検証したものです。今後のアップデータの適用によって無効になる場合があります。

最初に一番簡単な信号機のスクリプトを書いてみましょう。ここで実現すべきことは、信号機が他オブジェクトからの指示を受けて現示を切り替えることが可能なメソッドを提供することです。なお、ここでは最も一般的な三灯式信号を例にコーディングしていきます。

//信号スクリプト

//公開プロパティ
Var StdVarSignalCondition
//初期状態設定
set StdVarSignalCondition 3
call this StdMtdSignalG
//
//R現示メソッド
//
BeginFunc StdMtdSignalR
   set StdVarSignalCondition 1
   SetSignal StdVarSignalCondition
EndFunc
//
//Y現示メソッド
//
BeginFunc StdMtdSignalY
   ifeq StdVarSignalCondition 1
      set StdVarSignalCondition 3
      SetSignal StdVarSignalCondition
   endif
EndFunc
//
//G現示メソッド
//
BeginFunc StdMtdSignalG
   ifeq StdVarSignalCondition 3
      set StdVarSignalCondition 6
      SetSignal StdVarSignalCondition
   endif
EndFunc

左記サンプルでは、SetSignalに直値を投げ込まず、あえて変数経由にしています。また、その変数をグローバル定義しています。

これは、今回のケースでは扱いませんが、StdVarSignalConditionを外部参照することを可能にするための措置です。具体的には、編成に正方向進入検知センサー(あるいは他のセンサー)から前方の信号オブジェクト名を知らせてもらうことで、編成がその値をチェックし、その結果判明する信号状態に応じて速度を自動変更することを想定しています。ここらへんは改めて後日にVRM侍にて連載中のVRMスクリプト禅問答にて詳述の予定。乞うご期待。

 

加えて。

左記サンプルではY/G現示メソッドで信号状態をチェックした上で切り替えをおこなっています。これは、R→GやG→Yといった、運用上あり得ない遷移をブロックする意味合いを持ちます。

これも今回のケースでは必須ではないのですが、自動運転と連動した信号機を作る場合、こういった「例外避け」をしておかないと、複数編成の同時走行によって予期せぬ挙動を起こす場合があります。

三灯式信号に合わせてR/Y/Gの三種類の信号現示をおこなうメソッドを定義しています。また、ビュワー起動時の信号状態は0(無点灯)なので、最初に自分自身(this)のメソッドStdMtdSignalGをcallして、初期化をおこなっています。

この記述により、他オブジェクト(具体的にはセンサー)のスクリプトからcall {信号オブジェクト名} StdMtdSignalR/Y/Gとすることで、この信号を制御することが可能になります。

 

次に,、順序が前後しますが編成スクリプトを書いてみましょう。編成スクリプトに求められるのは、以下の3点が外部(この編成以外のオブジェクト)から参照できることです。

ここで紹介している手法は、一般的に言えば以下のようなことです。

[1] センサーで検知した編成に関する何らかの情報(速度や列車種別など)を得たい場合は、
[2] 編成スクリプトにその情報を収めるための公開プロパティを用意し、
[3] そのプロパティを最新の状態にするためのメソッドを作成する。
[4] センサーで編成を検知したら、編成名を取得し、[3]のメソッドを実行する。
[5] 続けて[2]で用意した公開プロパティの値を取得し、これを以降の処理の判断材料に使う。

 

これはVRM4である程度以上の自動化を試みる場合に必須になる考え方ですので、是非マスターしていただきたいと思います。

 

 

 

 

 

 

変数StdVarCarLengthは前述したように「車輌1両あたりの長さの平均値」という位置付けです。ここでは20にしていますが、編成に組み込む車輌に応じて調整してください。

編成長は後ほど時間演算をおこなう必要に応じてset命令ではなくsetf命令で実数として扱っている点に注意してください。

 

 

 

 

 

なお、変数名とメソッド名が被らなければ、編成スクリプトに他の処理(自動運転や灯火制御等)を組み込むことには何の問題もありません。

 

(i) 現在の走行速度がわかること

(ii) 現在の編成の長さがわかること

(iii) 現在の(レールに対する)進行方向がわかること

これをスクリプトで実現するには2つのことをしなければなりません。1つは外部から参照可能なグローバル変数(公開プロパティ)を定義すること、もう1つはそのグローバル変数に適切な値を必要に応じて設定するメソッドを定義することです。具体的には以下のようになります。

//編成スクリプト

//公開プロパティ
Var StdVarCurrentSpeed 
Var StdVarTrainLength 
Var StdVarTrainDirection
//定数
Var StdVarCarLength
setf StdVarCarLength 20
//
//公開プロパティ更新メソッド
//
BeginFunc StdMtdUpdateProperty
  //現在速度・・・(i)
  GetCurrentSpeed StdVarCurrentSpeed
  //編成長 ・・・(ii)
  GetNumberOfCar StdVarTrainLength 
  mul StdVarTrainLength StdVarCarLength
  //編成の向き・・・(iii)
  GetDirection StdVarTrainDirection
EndFunc

このレイアウトを走行する編成(厳密に言うと信号のある地点を走行する編成)には必ず上掲のスクリプトを記述してやります。

車輌の通過を検知したセンサーが、GetSenseTrain命令で取得した編成オブジェクトのメソッドStdMtdUpdatePropertyを実行することで、公開プロパティの値がその時点の速度、編成長、進行方向に更新されます(コメントにi〜iiiのローマ数字で示しています)。速度と進行方向はそれぞれGetCurrentSpeed/GetDirection命令で取得しているだけですが、編成長はGetNumberOfCar命令に車輌1両当たりの長さを乗算することで得ています。編成長=車輌数×1両当たりの長さ、というワケです。

あとはこの値を参照すれば、結果的にセンサーが自身を通過した編成の速度、編成長、進行方向を知ることができます。つまり、それに応じたアクションを起こすことができることを意味します。

 

いよいよ本丸となるセンサースクリプトを書きます。

これは非常に複雑なものになる上、信号への正方向進入うを検知するセンサーと逆方向のそれの2つを用意する必要があります。混乱を避けるため。最初に「何を実現しなければならないか」をまとめておくことにしましょう。

<正方向進入センサー>

(a) 編成の進行方向が信号に対して正方向であることを確認する。

(b) 編成がセンサーを通過したら信号をR現示に切り替える。

(c) 編成の速度、編成長から、編成が信号を通過するのに要する時間を見積る。

(d) (c)で算出した時間が経過した後、一定時間後に信号をY現示に切り替える。

(e) さらに(e)から一定時間後に信号をG現示に切り替える。

(f) 前回の編成通過から今回の編成通過までに要した時間から、信号の遷移時間(d,eにおける「一定時間」)を見積もる。

(g) 編成が信号に対して逆方向から進入した場合、適度な時間が経過した後に信号をG現示に切り替える。

<逆方向進入センサー>

(h) 編成の進行方向が信号に対して逆方向であることを確認する。

(i) 編成がセンサーを通過したら信号をR現示に切り替える。

信号とセンサーの位置関係については、正方向進入センサーは信号レールもしくはその直後。逆方向センサーは信号から十分に距離を置いた逆側になります。

特に逆方向センサーはポイントなどによって経路が分岐する地点に配置し「ここから列車が入ってくるからにはこの信号で対面する列車を止めなければならない」という位置に配置するとリアリティが出ます。

こんな感じです。

まずは実現すべき項目の少ない逆方向進入センサーのスクリプトから書いてみましょう。

//逆方向進入センサースクリプト

//制御対象信号機
VarSignal ObjPartnerSignal
get ObjPartnerSignal "{連動する信号名}"
//センサーイベント定義
Var VarEventID
SetEventSensor MtdTrainPass VarEventID
//
//メソッド
//
BeginFunc MtdTrainPass
  //編成の向きを取得する
  Var Tmp
  VarTrain ObjTrain
  GetSenseTrain ObjTrain
  call ObjTrain StdMtdUpdateProperty
  mov Tmp ObjTrain.StdVarTrainDirection
  //編成の向きを評価する・・・(h)
  ifeq Tmp 1
    //信号をR現示に切り替える・・・(i)
    call ObjPartnerSignal StdMtdSignalR
  else
    //何もしない
  endif

EndFunc

3行目の{連動する信号名}は、このセンサーによって制御される信号機に与えた名前に書き換えます。

たとえば、信号機の名前がSIGNAL1であるとすれば、3行目は・・・

 

get ObjPartnerSingal "SIGNAL1"

になります。

想像が付くと思いますが、レールの向きにさえ気をつければこのセンサースクリプトはget命令で取得する制御対象信号機の名前さえ変えれば、どんな信号機にも対応します。

余談ながら、VRM4第2号で新装されたスクリプトウィザードは、この「名前」の部分を既にレイアウト上に配置済みの部品から簡単に拾って、スクリプト内の必要な部分(左例の{連動する信号名}の部分)に補填する、というのが基本的な考え方になっています。これは、スクリプトウィザード用のスクロールを自作する場合、私が例示しているような流儀でのコーディングが必要となることを意味しています。

SetEventSensor命令によって列車の検知に連動して実行されるメソッドMtdTrainPassの冒頭にご注目ください。ここで、先に示した編成スクリプトに仕込んだメソッドStdMtdUpdatePropertyを実行しています。これにより、直後の行で参照される変数StdVarTrainDirectionに編成がセンサーを踏んだ時点での「向き」が反映される仕掛けです。

赤字で示したifeq命令の部分には注意が必要です。GetDirection命令で取得される編成の向きは、センサーが配置されたレールの向きに依存しています。この例ではこの値が1の場合を「逆進入」としていますが、レイアウトの構造によってはこれが逆であることもありえます。この場合は、else命令の前後を下に示すように入れ換える必要があります。

  ifeq Tmp 1
    //何もしない
  else
    //信号をR現示に切り替える・・・(i)
    call ObjPartnerSignal StdMtdSignalR
  endif

 

本当は ifeq {変数名} -1 としたいんですが、4.0.2.2の時点では直値にマイナス符号を使うと「変数」と解釈されて(未定義ゆえの)エラーになるようです。この点は改善していただきたいところ。

2005/09/05 追記

上記の問題(負の直値が解釈されない)はアップデータ4.0.2.3にて改善された模様。これでスクロール化が楽になった。

最後に、もっとも複雑怪奇となる正方向進入センサーのスクリプトを示します。パッと見の印象はその長さもあって難しく感じると思いますが、落ち着いて読み進めればそんなに特殊なことはしていません。

//正方向進入センサースクリプト

//制御対象信号機
VarSignal ObjPartnerSignal
get ObjPartnerSignal "{連動する信号名}"
//信号モード管理変数
//  0...初期状態
//  1...待機
//  2...正方向進入検知
//  3...信号遷移(1) Y遷移待ち
//    4...信号遷移(2) G遷移待ち
//    5...逆方向侵入検知
Var VarSignalMode
set VarSignalMode 0
//信号遷移時間初期値(最大値)
Var VarMsTransitDefault
setf VarMsTransitDefault 3000
//信号灯火数
Var VarSignalType
set VarSignalType 3
//計算用に加工
add VarSignalType 1
cnvfloat VarSignalType 
//時間管理変数&イベント
Var TimerID
Var VarTimer
Var VarMsPass
Var VarMsAgain
Var VarMsTransit
mov VarMsTransit VarMsTransitDefault
setf VarTimer 0
SetEventTimer this MtdTimer TimerID 100
//センサーイベント定義
Var EventID
SetEventSensor MtdTrainPass EventID
//
//編成通過検知メソッド
//
BeginFunc MtdTrainPass
  //信号をR現示に切り替え・・・(b)
  call ObjPartnerSignal StdMtdSignalR
  //編成の公開プロパティを取得
  VarTrain ObjTrain
  Var TmpSpeed
  Var TmpLength
  Var TmpDirection
  GetSenseTrain ObjTrain
  call ObjTrain StdMtdUpdateProperty
  mov TmpSpeed ObjTrain.StdVarCurrentSpeed
  mov TmpLength ObjTrain.StdVarTrainLength
  mov TmpDirection ObjTrain.StdVarTrainDirection
  //編成通過所要時間算出・・・(c)
  cnvfloat TmpSpeed
  mul TmpSpeed 1000.0
  div TmpSpeed 3600.0
  cnvfloat TmpLength
  div TmpLength TmpSpeed
  mul TmpLength 1000.0
  mov VarMsPass TmpLength
  //編成の向きの評価・・・(a)
  ifeq TmpDirection 1
    call this MtdBackward
  else
    call this MtdForward
  endif

EndFunc
//
//正方向進入メソッド
//
BeginFunc MtdForward
  //信号遷移間隔算出・・・(f)
  if VarSignalMode
    sub VarMsAgain VarMsPass
    div VarMsAgain VarSignalType
    mov VarMsTransit VarMsAgain
    //結果が最大値を超えた場合の処理
    if> VarMsTransit VarMsTransitDefault
      mov VarMsTransit VarMsTransitDefault
    endif
    //結果が0以下の場合の処理(例外対策)
    if< VarMsTransit 0.0
      mov VarMsTransit VarMsTransitDefault
    endif
  endif
  //時計リセット
  setf VarMsAgain 0
  setf VarTimer 0
  //信号モード変更
  set VarSignalMode 2
EndFunc
//
//タイマーメソッド
//
BeginFunc MtdTimer
  //タイマーカウンタ増加
  add VarTimer 100.0
  add VarMsAgain 100.0
  //信号モード2の場合、通過を待つ
  ifeq VarSignalMode 2
    if>= VarTimer VarMsPass
      set VarSignalMode 3
      setf VarTimer 0
    endif
  endif}
  //信号モード3の場合、遷移時間待ってY現示・・・(d)
  ifeq VarSignalMode 3
    if>= VarTimer VarMsTransit
      call ObjPartnerSignal StdMtdSignalY
      setf VarTimer 0
      set VarSignalMode 4
   endif
  endif
  //信号モード4の場合、遷移時間待ってG現示・・・(e)
  ifeq VarSignalMode 4
    if>= VarTimer VarMsTransit
      call ObjPartnerSignal StdMtdSignalG
      set VarSignalMode 1
   endif
  endif
  //信号モード5の場合、通過を待ってG現示・・・(g)
  ifeq VarSignalMode 5
    if>= VarTimer VarMsPass
        set ObjPartnerSignal.StdVarSignalCondition 3
        call ObjPartnerSignal StdMtdSignalG
        set VarSignalMode 1
    endif
  endif
EndFunc
//
//逆方向進入メソッド
//
BeginFunc MtdBackward
  //時計リセット
  setf VarTimer 0
  //信号モード変更
  set VarSignalMode 5
EndFunc

VRMスクリプトで演算をおこなうに際しては、整数形式(int)と小数形式(float)の扱いに注意が必要です。

setf命令で明示的に小数形式による代入をおこなう場合を除き、小数形式の演算命令に小数桁以下が0の値を直値で使う場合は、{整数部}.0 という書き方(つまり.0を末尾に加える)をしないと正しく計算されないようです。

左のサンプル中では、

mul TmpSpeed 1000.0

div TmpSpeed 3600.0

の末尾の「.0」などがそれに当たります。

 

 

 

 

 

 

左記サンプルでは時間計測を100ミリ秒=0.1秒毎に実行されるメソッドMtdTimerにおいて、用途の異なる2つの変数に100ずつ加算を繰り返すことでおこなっています。

この結果、特に編成通過の検出は最大0.1秒の誤差が生じますが、今回の例ではそもそもの編成通過所要時間の元値が概算値(編成を構成する全車両の長さが等しいと仮定している)なので、この程度でも実害はありません。

お手元のPCのCPU性能によっては、MtdTimerの実行間隔、すなわちSetEventTimer命令の時間定数をもう少し大きくした方がいいかも知れません。この場合、連動してMtdTimer内のadd命令の加算値を変更してください。

 

 

 

 

 

今回の例でタイマーイベントによって時間を計るという面倒な羽目になっているのは、ひとえにSetEventAfter命令が時間を変数で受けてくれないからです(構文エラーにはならないが、0ミリ秒後実行と解釈される)。

SetEventAfter命令が動的に算出される時間後のイベント実行をサポートしてさえくれれば、左のスクリプトは半分以下の長さになります。

この点はI.MAGiC開発者に御一考を賜りたいところですが、SetEventTimer/Afterが静的な値を要求するのにはそれなりの理由があるようにも思うので、無理は言えません。

 

 

 

 

 

 

先の逆方向進入センサースクリプト同様に、赤字で示した編成方向評価部については、else命令をはさんだ部分を入れ換えなければならない場合があります。

TOMIX信号レールに直接センサーを設置する場合は左サンプルそのままで問題ありません。ただし、こちらの拙稿に示したようなテクニックで信号を使う場合は、考慮が必要です。

 

 

 

 

 

これほど複雑な動作をおこなうスクリプトになると、他の書き方で同じ動作をするものが書けると思います。

今回のサンプルは必ずしも最適化されていません。特にMtdTimer内の信号モードに応じた処理の分岐部分は説明のしやすさを優先して故意に冗長な書き方をしています。腕に覚えのある方はよりスマートなコードに書き換えて、是非ネットに晒してくださいまし。

 

 

2005/10/27 修正

変数名の書き間違えを修正。一度直したものが9/5の追記時のバージョン管理の失敗で戻っちゃってた、orz

発見・報告してくださった512氏に多謝。

とんでもなく長いスクリプトですが、解説を加えてみます。一行一行読んでもあまり意味がないように思いますので、重要な部分のみをピックアップしていきます。それでも結構な分量になるでしょう。頑張って読んでみてください。

冒頭の変数ObjPartnerSignalは、先に示した逆方向進入センサーと同じです。続く「信号モード管理変数」がこの仕組みの肝となるものです。コメントにも記しているように、この信号機は6種類の状態を持ちえます。スクリプト内でこの値を順次変化させることで、一定時間待った後に信号がどのように変化すべきかを制御しています。大雑把にまとめると、この値は以下のように変化していきます。

ここでいう信号モードとは、信号が「今の瞬間どうであるか」と「次にどうするか」の組み合わせの中からあり得るものをピックアップしたもの、とお考えください。

<信号モード管理変数の遷移>

左側の筋が、概ねTOMIX製品の動作を真似たものになります。

右側の筋は、逆方向進入センサーが信号をR現示にしたままにしないために便宜上組み込んだロジックです。これにより、列車が信号を通過し終えた時点で信号がG現示に変化します。

続いて変数をいろいろ準備し、タイマーイベントとセンサーイベントを定義しています。これらについては使う場面で説明を加えることにします。

では、それぞれのメソッドを見ていくことにしましょう。メソッドMtdTrainPassはSetEventSensor命令によって設定されたセンサーイベントにより実行されます。つまり、列車がセンサーの上を通過した瞬間に、このメソッドに書かれたことが処理されることになります。

ここではまず、信号をR現示に切り替えています。続いて、編成の公開プロパティを更新し(call ObjTrain.StdMtdUpdateProperty)、その値を取得しています。この後、編成の速度と長さから、通過に要する時間を計算しています。これがVRMに列車末尾が通り過ぎたことを検知するセンサーがないことを補うための工夫に当たります。ややこしそうに見えますが、要するに・・・

@速度に1000をかける=単位をKm/時からm/時に変換

Aさらに3600で割る=単位をm/時からm/秒に変換

B編成の長さ(m)をAで得た秒速で割る=通過所要時間(秒)

Cさらに1000を書ける=単位を秒からミリ秒に変換

ミリ秒への変換は必須ではないが、VRMではミリ秒が時間の基本単位なので敢えてこうしています。

をやっているだけです。有り体に言えば小学校の算数レベル。mulとかdivという見た目に惑わされないように頑張ってください。早い話、mulは×、divは÷ですから。こうして変数VarMsPassに通過所要時間が得られました。改めて上掲の遷移図を見ると、この変数が「列車通過待ち」に使われることがわかると思います。

ちなみにこれは「踏切をいつ開くか」にも応用できます。試してみましたが、いい感じでした。リクエストがあれば解説を書きます。

続いて、編成の向きに応じて処理が分岐します。まずは正方向から列車が進入したものとして、メソッドMtdForwardへ目を移してみましょう。

 

メソッドMtdForwardの冒頭でif VarSignalModeとあるのは、この信号に初めて列車が進入したのか(信号モードは0:初期状態)、それとも2回目以上かをチェックするためです。初期状態の場合は、if〜endif間の処理はおこないません。これは、初めて信号を列車が通過する際には、前回の通過から今回の通過までの時間を計っている変数VarMsAgainの値が信用できないからです。この場合、信号遷移間隔を示す変数VarMsTransitはデフォルト値(スクリプトの冒頭で設定しているVarMsTransitDefault=3000ミリ秒)になります。ここでは、2回目で変数VarMsAgainにレイアウトを一周するのにかかった時間が記録されているもの考えて、何が起こるか見てみましょう。


<信号遷移間隔の考え方>

この算出方法がTOMIX製品とまったく同じかどうかには疑問がありますが、概ね近いと思って良さそうです。

上の図は、このメソッドにおける信号遷移間隔の算出方法を図示したものです。まず最初にsub命令でVarMsAgain(列車が一周してくるのにかかる時間)からVarMsPass(列車通過所要時間)を引いています。次に、この値をVarSignalType(このスクリプトでは4になる)で割っています。すると、上図右側の赤・黄・緑・緑の枠1つ分の時間になるのがおわかりいただけるでしょうか。

この結果得られた変数VarMsTransit(ミリ秒)を信号の切替タイミングにつかうと、上図に色づけしたように信号灯火が遷移していくことになります。つまり、列車が信号を通過している間はR現示、それから列車がもう少し離れるのを待ち、以降Y、Gと信号が遷移します。列車の速度が変化しなければ、再び列車が信号にさしかかる際には必ず信号はG現示になることが保証されます。

「速度が変化しなければ」というのはやや心もとない仮定ではありますが、この点はTOMIX製品もまったく同じです。

なお、この計算結果があまりに大きな値になったり、逆に例外的な出来事で負の数になってしまった場合にそなえて、続くif命令2つでVarMsTransitをデフォルト値に戻す処理を入れています。つまり、信号を通過後にしばらく駅停車して信号に再進入した(VarMsAgainは一周にかかる時間を大きく上回ってしまう)としても、信号遷移間隔は最大3秒に設定されるということです。

これもTOMIX製品と同じ挙動です。

最後に、時間計測用の変数をリセット(つまり0に戻す)し、信号モードを「2:列車通過待ち」に変更してこのメソッドは終わります。

 

実際に信号現示の遷移を担当するのがメソッドMtdTimerです。冒頭に2つのadd命令があり、変数VarTimerとVarMsAgainに100加算しています。このメソッドはSetEventTimer命令の設定によって100ミリ秒毎に実行されますから、これらの変数は「ミリ秒時計」になっていることになります。

以降、ifeq命令で信号モード(変数VarSignalMode)が2〜5の場合はそれぞれVarTimerの値とVarMsPassまたはVarMsTransitを比較して、信号を切り替えて信号モードを変化させています。これを図にすると先に示した<信号モード管理変数の遷移>になるわけです。

やや蛇足気味ながら最後にメソッドMtdBackwardについて。これは信号に編成が逆進入した場合の処理で、TOMIX製品にはない便宜上の措置です。こでは信号モード管理変数を5にしています。前段に書いたメソッドMtdTimerの最後の方に、この場合の処理がありますが、ここでは列車の通過を待って、信号をG現示に切り替えています。ちなみに、信号スクリプトの中でR→Gの遷移を禁止していましたから、ここでは前以て信号のプロパティStdVarSignalConditionに3(Y現示に相当)を代入しておいてから、StdMtdSignalGを実行しています。

で、こうなる

これだけ大変な思いをして、たったこんだけかい、という気もしますが・・・

さぁ、あなたもやってみよう!!

ここまで真面目に読んだ方、お疲れ様でした。

使い方を間違えさえしなければ、本稿で紹介したスクリプトをそのままコピー&ペーストして・・・あ、もちろん、いくつかのget命令に対してはあなたが信号部品に与えた名前を補う必要がありますが・・・さえやれば動作するはずです。原理が理解できた方は、ニ/四/五灯式信号用のスクリプトにも挑戦してみてください。

 

その気になればニ/三/四/五灯式すべての信号に対応するスクリプトも書けますが(既にそれに近いアイデアがサンプルコードに含まれています)それこそ魔法のようなスクロールを作るのでなければ、不毛な挑戦のような気もします。

量が量だけに、つまらない誤謬がある可能性もあります。バグに気付かれた方はBBSにご一報ください。即座に本稿に修正を加えます。また「このへんがよくわからない」といった質問にも可能な範囲で対応しますのでご遠慮なく。ただし「レイアウトファイルくれ」とか「代わりにレイアウト作ってくれ」には応じませんので悪しからず。

マジ半分・ネタ半分です。きっとウソもいっぱい書いてます。そこらへん適当に読んでください、よろしく。

 


Webmaster: ghost@nodus.ne.jp