AKAGI Rails

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

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式イベントの表面的な仕組み

AfterEventの基本的な動き

ATENXA APIリファレンスからの引用ですが,AfterEventを実行すると

interval秒後に引数args,キーワード引数kwargsでcallbackを実行します。 args*がNoneなら空のリストが使用されます。

したがって,

AfterEvent(5, pairsignal.SetStat, args=(0, 3))

を実行したら,5秒後に

pairsignal.SetStat(0, 3)

が実行されます。

VRMPoint.SetBranchのように,引数が1個の関数をコールバックに使うときは,

AfterEvent(5, pairpoint.SetBranch, args=(1))

のように,argsには長さ1のタプル(またはリスト)を与えるようにしてください。値をそのまま渡してはいけません。

AfterEventの戻り値はなんなのか

AfterEventの正体は関数ではなくクラスです。よってAfterEventの戻り値はAfterEventクラスのインスタンスです。つまり,

# 5秒後に注意現示
d["ev_up3"] = AfterEvent(5, pairsignal.SetStat, args=(0, 3))

の部分では,AfterEventクラスのインスタンスを生成して,センサーのdictに保存しています。

今回の使用例では,あとからイベントをキャンセルする場合があるので,AfterEventインスタンスを保存してあとから参照できるようにしています。AfterEventは実行したらイベント自体は有効になるので,キャンセルするつもりがなければ保存しておく必要はありません。

イベントをキャンセルする

繰り返しですがAfterEventは,インスタンスを生成するとただちにカウントダウンがスタートします。(threading.Timerのようなstartメソッドはありません。)すでにカウントダウン中のイベントをキャンセルしたい場合は,AfterEventオブジェクトのkillメソッドを呼びます。

d["ev_up3"].kill() のようにして保存してあったイベントをキャンセルすることができますが,すでに指定時間が経過してアクションが済んだイベントをキャンセルしようとするとエラーになる可能性があります。そのエラーは放っておくとその後の処理が行われずうまくいきませんので,try...except節でエラー処理をしています。(ただし,使用済みのイベントのクリーンアップに関するatenxa側の仕様は今後変更の可能性があります。例外処理の中身がただの pass は相当マヌケなコードなので。)

センサーのイベントハンドラの処理順序

atenxa.richeventの中身の話に移る前に,センサーのイベントハンドラでの処理順序が "catch" が先で "init" が後という,直感に反する順序である理由について一言触れておきましょう。

パターンA (デフォルトの書き方):

def vrmevent_xx(obj,ev,param):
    if ev == 'init':
        # do something
    elif ev == 'catch':
        # do other thing

パターンB (今回の書き方):

def vrmevent_xx(obj,ev,param):
    if ev == 'catch':
        # do something
    elif ev == 'init':
        # do other thing

上の2つのコードを比べてみますと,パターンAの場合,catchイベントが発生した際に必ずev"init"であるかどうかを評価し,違うことを確かめた上で"catch"と比較します。ビュワー起動時のたった1回を除き必ず無駄な比較をしてから次の比較をすることになります。一方パターンBでは,先に"catch"であるかどうかを評価するため,catchイベントの処理の際にはelif以降の評価は行われません。まあ,微々たる差ではありますが,pythonの比較は割とコストが高い(比較以外にも遅い処理いろいろありますが)ので,無駄な処理を省く意識を普段から持っておくといつか報われると思います。たぶん。

ただし,分かりやすさのため,initイベントは特別格として他のイベントを差し置いて一番先に書くという考え方は,それはそれでアリです。私も普段はこうしています。

atenxa.richeventの内部的な仕組み

ここまで書いたら飽きたので気になる人はソースコードを読んでみてください。