AKAGI Rails

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

VRMNXの時間系イベントが使いにくいなあと思っているアナタへ

実はそんな人いない説

はともかく。

まずはバージョン5以前の時間系イベントとVRMNXpyの時間系イベントの仕様の相違について,Afterイベントを例に見てみましょう。

バージョン5以前のイベントは,

//イベントを設定
SetEventAfter Target Method EventID Interval
// Target: 対象オブジェクト
// Method: 対象オブジェクトのメソッド
// EventID: イベントIDを受け取るグローバル変数(のポインタ)
// Interval: 時間間隔(ms)

BeginFunc Method
    // Intervalミリ秒後に実行する制御の中身
EndFunc

という仕様でした。一方でVRMNXpyでは

#イベントを設定
evid = target.SetEventAfter(Interval)
# target: 対象オブジェクト
# interval: 時間間隔(s)
# (返り値) evid: イベントID

#指定時間経過すると対象オブジェクトのイベントハンドラが呼び出される
def vrmevent_xx(obj,ev,param):
    if ev=='after':
        # Afterイベントが起きたときの制御の中身

聡明な読者諸兄には自明なことかと思いますが,VRMNXpyのイベントは,どのイベントIDであってもとりあえず同じコードを走らせてしまうという仕様になっています。イベントハンドラの中に様々な処理をベタ書きしているといつかバグの温床となるでしょう。

この問題を解決するひとつのアイデアとして,次のような実装を提案することができます。ただし,一部が理解のしやすさのため多少冗長な書き方になっています。

def SetEventAfterAKG(target, callback, interval):
    """ 実行対象の関数を明示するAfterイベントのラッパ
    * vrmapiのイベントハンドラに専用コードを仕込む必要がある *
    Parameters -----
    target: 対象オブジェクト
    callback: このAfterイベントで実行されるコールバック関数
    interval: 時間間隔(秒)
    Returns -----
    eventid

    callback関数はobj, ev, paramの3つのparameterを伴って呼び出されます。
    """
    # vrmapiにイベントを登録
    evid = target.SetEventAfter(interval)
    # 対象オブジェクトのdictにコールバック関数を登録
    d = target.GetDict()
    evkey = 'AKGCB{}'.format(evid)
    d[evkey] = callback
    return evid

# 今回Afterイベントで仕込みたい制御がこちらだとしましょう
def sample_cb(obj,ev,param):
    # 制御の中身(省略)
    pass

# イベントを設定する際のコード
evid = SetEventAfterAKG(target, sample_cb, interval=3)

# 対象オブジェクトのイベントハンドラの中身で・・・
def vrmevent_xx(obj,ev,param):
    if ev=='after':
        # 以下が専用コード
        d = obj.GetDict()
        evkey = 'AKGCB{}'.format(param['eventid'])
        try:
            callback = d[evkey]
        except KeyError:
            # ラッパ経由で登録しなかったAfterイベントにはDictにcallbackが入っていないのでエラーが送出される
            # お行儀は最悪だけどとりあえずシカトする
            pass
        else:
            # Dictからcallbackがエラーなく見つかったときだけコールバックを呼び出し
            callback(obj,ev,param)

これで一応,バージョン5互換(もどき)にAfterイベントのcallback処理を作り込んでいくことができます。イベントハンドラ(vrmevent_xx)の中に処理をベタ書きせず,外へ独立した書き方ができるのでいろいろ楽ちんであると思います。見た目的には,インデントを浅くする効果があり全体がPythonらしいスッキリとした仕上がりになるはずです。 実行時のことを考えると,ほどよく名前空間を切ることになるので不要なオブジェクトのメモリが早く開放されるのではないかと思います。Pythonの関数呼び出しは結構重たいことが知られています(注:リンク先のサンプルコードはPython2仕様である。)がframeイベントで何百個も関数を呼び出すとかでなければ大丈夫でしょう。

target.SetEventAfterAKG(...)ではなく,SetEventAfterAKG(target, ...)でイベントを登録するお作法に混乱するかもしれませんが,これはSetEventAfterAKGがvrmapiのSetEventAfterのように各オブジェクトのメソッドとしてでなく,グローバルな関数として定義することを想定しているからです。

このアプローチであればVRMNXpyにKillEventが存在しない問題も,割と簡単に解決します。KillEventのかわりに,Dictから当該のイベントのcallbackを保存しているメンバを取り除いてしまえばよいのです。

d = obj.GetDict()
d.pop('AKGCB{}'.format(evid_to_kill))