「きっと誰も気づいてくれないので、遠方/中継信号機スクロールについて自分で書く」
VRMスクリプト禅問答
コレの仕組みについて。単に主信号の灯火色を真似ているようで、実は内部的にいろいろと凝ったことをしているのだけれども、きっと誰も気づいてはくれないので、自分で書く。でも、きっと誰も理解はしない。だから、本稿は
解説ではなく自慢である。嫌なヤツ。
* * *
その1。
主信号と従属信号(遠方/中継信号)の灯火を連動させたい場合、その最適手(最も無駄が少なく確実でシンプルな方法)は「主信号の灯火制御メソッドから従属信号の灯火制御メソッドをcallする」である。レイアウトに特化したスクリプトを手書きするのであれば、ボクはそうする。が、こと汎用(どのレイアウトにも後付で組み込むことが出来る)スクロールについてはこれは最適手にならない。
VRM4の現行SCRIPTウィザードには、既に存在するメソッドに対して命令を追加したり一部を書き換えたりする機能がない。したがって「主信号の灯火制御メソッドから従属信号の灯火制御メソッドをcallする」を実現するには、主信号制御のスクリプトを流し込むスクロールに、従属信号の灯火制御メソッドへのcallが含まれていなければならない。
これは「3灯式主信号で閉塞制御し、かつ中継信号に灯火を反映するスクロール」だとか「5灯式主信号をキー操作で遷移し、かつ遠方信号に灯火を反映するスクロール」だといった派生バージョンが無限に増殖することを意味している。これでは、最適手が最適手にならない。
そこで今回は(と言うか、ボクの書くスクロールは大抵そうだけれども)「従属信号から一定間隔で主制御信号の灯火色を調べにいく」手法を採用している。この手法には、主信号が遷移していようがいまいが、常に従属信号側から灯火色を調べるためのcallが発生するため、特に非力なPCで運用されているVRM環境では速度低下(フレームレートの悪化)が起こる場合がある、という欠点がある。
一方で、スクロールとして考えた場合、主信号側の灯火制御のロジックと完全に独立したスクリプトが書けるため、主信号側がどんな灯火制御の手法を使っていても対応できる、という強みがある。当然、前述したような派生バージョンの無限増殖も起こらない。灯火反映の方法が同じであれば、1つのスクロールで済むことになる。
こう書くと「SetEventTimerで主信号側でGetSignalして、その値を従属信号にSetSignalするだけでしょ、何を大袈裟なw」と思う人がいるかも知れない・・・いないか、そんなヤツ・・・けれども、話はそれほど単純ではない。目に見える動作(
昨日のデモ動画を参照)の裏で、地味だが欠かすことの出来ないロジックが黙々と仕事をしている。
その2。
今回のスクロールから、灯火遷移の際に一瞬消灯するギミックを取り入れたが、この状態で単純に一定間隔で主信号の灯火を従属信号に反映しようとすると、この間隔で従属信号が点滅することになる。これを回避するために、実際のスクリプトは以下のようになっている。
<中継信号スクリプトより抜粋>
ifeq SignalCondition SignalConditionSave //@
else
call this gws10MtdSetSignal
mov SignalConditionSave SignalCondition //A
endif
※掲載の都合上、変数名を一部変更している
変数SignalConditionには、直前に主信号の灯火色から割り出した、中継信号に設定すべき灯火色を示す値が入っている。たとえば、主信号の灯火色が2=警戒であれば、中継信号のそれは3=注意といったように。
これを@で変数SignalConditionSaveとifeqで比較している。直後にelseがあるので、以下のcallとmovは、SignalConditionとSignalConditionSaveの値が異なる場合にのみ実行されることになる。
そして、Aの部分でSignalConditionSaveにSignalConditionの値をmov命令で代入しているので、SignalConditionSaveは前回設定された=今この瞬間設定されている中継信号の灯火色である。つまり、@のifeq命令は「設定しようとする灯火色が、現在の灯火色と異なる場合のみ処理を実行する」という意味がある。
ここでcallされているメソッドgws10MtdSetSignalが、一瞬消灯のギミックを含む灯火色更新のメソッドなので、これによって前述した“点滅”が回避されていることがおわかりいただけるかと思う。わかる?
その3。
従属信号がそうである以上、主信号側にも一瞬消灯のギミックが組み込まれていることを想定しなければならない。これが何を意味しているかと言うと、従属信号から主信号の灯火色を調べにいくロジックは、主信号側の都合をまったく考えずに一定間隔におこなわれるので、ひょっとすると主信号側が消灯した瞬間にその灯火色を調べにいく、という事態が起こり得る。つまり・・・
<主信号スクリプト(悪い例)>
Var SignalColor //灯火色を公開するグローバル変数
BeginFunc MtdGetSignal
GetSignal SignalColor
EndFunc
のように書いた場合、中継信号側からメソッドMtdGetSignalをcallすることによって、“主信号.SignalColor”として得られる灯火色値は、動作上の灯火色を反映していることもあるが、ひょっとすると0=消灯かも知れない、ということになってしまう。この問題を回避するために、実際のスクロールのスクリプトは以下のようになっている。
<主信号スクリプト>
Var gws10VarGetSignal //灯火色を公開するグローバル変数
BeginFunc gws10MtdGetSignal
Var Tmp
GetSignal Tmp //B
if Tmp //C
mov gws10VarGetSignal Tmp
endif
EndFunc
味噌は、BのGetSinal命令で、公開用のグローバル変数に直接値を取得するのではなく、一旦ローカル変数Tmpで受けている点である。
これを続くCのif命令で評価している。これは「Tmpの値が0でなければ以下の命令を実行する」の意味なので、Tmpが0=消灯の場合、続くmov命令は実行されないことになる。これにより、全体としてのメソッドgws10MtdGetSignalは、以下のような意味を持つことになる。
・メソッドgws10MtdGetSignalをcallすると、グローバル変数gws10VarGetSignalに信号灯火色の値が得られる。ただし、callの瞬間に信号が消灯していた場合、最後に得られた有効な値(1以上)が保持される。
とまぁ、こんな感じ。
* * *
以上、本稿で述べた3つの手法、[1]独立した追加機能を作る、[2]値が変化したときだけ発動する機能を作る、[3]値が0でないときだけ発動する機能を作る、は、それぞれいろんなシーンで応用が利くと思われるので書いてみた。
と言うのは真っ赤な嘘。単に「ほんの10分かそこらでこういうスクロールが書けちゃうボクちゃんてばスゴ〜イ!!」って言いたかっただけ。でも、デモ動画作るのとこのエントリ書くのにそれぞれ1時間以上かかってますから、残念。