Hatena::Groupfatalemployeetraining

資材部の懲りない面々・あれこれブログ

「間違った社員教育」製品版委託頒布中!

2011-06-04

テクスチャを.bmp->.ddsに変換する

| 20:50

本パッケージの特徴のひとつともいえる顔グラフィックアイコンですが、

  • COLORS betaでキャラクターを生成する
  • COLORS betaから「シェル作成」機能で全表情の立ち絵を生成する
  • Image Magick(Windows用)を使って顔部分のみくりぬき+並べて接合したbmp形式画像(800x400)を量産
  • スプライトとしてユニット登録

という手順で作成してきました。

しかし、この方法には欠点があり、RSE3.5側ではbmpjpgなどの形式のテクスチャを使うと、「黒」色部分を透明指定として扱うようになっています。この仕様のおかげで、アルファチャンネルのことなど気にしなくても簡単にビーム光などを作ることができるのですが、今回は逆に悪い方に作用してしまいました。

すなわち、黒髪や暗色系の髪のキャラが多いのです。そのため、透明指定しているつもりのないところで色抜けが生じてしまいました。

そこで、SEC2011出展時~SEC2011+a版までは、髪の色をすこし明るい色に変えて、色抜けが出ないようにしたのです…が。実は「まったく君の会社ではいったいどんな社員教育を(略)」のキャラクター達は、すべてもともとは仲間うちで遊んでいるTRPGのキャラクター達で…すべてキャラクターを設定したプレイヤーの方が存在します。グラフィックも(実は台詞の言い回しも)彼らの監修を受けているので…この選択は苦渋でした。

しかし、SEC2011で「dds形式に変換すれば、この制約から逃れられるよ!」という情報をいただきました。しかも、変換は「Paint.NETというフリーウェアでできるよ!」とのこと。もっともSEC2011での文脈は透過キャノピーなどを作るための話でしたが…

早速Paint.NETを落としてきて修正作業に入りました。

しかし、始めはうまくいきませんでした。…なぜか。それは、dds形式の制約によるものでした。

  • dds形式では、ピクセル数が2の乗数(2,4,6,8,...,256,512,1024,...)でなければならない。
  • 長方形で大丈夫という情報もあるが、正方形にしておくのが無難という情報も。

とりあえず1024x1024にキャンバスサイズを拡大し、800x400の画像を左上に寄せて貼り付けることで解決しました。現在、これをSEC2011+a2としてリリースしています。

(…1024x512でもいけそうですね。これはそのうち試してみます。)

2010-12-02

スレッドの起動には、ミッション開始からの通算総起動数に制限がある

まず、RSEにおけるスレッドの基礎知識。

おそらくスレッドの同時起動数にも制約があるとは思うんですが、今回目パチ口パク作成でハマッたのは通算総起動数、つまりミッション開始から何回create_thread()を実行したか(途中で終了するしないに関わらず)、です。

当初、目パチ口パクは、ある表情絵の表示が始まったら、目パチ用スレッド口パクスレッドを起動し、別の表情・別のキャラ絵に切り替わったり、絵自体が消えたりしたらスレッドを終了する形で記述していました。毎回かならず終了しているので同時起動数は抑えられますし、アニメーションしないときには終了していますから、計算処理的にも負担が抑えられ、コード記述もシンプルになります。

しかし、すこし長めにプレイすると、エラーが発生。

関数コールの段数が深いかも、とか関数名が長いかも、とエラーメッセ-ジにあるので、多少調節してみましたが改善せず。

自己回帰呼び出しとかが発生していないかもチェックしてみましたが、それもありません。

もしやと思い、単純なテスト専用ミッション(目パチ・口パクもはずしたシンプルなもの)を用意し、2つテストを別々に行いました。

  1. ボタンを押すたびに自己再帰関数再帰回数制限付き)を呼び出して実行テストする。毎試行が終了するごとに再帰回数制限を多くしていく。
  2. ボタンを押すたびにスレッドを作成する。ただしスレッドは1フレーム以内に計算が終了(本編に影響しない引き算1回のみ)しスレッド自体もすぐに終了するもの。

1番は相当な数、再帰の回数を増やしてもエラーが出なかった(150回くらい?結局エラーを見ていないのでもっと大丈夫かも)のに対し、2番は74回でエラー発生。また、2番のテストで、スレッドからさらに別の関数を呼ぶなどするとエラー発生までの回数は少なくなります。

この試行から、

  • ミッション中で行えるcreate_thread()の回数には制限がある。起動したスレッドを終了するかしないかには全く関係がない。起動されるスレッドの構成(内部で別の関数をコールするなど)によって上限がより厳しくなる。

ことがわかりました。

これは正直なところ、(疑似だけども)マルチスレッドを謳う言語としては、リソースリークと言える深刻な問題仕様と思います。

(だって、現状の仕様と用意されてる関数だけ見てると、thread_idはintだからintの変数の上限値による同時起動数制限は受けそうだけど…終了しさえすれば大丈夫、って見えるじゃないですか。)

とはいえ、もともと過去のRSEは並列実行できるスレッド数に仕様上、上限があるものでしたから、内部動作的に上限がある前提で作られている部分が残っていてもおかしくはないとも思いますし、終了したスレッドについて逐一動的に痕跡を消すのがめんどくさい内部仕様になってるのかな、とも想像はできます。(並列実行する関数群をまとめて配列に積みっぱなしで、頭から最後までぐるりとひとループして疑似並列実行、exit_thread()されたやつは読み飛ばすようにフラグつけるだけ、とかなんですかね。双方向リストじゃないのかなー。)

とりあえず目に見えない上限があることを前提にしたコードに修正しました。(目パチを管理するスレッド口パクを管理するスレッドを用意し、グローバル変数で用意したフラグで目パチや口パクの動作開始を制御する。フラグが増えること、目パチ・口パク動作開始してからの時間測定を別途カウンタ変数を用意し管理する必要があること、などコードはかなり複雑になります。実は目パチ・口パク以外にも、メッセージイベントスレッドの自作ライブラリスレッド起動回数制限を考慮しない仕様だったので動作を変更。)

先日のget_pos()の子ユニットに対する挙動およびそれと関連すると思われる航空機ユニットの子ユニット化不可問題と併せて、本件もいずれ仕様が改善されることを期待します。

2011/6/4 追記:

SEC2011でこの点を話したところ、スレッドの起動回数上限問題は、電プロさんによると、想定外動作であるとのことです。いずれ修正されるはず。修正されたら、自前でスケジューリングしないで済むので楽になります。期待してます!

一方、航空機ユニットの子ユニット化不可問題は、公式掲示板でさしあたり無理という回答をいただきました。4thでこのあたりの内部仕様も組み直しで変わる可能性が高いので、4thになってどうなるかに期待ですね。

2010-11-08

set_pad関数のこと

メモ。ロール100%で回すと、だいたい2.4deg/frameくらいの角速度。ピッチは100%で回すと、上昇が0.4deg/frameくらい。加工が0.5deg/frameくらい。

2010-11-05

で、このサンプルを改造していろいろ試してるわけですが、

いくつか分かった関数仕様をまとめておきます。

[catapult()]

航空機を発進させると、カタパルト元にしたユニットの座標よりy軸方向に+0.4ずれた位置に配置される。

この「+0.4ずらして配置」の話は、実は着陸車輪まわりの関数の説明に書かれている。

指定した座標から自動で+0.4ずらす動作は、初期のplcファイルに基づくユニットの配置と、このcatapult()関数で確認されている。

座標を指定して移動させるset_pos()関数では、この処理は差し挟まれ無い。

[set_parent()]

ユニットに、親ユニットを設定・変更する関数。しかし、一度子ユニットになったオブジェクトを、親の存在しない独立したユニットにする方法は残念ながら開示されていない(単にないのかも)。使用されていないラベル値や-1を設定しても何も起きなかった。しかしまあこれについてはrelease_object()してからのcatapult()なりput_group()なりで、0フレームでのユニットすり替えマジックショーをすれば解決するでしょう。

set_parent()を実行すると、その瞬間に、親ユニットと同じ座標、同じ姿勢角(親ユニットに割り付けられた座標系の原点座標、かつ相対姿勢角はすべて0)の状態になる。そして、以後親ユニットの運動にあわせて運動する。

なお、航空機ユニットを(たとえセクションを200,速度を0としても)、子ユニットとすることはできない(したかったのに…)。1フレームごとにsystem()中にエンジン側で計算される座標計算がおかしくなる。想像するに、これは後述するget_pos()の仕様によるものだと思われるのだが…

この残念な点は、当座、先日作成した、親子関係にないユニットを相対座標指定で移動する関数を使って擬似的にくっついていかせるしかなさそう。

[set_pos(), set_rot()]

位置・姿勢角を設定する。

  • 誰の子ユニットでもない(親ユニットが存在しない)独立なユニットの場合:
    • 位置座標は絶対座標系(マップ中心がx=2500,z=2500,y軸が高度、zは北が正、xは東が正、yは鉛直上が正)、姿勢角はxz平面に対するピッチ角、z軸に対する方位角、機体軸に対するバンク角、で計算される。CHILDフラグはTRUEでもFALSEでも動作は同じ。
  • 親ユニットが設定された子ユニットの場合:
    • 位置座標は相対座標系(親の機体軸進行方向がz軸、翼右方向がx軸、機体上方がy軸)、姿勢角は親の翼面に対するピッチ角、機体翼面に垂直な軸に対する方位角、機体軸に対するバンク角、で計算される。CHILDフラグTRUEにしないと、何も動作しない。

つまりCHILDフラグの真意は、スクリプト作成者が、そのユニットが何かの子ユニットであることをしっかり理解して、各種関数で操作する際に明示的にTRUEをつけて操作しないと、その子ユニットを独立に運動させることができない、という安全装置であるといえる。

でも「じゃあ闇雲にTRUEにしておけばいいんじゃん?」とされてしまうと、安全装置として成立しない罠。悩ましい。

なぜ親の存在しない独立したユニットでTRUEでも(FALSEと同じように)動くように作られているのか(この結果安全装置としては片手落ち)。逆に、なぜ、ユニット自身に「自分には親がいる/いない」を判断させて、自動で動作が切り替わるようになっていないのか(安全装置が必要ないなら、現状での動作はこれで実現できる)。(なんというか…そのせいで初期はTRUE/FALSEの切替で参照する座標系が変わるようになっているのかと誤解してしまっていた)。set_pos(),set_rot()はこの仕様のままでも、「自分の親はいない/親のラベルは○○である」を知ることができる関数(残念ながらそういう関数はまだないですがー)が追加されれば、実はこのCHILDフラグ隠蔽したラッピング関数をユーザー側で用意できる状態とも言える。…いっそ、CHILDフラグをFALSE指定すると親ユニットが存在するユニットではset_rotが機能しないのを利用して、(0フレームで瞬間的にある角度に回そうとしてみて、動くか動かないかをget_rotで測定するとかして)親がいるか/いないか調べる関数とか作るといいんだろうか。ちょっとトリッキーすぎるかな。

なお、航空機ユニットを1回だけこの関数で配置して、セクション200、速度0の動かないはずの状態で放置しておくと、数値計算誤差が残るのか、時間経過とともにびみょーに位置・姿勢角が動いていく。これを防ぐには、system()実行前にget_pos()、get_rot()しておき、system()実行後にその値をset_pos()、set_rot()で上書きしてしまう。そうするとsystem()で計算された座標の変化(誤差含む)はキャンセルされ、system()実行前の状態に設定されるのでぴたっと静止する。(もちろん思い通りに動かしたければ、get_pos/get_rot直後に位置・姿勢角を数値で動かせばよい)。まあ誤差が気になる人だけが対象の話ですが。

[move(), rel_rot()]

現在の位置・姿勢角からの相対的な移動・回転を行う。

使用する座標系、CHILDフラグの扱いについてはset_pos(),set_rot()と同じ。

[get_pos(), get_rot()]

現在の位置、姿勢角を得る。

  • 誰の子ユニットでもない(親ユニットが存在しない)独立なユニットの場合:
    • get_pos()の位置座標は絶対座標系(マップ中心がx=2500,z=2500,y軸が高度、zは北が正、xは東が正、yは鉛直上が正)、get_rot()の姿勢角はxz平面に対するピッチ角、z軸に対する方位角、機体軸に対するバンク角、で与えられる。
  • 親ユニットが設定された子ユニットの場合:
    • get_pos()による位置座標は相対座標系でなく絶対座標系(マップ中心がx=2500,z=2500,y軸が高度、zは北が正、xは東が正、yは鉛直上が正)、get_rot()による姿勢角は親の翼面に対するピッチ角、機体軸に対する方位角、機体軸に対するバンク角、で計算される。なぜget_pos()は相対座標系での座標取得ではないのか…。

[get_rot_abs()]

現在の姿勢角を得る。

ユニットが親であるか子であるかにかかわり無く、get_rot_abs()の姿勢角はxz平面に対するピッチ角、z軸に対する方位角、機体軸に対するバンク角、で与えられる。get_rot()と明確に動作が分かれてて分かりやすい。

[move_abs()]

絶対座標で指定した位置座標に移動する。moveと異なり、移動量のベクトルをさすのではなく移動先の位置座標を指定する必要があるので注意。さらにもう一つ。ここで用いる絶対座標は、get_pos()やset_pos()で用いられる絶対座標とは異なるので注意。

「move_abs(LABEL,0.0,0.0,0.0,FALSE);」で移動する先は、「set_pos(LABEL,2500.0,0.0,2500.0,FALSE);」で移動する先に等しい。すなわち、get_pos(),set_pos()で使っている座標系においてx=2500,z=2500,y=0を原点とする位置に平行移動した座標系を使って指定する。

また、CHILDフラグについて。これも注意が必要。

  • 誰の子ユニットでもない(親ユニットが存在しない)独立なユニットの場合:
    • CHILDフラグがTRUE/FALSEいずれの場合でも、move_abs()に用意された座標系における指定された位置座標に移動する。
  • 親ユニットが設定された子ユニットの場合:
    • CHILDフラグがFALSEの場合は動作しない。
    • CHILDフラグがTRUEの場合、座標計算がおかしくなり(たぶん内部での座標移動計算の累積がうまくない)、はるかかなたの座標に飛ぶ。

つまり、現状は子ユニットに対して使ってはいけない、ということ。じゃあset_pos()でいいじゃん!ということになってしまうのですが…過去互換性とかのためにあるのかな?

さて、たとえば親にR-44を指定し、子として何かユニットをset_parent()でつけるテストで。

子にFRAKの本体(砲台上部)を使う場合と、子にR-21を使う場合で気がついたことなんですが、以下の症状が確認できました。

なおR-44、FRAKの本体(砲台上部)、R-21の3つとも、事前にset_section()を使ってセクション値を200(スクリプトがコントロールする)、set_speed()を使って速度ゼロにしてあります。

  • R-21の親をR-44に指定→位置の座標変換に異常、正常な位置に出現せず(座標x,y,zはめちゃくちゃに変わり続ける)
  • FRAK本体の親をR-44に指定→正常に座標変換され、R-44の上に出現
  • FRAK本体の親をR-44に指定→正常に座標変換され、R-44の上に出現→get_pos()でFRAK本体座標を取得、取得した座標(絶対座標値)をそのままset_pos()+CHILDフラグTRUEで代入(相対座標の値として無理矢理指定)→R-21の親をR-44に指定したときとまったく同様の症状(位置の座標変換に異常、正常な位置に出現せず座標x,y,zはめちゃくちゃに変わり続ける)

これから推測されることなんですが、航空機はその運動を、スクリプトがsystem();でエンジン側に処理を返してる間に計算しています。このときに、エンジン内部では「get_pos()で現在座標を取得→速度・角速度を用いて1フレーム後の位置・姿勢を計算→set_pos()で設定」としてるのではないでしょうか。もしこうなっているなら、get_pos()の仕様が「ユニットが親・子どちらであるかに関わらず絶対座標を返す」ようになっている一方、set_pos()がCHILDフラグTRUE時には「移動先を親の相対座標で指定する」になっている結果として、速度ゼロでユニットが動かないにも関わらず問題が発生している、と言えます。

get_pos()の仕様をget_rot()と同等にすれば現状の問題は解決すると思われます。(その際はget_rot_abs()と同様get_pos_abs()が欲しくなると思います。)

ただ、すでにget_pos()を現状の仕様で子ユニットに使ってうまく動くようにしちゃってる既存ミッションがあるかもしれないので…簡単には仕様変更できないかもですね。

あと、たとえ解決したとしても、子ユニットとして登録した航空機を飛行させると、「親ユニットの座標系に対して相対的に飛行運動する(さも親ユニットの座標系が絶対座標系の空であるかのように、です)」ので、親ユニットがロールしたりすればもう即座に、物理法則を無視した運動になってしまいます。なので需要自体はすごく低いとは思うんですが…

じゃあ、なんで欲しいんだよ!というと。

航空機航空機をくっつけたような状態(ジャンボ機の背中にシャトルがついてるような状態)を作りたいわけです。

それだけならば「なんだ、おまけでくっつくほうのユニットを、形だけ飛行機にして航空機扱いじゃなくOBJECTとかFACILITYとか扱いで登録しておけばいいじゃない」となるのですが、さらにやりたいことがあります。

くっついている複数の航空機、両方に航空機としてのターゲットマーカを出して攻撃可能にしたい、んですね。

ターゲットマーカを出すだけなら、航空機扱いじゃなくても出力されます。しかし、航空機じゃないものは空を飛ぶはずがない/高速で移動するはずがない、ことになっているのか、高速移動しても機銃の照準器が未来予測位置に移動しません。

つーことで、えらく細かいことをやろうとして気がついたことだったりします。

で、当座手元では、すでに紹介したとおり、「子ユニットじゃないけど相手ユニットの相対座標系を使って移動できる」関数を自作したので、これで対処することにします。

2010-10-31

RSEの仕様について調べてみた

set_pos, get_pos, set_rot, get_rot, rel_rotまわりの動きがよくわからなかったので、これらの関数トライアルするだけのミッションをくんでいろいろ試してみた。得られた知見を以下にまとめておく。


  • 世界座標系は、x軸が東西方向東向き正、y軸が鉛直方向上向き正、z軸が南北方向北向き正の左手座標系になっている。内部がDirectXで書かれているので自然な流れだけど、よく数学で出てくる右手座標系と鏡像反転なので注意が要る。
    • マップの中心座標はx=2500,z=2500(ひょっとするとx=2499,z=2499?)。
    • マップエリアはx=0~5000、z=0~5000。そのうちコースアウト領域はx<=1000、x>=4000、z<=1000、z>=4000(情報ありがとうございます、JoJoさん)。
  • 機体に割り付けられた座標系は進行方向(機体の軸方向)がz軸、機体上方がy軸、ピッチ回転軸(左から右に向かう方向が正)がx軸。
  • どのユニットの子ユニットでもないユニットは、child_flagに何を設定しようが、child_flag=FALSE(=0)の場合の振る舞いになるようだ。
  • 姿勢回転角は以下のようになっている。
    • x軸回転角は、機体に割りつけられた座標系におけるx軸(=すなわちピッチ軸)周りの回転で、ピッチアップ(機首上げ)する方向が正。単位は[deg](度)。
    • y軸回転角は、機体に割りつけられた座標系ではなく、機体座標系を通る鉛直軸(上方を正)とする軸周りの回転。すなわち方位角になる。機体を上空から見て、時計回りが正(機体後ろから見て右に機体が回るのが正)。単位は[deg](度)。
    • z軸回転角は、機体に割りつけられたz軸(すなわち機体軸、ロール軸)周りの回転。すなわちロール角になる。機体後ろから見て反時計回りに回るのが正。単位は[deg](度)。
    • 物体の姿勢を3軸の回転で表記すると、一般にその回転をかける順番で結果が異なる。RaidersSphereの場合、ピッチ軸回転→方位角回転→ロール軸回転の順で行われているようだ。すなわち、"set_rot(LABEL, rx, ry, rz, FALSE);"と、"set_rot(LABEL, 0, 0, 0, FALSE); rel_rot(LABEL, rx, 0, 0, FALSE); rel_rot(LABEL, 0, ry, 0, FALSE); rel_rot(LABEL, 0, 0, rz, FALSE);"は同じ。
    • 絶対座標系の軸で回転させて同じことを実現するためには、まずz軸周りにあらかじめロールさせておき、次にx軸周りにピッチアップ、最後にy軸周りに回して方位設定となる。
    • とすると実は、ピッチアップ90度してからヨー90度しよう、などと考えると回転できない特異状態になっていることがわかる(ロール軸と方位角軸が一致してしまうため)。その場合、望む姿勢にするには別の回し方をしないといけない。つーことで「ヨー軸回転」専用の関数を作ると便利そう。>作ってみた。
  • system.incでシステムコール関数として三角関数が用意されているが、これらの取り扱う角度の単位は[rad](C言語三角関数と同じ)。get_rot, set_rot, rel_rotなどで扱う単位は[deg]なので、以下の換算を意識して使う。
    • 円周率を"const float PI = 3.1415926535897932384626433832795;"などとしておく。(floatはこんなに精度でないが、値を電卓からコピーしている)
    • deg_value[deg]->rad_value[rad]の場合: "rad_value = deg_value * PI / 180.0;"
    • rad_value[rad]->deg_value[deg]の場合: "deg_value = rad_value * 180.0 / PI;"
    • system.incにatan2があるのはめちゃくちゃありがたい。(さっそくあとで使います)
  • テストしていて気がついたことだが、set_sectionでセクション値を200にし、set_speedで速度を0にしていても、ユニットの姿勢を水平じゃない姿勢に設定したまま何もせず放置しておくと、重力の影響が計算されるのか?、徐々に姿勢が変化していってしまう。これはユニットがFIGHTER,PLANE属性ではなくFACILITY属性などでも発生する。そのため、思い通りの姿勢を維持させたい場合は常時set_rotなどで姿勢を固定しておく必要がある。(位置は変化しない。)