この記事は第2のドワンゴ Advent Calendar 2017の13日目の記事です。
最初に
電子工作の基本知識のないまま書かれる記事となっています。
TASとは
Tool-Assisted Speedrun
や Tool-Assisted Superplay
などの頭文字を取ったアクロニムです。
ここでは Tool-Assisted Speedrun
や Tool-Assisted Superplay
などを区別せず、TAS
として説明します。
掻い摘んだ説明をすると、基本的にゲームのエミュレータの諸機能(QS/QL, 実行速度のスロー化やコマ送りなど)を使い、
毎フレームごとの入力を(リプレイファイルと呼ばれたりするファイルに)保存、[作業|完成]後にリプレイファイルの各フレームの入力を等速で行うことによって 理論上再現可能なすごいプレイ映像
を作り出すことをTASと呼びます。
言葉の定義については様々な議論がありますが、その辺りについての詳しくは下記リンクを参考にしてください。
認識としては上に書いたもので大体あっているかと思います。
すごいプレイの魅力
https://t.co/2WonL4gzwb
— 名有りさん (@naari_) 2017年12月13日
アドベントカレンダーに遅刻している
上の動画を見ていただければ少しは魅力が伝わるかと思います。人間でも練習しなければ難しい技を難なく決めていく様を見ているのはとても気持ちが良いものだと思います。
— 名有りさん (@naari_) 2017年12月13日
また、上のようなものもあります。こちらはゲームクリアまでに42秒と掛かっていないため、是非目を通して頂きたいです。
所謂任意コード
の入力、実行をスーパーマリオワールドの上で実現し、通常では確実にありえない短さでゲームクリアしています。
これと同じことを人間がやるためには、少なくともフレームパーフェクトかつ超複雑な入力を5フレームの間は繰り返さなければいけません。
恐らく人間が正攻法でこの入力をすることは不可能かと思いますが、TASでは毎フレームごとにコマ送りをしつつ正確な入力をすることができるので、入力の難度については完全に無問題となっています。
理論上再現可能であること
TASにおいて個人的に最重要である要件に、理論上再現可能
というものがあります。
上に貼った42秒足らずでクリアするようなプレイですが、もし仮に実際に全く同じ入力を実機で行うことができれば再現することができるはずです。
この記事の本題は、実機に対する入力をArduinoを通して行うことです。
TASBOT
茶番じみた文章を長々と書きましたが、実際に実機でのリプレイファイルの再現は既に行われています。
— 名有りさん (@naari_) 2017年12月13日
これはAGDQというRTAの大型イベントの様子ですが、TAS専用の時間が設けられており、実際のゲームハード本体に対し、TASの入力をそのまま再現しています。
このような、TASの入力をそのまま再現する専用ハードのことをどうやらTASBOTと呼ぶようで、このTASBOTによるゲームプレリがAGDQでは恒例となっています。
作りたいもののイメージとしてはまさにこれであり、全く同じようなものを作りたく思っています。
また、単純に好きだという理由とSRAMの状態管理や擬似乱数の生成などの事情も兼ねて、ここではスーパーファミコン(以下SFC)のTASBOTを制作しています。
作るもの概要
- シリアル通信によりPCからコントローラーに入力内容を渡す
- コントローラー側で入力内容を理解し、SFCが内容と同じ情報を受け取るように振る舞う
- SFC本体からコントローラーに入力が催促される度にPCに信号を送る
SFCのコントローラー入力を模倣する
TASBOTを作るということは、つまり何からの手段で制御するコントローラー作りとも言い換えることができます。
SFCで動くコントローラーを作るためには、SFCからの信号を受け取り、正しい形でSFCに返してやることが必要です。
ここでは実際のSFCコントローラーの動きを見ます。
ポート
SFCのコントローラーの差込口には7つのピンがあり、このピンによってSFC本体に接続されます。
これが実際のプラグの形になります。
丸くなっていない方から順に
[ VCC, Clock, Latch, Data1, Data2, Select, GND ]
という順番で並んでいます。
Data2ピンとSelectピンに関しては、通常のコントローラーには配線されておらず、マルチタップ等に配線されています。
処理の流れ
SFC本体が基本的なコントローラーに対する入力の受け取り方は以下の流れになります。
ここより下に現れる Rising
とは、LowからHighに変化
することを指しています。Arduinoの attachInterrupt に渡す Mode に使われる定数に存在している言葉です。
-
Latch
とClock
がRising
する -
Data1
がBボタン
が押されていればLow
押されていなければHigh
に変化 -
Clock
がRising
する -
Data1
がYボタン
が押されていればLow
押されていなければHigh
に変化 -
Clock
がRising
する -
Data1
がSelectボタン
が押されていればLow
押されていなければHigh
に変化
以下同じようにClock
がRising
する度にボタンの投下状態を調べData1
の出力を切り替える - 16回以降の
Clock
のRising
後はData1
はLow
に変化する - 一定回数
Clock
がRising
した後に先頭に戻る
ClockのRising回数とボタンの対応表は以下の通りです。
ClockのRising回数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
対応するボタン | B | Y | Select | Start | ↑ | ↓ | ← | → | A | X | L | R | 0 | 1 | 2 | 3 |
最後の 0123
は識別符のようなもので、コントローラーによっては0番のみ常に投下状態にあったりするそうです。基本的なコントローラーについては0-3全て押されていない状態となります。
マルチタップ接続時の挙動
1, 2, 3, 4番の4つのコントローラーが接続されているマルチタップの場合は、上記挙動に次のような挙動が加わります。
-
Select
がHigh
の場合Clock
の信号が1, 2番のコントローラーに伝わる- 1番コントローラーと2番コントローラーが処理される
- 1番の出力は
Data1
- 2番の出力は
Data2
- 1番の出力は
- 1番コントローラーと2番コントローラーが処理される
-
Select
がLow
の場合Clock
の信号が3, 4番のコントローラーに伝わる- 3番コントローラーと4番コントローラーが処理される
- 3番の出力は
Data1
- 4番の出力は
Data2
- 3番の出力は
- 3番コントローラーと4番コントローラーが処理される
-
Latch
がHigh
の場合、以下の通りとなる-
Data1
がLow
-
Data2
がHigh
-
実装する
あとはお手持ちのArduino等でこの動きを模すことで、コントローラーとして認識されるようになります。
実装する際に、digitakWriteを使うとおそらく間に合わないとおもうので、直接ポート操作をすることになると思います。
PORTD |= (1 << PINDAT);
PORTD &= ~(1 << PINDAT);
最初は手元にあり、使われていなかったRaspberry Piを使おうかと思っていましたが、上記の処理を実行するためにはシビアな時間内で処理を終わらせる必要があり、コンピューターであるRaspberry Piでは不可能(?)な気がしたため、マイコンであるArduinoを選択しています。
エミュレーター上のTASの入力を出力する
SFCコンと同じ出力をする装置が完成したところで、実際に受け渡す入力の情報をこちらで準備します。
ラグフレームを取り除く
どう準備するかの話の前に、SFC本体が Latch
を Rising
させるタイミングについての話をします。
Latchが常に毎フレームで Rising
しているかというとそうではなく、ラグフレームという状態である時には Latch
は Rising
しません。
1フレーム内で実行すべき処理が全て終わらなかった場合にラグフレームの状態となり、次のフレームにも処理が継続されます。
そのため、ラグフレームのある状態(各データ読み込み時, 画面上のスプライトが多い時等)ではコントローラーの入力は受け付けないようになっており、そのような状態では Latch
の Rising
の回数が 秒間60回を下回ることになります。
今回、入力のタイミングを 入力が催促される度にPCに信号を送る
としたのは60fpsの間隔のタイミング保持の役割をSFC側に持たせるためで、 Latch
が Rising
することでこの要項を満たしていますが、上記のラグフレームを考慮することが必要になります。
具体的には、ラグフレーム時に行われた入力を取り除くことが必要になります。
TASの生成物であるリプレイファイルは、基本的にそのフレームがラグフレームであるという趣旨の情報は保持せず、単純に毎フレームの入力状態のみを保持していることが多いため、ラグフレームの入力を取り除くには実際にエミュレーターで動かしながらラグフレームかどうかを判断する必要があります。
これはエミュレーター上のLuaJIで動くluaスクリプトを書き、 io.open
や write
によってファイルに書き出すことによって実現できます。
エミュレーターの再現度の話
実際にSFCのエミュレーターであるBizHawkで書いたluaスクリプトを実行したのですが、どうも暗転処理を挟む度にdesyncしてしまうようです。
原因は単純で、実機のラグフレームの数とBizHawkのラグフレームの数が違うことでした。実機より高効率(?)に処理をしているのが見てわかりますが、ここではより完璧なエミュレーションが必要なため、1フレームで処理できる命令数に関しても左右されてしまうようでした。
SFCのエミュレーターの中に、bsnesというオープンソースのエミュレーターが存在しており、そのbsnesをベースとした、主にTAS制作に使われるlsnesというエミュレータがあり、こちらは実機とサイクル数までも一致させようというコンセプトもあるエミュレーターになっています。
lsnes上で実行することで、その再現度の高さからラグフレームの数も実機と同じになり、上記のラグフレーム数の問題も解決されました。
フレーム数調整
ただ、BizHawkで同じように作ったファイルであっても、ラグフレームの長さを考慮して入力と入力の間に新しく入力を挟み調整することでdesyncが回避できたことも確認しましたが、多くのラグフレームが挟まれる度に調整をしなければならないので、フレーム数調整は見送ることにしました。
完成(仮)
これで、実際に再生されるラグフレームのないリプレイファイル
、それをSFCに入力するTASBOT
が揃いました。
今回は時間の都合で1P1コンのみのTASBOTになってしまいましたが、任意コードの実行等でなければTASの動きをエミュレーター上と同じく再現することが出来ました。
実機TAS https://t.co/Es12Ts9Zrg
— 名有りさん (@naari_) 2017年12月13日
↓こちらが元の動画です。
— 名有りさん (@naari_) 2017年12月13日
↓こちらは最初にコンソールと、SFCに接続している様子を映していますが、コントローラー側の処理落ちのため最後でdesyncしています。
— 名有りさん (@naari_) 2017年12月13日
良くない点
処理落ちによるdesync
シリアル通信によってコントローラーの入力を試みているため、1/60秒の猶予があるとしてもシリアル信号出力側の遅延によってdesyncしてしまうことが多々あります。
SDカードによる読み込みだとか、数フレーム単位のバルクにして送信するなど考える事はありそうですが、検討中です。
また、今回はCOMS4021を2つ使って作られる回路をArduinoで模してしまいましたが、これが大きくクロック数を消費しているのは明らかなので、実際に4021を2つ使うのも検討されるべきかと思います。
遅刻
本当は1P側2P側、合わせて8つのコントローラーで任意コード実行させるまで仕上げたかったのですが、遅刻をしてまでも間に合いませんでした…
制作に時間がかかっているのが、単に自分の電子工作に対する知識不足から来ているものかと思いますが、着実に完成には向かっているため、完成次第追記する予定だと思います。
終わりに
電子工作に手を付ける前、自分の目に映る60fpsの世界はとてもではないが追いつけるものではないという認識でしたが、自分の使っているMacbook Proよりはるかに非力なArduinoでさえも60fpsの世界に追いつくことができるというのは当たり前であるべきですが、少し不思議な間隔もありました。
普段はこんなに高級でパワーのあるマシンを使っているのだから、早いコードは書けて当然なのだ