パラメータ領域

First Published:

VRMスクリプトをより柔軟に記述するために、「パラメータ領域」というのを考えてみました。メソッドにも引数を持たせてやろうという話です。

分かる人には、これだけで分かってもらえると思いますが、決して簡単な話というわけではないようです。ここまでで分からなかった人もまぁ読んでみてください。

注意!◆この記事に収録されているサンプルコードについて、AKAGIは一切その動作保証をいたしません。

いきなりサンプル

あれこれ説明するより、サンプルを見てもらったほうが簡単だと思います。次のサンプルでは、センサーを通過した編成を速度調整させます。編成の最高速度に関わらず、センサーが指定した速度に調整させます。

//センサー
Var Eid
SetEventSensor MtdSpeedCtrl Eid

BeginFunc MtdSpeedCtrl
    VarTrain pTrnSns
    GetSenseTrain pTrnSns
    setf pTrnSns.Prm1 45.0  //目標速度(N-km/h)
    set  pTrnSns.Prm2 10000 //調整時間(ms)
    call pTrnSns FncTimerSpeed
    setzero pTrnSns.Prm1
    setzero pTrnSns.Prm2
EndFunc
//編成
Var Prm1
Var Prm2

BeginFunc FncTimerSpeed
    Var tgtspd
    mov tgtspd Prm1    //目標速度
    Var adjtim
    mov adjtim Prm2    //調整時間
    Var topspd
    GetTopSpeed topspd
    div tgtspd topspd  //目標電圧=目標速度÷最高速度
    SetTimerVoltage tgtspd adjtim
EndFunc

簡単かつ単純なサンプルなので一見するとごく普通のコーディングに見えます。敢えて変なところを指摘すると、センサーが編成のFncTimerSpeedをcallしたあとに直前に代入した変数をsetzeroで初期化している点と、メソッドの接頭辞がMtdとFncの2種類ある点です。

それから、センサーが編成に値を渡す変数も、はじめからVarTgtSpdとVarAdjTimにすればいいのに、わざわざPrm?変数をクッションにおいています。さて、何を考えているのでしょうか。

何がしたいのか? -MethodとFunction-

Function(数学用語、マシン語)の邦訳には「関数」が当てられています。数学のy=f(x)の形に覚えのある方も多いと思います。すべての場合に当てはまるわけではありませんが、関数はxにあたる「引数(parametor)」とyにあたる「返り値(return)」を持ちます。与えられたxの値に対し決められた処理(計算)をして、yに結果が反映されるわけです。

Method(順序、筋道などの邦訳が当てられますがメソッドでいいです)も一般的には引数を持つことが多いですが、VRMに限って言えば一定の手続きをしてくれるだけで引数を持ちません。(これから先、「メソッド」はVRMにおけるBeginFunc-EndFuncで括られる引数を持たないものを指すこととします。)

メソッドが引数(パラメータ)を持たないのが、私にはとても不便に感じられました。Begin'Func'といいながら引数を持てないなんて!メソッドにも、スクリプト命令と同じように引数があればSetTimerSpeedみたいな新命令を作るのと同じことができるのに!

Prm?変数の役割

メソッドでの処理で、メソッド外から数を受け取りたいときにはグローバル変数を使用しなければなりません。ところが、メソッド毎に必要な変数をイチイチ定義していると初期化コードが散らかってきます。そこで、メソッド間の値の受け渡しを特別に定義された変数に一元化しようと考え、パラメータ領域としてグローバル変数Prm?(?は自然数)を定義しました。(これから先、「関数」はグローバル変数Prm?の値を引数とみなして処理を行うメソッドを指すものとします。)

各関数にPrm?の使い方の約束事を設け、それに合わせてcall元がPrm?に必要な値をsetし、関数はそれを使って処理を行います。処理が終わったら念のためsetzeroで掃除をしておいてやりましょう。

ひとつの関数がPrm?変数を使用するのは一瞬であるため、次の一瞬では別の関数が同じPrm?を使うことが出来ます。ここが最大のポイントです。冒頭の例のように、他オブジェクトのみで取得できる値を受け渡すためにわざわざ新たな変数を定義する必要はありません。

ちなみに、冒頭で関数内のローカル変数にPrm?の値をmovするのは、変数名にあまり意味が無いPrm?よりも、意味のある名前の変数のほうがコードを書きやすいからです。Prm?を直接いじっても、目立った問題はありません。(むしろ、余計なローカル変数は定義しないほうが処理が軽いという説もあります)

気になる人には気になる変数の型の問題(時によって中身の型が異なる)ですが、VRMにおいては一応気にしなくても済むことになっています(setまたはsetf時に以前の型はクリアされます)。代入するときの型と使用するとき必要とされる型が異なると処理が重くなるので、この点だけ注意します。

返り値を使う

例えば、何らかの目的でセンサーが通過した編成の走行速度を知りたいものとします。GetCurrentSpeed命令は編成に記述しなければならないので、センサーがこの結果を知るには編成のグローバル変数をひとつ使わなければいけません。

//センサー
Var Eid
SetEventSensor Mtd Eid

BeginFunc Mtd
    VarTrain pTrnSns
    GetSenseTrain pTrnSns
    //引数の設定はなし
    call pTrnSns FncCrtSpd
    Var trainspd
    mov trainspd pTrnSns.Prm1  //ローカル変数にコピー
    setzero pTrnSns.Prm1       //値の破棄も忘れずに。
    DrawMessage trainspd
EndFunc
//編成
Var Prm1
Var Prm2

BeginFunc FncCrtSpd
    GetCurrentSpeed Prm1
EndFunc

この例では、編成のGetCurrentSpeedの結果をPrm1に保存し、センサーが自身のローカル変数にコピーして使用(といってもログに出すだけ)しています。

編成の走行速度など時によって値が変わりうるものは、専用の変数を作って値を入れっぱなしにしても意味がありません。パラメータ領域を使えばグローバル変数が増えず、無意味な値が残らないので一石二鳥です。

さらなる活用

複数のメソッドで、ある程度の長さの共通部分がある場合、それをまとめることができ、のちのメンテナンスの手間を軽減させることが出来ます。

冒頭で示した速度調整のサンプルをモリモリ増強して、指定加減速度で調整するようにします。おまけにショボイAutoSpeedCtrl命令の代わりも作ってみましょう。

//編成
Var Prm1
Var Prm2 //パラメータ領域

//専用領域
Var VarDstCtrl
Var EidDstCtrl

Var VarAccel
Var VarBrake
setf VarAccel 4.0
setf VarBrake -5.0 //加速度と減速度の設定[km/h/s]

BeginFunc FncSpdCalc //int所要時間[ms] = FncSpdCalc(開始速度, 終了速度)
    Var bgnspd
    Var endspd
    mov bgnspd Prm1
    mov endspd Prm2   //パラメータのコピー
    sub bgnspd endspd //速度の差
    if> bgnspd 0.0    //減速の場合
        div bgnspd VarBrake
    else              //加速の場合
        div bgnspd VarAccel
    endif
    mul bgnspd 1000.0
    cnvint bgnspd
    mov Prm1 bgnspd
EndFunc

BeginFunc FncSpdCtrl //null = FncSpdCtrl(float目標速度)
    Var tgtspd
    Var crtspd
    mov tgtspd Prm1  //パラメータのコピー
    GetCurrentSpeed crtspd
    ifeq tgtspd crtspd //例外処理
        DrawMessage "[ERROR]同じ速度に調整しようとしています"
        return
    endif
    Var time
    mov Prm1 crtspd
    mov Prm2 crtspd
    call this FncSpdCalc
    mov time Prm1   //返り値(速度調整時間)のコピー
    setzero Prm1
    setzero Prm2
    Var topspd
    GetTopSpeed topspd
    div tgtspd topspd //目標電圧の算出
    SetTimerVoltage tgtspd time
EndFunc

BeginFunc FncDstCtrl //null = FncDisCtrl(float目標速度, float調整距離[N-mm])
    KillEvent EidDisCtrl //二重制御の禁止
    setzero VarDstCtrl
    Var tgtspd
    Var distance
    mov tgtspd Prm1
    mov distance Prm2    //パラメータのコピー
    Var crtspd
    GetCurrentSpeed crtspd
    ifeq crtspd 0.0
        DrawMessage "[ERROR]0km/hからの距離指定加速はできません"
        return
    endif
    Var ctrltime
    mov Prm1 crtspd
    mov Prm2 tgtspd
    call this FncSpdCalc
    mov ctrltime Prm1    //ctrltime == 減速に要する時間
    setzero Prm1
    setzero Prm2
    Var ctrldis
    mov ctrldis crtspd
    add ctrldis tgtspd
    mul ctrldis ctrltime
    mul ctrldis 0.9259259  //ctrldis == 減速に要する距離
    Var topspd
    GetTopSpeed topspd
    if>= ctrldis distance //減速が間に合わない場合
        div tgtspd topspd  //目標速度を電圧に変換
        AutoSpeedCtrl distance tgtspd
    else
        sub distance ctrldis //distance == 現状維持の距離
        div distance crtspd
        div distance 0.00185185
        cnvint distance //distance == 現状維持の時間[ms]
        div crtspd topspd
        SetTimerVoltage crtspd distance //速度を固定(カーソル操作禁止)
        mov VarDstCtrl tgtspd
        SetEventAfter this FncDstCtrl2 EidDstCtrl distance
    endif
EndFunc
BeginFunc FncDstCtrl2
    mov Prm1 VarDstCtrl
    call this FncSpdCtrl
    setzero Prm1
    setzero VarDstCtrl
EndFunc

最早サンプルとは言えないほどに長いし、色々な例外処理まで組み込んでいるので解りづらいかもしれません。中身のコードの具体的な解説は省略しますが、Prm?に適当な値を入れてやった上でcallすると…(Prm?のsetzeroは呼び出し側で行う)

  • FncSpdCtrl 指定された加減速度で速度調整を始めます。
  • FncDstCtrl 指定距離・指定加減速度で指定速度に調整します。
  • FncSpdCalc 指定加減速度で速度調整するのに必要な時間を返します。

FncDstCtrlは指定より前に調整が終わらないように調整開始を待機します。待機時間中、最終的な目標速度を保存するために専用の変数を設けています。ちなみに、調整が間に合わない場合にはAutoSpeedCtrlで既定の加減速度を無視して調整を始めます。

FncSpdCalcは、計算結果を返すだけでオブジェクトの見た目に反映されませんが、二つのメソッドの共通部分を統合し、のちのメンテナンスの手間を軽減させます。電圧を求める部分も独立させていいような気もしますが、そんなにメリットが大きくないのでそのままにしています。

ちょっと説明が長くなってしまいましたが、色々な値の受け渡しが必要なときも、パラメータ領域の考え方を用いれば少ない数のグローバル変数で済みます。メモリの節約にも繋がり、軽量化が期待…できるかもしれません。

ちなみにこの例では、関数の呼び出し構造が入れ子になっている(関数の終了までに、他の関数の呼び出しのためにパラメータ領域が書き換わる)ため、Prm?を直接操作してはいけません。必ずローカル変数にコピーします。

ちょっと荒技

今までパラメータ領域はPrm?変数二つでまかなっていましたが、これが足りなくなったらどうしますか?新しくPrm3変数を定義してもいいとは思いますが、それじゃ能がないのでベクトル型変数を利用する方法を紹介します。

ベクトル型変数も他の一般変数と同様にVar命令で定義できます。ということは、必要な領域も他の一般変数と同じと推測されます。ベクトル型変数はfloat型の値を3つ持てますから3倍お得ですね。逆に、使わないと損だとも言えますが。

あくまで、ベクトル型変数の各要素の値はfloatなので、メソッド内での取り扱いには気をつけて下さい。一応大丈夫らしいのですが。また、ベクトル型変数をsetzeroしても型はベクトル型のまま(0.0, 0.0, 0.0)になり、3つの要素が残る点にも注意が必要です。そのままmovして使うとエラいことになるかもしれません。 (set, setf, SetVector命令でPrm?変数の型はリセットされます)

//呼び出し側
SetVector Obj.Prm1 1.0, 2.2, 0.5
call Obj Fnction
setzero Obj.Prm1
//呼び出され側(?)
Var Prm1

BeginFunc Function
Var a
Var b
Var c
GetVectorX a Prm1
GetVectorY b Prm1
GetVectorZ c Prm1
cnvint a
/////////////////
EndFunc

ひとつのベクトル変数=3個の引数だとちょっと心細い感じですが、二つ用意しておけばint,float2個とか、ベクトル型2個で最大6個などと柔軟に運用できるんじゃないかと思います。パラメータ領域の変数をいくつ定義するかは、まぁお好きになさってください。

ちょっと裏技

裏技でもなんでもないですけど、思いつかないと使えないと思うので書いておきます。

一般変数とそれぞれのオブジェクト変数は相互に型変換できないので、一般変数として定義されているパラメータ領域Prm?にget命令で無理矢理オブジェクトをぶち込もうとしてもダメです。しかし、Ver.5.0.5.43ぐらいからGetObjectID命令が実装され、オブジェクトIDという形で整数型でオブジェクト参照ができるようになりました。関数で柔軟なオブジェクト参照を行うために活用できます。

//呼び出し側
GetObjectID obj.Prm1 OBJECTNAME
call obj Fnction

活用できるとは思いますが、具体的な用途が思いつきません(汗)。連結・開放でひょっとすると使えるかも…?

まとめ

長いサンプルやら何やらで結論が見えづらくなっていると思うのでまとめます。

  • オブジェクト間・メソッド(関数)間の値の受け渡しを簡単かつ単純に出来る。
  • スクリプトの初期化部分がスッキリし、メモリの削減に繋がる
  • 共通している処理の独立が容易にでき、保守性が向上する

いざ書きだしてみると、こんだけかよ、な気分でもありますが、今や私はコレなしではスクリプトが書けないぐらいにパラメータ領域を愛用しています。後半の長いサンプルが象徴的ですが、パラメータ領域はスクリプトの構造が複雑化すればするほど有用性が高まってきます。

ここまで読んできて、もしサッパリ理解できなかった方がいらっしゃっても、それは多分、あなたがアホなのではなくAKAGIがアホなんです。必要なら直接、ご質問いただければできるだけ丁寧に回答しますが、パラメータ領域自体が何にでも使えばいいというものでもないので、分かるようになるまでお預けにしておくとか、分からないまま放置しておくのもそれはそれでアリだと思います。

用語集

この記事内では、公式のVRMスクリプトの仕様とは関係の無い、独自の用語を使用しています。念のため列挙しておきます。

パラメータ領域
メソッドに外部から値を引き渡すために特別に定義されたグローバル変数群。一般的なマシン語の「スタック」に近いものをプロパティで代用している。
Prm?
パラメータ領域の個別のグローバル変数(一般変数)の変数名。?には自然数が当てはまる。。
メソッド
この記事では、VRMスクリプトの仕様上、一般的な「メソッド」を指す。BeginFunc-EndFuncで括られる一連の処理。
関数
この記事では、パラメータ領域を使用して処理を行うメソッドを指す。メソッドと差別化を図るためこう呼んでいる。