AKAGI Rails

鉄道模型シミュレーターで遊んでいたはずが、気づいたらPythonなども。

編成側からセンサー間の距離測定ができるか

VRMNXpyで,2つのセンサー間の距離を,編成側から検測してみたいと思います。

単純な直線距離であれば,センサーの座標から簡単な計算で求めることができますが,線路に沿った距離はそう簡単には求められません。もちろん,丁寧に計算すれば求められますが,とても面倒です。ですので,Python処理を駆使して,線路上を走り回る編成オブジェクトから距離測定をしてみたいと思います。

フレームイベントを有効にしてデータを蓄積して距離を測るのですが,基本原理はおおまかに2つ考えられます。

  1. 毎フレームの座標  \vec{x} =trainobj.GetPosition() を記録する。1フレームで座標上を移動した距離  \sqrt{| \vec{x_t} - \vec{x_{t-1}}| ^2} を累積して距離とする。

  2. 毎フレームの描画時間  t と,編成の走行速度  v = trainobj.GetCurrentSpeed() を記録する。1フレームで座標上を移動した距離  v \Delta t を累積して距離とする。

1フレームの描画にかかる時間は必ずしも一定ではないので,時間積分を伴う 2. のやり方は誤差が出そうにも思われますが,かわりに実装上の演算が四則演算で済む(ルートの計算がいらない)ので計算コストが安そうです。

ということで,実装してみました。(コードは汚いので貼りませんw)

距離測定の結果

1周2851.48mmの変形オーバルコースに1個だけセンサーを設置しました。同じセンサーを2度踏むことになりますがで,コース長を繰り返し測ることができます。車両は雰囲気重視でDE10 + TOMIXクリーニングカー(USO800鉄道氏自作車両)です。East-iもってないので。

2回繰り返して測定し,このときは2回とも約2849mmという測定結果でした。若干短いですがこんなもんでしょう。

閉塞制御を実現する場合に,歓呼センサーと直下センサー間の距離を測定する用途などに使えるかなと思っています。

今回はセンサー間の距離測定を行いましたが,ほかにも,編成部品にレイアウト上のデータを色々集めさせることができるかもしれません。

  • センサーを通過する順序を記録する。
    • 閉塞の基本データを自動で生成する。
  • 線路上の走行位置をひたすら記録して走行経路の形状を取る。
    • 移動閉塞の基礎データに?

ATENXAでの閉塞式信号制御は,過去のレイコン作品で閉塞制御を組み込んだのが死ぬほど面倒くさかったことを教訓とし,組み込みの手順がさらに簡単になるようにしたいと思って,いろいろ考えています。それでは。

VRMNXpyで名前空間はどうなっているのか

本稿では、VRMNXpyにおける名前空間、すなわちあるオブジェクトが参照されうる範囲(見える範囲=スコープともいう)がどのようになっているのかについて述べていきます。また、その結果として,どのような実装パターンが良さそうか、についても考察を与えます。

なおAKAGIの理解の範囲内では、Pythonにおいて変数とオブジェクトの違いは曖昧である(ことにしておいたほうが都合がいい)ので、本稿においてもこれらの用語を混同して使用します。混同しているとハマる罠があるのは知っていますが,一旦これは棚上げにします。

ローカルとグローバル

関数 (function) の中で生成されたオブジェクトは「ローカル変数」となり、実行中のその関数の中でのみ参照や書き込みが許され,その関数の処理が終わると廃棄されます。別の関数からはもちろん、一度処理が終わった関数をもう一度呼び出したとしても、以前の実行時の変数の内容は破棄されていて知ることはできません。

一方で関数の外で生成されたオブジェクトはグローバル変数となり、削除しない限りはその後のランタイムのどこからでも参照することができます。ですから,なにか作った値を,将来,別の関数などから参照したいと思っているのなら,何らかのグローバル変数に保存しておく必要があります。

グローバル変数に関数内から何かを書き込む

前述のように,将来,なにかの値を今実行中の関数とは別の関数で使いたい場合には,グローバル変数に保存しておく必要があります。何らかの関数内からグローバル変数の内容を書換えたいときには一手間かかります。

some_global_var = None # これはグローバル変数

def some_function(arg):
    global some_global_var
    # グローバル変数 some_global_var をこの関数内から書き込み可能にする
    
    result = arg * 2
    some_global_var = result # グローバル変数に保存した

このように,関数内でglobal文を用いて,グローバル変数some_global_var と解釈するように明示する必要があります。

VRMNXpyの実践上,注意しておくべき事柄は,イベントハンドラも関数であるという点です。ですから,イベントハンドラ内で vrmapi を通じて取ってきた何らかの値を将来使えるように保存しておきたければ,保管用のグローバル変数イベントハンドラの外で定義する必要があります。

具体例の紹介は省略しますが,グローバル変数を使わない別の方法もあります。それはVRMNXの各オブジェクトが持つdictを使う方法です。

部品を跨ぐコードのスコープ

VRMNXでは,レイアウト上に配置されたセンサーや編成や車両や信号機といった「部品」ごとそれぞれに,Pythonスクリプトを書き込む「スペース」が用意されています。

バージョン5までのVRMスクリプトとは,部品とスコープの扱いがだいぶ変わりました。

早い話, すべての部品は1つのスコープを共有していて,全部品のPythonスクリプトから,全部品のグローバル変数を直接参照可能 です。ただし,いくつかの注意事項があります。なぜなら,VRMNXがビュワー起動までにPythonスクリプトをどう扱っているか,イメージできていないと若干トリッキーだからです。マニュアルにはちゃんと書いてありませんが,実験的なコードをいろいろ書いて試した範囲では,ビュワー起動時に以下のような手順を踏んでいることが推測されます。

  1. ビュワー起動時に,レイアウトオブジェクトのPythonスクリプトが実行される。
    • 注) イベントハンドラを含む関数は,名前が拘束されるだけで中身の実行はまだされない
  2. その他の部品のPythonスクリプトが実行される。(順番は不定らしい。)
  3. レイアウト→その他部品の順で,initイベントがキックされる。
  4. 画面の描画が始まる

(この程度に理解しておけば,今のところVRMNXpyをそれなりに思った通りに動かせます。)

この挙動の中で注意すべきなのは,1や2の最中で,まだ定義されていない別オブジェクトのグローバル変数を参照しようとすると参照エラーが起きる可能性がある点です。この場合,ビュワーがクラッシュすることも想定されますが,一方でVRMNXビュワーが完全に起動していないのでLOGウィンドウが使い物にならないかもしれません。ですから, 初期化部分では不用意な動作を書くべきではありません。 vrmapi も不用意に使用しないほうがいいと思っています。 せいぜい,モジュールのインポートや,初期化だけしておきたいカラの変数の定義をするに留めておくのが良いでしょう。これは,VRMNX用の自作モジュールにも当てはまるでしょう。

ちょっと古い作例ですが,自作踏切モジュールは,踏切部品(親)のPythonスクリプトの初期化部分で定義したグローバル変数を,子の踏切部品からも参照しちゃう作例になっています。(ただしこのような実装は可能ですが,私のVRMNXpyの理解の途上でやってみた実装に過ぎず,別の部品のコードで定義したPythonオブジェクトを参照するという分かりにくさがあるので,常におすすめできる方法ではないと今は考えています。)

VRMNXpyの実装パターンはどうなっていくか

ここまでグローバル変数の話をしましたが, グローバル変数を使わない 方法によりわかりやすい実装をすることができるのではないかと思っています。グローバル変数の代わりに使うのが,レイアウト外部の自作Pythonモジュールです。

撮る夫くんやATENXAは,いろいろなレイアウトに使い回せる汎用性を意識して作っているものですが,レイアウトごとに,ad hocPythonモジュールを用意することはできるでしょう。例えば,

(root)
├ MyLayout.vrmnx
└ mymodule.py

のように自作モジュール (mymodule.py) を用意します。中身は簡単に次のように想定します。

# mymodule.py

my_global_var = None # このグローバル変数をなにかに使おう

レイアウトスクリプトの中で,vrmnxの次にmymoduleをimportします。

# レイアウト
import vrmnx
import mymodule

def vrmevent(obj, ev, param):
    pass # 以下省略

すると,例のグローバル変数は他の部品からは mymodule.my_global_var で参照できます。

このようにして,

というルールを敷いておけば,別の部品のグローバル変数のつもりで参照したり書き込んだりという混乱の元を,いくぶんか減らせるのではないかと思います。

(小規模なレイアウトで小規模なギミックを実装する範囲に留まるのであれば,これに限りませんが。)

さらにグローバル変数に対するアクセスポリシーを厳しくするには,アンダースコア _ で始まる変数名を使うことです。Pythonでは,アンダースコアで始まる変数は「内部ONLY」の意味を持つと暗黙に約束されています。(たとえグローバル変数であっても。)アンダースコアで始まる内部ONLYのグローバル変数は,グローバル変数ながら,モジュールの外部からのアクセスをしてはいけないという明確な表示になります。詳しくは,PEP 8の「実践されている命名方法」などを参照されるといいでしょう。

撮る夫くんでは,内部ONLYのグローバル変数を多用しています。興味のある方は撮る夫くんのソースをご覧になってみてください。

まとめ

あまりまとまりがありませんが,VRMNXpyの変数のスコープについて語ってみました。VRMNXならではの重大事項として,各部品はスコープを共有しているということに注意が必要です。さらに,ビュワー起動時の内部動作がどうなっているかについて多少の想像を及ばせておく必要があります。このほかは,Python一般の知識がきちんと通用すると思いますので,分かりやすく,間違えにくいコードを書いていく努力をしていけばよいものと思われます。

自作車両201系気動車の車両テクスチャで遊ぶ

しばらく更新が途絶えていましたが,一応元気にやっています。

さて,昨年リリースした自作車両キハ201系は,VRM5規格ですので当然,車両テクスチャの書き換えに対応しています。(もちろん731系も!)

今回は,自作車両の配布アーカイブに同梱はしていますが誰も気がついていない(?)キハ201系用書き換えテクスチャを少し紹介します。配布アーカイブには,既に設定を済ませたVRMNX規格編成ファイルも入れてありますので,NXのかたはすぐにロードできます。

題材として快速「ニセコライナー」をチョイスしました。編成はD-103/D102の6両編成。3両は小樽止,残りの3両で倶知安をめざす下り列車を再現しています。2倍サイズの高精細度テクスチャで再現しています。

f:id:AKAGI-vrmstation:20220125232924p:plain
ニセコライナー

f:id:AKAGI-vrmstation:20220125233133p:plain
快速幕は赤地紋

標準テクスチャは,ヘッドマークがあった時代の731系快速「マリンライナー」にあわせて登場初期の国鉄デザインでしたが,今回作成したニセコライナーはリニューアル後のデザインになっています。

続きを読む

ATENXA式イベントの仕組み

前回はatenxa.richeventを使った時限式信号機のサンプルを紹介しました。

akagi.hateblo.jp

今回はその別語として,ATENXA式イベントシステムの仕組みをもう少し詳しく書いていきます。

サンプルコード

昨日の記事からの再掲です。まずはレイアウト。

#LAYOUT
import vrmapi

# レイアウトと同じ場所のatenxaパッケージをimportするためにこの2行を書く
import os, sys
sys.path.insert(0, vrmapi.SYSTEM().GetLayoutDir())

from atenxa.richevent import richevent # 有効化コマンドをインポートした

def vrmevent(obj,ev,param):
    richevent(obj,ev,param) # ATENXA式イベントシステムを有効化
    if ev == 'init':
        pass
        # 以下省略

そして,次がセンサーのスクリプトです。

#OBJID=515
import vrmapi
from atenxa.richevent import AfterEvent #AfterEventを読み込み

def vrmevent_515(obj,ev,param):
    if ev == 'catch':
        # センサー通過時の動作
        pairsignal = vrmapi.LAYOUT().GetSignal("RAILSIGNAL_516") # ペアの信号のデータ名を設定
        d = obj.GetDict()
        if param['tire'] == 1:
            # 先頭台車の検出時
            # 既存のタイマーイベントを削除
            try:
                d["ev_up3"].kill()
            except KeyError:
                pass
            try:
                d["ev_up6"].kill()
            except KeyError:
                pass
            # 即時信号を停止現示に
            pairsignal.SetStat(0, 1)
        else:
            # 後尾台車の検出時
            # 5秒後に注意現示
            d["ev_up3"] = AfterEvent(5, pairsignal.SetStat, args=(0, 3))
            # 10秒後に進行現示
            d["ev_up6"] = AfterEvent(10, pairsignal.SetStat, args=(0, 6))
        
    elif ev == 'init':
        # 起動時の初期設定
        # 前後台車の両方を検知
        obj.SetSNSMode(2)

昨日も強調しましたが信号機には何も書く必要がありません。

続きを読む

時限式信号機をATENXAで

怎麼生

ATENXA踏切が使えるようになり次は信号機を実装してほしいです。かといって閉塞の設定は面倒くさいので,TOMIXの信号機のように,列車通過で青→赤にしたあとは時限式で順に黄→青になってくれればいいです。なんとかなりませんか。

f:id:AKAGI-vrmstation:20210730233710p:plain

  • 怎麼生
  • 説破
    • ATENXA式イベントシステムとは
    • atenxaのセットアップ(レイアウト)
    • 信号機とセンサーの配置
    • センサーのスクリプトを設定
  • 著語

説破

時限式信号機はATENXA式イベントシステムで簡単に実装することができます。Python言語の便利なところを活用して,スクリプトを書くのはセンサーだけで,信号機はノータッチで済む方法をご紹介します。

ここでは,自動センサー1つではダイアログ設定のみでの実現が難しい,

  • 編成の先頭が通過すると赤にする
  • 先頭の最後尾が通過してからx秒で黄,y秒で青にする

というギミックを実装し,多少自分でスクリプトを書く努力が報われるようにしてみます。

続きを読む

ATENXA自動踏切,機能を公式にパクられる

とくに申し合わせたわけではありませんが,ATENXA自動踏切の機能がめでたく純正VRMに実装され自動センサーのダイアログから設定することができるようになりました。めでたしめでたし。

akagi-rails.github.io

↓疑惑の公式機能

vrmcloud.net

自動センサーの方向表示機制御およびVRMCrossing.Inc/DecAutoSignStatusと,ATENXAの自動踏切制御をまぜこぜにするとよくないそうですのであしからず。

公式機能を使う場合は,センサーの数 m, 警報機の数 n に対して,mn 個の動作設定をしなくてはいけませんが,ATENXAの場合は m+n 個の設定で済むよう設計しております。また,遮断器ごとに異なる動作遅延時間を設定することができるので,特に大規模な踏切においてはATENXAのほうが設定が楽ちんだと思います。

ATENXA公開しました。

ATENXA - Advanced Toolkit for Enhancing VRMNX by AKAGI

akagi-rails.github.io

Discordで先行公開しておりましたが,VRMNXpyで多彩なギミックを実現するPythonパッケージATENXA(アテンジャ,もしくはアテンザ)を公開しました。撮る夫くんに引き続き,githubリポジトリ内で管理しております。

主要な機能

現時点で出来ている主要な機能です。

自動踏切

今回,こちらの実装が一段落したので初回のリリースをしました。遮断機の動作遅延や,方向表示機の点灯に対応しています。単線・複線に限らずどんな配線のパターンにも対応しています。

スクリプトの組み込みは,基本的に1アイテム1行で,簡単です。Pythonのリストや辞書といったデータ形式のことはほとんど意識せずに,ほぼコピペで動くものを仕立てられるようにしました。詳しいことはチュートリアルをご覧ください。

漆黒さん,ばんえつラインさんに早速ご使用いただいています。

失礼ながら申し上げると,漆黒さんはたぶんお年も召しておられるはずで,Pythonベースのスクリプトで使ってもらえるかしら…と不安に思っていた部分があったわけですが,多少お手間をかけさせてしまいましたが無事作動に漕ぎ着けられたとのことで,まず感服している次第でありますが,簡単に組み込める仕様を徹底した甲斐があったなと安心しているところです。

ATENXA式イベントシステム

ワンライナーの簡単機能組み込み以外に,スクリプトが自分で書ける人向けの便利な機能も持っています。

VRMNXのイベントの取り扱いは,旧バージョンに比べて難化しています。VRMNXpyの設計自体,どういう実装をユーザーにさせたいのかあまりハッキリしない気がするので,こういうものを用意してみました。

ざっくり言えば,旧バージョンのイベントシステムのような感覚で,時間系イベントを定義することが出来ます。特徴として,実行する関数だけでなく,そこに渡す引数を一緒に指定することが出来ます。例えば,AfterEventであれば

AfterEvent(1.5, trn.SetTimerVoltage, args=(5.0, 0.2))

を実行すると,1.5秒後に

trn.SetTimerVoltage(5.0, 0.2)

が実行されます。

例えばですが,センサー検知,減速・停止,数秒後発車,みたいな動作をセンサーのスクリプトへの記述だけで完結するような実装も可能です。 もちろん自動踏切もこのイベントシステムに依存しています。(というか,自動踏切がこのイベントシステムの実証実験みたいな感じです。)

NXになってKillEventがなくなりましたが,ATENXAの中ではこれも使えるようにしました。これでウザいフラグ管理からはおさらばできます。たぶん。

レイアウト固有の実装をATENXA式イベントシステムで行うには,レイアウトスクリプトricheventを記述する必要があります(VRMAPIからのイベントの橋渡しになります)。マニュアルも割と一生懸命書いたので,ぜひこれを使っていろんなギミックを作ってみてください。

ログ出力

中身は他でもなく,昔記事にしたものを取り入れました。

akagi.hateblo.jp

新しく,標準Pythonのpprintの機能を模擬した, pprintLOG も用意しました。 リストや辞書を見やすく整形してログに出力できます。もっぱら自分がデバッグに使ってます(爆

スプライトのラッパーと速度計

VRMNXのスクリーンスプライトは実装が気に入らないので全部ラッパを書きました笑

スクリーンスプライトを使用した速度計も作って,これもワンライナーで簡単に組み込めるようにしました。

シンプルな速度計

akagi-rails.github.io

MeterSimpleの正体はクラスです。中身に,スプライトを初期化するメソッドや,速度を取得してスプライトを表示するメソッドを持っています。ということで実は,だれでもこのクラスを継承して必要部分のコードをオーバーライドすることにより,好きなリソース画像を使った速度計を作ることが出来ます。 興味のある方はGitHubで実装も見てみてください

凝ったこともやろうと思えば出来ます。atenxaをインポートして何某かの仕掛けを作る例として,後日このサンプルをご紹介するつもりです。

どこかで見たことあるような・・・

計画中の機能

ATENXAは他にもいくつかの機能を計画中です。

時隔式信号機など簡易ギミックシリーズ

TOMIXのTCS信号機のように,列車通過から一定時間で赤→黄→青のように現示がアップする信号機のギミックを予定しています。ただし,ちゃんとしたモジュールになるかどうかは未定です。そんなに難しくないので,atenxa.richeventを使ったコーディングのサンプルとして供するかもしれません。

簡易ギミックシリーズとして,他に,解放ランプ付きレール(みたいなセンサー)も検討中です。

自動運転

これまたTOMIXのTCS自動運転ユニットか,もしくはA列車で行こう9でいうところの「個別発車」によらないシンプルなダイヤ設定の機能か,そのあたりを参考にすると思いますが,自動運転の機能は実装したいと思っています。

ダイヤ運転は,ダイヤを外部ファイルに用意して読み込むなどして実現できる可能性は高そうですが,検討事項も多いので将来課題です。

閉塞制御

信号機と連動するATSとかATCが,当面のATENXAのゴールです。旧バージョンで開発にチャレンジしていたakg/2.0シリーズでは,追い越しまではなんとか作ったものの折り返し運転に対応できずに終わってしまいましたので,そこもリベンジしたいですね。

ATENXAのネーミングはATACSにあやかった*1ところでもありますが,移動閉塞をやる気はありませんw

一度プロトタイプは作ってみたのですが,組み込みの手間の削減を徹底すべく,1からやり直しています。

おわりに

以上,初回リリースに付けた機能に加えて,色々と風呂敷を広げるような話もしてみました。今のところ,ここに書いたようなことしかアイデアになっていないので,「他にこういうことがしたい!」というアイデアをお持ちの方は,コメント,Discordチャンネル,GitHubなどでお寄せ願います。また,ATENXAではリクエストやバグレポートのみならず,GitHubのシステムを活用してプルリクエストも歓迎します。これはPythonという汎用言語と融合したVRMNXならではの成長の仕方の1つと思われ,チャレンジしてみる価値があるはずです。マージするかは気分次第ですが(笑)

*1:開発初期は閉塞制御をメインに考えていたので,「ATなんとか」にNXをねじ込むことを考え,某自動車みたいな名前になりました。Advanced Toolkit...は後付けですw