エンジニア的ゲーム理論

FFXIVやPCが好きなフリーランスのエンジニアが理屈をこねる。

私がスペスペたいむに惚れたワケ

私とタイムライン

これまでそこそこ長いこと趣味でACT用タイムラインを作ってきた。

別にタイムラインがないとレイドがクリアできないわけでもないし、むしろ作っている過程でだいたい覚えてしまうので必要かと言われればそうではない。ただ、人間は完璧な生き物ではないので、コンディションによってどうしてもうっかり忘れることもあるし、特に高難易度コンテンツではその"うっかり"のせいで他のメンバー全員に迷惑を掛けてしまうことが多々ある。そういうことを起こす可能性を少しでも下げるためにタイムラインを活用していて、また同様にそうした理由で自分の時間を無駄にしたくないので親しい人に共有していたりする。

とにかく、使えるものを正しく使って楽できるなら、少しでも事故が起こる要素を減らせるなら、それが一番だと思っている。誰かに直接的に迷惑を掛けることもない。神経を研ぎ澄ませて脳をフルに動かしてプレイするのも楽しいけど、毎週の消化ともなれば軽く雑談でもしながらダラダラとやりたいのだ。当然、全く共感できない人もいると思うけど、世の中にはそういうタイプの人間もいるのだということを覚えておいてほしい。

そんな建前は置いておいて、自分が組み立てたものが自分の想定通りに動いた瞬間っていうのが、何よりも嬉しいのである。それがタイムラインでも同じだった。本当に結局それだけの理由だと思う。私はプログラマではないので、やはりツール自体を作ることはハードルが高い。けれど、システムエンジニアというのは様々なツールを深く理解して活用し、それらを繋ぎ合わせたりするのが仕事である。必要ならちょっとしたコードぐらい書く。どうも、そういうところが性分に合っていたようだ。

外部ツールについての包括的な話はそのうち書きたいけれど、今は面倒くさいからその良し悪しは触れないことにする。

スペスペたいむ以前のお話

BindingCoil.ACTTimeline やF Z.Timelineなどの従前のACTタイムラインは、テキスト形式で定義したスクリプトを上から順に評価していくだけのものだった。

戦闘開始からの絶対的な秒数でアクティビティを定義していく。分岐の実装は、例えば本来なら到達しないであろう時間空間に定義したグループをある種のルーチンのように扱って、特定のアクティビティに達した/sync条件に合致した場合に無理やりjump機能で指定したルーチンに飛ばす、という力技で対応していた。

分岐が複数回に及ぶような戦闘で、それらを正確に記述しようと思うと、その分岐に応じ指数関数的に記述量が増えていくのが課題だった。

このやり方で苦戦した典型例がシグマ零式3層のガーディアン戦だった。ガーディアンが特定のタイミングでロードするプログラムにより、後の行動が分岐する。この分岐が2箇所で2ルートずつあったため、計4パターンのタイムライン定義を行う必要があった。

タイムラインよるギミック読み上げ対策かどうかは分からないけれど、「2つある行動のうち、前回しなかった行動を取る」というのは、少なくともタイムラインを作る側の人間としては新しい試みだったように感じる。

今となっては過去のレイドであるガーディアン戦のタイムラインを今更公開するつもりはないので要約すると、以下のような構成になっていた。

# 戦闘開始
0 最初のローディングまでのタイムライン
...
# 最初にローディングしたのがビブリオタフだった場合 → 1000の分岐ルート1にjump
45 最初のローディングがダダルマーだった場合のタイムライン
...
142 エアフォース
...
211 オルトロス
...
294 ウイルス
...
404 エアフォース
...
# ここでローディングしたのがビブリオタフだった場合 → 2000の分岐ルート2にjump
470 ダダルマーをローディングした場合のタイムライン
...
532 ビブリオタフ(床踏み)
...
613 オルトロス
(戦闘終了まで続く)

# 分岐ルート1
1000 最初にローディングしたのがビブリオタフだった場合のタイムライン
...
1097 オルトロス
...
1180 エアフォース
...
1250 ウイルス
...
1360 オルトロス
...
# ここでローディングしたのがビブリオタフだった場合 → 3000の分岐ルート3にjump
1459 ダダルマーをローディングした場合のタイムライン
...
1515 ビブリオタフ(床踏み)
...
1585 エアフォース
(戦闘終了まで続く)

# 分岐ルート2
2000 最初のローディングがダダルマーで、ウイルス→エアフォース後のローディングがビブリオタフだった場合のタイムライン
...
2085 ダダルマー(衝撃波)
...
2148 オルトロス
(戦闘終了まで続く)

# 分岐ルート3
3000 最初のローディングがビブリオタフで、ウイルス→オルトロス後のローディングもビブリオタフだった場合のタイムライン
...
3125 エアフォース
...
3070 ダダルマー(衝撃波)
(戦闘終了まで続く)

ひとつのルートを正規ルートと仮定して書き、正規ルート上で分岐が発生したらパターンに応じて派生ルートへ無理やり誘導していた、というのが何となく分かればそれだけでいい。とにかく可読性が悪いのも一目瞭然かと思う。

どうかこうにかタイムラインの制御は成功したものの、何というか戦闘の流れに対して不自然な感じはどうしても否めなかった。

本来、戦闘の流れというのは

フェーズ1開始
 アクティビティA
 条件分岐
  分岐ルート1開始
  分岐ルート1終了
 アクティビティB
フェーズ1終了
フェーズ2開始
...

といった具合に構成されているので、分岐ルート1が終わったらフェーズ1の然るべき場所に戻って継続する、というのが、プログラマブルなタイムラインの書き方としては一番綺麗になるはず。

スペスペたいむと出会って

スペスペたいむによるxml形式でのタイムライン定義と出会い、そうした思いは確信に変わった。実際のサンプルはここには記載しないけれど、本ブログでもいくつかサンプルタイムラインを公開しているのでそちらを見て頂ければ一目瞭然だと思う。

パターンによる分岐、またはフェーズを単位としてサブルーチンを組み、パターンに応じてそれらを適宜loadしていく(今のサブルーチンの末尾に呼び出したサブルーチン内のアクティビティを追加する)フェーズが移行したなら現在ロードされてるアクティビティをtruncateしてサブルーチンごと移行する。

書くのが楽とかそういう次元の話じゃない。昨今のバトルシステムに対応したドキュメント構造そのもの、その在り方が正しいと感じた。ドキュメント構造という概念のないTSVでは成し得なかったことの数々が実現出来ているのである。

実際にスペスペたいむでタイムライン作成を始めたのはアルファ編の途中からだけども、アルファ編ではさらに読み上げ対策によるログの難読化が進んだような気がしている。ただ一行のログを見れば次のギミックが分かる、というこれまでのバトルシステムにおける、ある種の欠陥を塞いできた(ような気がする)。

この典型例がアルファ零式2層のミドガルズオルム戦だと思っている。ミドガルズオルムが横回転をしたら、次はアースシェイカーと同時に、内周か外周どちらかが安全地帯になる攻撃を仕掛けてくる。これはログ1行を読んだだけでは判断がつかず、最初のアクティビティと続くアクティビティ、2つの要素を合わせて評価しなければ、タイムライン的には適切な判別が出来ない。

これに対応するために、anoyettaさんを始めとしたコミュニティの人たちと相談して、フラグ処理やら何やら新しい機能を付け足したりしてもらったわけです。(この過程がとにかく楽しかった。)

呼び出し元の機能拡張に合わせて、任意の要素に任意の属性を後から追加できるのはxmlならではだと思う。これに加えて、サブルーチン呼び出しの方法の差異やトリガーのネスト構造の最適化などを考慮し、様々なカスタマイズを行って自分の使いやすいタイムラインを作ることができた。さらに色々と機能拡張をリクエストして使いやすくなっているので、追々リファクタリングなんかもしたい。

今後どうなるんだろうね

しばらく大型パッチがないので、作る楽しみはしばらくお預けかな。5.0でバトルシステムを始めとしていろんなアップデートが入るらしいので、その時が楽しみで仕方ない。

当然、今後もバトル開発チームは色々頭を捻ってツール対策というものはしてくるんじゃないかと思う。けれど、一方で一番簡単なツール対策であるところの「データの全マスク」をしない最大の理由は、彼ら開発チームもユーザコミュニティとの間で「そうきたかー」「これならどうだ」みたいな感じで鍔迫り合いを楽しんでいる所にあるのかもしれない、と、個人的には思ってたりします。