tanaka's Programming Memo

プログラミングについてのメモ。

Unity2018.3.1にしたらRigidBodyのkinematicでなんかエラーが出た

こんなエラーが出ました。

[Physics.PhysX] RigidBody::setRigidBodyFlag: kinematic bodies with CCD enabled are not supported! CCD will be ignored.

CCDってなんだ。これだ・・・Unity - Manual: Continuous collision detection (CCD)

高速時にオブジェクトを抜けなくするための設定です。isKinematicを有効にした場合、Rigidbodyによる移動ではなくなるので、CCDが効かなくなるということで、2018.3ではエラーになってしまうようです。

エラー表示にはなってますが、Playできるので本来は警告で表示するはずだったのを、エラーログで出しちゃってるっぽいです。ただ、効果がないなら直した方がいいのでエラー表示でもいいかも。

目次

原因

以下のような設定をしていると、エラーになります。

f:id:am1tanaka:20190115191150p:plain
エラー例

Is Kinematicを有効にする場合は、Collision DeteictionDescreteにします。

以下ならOK。

f:id:am1tanaka:20190115191530p:plain
正しい設定

問題のゲームオブジェクトを探す

Hierarchyウィンドウの検索バーにRigidbodyと入力すれば、Rigidbodyを持っているゲームオブジェクトが見つかります。表示されたオブジェクトをチェックして、問題ないか確認します。

f:id:am1tanaka:20190115195145p:plain
オブジェクトの見つけ方

問題のある個所が見つからない時

Hierarchyにあるオブジェクトが全て問題ないのに、Playの終了時にエラーが発生する場合があります。その場合はプレハブを確認してみてください。登場しているオブジェクトに問題がなくても、プレハブにisKinematicが有効で、Discrete以外の衝突検出モードを設定しているとエラーが出ました。

スクリプトでの切り替え方

スクリプトで切り替えたい場合は以下のような感じです。

Rigidbody rb = GetComponent<Rigidbody>();
rb.collisionDetectionMode = CollisionDetectionMode.Discrete;
rb.isKinematic = true;

参考URL

WindowsでiOSアプリのビルドとデプロイができるiOS Project Builder for Windows

iOS Project Builder for Windowsとは

Windowsマシンで、iOS用にUnityプロジェクトをビルドできるアセットです。

2012年ごろのMacBook Airを持っているのでiOSアプリのビルドはできるのですが、メインの開発環境はWindowsなのでビルドするのが面倒でした。処理も遅いですし。ということで、このアセットは理想的なのでは?ということで、50%セールで購入してみました!

WindowsSDKツールをインストールしたら、必要なプログラムをUSBメモリーにコピーして、Xcodeがインストールされたmac上でセットアッププログラムを実行して必要なデータを生成します。macでの作業はそれだけで、あとはWindows側で少しセットアップをしたらUnityプロジェクトのiOS用ビルドと実機へのインストールができるようになります!めちゃ便利です。

(このリンクはアフィリエイトリンクです。お気に召したらこのリンクからご購入いただけると活動の励みになります!)

  • 2018/12/31 iOS11以降では32bitアプリは動かないので、ビルドから外す設定を追記しました

目次

必要なもの

以下、必要なものです。

セットアップ手順

ツールをWindowsにインストール

このアセットの本体は、Windows用のアプリケーションです。そうと知った時はちょっと衝撃でした。ということで、作業はWindowsで始めます。

  • 適当なUnityプロジェクトを開いて、iOS Project Builder for Windowsをダウンロードして、インポートします
  • 以下の通り、iOS Project Builder for Unity Installerという実行ファイルのインストーラーがインポートされるので、ダブルクリックして起動します

f:id:am1tanaka:20181227222650p:plain
インポートしたフォルダー

  • インストール作業が始まるので、I Agree > Next > インストールしたいフォルダーを設定してInstallと進めます。大きさは320MB程度です
  • インストールが完了したら、Closeをクリックして閉じます

f:id:am1tanaka:20181227223200p:plain
インストール完了

以上で、必要なツールのインストールは完了です。readme.htmlWebブラウザーで開きます。このドキュメントにセットアップ方法やトラブルシューティングなどの詳細が書かれています。普通に使うだけならこのブログでいけると思いますので閉じて構いません。

f:id:am1tanaka:20181227223620p:plain
readme.html

インストールが完了したら、UnityにインポートされたiOS Project Builderフォルダーは削除して構いません。

f:id:am1tanaka:20181228205639p:plain
iOS Project Builderフォルダーを削除

初めての設定

最初に1回やっておく必要がある設定です。

SDK用データの生成

  • スタートメニューから、iOS Project Builder for Unity > SDK migration assistantをクリックして開きます

f:id:am1tanaka:20181227225801p:plain
MigrationAssistant

  • 以下のような2つのファイルが入っているフォルダーが開きます。これらが入っているMigrationAssistantフォルダーを、USBメモリーにコピーします

f:id:am1tanaka:20181227230057p:plain
Copy files

  • macを起動したらUSBメモリーを挿して、コピーしたMigrationAssistantフォルダー内のMigration assistant(step 1, Mac).commandをダブルクリックして起動します
  • ターミナルが開いて必要なファイルの作成が始まります。以下のようなウィンドウが表示されたらGot itをクリックして作業を進めます

f:id:am1tanaka:20181227233001p:plain
Got itをクリック

  • キーチェーンのログインパスワードを聞かれるので、パスワードを入力して常に許可をクリックしてください

f:id:am1tanaka:20181227233125p:plain
Keychain

f:id:am1tanaka:20181227233137p:plain
パスフレーズ

USBメモリーのフォルダー内にSDK.zipKeychain.zipが生成されたら完了です。USBメモリーを取り出して、Windowsに戻ります。

  • WindowsUSBメモリーのフォルダーを開いたら、Migration assistant (step 2, PC).cmdをダブルクリックして起動します

f:id:am1tanaka:20181227233243p:plain
MigrationAssistant Step2

  • コマンドプロンプトが起動して、セットアップ作業が始まります。以下のようにメッセージが表示されたら完了です

f:id:am1tanaka:20181227233518p:plain
Press any key

[A]キーなどを押せばウィンドウが閉じて、準備完了です。

署名の設定

デフォルトで利用される署名の設定をします。

  • Windowsで、スタートメニューからiOS Project Builder for UnityKeychain Toolを起動します

f:id:am1tanaka:20181228164241p:plain
Keychain Tool

署名設定がある場合、iPhone Developer: <名前>や、iPhone Distribution: <名前>といった証明書や、1つ以上のPrivat keyProvisioning Profileがいくつか一覧に表示されます。

f:id:am1tanaka:20181228170039p:plain
署名設定がある場合のKeychain Tool

maciOSアプリをビルドできるように設定されていなかった場合、リストボックスには何も表示されません。その場合は、こちらの記事などを参考にしてmacの設定を完了させてから、改めてMigrationAssistantでデータを作成してください。

署名設定は、証明書であるcertificateと、証明書から生成する時にパスフレーズで保護したprivate keyと、アプリのインストールを許可したiOSバイスを示すprovisioning profileの3つによって構成されます。

  • Provisioning profile欄をクリックして、利用可能なものがあれば設定します
    • CertificatePrivate keyは自動的に選択されます
  • Private key passphraseに、Migrationする際に設定したパスフレーズを入力します

f:id:am1tanaka:20181228173324p:plain
Keychain Toolの設定

設定が完了すると、上図の左下のロックアイコンに緑色のチェックが表示されます。

Note: generic identityについて

特定のアプリ用ではない、開発用のProvisioning profileを設定したい場合は、genericを選択します。リリースのためにアプリをビルドする時になったら、配布用のcertificateと特定のprovisioning profileに変更するとよいでしょう。

ビルド設定は以上で完了です!Keychain Toolのウィンドウはそのまま閉じて大丈夫です。

iOSプロジェクトのビルドとデプロイ

WindowsのUnityで、iOSプロジェクトをビルドする手順です。

  • 対象のUnityプロジェクトをWindowsのUnityで開いて、ビルド設定をiOSにします
  • FileメニューからBuildを選択して、iOSなどのフォルダーを作成してそこにビルドします
    • これで、Xcode用のプロジェクトが指定したフォルダーに生成されます
  • Windowsで、スタートメニューからiOS Project Builder for UnityProject Build for Unityをクリックして起動します

f:id:am1tanaka:20181228174712p:plain
Project Builder for Unityを起動

  • 右上のBrowse...ボタンをクリックして、UnityでiOS用にビルド先として指定したフォルダーを開きます

f:id:am1tanaka:20181231113545p:plain
iOS Builderの設定

  • Provisioning profilePrivate key passphraseが空欄だったら、Keychain Toolで設定したのと同様の内容を設定します
  • 32bitアプリはiOS11以降では使えないので不要です。Build for32-bit (armv7)のチェックを外します(2018/12/31追記)

以上の設定は保存されるので、2回目以降は設定不要です。また、Save configでファイルに保存できます。

以上できたら、Build and deployボタンをクリックします

エラーがなければ、しばらく待っていると以下のような画面が表示されて、ビルドが完了します。

f:id:am1tanaka:20181228210959p:plain
ビルド完了!

  • iPhoneなどのiOSバイスSafariを起動して、画面に表示されているURL(上の例なら、192.168.11.2:5003)を開きます
  • 以下のような画面がスマホに表示されたら開くをタップします

f:id:am1tanaka:20181228212228p:plain
開く

  • 以下のように表示されたら、インストールをクリックします

f:id:am1tanaka:20181228212242p:plain
インストール

  • スマホ側に以下のようなRetryボタンが表示されますが、ボタンを押さずにそのままにしておいてください

f:id:am1tanaka:20181228212354p:plain
閉じる

  • PCの画面でdeployのパーセントが表示されて、完了したらウィンドウが閉じます

f:id:am1tanaka:20181228212319p:plain
デプロイの進捗

以上で、スマホへのインストールは完了です。ホーム画面でアプリを探してみてください。新しくインストールされたアプリが見つかると思います。タップすれば、起動して動作を確認することができます。

f:id:am1tanaka:20181228212414p:plain
インストール完了!

エラーが発生した時 fatal error: Defines.h file not found

Cross Platform Native Pluginsを使ったプロジェクトをビルドしようとしたところ、以下のようなDefines.hが見つからないというようなエラーが発生しました。

f:id:am1tanaka:20181228184141p:plain
Defines.hが見つからない!

トラブルシューティングに解決策が書いてありました。サードパーティー製のプラグインを利用する場合にこのようなヘッダーが見つからないエラーが発生することがあるようです。extra compiler flagを設定することで解決できます。-I<relative_path_to_the_directory>の書式で、必要なヘッダーファイルがあるパスを指定します。

例えば、-ISomeDir/SomeOtherDir/Dir/WhereMyHeaderIsというような感じで、ハイフンに大文字のアイに続けてパスを記載します。複数指定する場合は、-Ipath1 -Ipath2 -Ipath3のように半角スペースで区切ります。

解決手順

  • エクスプローラーの検索欄に見つからないファイル(今回の場合はDefines.h)を入力します

f:id:am1tanaka:20181228185050p:plain
Defines.hを探す

  • 見つかったファイルを右クリックして、ファイルの場所を開くをクリックします

f:id:am1tanaka:20181228185222p:plain
ファイルの場所を開く

  • アドレスバーを右クリックして、アドレスのコピーをクリックします

f:id:am1tanaka:20181228185449p:plain
アドレスのコピー

  • iOS builderExtra compiler flagsにチェックを入れて、テキストボックスに-Iを入力して、続けてコピーしたアドレスを貼り付けます
  • パスの区切りがバックスラッシュ(\)だと失敗するので、全てスラッシュ(/)に書き換えます

f:id:am1tanaka:20181228191344p:plain
Extra compiler flagsの設定

以上設定したら、Build and deployボタンをクリックしてビルドをし直します。

まとめ

以上です。準備の手順は少し多いですが、一度設定すればあとはビルドツールを起動してボタンを押せばスマホですぐテストができるので画期的です!ビルド時間はPCの性能によって異なりますが、参考までに私の環境だと10分ぐらいでUnityのiOSビルドとiOS Project Builder for Windowsのビルドが完了します。

時間や手間の削減になるので、私と同じような強いWindowsPC+貧相macの環境だった場合、購入して損はないと思います^^

(このリンクはアフィリエイトリンクです。お気に召したらこのリンクからご購入いただけると活動の励みになります!)

ボクセルキャラを粘土で作る!

この記事はVoxel Advent Calendar 2018 20日目のものです。ちょっと趣向を変えて、リアルにボクセルのキャラクターを作る記事です。

昨日は、はやなささんの自分のボクセル創作を振り返った話 - hayanasa’s Flower Factoryでした。明日はMagicaVoxelからUnityにキャラクターを読み込むときにめちゃくちゃお世話になっているブログを書いてくださったdaishihmrさんです!

目次

ボクセルキャラを作る!

現在、ボクセルで描いたキャラクターを主人公にしたパズルゲームVoxelorerBird(ボクセローラーバード)を開発してます。主人公はこんなキャラ。

f:id:am1tanaka:20181218220432p:plain
ハク

なんかグッズも欲しいなぁと思って粘土で作ることにしました。

f:id:am1tanaka:20181215121442j:plain
ドン!

とりあえずこんな感じ。仕上がりはちょっと微妙ですが、一応それっぽい感じにはできそうです。ということで、材料や作り方をご紹介します!

材料

キューブ型

レジンや粘土用のキューブのシリコン型です。アートギャラリーフローレのソフトモールド キューブ型を使ってます。これに粘土を詰めて、キューブを作ります。

f:id:am1tanaka:20181215120041j:plain
シリコン型

いくつかのサイズがありますが、2番目に小さい3.5mmのものがちょうどよい感じです。同時に8つ作れます。

粘土

粘土は何種類か試してみて、Heartyが個人的には一番使いやすかったです。

f:id:am1tanaka:20181215120108j:plain
Hearty

マットな感じになるので、今回のような動物キャラには向いていると思います。

もう1種類よかったのがGraceです。Heartyに比べるとちょっとお高いですが、ピカピカな硬質な仕上がりになります。目とクチバシはGraceの黒で作りました。

f:id:am1tanaka:20181215120129j:plain
Grace

アクリル角棒 3mm

粘土を詰めるために東急ハンズで買いました。

f:id:am1tanaka:20181215120207j:plain
アクリル角棒3mmx3mm

最初は爪楊枝の裏側で詰めていましたが、角まで押し込むのに時間がかかりました。角棒だと面積があるので押し込みやすいです。東急ハンズで100円ちょっとだったと思いました。

木工用ボンドと筆

キューブを接着するためのセットです。木工用ボンドは普通の速乾タイプ。筆は100円ショップで買いました。

f:id:am1tanaka:20181215121221j:plain
木工用ボンドと筆

木工用ボンドを水で薄めて使うのですが、受け皿はインスタントコーヒーの蓋を使ってます。まあ、何でもいいです。

その他

あとは、爪楊枝、サランラップ、粘土を保存するためのビニール袋といった日用品で全てです。

今回のキャラ用の材料費

シリコン型は4つ買いました。白と黒のキューブをそれぞれ同時に16個ずつ、合計32個作れる体制です。モノトーンのキャラなので、粘土はHeartyの白と黒、目とクチバシ用のGraceの黒が1袋ずつです。一番使ってる白でもまだ半分も減ってません。当面は1袋で十分です。

あとは一つずつということで、スターターキットは4千円ちょっとでした。一度揃えれば、あとは粘土とか消耗品は安いので結構この後はお安そうです。

値段
シリコン型4つ 2,580円
Hearty白と黒 940円
Grace黒 164円
木工用ボンド 185円
108円
アクリル棒 150円ぐらいだったかな?
合計 4,127円(ぐらい)

キューブの作り方

粘土のキューブの作り方です。

こねる

まずは粘土を10秒~20秒ほどこねて、柔らかさを均等にします。最初は水分が多くて、手にくっつくような状態かも知れません。それだと乾くのが遅くなるので、平たくして5分ぐらい乾かしてから使った方がよいかも知れません。

粘土がべたべたしなくなって滑らかになったら、シリコン型に詰めていきます。

シリコン型に詰める

キューブの半分ぐらい埋まる程度の薄さで、シリコン型に押し付けるようにして粘土を延ばします。あとは、アクリル棒でしっかりと角まで粘土がいきわたるように押していきます。

f:id:am1tanaka:20181215120316j:plain
3.5mmのCubeに粘土を詰める

裏側から見ると、詰まり具合が見えますので、角までうまく粘土が行き渡っていたら、さらに粘土を足して、キューブからはみ出して構わないので、アクリル棒で押し込みます。

余分な粘土を削り取る

粘土がキューブにしっかりと詰められたら、爪楊枝の腹の部分を使って、余分な粘土を取り除きます。

f:id:am1tanaka:20181215120455j:plain
ならす

大雑把にやると粘土がめくれたりするので、爪楊枝をよい塩梅で転がしつつ押し広げる感じで平らにします。粘土は乾燥すると少し縮むので、へこむよりは、少し盛り上がっているぐらいの方がよいと思います。シリコン型にはみ出した分は、粘土のかたまりを押し付けるとくっついて取りやすいです。

この作業に一番時間がかかります。道具を工夫すればもっと奇麗で手早くできるかも知れません。まだ研究中です。

2.5mmキューブ

ついでなので、2.5mmの一番小さいキューブも作ってはいます。このサイズだと爪楊枝の裏側で押し込めます。

f:id:am1tanaka:20181215120718j:plain
2.5mmは爪楊枝で

表面をならす

詰め終わったら、ビニールなど表面がなめらかなものを敷いて、上から爪楊枝でならして、表面のでこぼこを減らします。

f:id:am1tanaka:20181215121007j:plain
表面をならす

効果はあるようなないような、まあ、おまじないみたいなものでやってます。

完了!

ということで、一丁あがりです。

f:id:am1tanaka:20181215121135j:plain
詰め終わり

自分の場合、4セット揃えるのに30分ぐらいかかります。時給換算するとこれはどうだろう...という感じですが、息抜きの手の運動ということで。

粘土をしまう

粘土はとても1回では使いきれません。残りは、こんな感じでラップで口を閉じた上で、ジップロックに入れて空気を抜いて、冷蔵庫で保管しています。

f:id:am1tanaka:20181215120946j:plain
仕舞い方

夏から4ヶ月ぐらい経ってますが、ちゃんと空気を抜いておけばまだ使えています。

乾燥

最初は、詰めたらすぐに取り外して、どんどん量産できるものと思っていました。実際やってみると、完全に乾かないと形が崩れてしまって取り出せませんでした。

真夏で半日冬だと1日はかかります。のんびり待ちます^^; 冷蔵庫だと乾燥しているので早く乾くという情報もありますが、うちの冷蔵庫は乾燥しない感じの冷蔵庫らしく全然乾きませんでした...。一度、試してみるとよいと思います。うまくいくと結構早く乾燥するかも知れません。

乾いたらシリコン型を裏返して、裏からデコピンをすればポロポロと外れます。完全に乾いていれば形は崩れませんので、粘土の周りのシリコンを少し押して、キューブが少し出たところを爪に引っかけて取り出しても大丈夫です。ということで、こんな感じでできあがります!

f:id:am1tanaka:20181215121318j:plain
出来上がり

シリコン型を4つ持っていても、1日で作れるキューブの数は32個。夏なら頑張って64個。キャラクターを作るのはなかなかの長期戦です^^;

接着

キューブができたら、木工用ボンドで接着します。木工用ボンドをそのまま使うと、粘土をくっつける時にはみ出して、そこが変色してしまったりします。そこで、水で薄めることにしました。割合は適当で1対1ぐらいでよいと思います。何らかの器に木工用ボンドを少し出して、同じぐらいの量の水も一緒に垂らして、筆で薄めながら粘土の接着面に薄く塗ります。

組み立て方は、表面の層から1列をまずくっつけて、繋げた列同士をまたくっつけて面を作るという感じでやりました。例えば、まずは以下の白、黒、黒、黒、白の5つのキューブをくっつけて1列作ります。

f:id:am1tanaka:20181219203039j:plain
最初の1列

接着は結構適当で大丈夫で、ボンドを塗ってはくっつけ、ボンドを塗ってはくっつけしてまず5つ繋げてしまってから、平らな面に押し付けながら、端から爪楊枝の後ろでぎゅうぎゅう押して10秒ぐらい待つと離れなくなります。そんな感じで、次は白を7つ、その次は白、Graceの黒、白3つ、Graceの黒、白で1列、という感じで顔のパーツを1列ずつ作っていきます。

30分ほどすれば、少し力が加わったぐらいだと大丈夫になるので、作った列同士をボンドでくっつけていきます。あまり強く力を加えるとまだこの段階では形が崩れるので、爪楊枝の腹の部分や、アクリル角棒などをうまく使って力を加えてくっつけました。1日放っておけば、かなりがっちりと乾きますので、列だけ作っておいて、あとは次の日とする方が確実かも知れません。

1列できたら、もとのボクセルモデルを見ながら1列奥を同じようにくっつけていきました。裏から見るとこんな感じです。

f:id:am1tanaka:20181219203807j:plain

キューブ自体がそれほどちゃんとした立方体ではないので、力技っぷりがあふれる裏側ですが、表はなんとか形になってます^^;

完成

改めて現在できてるやつ。

f:id:am1tanaka:20181215121442j:plain
ななめ

f:id:am1tanaka:20181215121424j:plain
見上げ

f:id:am1tanaka:20181215121451j:plain
見下ろし

デジゲー博に連れて行きました。

f:id:am1tanaka:20181219205234j:plain
デジゲー博のブース

f:id:am1tanaka:20181219205254j:plain
ココ

まとめ

クオリティは微妙な感じですが、立体物が作れるのは面白いです。また、立体にせずに平面でくっつけて、ニスで仕上げてキーホルダーにするとかもできるかも知れません。

粘土は、基本的な色は揃っていて発色もいいです。粘土を混ぜて色を作ることもできますし、白の粘土に絵具で着色することもできます。そのあたりは自由度が高いと思います。粘土に磁石を練りこんで、マグネットにできると面白いなとか思ってます。

シリコン型はレジンにも使えるので、レジンのキューブを作ることもできます。そうすればもっと奇麗なものが作れるかも知れません。粘土でも、器用で仕事が丁寧な人ならもっと奇麗にできそう。。。

ちょっと熱が冷めて最近あまり作業を進めてませんが、またぼちぼち組み立てたりキューブを作っていこうと思います^^

明日は、daishihmrさんです!

ML-Agents Beta 0.6.0のリリースノート

Unityで手軽に強化学習などのAIを使うことができるML-Agentsの0.6.0が出てました。

github.com

Releaseページ

とりあえずリリースページにあった内容をざっくりと。

重要

BrainがMonoBehaviourからScriptableObjectに変更されました。これにより、Brainをプレハブに設定して、同じBrainをマルチシーンで使えるようになりました。詳しくはml-agents/Migrating.md at 0.6.0 · Unity-Technologies/ml-agents · GitHubを参照してください。

  • InternalExternalBrain Typeが、LearningBrainアセットによって置き換えられました
  • Heuristic Brainタイプが、HueristicBrainアセットに置き換えられました
  • Player Brainタイプが、PlayerBrainアセットに置き換えられました
  • Brainは、AcademyコンポーネントBroadcast Hubを通して、Pythonのトレーニングプロセスから参照されるようになりました

新機能

  • [Unity] Demonstration Recorder. Unityエディターから、Agentの行動や観測内容を記録することができるようになりました。記録したデータを使って、Agentを後でトレーニングすることができます。記録したトレーニングデータを使うことで、複数のトレーニングセッションでトレーニングすることができるようになりました
  • [Communication] Windowsマシン上のprotobuf-definitions内のprotobufオブジェクトを生成するためのmake_for_win.batが追加されました
  • LearningBrainに、モデルとBrainのパラメーターが一致していなかったときに警告するデバッグメッセージを追加しました

変更点

  • 学習済みモデル(trained model)からGraph Scopeが削除されました。同じセッション内で複数のBrainがトレーニングされる際、複数のGraph Scopeを1つのGraphにまとめるのではなく、Brainごとに1つのGraphが生成されるようになりました

既知の不具合

  • Windowsでは、[Ctrl]+[C}キーでトレーニングを終了した時にモデルが保存されません

まとめ

Brainタイプが変更になったので、0.5.0のプロジェクトを利用する場合は、Agentの再設定が必要なようです。ScriptableObjectにBrainの設定をするようになるので、再利用が楽になりそうです。年末年始にエアホッケーのトレーニングに手をつけようと思ってましたが、0.6.0でやってみようと思います。

参考URL

Photon BoltでPhysicsのボールは動くのか

アドベントカレンダー1日目(12/1)の記事です。

f:id:am1tanaka:20181129195310g:plain
ゲーム画面

明日はRaspberlyさんの3Dモデルの影を別の影に差し替える - Raspberlyのブログです!

  • (2018/12/12 Network Rateの設定で動きを滑らかにすることと、Boltアイコンの消し方について追記しました)
  • (2018/12/7 PUNのvelocityの同期について追記しました)

目次

Photon Boltとは

マルチプレイヤーゲームのネットワーク部分を担当してくれるUnity用のネットワークエンジンです(ビジュアルスクリプティングのBoltの話ではございません!!Exit Gamesが提供しているネットワークエンジンの一つで、最近流行っている人が集まって対戦するようなリアルタイムネットワークゲームを開発するのに向いています。

詳しくは以下の公式サイトや、アセットストアをどうぞ。

www.photonengine.com

注意点!

Photon Boltの最大の注意点の一つは、現時点でWebGLに対応していない!!ということだと思います。1週間ゲームジャムとかでは使えませんのでご注意を!PUNの方は使えます。

目的

学園祭のような人が集まる場所で、同じ教室内のLAN環境で、エアホッケーのネットワーク対戦ゲームを動かしたいというのが今回の出発点です。ビルドターゲットはWindowsmacなどのPCです。

エアホッケーならRigidbodyvelocityを同期できれば簡単に作れそうです。しかし、Photon Boltにはvelocityを同期するためのState設定がありません。そこで、ブログタイトルということになります。結論から言えば、

動きます!!

このブログでは、実際に動かした習作プロジェクトを動かす手順と、どのように設定をしたのかをご紹介します。

前提

UnityへのPhoton Bolt Freeのインストールと、はじめようを試しておくのが望ましいです。

ブログのUnityは2017.4.15f1です。2018以降だと一部メニューの場所が異なるのでご注意ください。ブログ執筆時のPhoton BoltのUnityのサポートバージョンは2017.4.13以降です。

動作確認

とりあえず動きが見てみたい!という方は、Windowsmacそれぞれの実行ファイルを用意しましたので、以下の手順でお試しください。

  • こちらを開きます
  • Windows用はRollerBallBolt.zipmac用はRollerBallBolt-mac.zipです。最新版のものをダウンロードして、適当な場所に展開します
  • RollerBallBoltの実行ファイルを2つ起動します。あるいは、2台のPCでそれぞれ1つずつ起動します
  • 好きな画面サイズ、あるいはフルスクリーンを選択して、Play!をクリックして起動します
  • 以下のようなメニューが起動するので、先に起動する方でStart Serverを押します

起動画面
起動画面

  • 接続が完了して、赤いプレイヤーとボールが表示されたら、もう一方でStart Clientをクリックします
  • 接続が完了したら、それぞれのPCでマウス操作して、ボールを打ち合えます

ログが邪魔な場合(というか邪魔なので)、[Tab]キーを押すと非表示にできます。

f:id:am1tanaka:20181127174903p:plain
ゲーム画面

プロジェクトの準備手順

プロジェクトをUnityで動かすための手順です。Photon Boltは、途中から組み込むのがちょっと面倒だったりします。手順にお気をつけください。指示通りにやっているのに動かない場合、Unityを閉じてプロジェクトを開き直すとか、Unityが2017.4以降かを確認してみてください。

アカウントの入手とApp IDの取得

サインイン | Photon Engineを開いて、Photonアカウントの作成と、Bolt用のアプリを作成して、App IDを生成してください。

リポジトリーのダウンロード

  • こちらを開いてください
  • Cloneするか、Download ZIPでリポジトリーをダウンロードして、作業をしたいフォルダーに展開してください
  • Unity2017.4.13以降で、プロジェクトを開きます

この時点ではエラーが大量に出ます。引き続き、以下を設定します。

初期設定

  • Asset StoreからPhoton Bolt Freeをダウンロードしてインポートします。bolt/project.bytesはこちらで用意したものを利用したいのでチェックを外してください

f:id:am1tanaka:20181128154831p:plain

Photon Boltは、同時接続20接続(マッチング後にサーバーを利用しなくなったアカウントも含みます)などの制約以内であれば、無料で利用できます。開発中や、今回のような試用の場合はFree版でいけます。

まだエラーが出ます。設定を続けます。

  • Assetsメニューを開いて、Boltがメニューにあるか確認します。見当たらなかったら、Unityを閉じてプロジェクトを開きなおします
  • Assetsメニューから、Bolt > Compile Assemblyを選んで、パッケージをコンパイルします。ビルドが完了するのを待ってください
  • Windowメニューから、Bolt > Wizardを選んで、ウィンドウが開いたらNextをクリックします
  • PhotonのWebページで作成したApp IDを入力します
  • 変なリージョンに行かれると接続ができなかったりしたので、テスト時はRegion[jp] Japan :: Tokyoにしておきます
  • 以上選択したらNext

f:id:am1tanaka:20181129125930p:plain

  • Core Packageをクリックしてデータをインポートしたら、Next
  • Doneをクリック
  • Assetsメニューから、Bolt > Compile Assemblyを実行

以上で、エラーがなくなり、構築完了です。最後に、ビルドの際にエラーの原因になるのでコメントアウトしておいたコードを復活させます。

  • Projectウィンドウから、RollerBallBolt > Scriptsフォルダーを開いて、Menu.csをダブルクリックして開きます
  • 10行目付近になる/*と、54行目付近にある*/を削除します
  • 上書きしてUnityに戻ります

以上でプロジェクトの設定は完了です。動作を確認しましょう。

動作確認

Photon Boltでは、開発を楽にするために実行形式のファイルを自動的にビルドして、ホストとクライアントで起動してくれる機能があります。それを使ってGameシーンをネットワークで実行してみます。

  • Windowメニューから、Bolt > Scenesを選択します
  • Bolt Scenesウィンドウで、Clients欄を1にします。これで、クライアントアプリが1つ起動するようになります
  • Gameと書いてある欄の右のDebug Startをクリックします

f:id:am1tanaka:20181128160121p:plain

ビルドが始まって、しばらく待つとアプリが1つ起動します。起動したアプリのPlay!ボタンをクリックすると、Unityでホスト、クライアントアプリでクライアントが動きます。マウスを動かすと、Player1(赤いやつ)とPlayer2(青いやつ)が同時に動きます。1台のPCで動かしているので、どちらも同じマウスの入力で動くからです。

f:id:am1tanaka:20181127174903p:plain
実行画面

設定の調整(2018/12/12追記)

Photon Boltの設定がデフォルトのままなので、クライアント側の動きがガタガタで、オブジェクトに雷マークが表示されています。これらを直すための設定は以下の通りです。

  • Windowメニューから、Bolt > Settingsを選択します
  • Network Rate1にします
    • これで、状態の同期を1フレームごとに行うようになるので、クライアントの動きがスムーズになります
    • ネット環境によっては、動きがおかしくなることがあるので、その時は2にしたり3に戻したり調整してみてください
  • 必要に応じて、Miscellanceous欄の以下のチェックを外します
    • Show Debug Info
      • オブジェクトに表示されるBoltアイコンが消えます
    • Show Help Buttons
      • ヘルプボタンを非表示にします
    • Visible By Default
      • 情報ウィンドウをデフォルトで非表示にします

サンプルプロジェクトの動かし方については以上です。ここからは、このプロジェクトの解説です。

実装方法の解説

Photon BoltでPC間で同期したいデータは、Stateを作って、それにデータを割り当てます。そこにRigidbodyvelocityとかを同期する設定があればいいのですがありません。UNetにはあったので油断していました。

Photon Boltでは、Commandsを使って、クライアントからホストに操作情報を送らせて、サーバーでまとめて制御した結果をクライアントに戻して反映させる、という仕組みがあります。ざっくりこんな感じ。

f:id:am1tanaka:20181129142906p:plain
(public) RollerBallBoltChart | Sketchboard

こちらの方が正確な物理シミュレーションが可能で、誤差も少なくなることが期待できます。ということで、CommandsでマウスのX-Yの移動量を送信して、プレイヤーの座標を結果として返すようにしました。

ほとんどの処理はサーバー側で行って、クライアントは端末扱いです。これをどのように実装したかを大まかにまとめます。詳しいところは公式ドキュメントなどをどうぞ。

制作過程の概要

プロジェクトの作成

新規プロジェクトを作成して、以下をやりました。

  • Asset StoreからPhoton Boltのインポート
  • AssetsメニューからBolt > Compile Assemblyを実行
  • Windowメニューから、Wizardを起動して、Boltのコアパッケージのインストール、App ID、リージョンの設定
  • AssetsメニューからBolt > Compile Assemblyを実行

シーンの作成

サーバーとクライアントのどちらでログインするかを決めるためのボタンと、サーバー接続のコードを実装したMenuシーンと、Gameシーンの2つを用意しました。

空のシーンを作成して、Main CameraにBolt 102 - Getting Started | Photon EngineにあるMenu.csをアタッチしました。シーン名がサンプルのものと違う場合は、そこを変更するのを忘れずに。

f:id:am1tanaka:20181129133105p:plain
Menu画面

Gameシーン

ゲームに必要なオブジェクトを作って配置します。

f:id:am1tanaka:20181129144913p:plain
画面レイアウト

ビルド設定

シーンができたらビルド設定をします。作業する前に、Assets > Bolt > Compile Assemblyを実行します。Photon Boltは、シーンやオブジェクト、スクリプトなどを事前に把握する仕組みがあり、Compile Assemblyをしないと新しく作成したものがプロジェクトに反映されず、エラーの原因になったりします。

ビルド設定は、Boltチュートリアル - チャプター1 | Photon Engine「シーンを立ち上げる前に、全てが動作していることを確認するため、・・・」以降の手順が参考になります。

Stateの作成

PC間で同期するデータを決めるStateを作ります。今回のプロジェクトは物凄くシンプルなので、プレイヤーもボールもTransformのみを共有すればOKです。ということで、以下のようなTransformStateを作りました。

f:id:am1tanaka:20181129152641p:plain
TransformState

利用するプロパティはTransform型のTransformだけです。

SpaceはデフォルトがLocalだったのでそのままにしました。親子階層がないのでWorldでもよさそうではあります。この辺は調査不足で、明確な理由があってこうしている訳ではありません。

クライアントの動きを滑らかにする設定(補間とNetwork Rate)

Smoothing AlgorithmNoneのままにしました。Interpolation(内挿)とExterpolation(外挿)の違いは以下にあります。

内挿と外挿 | Photon Engine

ざっくりと、Interpolationは過去のデータを利用して動きを滑らかに見せます。過去の場所の間に補間するので、突飛な場所に移動することはありません。ただし、少し古い座標に表示されることになります。

Exterpolationは過去の動きの傾向から、次の座標を予想して動かします。レスポンスは速いですが、予想が外れた場合はいたことがない座標に移動してしまいます。

今回のようにマウス操作&Physicsベースだと、動きの予想がしずらいのでExterpolationでの予測精度があまりよくありませんでした。また、Interpolationは遅延が気になります。

ということで、Noneのままにして補間はしないことにしました。そのままではガタついて見えてしまいますが、これはStateの送受信の頻度が原因です。デフォルトの設定では3フレームに1回同期する設定になっています。今回の場合、送信するデータが非常に少ないので、1フレームに1回にするという力業で補間せずに済ませました。この辺は設定のコツがあるかも知れません。今後の調査項目です。

設定方法は以下の通りです。

  • WindowメニューからBolt > Settingsを選択します
  • Network Rate1に変更します

f:id:am1tanaka:20181212133952p:plain
Network Rate

以上です。ネット環境によっては、動きがおかしくなる場合があります。その際は値を戻したり2にしたりして調整してみてください。(2018/12/12追記)

Commandの作成

クライアントのデータをやりとりするために、以下のようなRollerBallBoltCommandを作成しました。

f:id:am1tanaka:20181129181335p:plain
RollerBallBoltCommand

入力のMouseVector型で、動かす時に楽をするためにXZで送ることにします。

結果はXYZ全て利用するVector型で、そのままローカル座標に放り込みます。

効果がよくわかりませんでしたが、なんとなく奇麗に動いている気がするのでSmooth Corrections(スムーズに補正する)にチェックを入れました。

ボールプレハブの作成

ゲームオブジェクトをネット対応させます。ネットワーク上で共有するオブジェクトにはBoltEntityスクリプトをアタッチして、ネットワークに接続した時にBoltNetwork.Instantiate()で生成します。

ボールは、Rigidbodyをくっつけて、位置を同期させるだけです。Sphereを作成して、以下の通りRigidbodyBoltEntityをアタッチします。

f:id:am1tanaka:20181129195533p:plain
Ball Inspector

また、新規にスクリプトを作成してBallなどの名前にしておきます。TagBallを追加して、割り当てておきます。

BoltEntityは、アタッチ直後はエラーが発生します。例のごとく、AssetsメニューからBolt > Compile Assemblyでビルドしてから、State欄をITransformStateに設定します。

Ball.csは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace RollerBallBolt
{
    public class Ball : Bolt.EntityBehaviour<ITransformState>
    {
        /// <summary>
        /// transformをIPlayerStateのTranformに割り当てます。
        /// </summary>
        public override void Attached()
        {
            state.SetTransforms(state.Transform, transform);
        }
    }
}

アタッチされた時にtransformStateTransformに割り当てているだけです。

以上できたらプレハブ化して、ボールの出現位置に配置します。

プレイヤーの作成

プレイヤーを作成します。形はCylinderにして、メッシュと当たり判定の形状を変えたかったので、親子階層にして親にSphere Collider、子にCylinderを設定しました。

f:id:am1tanaka:20181129200831p:plain
Player

設定は以下の通りです。

  • TagPlayer
  • Sphere Colliderをアタッチして、良い場所と大きさに調整
  • Rigidbodyをアタッチして、Use Gravityを外し、Collision DetectionContinuous DynamicFreeze PositionYにチェック、Freeze Rotationを全てチェック
  • Bolt Entityをアタッチして、Compile Assemblyを実行後、StateITransformStateに設定
  • 新規スクリプトを作成して、Playerの名前に変更します

Player.csスクリプトは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using Bolt;
using UnityEngine;

namespace RollerBallBolt
{

    public class Player : Bolt.EntityBehaviour<ITransformState>
    {

        [Tooltip("送られてきた操作を速度に変換するレート。Mouse XとMouse Yは-1~1に正規化されているので、動作環境の解像度は考えなくて構いません。"), SerializeField]
        float input2Speed = 20f;

        Rigidbody rb;

        float _x;
        float _y;

        #region System Loop

        private void Awake()
        {
            rb = GetComponent<Rigidbody>();
        }

        private void Update()
        {
            PollMouse();
        }

        #endregion System Loop

        #region Bolt Events

        /// <summary>
        /// transformをIPlayerStateのTranformに割り当てます。
        /// </summary>
        public override void Attached()
        {
            state.SetTransforms(state.Transform, transform);
        }

        /// <summary>
        /// 操作権を持つプレイヤー(Player1はホスト、
        /// Player2はクライアント)で呼び出されます。
        /// 操作をBoltEntityに渡します。
        /// </summary>
        public override void SimulateController()
        {
            IRollerBallBoltCommandInput input = RollerBallBoltCommand.Create();

            Vector3 data = new Vector3(_x, 0, _y);
            input.Mouse = data;

            entity.QueueInput(input);
        }

        /// <summary>
        /// オブジェクトのオーナーで呼び出されます。
        /// これはPlayer1, 2ともにホストで呼び出されます。
        /// 入力を受け取って動かします。
        /// Player2からは、クライアントに結果を送信します。
        /// </summary>
        /// <param name="command">送られてきた操作コマンド</param>
        /// <param name="resetState">操作権を持っていたらtrue</param>
        public override void ExecuteCommand(Command command, bool resetState)
        {
            RollerBallBoltCommand cmd = (RollerBallBoltCommand)command;

            if (resetState)
            {
                // Player2。送られてきたコマンドのデータを反映させます
                transform.localPosition = cmd.Result.Position;
            }
            else
            {
                // 入力を使ってオブジェクトを動かします
                rb.velocity = cmd.Input.Mouse * input2Speed;

                // ホストとクライアントの双方で呼び出されます
                // 現在の座標を送信します
                cmd.Result.Position = transform.localPosition;

            }
        }

        #endregion Bolt Events

        #region My Methods

        void PollMouse()
        {
            _x = Input.GetAxis("Mouse X");
            _y = Input.GetAxis("Mouse Y");
        }

        #endregion My Methods

    }
}

Advencedチュートリアルをベースに改造したものです。ポイントは以下の通りです。

  • Bolt.EntityBehaviour<ITransformState>を継承します
  • Update()でマウスの移動量を記録しておきます
  • アタッチされた時に、transformStateTransformに割り当てるコードをAttached()メソッドに実装します
  • SimulateController()は、操作をシミュレートする際にBoltから呼び出されるメソッドです。ここで、Update()で記録しておいたマウスの移動量をRollerBallBoltCommandInputMouseに設定して、QueueInput()で登録します
  • ExecuteCommand(command, resetState)に、コマンドを実行するための処理を実装します。commandは送受信するためのRollerBallBoltCommandインスタンスresetStateはサーバーかクライアントのどちらで動いているかを表していて、trueの時はコントローラー(クライアント)側の処理、つまり、送られてきた結果を自分のローカル座標に反映させます。falseの時は、オーナー(サーバー)側の処理で、QueueInput()で蓄えられた入力データを速度に反映させて、オブジェクトを動かし、結果をcommandに渡します

あとは、Player1用とPlayer2用で別のマテリアルを設定してプレハブ化して、出現させたい場所に配置しておきます。

f:id:am1tanaka:20181129202856p:plain

f:id:am1tanaka:20181129202905p:plain

フィールド

枠や地面などのフィールドは、ネットワークで共有する必要はないので、特に何もせずにそのまま配置しておきます。

f:id:am1tanaka:20181129203141p:plain

オブジェクトを配置する機能の作成

チュートリアルでは、ランダムに座標を設定したりしてスクリプト側で座標を作っていましたが、最初から登場させたい場所にゲームオブジェクトを配置して、そこにInstantiateした方が楽だろうということで作った機能です。

  • 新規に空のGame Objectを作成して、NetworkSceneManagerなどの名前にします
  • 新しいスクリプトを作成して、NetworkSceneManagerなどの名前にします

スクリプトは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace RollerBallBolt
{
    public struct OBJECT_RECORD
    {
        public int id;
        public Vector3 position;
    }

    public class NetworkSceneManager : MonoBehaviour
    {
        public static NetworkSceneManager Instance
        {
            get;
            private set;
        }

        public List<OBJECT_RECORD> playerPositions {
            get;
            private set;
        }
        public OBJECT_RECORD ballPosition
        {
            get;
            private set;
        }

        private void Awake()
        {
            Instance = this;

            playerPositions = new List<OBJECT_RECORD>();

            GameObject[] gos = GameObject.FindGameObjectsWithTag("Player");
            OBJECT_RECORD or;
            foreach(GameObject go in gos)
            {
                or.id = go.GetComponent<BoltEntity>().prefabId.Value;
                or.position = go.transform.position;
                playerPositions.Add(or);
                Destroy(go);
            }

            GameObject ball = GameObject.FindGameObjectWithTag("Ball");
            or.id = ball.GetComponent<BoltEntity>().prefabId.Value;
            or.position = ball.transform.position;
            ballPosition = or;
            Destroy(ball);
        }

        /// <summary>
        /// BoltEntity.prefabId.Valueを受け取って、該当する
        /// プレハブIDで記録された座標を返します。
        /// </summary>
        /// <param name="id">プレハブID</param>
        /// <returns>座標を返します</returns>
        public Vector3 GetPlayerPosition(int id)
        {
            foreach(OBJECT_RECORD or in playerPositions)
            {
                if (or.id == id)
                {
                    return or.position;
                }
            }
            return Vector3.zero;
        }
    }
}

Awake()で、PlayerBallのタグが付いたオブジェクトを検索して、プレハブIDと座標を記録したらDestroyします。サーバーでオブジェクトを生成する際に、プレハブIDで記録しておいた座標を検索して、その座標にゲームオブジェクトを配置しています。

ネットワーク管理スクリプト

最後に、シーンが始まった時に、プレイヤーやボールを生成するためのオブジェクトとスクリプトです。これは、ゲームオブジェクトにはアタッチしません。新規にスクリプトを作成したら、RollerBallBoltServerCallbacksなどの名前にします。コードは以下の通りです。

using UnityEngine;
using RollerBallBolt;

namespace RollerBallBolt
{

    [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")]
    public class RollerBallBoltServerCallbacks : Bolt.GlobalEventListener
    {
        /// <summary>
        /// このプログラムがシーンを読み込んだ時の処理
        /// </summary>
        /// <param name="map"></param>
        public override void SceneLoadLocalDone(string map)
        {
            // プレイヤー1を生成して、操作を担当します
            BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player);
            be.transform.position = NetworkSceneManager.Instance.GetPlayerPosition(be.prefabId.Value);
            be.TakeControl();

            // ボールを作ります
            be = BoltNetwork.Instantiate(BoltPrefabs.Ball);
            be.transform.position = NetworkSceneManager.Instance.ballPosition.position;
        }

        /// <summary>
        /// クライアントがシーンを読み込んだ時に報告されるコールバック
        /// リモート用のプレイヤーを生成
        /// </summary>
        /// <param name="connection"></param>
        public override void SceneLoadRemoteDone(BoltConnection connection)
        {
            // プレイヤー2を生成して、操作を接続先に任せます
            BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player2);
            be.transform.position = NetworkSceneManager.Instance.GetPlayerPosition(be.prefabId.Value);
            be.AssignControl(connection);
        }
    }
}

クラスは、Bolt.GlobalEventListenerを継承します。

    [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")]

上記の属性により、このクラスはサーバー、かつ、Gameシーンのみで実行されます。

SceneLoadLocalDone()は、サーバーのGameシーンが準備できた時にサーバーで実行するコードです。Player1Ballを生成して、Player1はサーバー自身が制御するのでTakeControl()を呼び出します。

SceneLoadRemoteDone()は、クライアントのGameシーンが準備できた時にサーバーで実行するコードです。Player2を生成して、操作はクライアントが担当するので、AssignControl()で接続先(connection)に割り当てます。

制作過程は、おおよそ以上の通りです。

オブジェクトのBoltアイコンを消すには(2018/12/12追記)

デフォルトのままですとオブジェクトに雷のBoltアイコンが表示されっぱなしになります。これを消すには、WindowメニューからBolt > Settingsを選んで起動して、Miscellanceous欄のShow Debug Infoのチェックを外します。

他にも、Show Help ButtonsVisible By Defaultのチェックを外すと、デバッグ用のボタンや情報ウィンドウを消すことができるので、適宜、設定するとよいでしょう。

まとめ

Physicsで制御するゲームオブジェクトを、Photon Boltを使ってネットワーク上で共有することができました。

UNetに比べると手順はやや多い印象ですが、動いたあとの安心感があります。サービス停止の心配ないし...。

エアホッケーブロック崩しピンボール、コインプッシャーなど、Physicsを使えば簡単に作れるタイプのゲームがあります。今回ご紹介した方法を利用すれば、そのようなゲームをネットワーク対応させることができそうです。

WebGLが使えたらなぁ...と願いつつ、明日のRaspberlyさんの3Dモデルの影を別の影に差し替える - Raspberlyのブログにバトンタッチします。

raspberly.hateblo.jp

Physicsの同期について(2018/12/7追記)

Photon Unity Networking(PUN)では、PhysicsのRigidbodyのvelocityを同期するためのクラスPhotonRigidbodyViewがあります。

Photon Unity Networking: PhotonRigidbodyView Class Reference

Photon Boltでも、Statevelocity用のVector3を追加して速度の同期を試みることはできます。ただ、このAPIリファレンスの解説にある通り、Rigidbodyのvelocityを同期するだけではネットワーク間のオブジェクトの動きが変わってしまう可能性があります。

今回のように、物体の衝突による動きが直接ゲーム性に関わる場合、PC間の挙動の違いでボールの飛ぶ方向が変わるのは困ります。座標を送って補正したとしても、動きが不自然になることが予想されます。そのため、Commandsで操作を送ってサーバーがまとめて処理する方式にしました。

Rigidbodyのvelocityを使えば、各PC上で座標計算ができるので動きを綺麗にすることができます。弾などの単純な軌道で、衝突時に跳ね返る必要がないようなオブジェクトの場合は、Rigidbodyのvelocityを同期する手法は使えると思います。どのような動きをさせたいかで採用する技術が変化してくるので、研究のし甲斐があると思います。今回と同じ内容のものを、PUNのvelocity同期で実装したらどうなるかも見てみたいところです。

参考URL

www.photonengine.com

assetstore.unity.com

UnityのML-Agentsで、新しい学習環境を作成する

ML-Agentsの動作環境は整えられました。次は、新しいプロジェクトにML-Agentsを組み込む手順を確認します。以下のドキュメントを読んでいきます。

github.com

以下は、元記事から変更や補足したところです。

  • ブレインの名前の変更
  • ターゲットに近づくとリワードがもらえる機能のコードの追加
  • 学習手順と再生手順とまとめを追加

目次

動作環境

以下で確認しました。

  • Windows10
  • Unity2018.2.10
  • ML-Agents Beta 0.5.0a

ML-Agentsのリポジトリーをクローンしていなかったり、環境構築ができていない場合は、先に以下の記事を完了させてください。

am1tanaka.hatenablog.com

このチュートリアルで作るもの

ランダムな場所に登場するキューブを取るようにボールを制御するAIをトレーニングします。また、ボールが地面から落下しないようにもします。

  1. Unity上でAIをトレーニングするための環境(environment)を作ります。環境は、いくつかのオブジェクトによるシンプルな物理シミュレーションから、ゲームやエコシステム全体まで含みます
  2. Academy(アカデミー)サブクラスを実装してゲームオブジェクトにアタッチして、環境を組み込んだUnityのシーン内に配置します。このゲームオブジェクトは、シーン内の様々なブレイン(Brain=AIの脳)の親として動作します。アカデミークラスには、AIエージェントとは別にシーンを更新するいくつかのオプションのメソッドを実装できます。例えば、環境内に、AIエージェントやその他のオブジェクトを追加、移動、削除したりできます
  3. 1つ以上のAIのブレインオブジェクトを、アカデミーの子供としてシーンに追加します
  4. エージェントサブクラスを実装します。エージェントサブクラスは、環境を観察するためのコードを定義して、行動を割り当てて、強化学習のための報酬(Reward)を計算します。また、学習が終了したり、タスクが失敗した時に、エージェントをリセットするメソッドを実装します
  5. 作成したエージェントサブクラスをゲームオブジェクトにアタッチします。アタッチしたオブジェクトは、シミュレーション内のエージェントとしてシーン内で行動します。エージェントには必ずブレインオブジェクトを割り当てる必要があります
  6. Brain TypeExternalで学習させます
  7. Brain TypeInternalにして、学習してできたモデルデータを使ってUnityでAIを動かします

Unityプロジェクトの作成

まずは、新規でUnityプロジェクトを作成して、ML-Agentsを組み込みます。

  • Unity2017.4以降を起動して、RollerBallという名前で新しいプロジェクトを作成します
  • エクスプローラーで、ダウンロード(あるいはクローン)して展開したml-agentsフォルダーを開きます
  • UnitySDK/Assetsフォルダー内のML-Agentsフォルダーをドラッグして、UnityのProjectウィンドウにドロップしてインポートします

f:id:am1tanaka:20181110121550p:plain

Unityのバージョンによって表示は変わりますが、おおよそ以下のようになります。

f:id:am1tanaka:20181110121637p:plain

ML-Agentsツールキットを有効にする

公式ドキュメントだと、ML-Agentsの設定の手順が抜けているので、以下のようなエラーが発生すると思います。

f:id:am1tanaka:20181110124628p:plain

こちら「この段階ではまだエラーが発生します。設定を続けます。」という文の続きを設定します。以下、簡単な手順です。

  • Editメニューから、Project Settings > Playerを選択します

以下を、利用したいプラットフォームごとに実施します

  • Other Settingsを開きます
  • Scripting Runtime VersionExperimental、あるいは.NET 4.6 Equivalent、あるいは.NET 4.x Equivalentにします
  • Restartするかのウィンドウが表示されたら、Restartします

Unityが一度閉じて再起動するのを待ってください。これでエラーが出なくなります。

環境(Environment)を作る

ML-Agentsの環境用のシーンとして、エージェントが乗っかる床となるPlane、エージェントがゴールや目標として探索するCube、エージェントを表すSphereを作成します。

床のPlaneを作る

  • HierarchyウィンドウのCreateをクリックして、3D Object > Planeを選択します
  • 作成したPlaneの名前をFloorにします
  • 作成したFloorをクリックして選択します
  • InspectorウィンドウのTransform欄を以下の通り設定します
    • Position = 0, 0, 0
    • Rotation = 0, 0, 0
    • Scale = 1, 1, 1
  • InspectorウィンドウのMesh Renderer欄にあるMaterialsの左の三角をクリックして開いたら、Element 0の右の〇をクリックして、ウィンドウからFloorマテリアルを選択して割り当てます

f:id:am1tanaka:20181110123205p:plain

以上で床は完成です。

取るためのキューブを作る

  • HierarchyウィンドウのCreateをクリックして、3D Object > Cubeを選択します
  • 作成したCubeの名前をTargetにします
  • 作成したTargetをクリックして選択します
  • InspectorウィンドウのTransform欄を以下の通り設定します
    • Position = 3, 0.5, 3
    • Rotation = 0, 0, 0
    • Scale = 1, 1, 1
  • InspectorウィンドウのMesh Renderer欄にあるMaterialsの左の三角をクリックして開いたら、Element 0の右の〇をクリックして、ウィンドウからBlockマテリアルを選択して割り当てます

エージェントとするSphereを作る

  • HierarchyウィンドウのCreateをクリックして、3D Object > Sphereを選択します
  • 作成したSphereの名前をRollerAgentにします
  • 作成したRollerAgentをクリックして選択します
  • InspectorウィンドウのTransform欄を以下の通り設定します
    • Position = 0, 0.5, 0
    • Rotation = 0, 0, 0
    • Scale = 1, 1, 1
  • InspectorウィンドウのMesh Renderer欄にあるMaterialsの左の三角をクリックして開いたら、Element 0の右の〇をクリックして、ウィンドウからChekerGoalマテリアルを選択して割り当てます
  • Add Componentをクリックします
  • PhysicsからRigidbodyを選択してアタッチします

後程、このオブジェクトにエージェントサブクラスをアタッチします。

アカデミーとブレインを持たせる空のゲームオブジェクトを作る

  • HierarchyウィンドウのCreateをクリックして、Create Emptyを選択します
  • 作成したGameObjectの名前をAcademyにします
  • 作成したAcademyオブジェクトを右クリックして、Create Emptyを選択して、子供に空のゲームオブジェクトを作成します
  • 作成した子供のGameObjectの名前をRollerBallBrainにします(元記事では単にBrainでしたが、後の設定のため変更しました)

f:id:am1tanaka:20181113214728p:plain

以上でシーンが完成です。Main Cameraの座標や角度を調整して、Gameウィンドウで見やすくなるようにしてください。

f:id:am1tanaka:20181110125921p:plain

アカデミーの実装

アカデミーオブジェクトは、シーン内のML-Agentsを動かして、観測-意思決定-行動というシミュレーションループを制御します。ML-Agentのシーンごとに1つのアカデミーのインスタンスが必要です。アカデミークラスのベースクラスはabstractなので、メソッドを実装する必要がなくてもサブクラスを作成する必要があります。

まずは、先に作成したアカデミーオブジェクトに新しいスクリプトを追加します。

  • HierarchyウィンドウからAcademyオブジェクトをクリックして選択します
  • Inspectorウィンドウで、Add Componentをクリックします
  • New Scriptを選択して、RollerAcademyという名前で作成します
  • 作成したRollerAcademyスクリプトをダブルクリックして、エディターで開きます
  • MLAgentsの機能を使うために、using MLAgents;を定義します
  • クラスの継承元をMonoBehaviourからAcademyに変更します
  • Start()Update()メソッドを削除します

基本的な機能はベースクラスのAcademyが持っているので、実装は不要です。以下の通りにしたら、上書き保存をしてUnityに切り替えます。

using MLAgents;

public class RollerAcademy : Academy { }

デフォルトの設定のまま使います。Inspectorウィンドウで変更する項目はありません。

f:id:am1tanaka:20181111010139p:plain

ブレイン(Brain)を作る

ブレインオブジェクトは、行動を決める処理を担当します。エージェントは自らが観測した情報をブレインに送ります。ブレインは送られてきた観測情報を元に行動を決定してエージェントに伝えます。エージェントは、ブレインから受け取ったデータに従って行動します。Brain Type設定で、ブレインがどのように意思決定を行うかを選択することができます。アカデミーやエージェントクラスと違い、ブレインは既存のスクリプトをそのまま利用します。

  • Hierarchyウィンドウから、Academyの子供のRollerBallBrainオブジェクトを選択します
  • Inspectorウィンドウで、Add Componentをクリックします
  • Scripts > MLAgents > Brainを選択して、コンポーネントをアタッチします

f:id:am1tanaka:20181113214956p:plain

設定は改めて行います。現時点ではBrain TypePlayerのままにしておきます。

エージェントの実装

エージェントを作成します。

  • Hierarchyウィンドウから、RollerAgentオブジェクトをクリックして選択します
  • Inspectorウィンドウで、Add Componentをクリックします
  • New Scriptを選択して、RollerAgentという名前で作成します
  • 作成したRollerAgentスクリプトをダブルクリックして、エディターで開きます
  • Academyの時と同じように、usingを設定して、ベースクラスをMonoBehaviourからAgentに変更します

以下のようなコードになります。

using MLAgents;

public class RollerAgent : Agent { }

上書き保存をして、Unityに戻ります。

ここまでの手順は、あらゆるUnityのプロジェクトにML-Agentsを加える基本的な手順です。次に、強化学習を使ってSphereのエージェントがキューブを取るように動かすためのロジックを加えていきます。

今回のシンプルなシナリオでは、アカデミーオブジェクトに環境の変更は実装しませんが、シミュレーション前やシミュレーション中に、床の大きさを変えたり、エージェントやその他のオブジェクトを追加したり削除したりしたい場合は、アカデミーにメソッドを自由に追加して実装することができます。ここでは、エージェントが成功したり失敗した時に、エージェントや目標物をリセットすることだけを行います。

エージェントの初期化とリセット

RollerBallのエージェントは、Targetに触れたら自分自身でそのことを記録して、Targetをランダムな座標に配置し直すリセット関数を呼び出します。また、RollerBallが地面から落下したら、リセット関数によって地面に戻します。

Targetを追うためには、エージェントはTargetTransformを知っている必要があります。そのために、RollerAgentクラスにpublicTransformを定義します。これにより、Inspectorウィンドウで、ターゲットを設定できるようになります。エージェントの速度をリセットしたり、エージェントを動かすための力を与えるために、Rigidbodyコンポーネントの参照が必要です。Rigidbodyは、Unityで物理シミュレーションをするための基本的なエレメントです。スクリプトのゲームオブジェクトにアタッチされているRigidbodyへの参照は、Start()メソッド内で、GameObject.GetComponent<T>()を使って取得します。

RollerAgent.csは以下のようになります。

using System.Collections.Generic;
using UnityEngine;
using MLAgents;

public class RollerAgent : Agent {
    Rigidbody rBody;
    private void Start()
    {
        rBody = GetComponent<Rigidbody>();
    }

    public Transform Target;
    private float previousDistance = float.MaxValue;
    public override void AgentReset()
    {
        // 前回の距離をリセット
        previousDistance = float.MaxValue;

        if (this.transform.position.y < -1.0)
        {
            // エージェントが落ちた
            this.transform.position = Vector3.zero;
            this.rBody.angularVelocity = Vector3.zero;
            this.rBody.velocity = Vector3.zero;
        }
        else
        {
            // ターゲットを新しい場所へ移動させる
            Target.position = new Vector3(
                Random.value*8-4,
                0.5f,
                Random.value*8-4
            );
        }
    }
}

次は、Agent.CollectObservations()関数(情報を収集する関数)を実装します。

環境を観測する(Observing the Environment)

エージェントは、収集した環境の情報をブレインに送ります。ブレインは送られてきた情報を使って行動を決めます。エージェントをトレーニングする時、あるいは、トレーニングされたモデルを利用する時に、観測したデータをfeature vector(観測結果を表すベクトルデータ)としてニューラルネットワークに与えます。エージェントの学習を成功させるためには、正しい情報を与えなくてはなりません。どの情報を与えるのが正しいかを決めるよい指標は、問題を解決するために何が必要かを分析して考えることです。

今回の場合、エージェントは以下の情報を集めます。

  • ターゲットの座標。一般的に、絶対的な座標よりも、エージェントから見た相対的な座標を使う方がよいでしょう。今回は、Plane上を移動するだけでY座標は変化しないので、X-Z座標のみ利用します
// エージェントからターゲットへの相対座標を計算する(Calculate relative position)
Vector3 relativePosition = Target.position - this.transform.position;

// 相対座標(Relative position)
AddVectorObs(relativePosition.x / 5);
AddVectorObs(relativePosition.z / 5);
  • 床の四隅からのエージェントの距離。エージェントを床に留まらせるための情報です
// 床の角までの距離 (Distance to edges of platform)
AddVectorObs((this.transform.position.x + 5) / 5);
AddVectorObs((this.transform.position.x - 5) / 5);
AddVectorObs((this.transform.position.z + 5) / 5);
AddVectorObs((this.transform.position.z - 5) / 5);
  • エージェントの速度。ターゲットを通り過ぎたり、床から転落しないように速度を学習させるための情報です
// Agent velocity
AddVectorObs(rBody.velocity.x / 5);
AddVectorObs(rBody.velocity.z / 5);

全ての値は5で割って、正規化をしています。これは、ニューラルネットワークが受け取る値が-11の範囲だからです。何故5かというと、今回の床のサイズが10なので、-55でどこにいるかが表されるからです。

(補足) この処理では1を越えることがあるため、正規化としては不十分です。後程、正規化フラグを設定します。 (補足ここまで)

観測した状態(state observation)の数は8つです。それをAddVectorObs()関数でブレインに渡します。

以上をまとめると以下のようになります。RollerAgent.csに加えてください。

public override void CollectObservations()
{
    // 相対座標を計算(Calculate relative position)
    Vector3 relativePosition = Target.position - this.transform.position;

    // 相対座標を正規化して設定(Relative position)
    AddVectorObs(relativePosition.x/5);
    AddVectorObs(relativePosition.z/5);

    // 床の隅からの距離を正規化した値を設定(Distance to edges of platform)
    AddVectorObs((this.transform.position.x + 5)/5);
    AddVectorObs((this.transform.position.x - 5)/5);
    AddVectorObs((this.transform.position.z + 5)/5);
    AddVectorObs((this.transform.position.z - 5)/5);

    // エージェントの速度(Agent velocity)
    AddVectorObs(rBody.velocity.x/5);
    AddVectorObs(rBody.velocity.z/5);
}

エージェントコードの最後は、Agent.AgentAction()関数です。ブレインが決定した行動を受け取ります。

Actions

ブレインが決めた行動は、アクションの配列としてAgentAction()関数に渡されます。この配列の要素数は、エージェントのブレインの設定であるVector Action Space TypeVector Action Space Sizeによって決まります。RollerAgentでは、Continuous Vector Action Space(連続的な行動空間)で、ブレインから2つのcontinuous control signals(連続的な制御信号)を必要とします。よって、Brain Vector Action Sizeには2を設定します。最初のエレメントであるaction[0]X軸方向に加える力で、2つ目のaction[1]Z軸方向に加える力とします(もし、Y方向の移動もあった場合は、Vector Action Size3になります)。ニューラルネットワークから返される値は、-11の間の値になります。ブレインは、これらの値がどのようなものなのかは把握しません。トレーニングは、観測した入力に対して返す値を調整した結果、どのようなリワード(報酬)が得られるかということだけに注目して行われます。

RollerAgentaction[]配列の値をRigidbodyコンポーネントであるrBodyAddForce()で反映させます。

Vector3 controlSignal = Vector3.zero;
controlSignal.x = action[0];
controlSignal.z = action[1];
rBody.AddForce(controlSignal * speed);

実際のコードは後述します。

Rewards(リワード=報酬)

強化学習ではリワードの設定が必要です。リワードはAgentAction()関数内で割り当てます。学習アルゴリズムは、シミュレーションのステップごとにエージェントにリワードを与えて、エージェントにとって最適な行動が何かを学習するのに利用します。エージェントが目的のタスクを達成したら(今回ならTargetオブジェクトに辿り着いたら)報酬を与えて、望まない行動をしたら(今回なら床から転落したら)報酬を取り上げます。

レーニングを短縮するために、エージェントが最終的なタスクを達成するのを助けるための補助的なリワード(sub-rewards)を設定する方法があります。例えば、Targetとの距離が前回より近づていたらリワードを少し与えつつ、毎ステップごとに少しリワードを減らすような設定を加えることで、エージェントはTargetに近づくことが有利だと学習でき、Targetに到着しやすくなります。

RollerAgentは、Targetに辿り着いたかを検出するために距離を計算します。算出した距離が到達したと判断する値以下になっていたら、エージェントに1.0のリワードを与えて、完了したことを報告するためにDone()を呼び出します。

float distanceToTarget = Vector3.Distance(this.transform.position,
                                          Target.position);
// Reached target
if (distanceToTarget < 1.42f)
{
    AddReward(1.0f);
    Done();
}

メモ: Done()を呼び出してエージェントが完了したことを知らせると、リセットするまで動作が停止します。Agent.ResetOnDoneプロパティーをインスペクター上でTrueに設定しておくことで、完了したらすぐに自動的にエージェントをリセットすることもできますし、アカデミーがMaxStepに到達して、環境をリセットするのを待つこともできます。今回はResetOnDoneプロパティーTrueに設定してリセットします。MaxStep0のままにして、アカデミーが環境をリセットしないようにします。

(補足)

Targetへの距離が縮まったら少しリワードを与えて、学習効果を高めるためのコードです。元記事には変数は定義されていたものの、リワードを与えるコードがなかったので補足します。

        // 近づいたサブリワード
        if (distanceToTarget < previousDistance)
        {
            previousDistance = distanceToTarget;
            AddReward(0.03f);
        }

(補足ここまで)

以下は、ステップごとにリワードを少し減らすことで、タスクをより手早く完了できるようにするための工夫です。

// タイムペナルティ
AddReward(-0.05f);

最後に、プラットフォームから転落した時にリワードを大きく減らす処理と、Doneを呼び出して次のステップで自分自身をリセットさせる処理です。

// プラットフォームからの転落
if (this.transform.position.y < -1.0)
{
    AddReward(-1.0f);
    Done();
}

AgentAction()

行動とリワードの考え方は以上の通りです。AgentAction()関数の完成形は以下のようになります。RollerAgent.csに加えてください。

    public float speed = 10;

    public override void AgentAction(float[] vectorAction, string textAction)
    {
        // Targetとの距離
        float distanceToTarget = Vector3.Distance(
            this.transform.position,
            Target.position);

        // Targetに接触したか
        if (distanceToTarget < 1.42f)
        {
            AddReward(1);
            Done();
        }

        // 近づいたサブリワード
        if (distanceToTarget < previousDistance)
        {
            previousDistance = distanceToTarget;
            AddReward(0.03f);
        }

        // 近づかなかったペナルティ
        AddReward(-0.05f);

        // プラットフォームから転落
        if (this.transform.position.y < -1.0)
        {
            AddReward(-1);
            Done();
        }

        // 動作の設定。sizeが2なのでvectorActionは2つ
        Vector3 controlSignal = Vector3.zero;
        controlSignal.x = vectorAction[0];
        controlSignal.z = vectorAction[1];
        rBody.AddForce(controlSignal * speed);
    }

仕上げのエディター設定

全てのゲームオブジェクトとML-Agentコンポーネントの設定が完了したので、データを接続していきます。

  • HierarchyウィンドウのAcademyオブジェクトの左の三角をクリックして開いて、子供のRollerBallBrainオブジェクトを表示します
  • HierarchyウィンドウのRollerAgentゲームオブジェクトをクリックして選択します
  • Hierarchyウィンドウから、RollerBallBrainオブジェクトをドラッグして、InspectorウィンドウのBrainフィールドにドロップします
  • Inspectorウィンドウで、Dicision Frequency1から5に変更します
    • エージェントは、この設定のステップ数ごとにブレインへ観測データを送信して意思決定を要求します。行動(Action)は1ステップごとに行われます(バランスボールのサンプルもこの値は5でした)

また、HierarchyウィンドウからTargetオブジェクトをドラッグして、RollerAgentTargetフィールドにドロップします。

f:id:am1tanaka:20181113220102p:plain

最後に、RollerBallBrainゲームオブジェクトをクリックして選択したら、Inspectorウィンドウで以下を設定します。

  • Vector Observation Space Size = 8
    • ブレインに送る観測データベクトルの次元数(要素数)
  • Stacked Vectors = 1
  • Visual Observation = 0
    • 観測データとして利用するグラフィックの数。今回は未使用なので0
  • Vector Action Space Type = Continuous
    • 行動結果を表すベクトルを、整数(Discrete)で返すか、小数(Continuous)で返すか
  • Vector Action Space Size = 2
    • 行動結果として返すベクトルの次元数(要素数)
  • Action Descriptions = 2
    • Vector Actionの各要素の説明
  • Brain Type = Player
    • 意思決定の種類
      • Player = 人が操作する
      • Heuristic = 行動決定のためのアルゴリズムDecitionインターフェースを実装したクラスを作成して、自前で実装する
      • External = Pythonと通信して、TensorFlowなどに意思決定させる
      • Internal = TensorFlowを使って学習済みのモデルで意思決定する

f:id:am1tanaka:20181113134844p:plain

以上で、トレーニングを開始する前に、環境を試す準備ができました。

環境を試す

実際にトレーニングを開始する前に、手動で環境を試すのはよい手順です。Brain TypePlayerのままにしてあるので、キー入力でエージェントを動かすことができます。前後左右の4方向に4つのキーを割り当てます。

  • HierarchyウィンドウでRollerBallBrainオブジェクトをクリックして選びます
  • InspectorウィンドウのKey Continuous Player Actions欄を開きます(この設定は、Brain TypePlayerの時のみ表示されます)
  • Size4にします
  • 以下の通り、設定します
Element Key Index Value
Element 0 D 0 1
Element 1 A 0 -1
Element 2 W 1 1
Element 3 S 1 -1

Indexの値は、AgentAction()関数に渡す配列のインデックスを表します。Valueの値は、Keyが押された時にvectorAction[Index]に渡される値です。

Playすると、[W][A][S][D]キーで、Agentを操作することができます。エラーが発生しないことと、Targetに触ったり床から落ちたらエージェントがリセットされることを確認してください。ML-Agents SDKには、ゲームウィンドウにエージェントの状態を簡単に表示させることができるMonitorクラスが用意されています。より詳細な状態が見たい場合は利用するとよいでしょう。

(ここではやりませんが)作成した環境とPython APIが想定通りに動くかどうかを、Jupyter notebookを使って試しておくことができます。確認のためには、notebooks/getting-started.ipynbを利用します。ノートブックのenv_nameに、この環境をビルドして生成した実行ファイル名を設定します。

学習の準備が整ったら、ブレインのBrain TypePlayerからExternalに変更します。ここから先の手順は、Training ML-Agentsと同じです。

シーンレイアウトを振り返る

最後に、作成した環境でエージェントがどのように動作するかを振り返っておきましょう。

今回のトレーニングをUnity ML-Agentsで実行するために、3つのゲームオブジェクトを作成しました。

  • Academy
  • Brain
  • Agents

約束事です。

  • 1つのシーンには、Academyゲームオブジェクトを1つだけ配置できます
  • Brainは1つのシーンに複数配置できますが、それらはすべてAcademyゲームオブジェクトの子にする必要があります

以下、シーンのHierarchyの見え方の例です。

example hierarchy

(https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Learning-Environment-Create-New.md より)


元記事には学習手順と学習結果を利用する手順が省略されているので、以下に補足します。

学習手順

前提

Unity ML-AgentsをWindows10で使う 2018年11月版 - tanaka's Programming Memoが完了していることを前提にします。

準備

まずはブレインを外部に変更します。

  • HierarchyウィンドウでRollerBallBrainをクリックして選択します
  • Inspectorウィンドウで、Brain TypePlayerからExternalに変更します

学習の設定をします。デフォルトの5万ステップだと、あまり学習の成果が見られません。3D Ballのサンプルは12個のエージェントを同時にトレーニングしていたので2万ステップ程度で効果が表れましたが、こちらは同時に1つのエージェントしかトレーニングしないので、その10倍の20万ステップは最低でも学習させたいところです。余裕をもって、50万ステップに設定します。

また、ニューラルネットワーク-11の範囲のデータを受け取る仕様になっています。ところが、今回のサンプルでは状況に応じてこの範囲を超えてしまうことがあり、学習の妨げになります。そこで、正規化フラグを有効にすることで範囲外の数値が来たら-11の範囲に収めるように設定します。

  • ML-Agentsのリポジトリーからダウンロードしたml-agents-masterフォルダー内のconifg/trainer_config.yamlを何らかのテキストエディターで開きます
  • 22行目のcuriosity_enc_size: 128という行と、24行目のBananaBrain:の間に、以下を追加します
24:
RollerBallBrain:
    normalize: true
    max_steps: 5.0e5
  • 上書き保存します

これで、UnityのRollerBallBrainという名前のブレインをトレーニングする場合、以下が有効になります(このためにBrainの名前をRollerBallBrainに変更しました)。

  • 入力データを正規化(normalize)する
  • ステップ数を50万回(5.0e5は、5x10^5の意味)

学習させる

Pythonを使って学習を開始します。

  • Windowsの検索バーにanaと入力して、Anaconda Promptを起動します
  • cdコマンドで、ml-agents-masterのフォルダーに移動します。作成したUnityProjectではなく、ML-AgentsのGitHubリポジトリーをダウンロードして展開したフォルダーの方です
  • 以下を入力します
mlagents-learn config/trainer_config.yaml --run-id=RollerBall --train
  • Unityのアスキーアートが表示されて、INFO:mlagents.envs:Start training by pressing the Play button in the Unity Editor.という表示が出たら、Unityに切り替えて、Playすれば、学習が始まります。

f:id:am1tanaka:20181113224106p:plain

ログの見方

コンソール画面に謎の文字が表示されますが、ここから学習がどのように進んでいるかを読み取ることができます。このログは、設定した回数ごとの集計になっています。デフォルトでは千回に1回、集計されて以下のように表示されます。

f:id:am1tanaka:20181114172135p:plain

Step以降を見ていきます。上の例ではStep: 360000.になっています。359001回~360000回のステップのトレーニングが完了した時の情報ということです。

大切なのは、それに続くMean RewardStd of Rewardの2つの数値です。

Mean Reward

リワードの平均値です。今回の例なら、36万ステップまで千ステップの平均リワードが1.149だったということです。この値は、最初は-15ぐらいでしたので、かなりの学習成果が上がっているということになります。

この値は、上がったり下がったりを繰り返しつつ、徐々に大きくなっていくはずです。5万~10万ステップぐらいまでは変化しているように見えない場合もありますので、慌てずじっくりと眺めてください。

20万ステップしても変化がないようなら、学習効果が得られない設定になっている可能性があります。プログラムや考え方を見直してみてください。

Std of Reward

リワードの標準偏差です。標準偏差とは、データのバラつきを表す値です。全てのリワードが同じ値ならこの値は0になり、バラけているほど大きな値になります。

AIがめちゃくちゃに動いている最初の方はリワードがバラバラになるので、大きな値になりがちです。頭が良くなるにつれて行動が似てくるのでリワードのバラつきが減り、この値は小さくなっていくのが一般的です。

いきなりこの値が小さい場合、めちゃくちゃに動いてもリワードが変わらないということなので、エージェントの設定を失敗している可能性があります。

条件によっては学習が進むほどこの値が大きくなることもあります。この値が小さくなれば十分に学習できた、と単純には言えないということです。バラつきを表す値ということを考えて、値を読み取ってください。

学習は何ステップがよいか

条件によって結構変化しますが、とりあえず2Dのものなら50万ステップ程度が目安のように感じています。学習をさせすぎると成績が悪化する過学習という現象が起きる場合があります。学習後にMean Rewardを確認して、スコアが悪化していくようなら、丁度良さそうなステップ数を確認して、その回数で学習し直すとよいでしょう。

まだスコアが伸びそうな場合は、trainer_config.yamlmax_stepsの値を増やして上書き保存をしてから、Anaconda Promptで以下を実行すれば、先ほどのデータの続きから学習を再開できます(すでに学習ステップ数がmax_stepsに達していたら、学習を再開させてもすぐに終わってしまいます。学習回数を増やす場合は、trainer_config.yamlmax_stepsも増やしてください)。

mlagents-learn config/trainer_config.yaml --run-id=RollerBall --train --load

今回のプロジェクトだと、30万ステップぐらいでほぼ安定して、50万ステップまで少しずつ精度が上がっていった(Std of Rewardが小さくなっていった)感じでした。

考察(おまけ)

前よりターゲットに近づいた時に与えるリワードを0.1にして、毎回の減点より大きい数値にして試すと、100万ステップ学習させてもスコアが伸び続けました。面白いのが、標準偏差も大きくなっていったことです。近寄るごとにリワードがもらえる設定だと、Targetをすぐに取るよりも、ゆっくり近づいて取った方がリワードが多く得られるようでした。そうなると、どこにTargetが出るかによって点数に差が出るようになります。リワードが出現位置の乱数に影響を受けるようになるため、標準偏差の値が大きくなっていたものと思われます。

なるべく早くTargetを取るAIにしたかったので、リワードを見直して今回のパラメーターにしました。

再生手順

最後に、学習したモデルデータをUnityのプロジェクトに組み込んで再生してみましょう。

TensorFlowSharpプラグインをプロジェクトにインポートします。ダウンロードしていない場合は、こちらからダウンロードして、インポートしてください。PC上で試すだけなら、AndroidiOSプラグインは不要なので、チェックを外した方が容量やインポート時間を節約できます。

f:id:am1tanaka:20181114001257p:plain

インポートが完了したら、TensorFlowを有効にするための設定をします。

  • Editメニューから、Project Settings > Playerを選択します

以下を、利用したいプラットフォームごとに実施します

  • Other Settingsを開きます
  • Scripting Defined Symbols欄に、ENABLE_TENSORFLOWを入力して、[Enter]キーを押します
  • [Ctrl]+[S]キーを押して保存します

以上で、Brain ModeInternalが利用できるようになります。

学習したモデルデータをプロジェクトに読み込みます。モデルデータはml-agents-masgterフォルダーの中に、models > run-idで設定した名前のフォルダーの中に入っています。

  • エクスプローラーなどでml-agents-masterフォルダー内の、models/RollerBallフォルダーを開きます
  • .bytesファイルをドラッグして、UnityのProjectウィンドウにドロップします

f:id:am1tanaka:20181114010242p:plain

最後に、ブレインの設定を切り替えて、モデルデータを設定します。

  • Hierarchyウィンドウで、RollerBallBrainをクリックして選択します
  • Inspectorウィンドウで、Brain TypeInternalに変更します
  • Projectウィンドウからモデルデータをドラッグして、InspectorウィンドウのGraph Model欄にドロップします

f:id:am1tanaka:20181114010533p:plain

以上で完了です。Playをすれば、学習させたAIがRollerAgentを操作してTargetを取っていくのが確認できます。

f:id:am1tanaka:20181114011102g:plain

まとめ

新規プロジェクトから学習用の環境を構築して、学習、結果の利用ができました。入力や出力を当てはめられるものであれば、自前のプロジェクトに組み込めそうです。

学習させる上で、以下の知見が得られました。

  • 学習は50万ステップ程度は見込んでおく
  • 同時に複数のエージェントを学習できるように、3DBallのように親オブジェクトからの相対座標で動く環境を作るとよい(この記事の作り方だと、同時に学習できないのでステップ数が多くなるので)
  • サブリワードの設定は、学習の回数を減らしたりイメージに近い行動をさせるのに大いに有効
  • 正規化も重要。コード側で正規化をしていない場合は、設定のnormalizeフラグを利用する

一年前はこの辺りで他のことを初めてしまってとん挫したので、今回は自分で作った環境でAIを鍛えたいです。

参考URL

Unity ML-AgentsをWindows10で使う 2018年11月版

f:id:am1tanaka:20181107163745g:plain

Unityで機械学習(Machine Learning)が使えるML-Agentsについて、1年前に導入の記事を書きました。あれから大幅に変更されて、動作環境やセットアップ手順が変わったので、2018年11版としてまとめ直します。

  • レーニング時に、待ちすぎると待機が終わる注意を追記(2018/11/9)
  • TFSharpのインポート時に、必要がなければAndroidiOSのチェックを外すとよいというコメントを追記(2018/11/8)

目次

Unity ML-Agents Toolkit (Beta)について

https://github.com/Unity-Technologies/ml-agents よりざっくりと。

Unity Machine Learning Agents Toolkit(ML-Agents)は、AIで動くエージェントをゲームやシミュレーションでトレーニングするための環境を提供します。エージェントは、強化学習(Reinforcement learing)、模倣学習(Imitation learning)、遺伝的アルゴリズム学習(Nueroevolution)、その他の機械学習を簡潔に使えるPython APIを通して学習させることができます。また、TensorFlowで構築された最新鋭のAIエージェントを2D、3D、VR/ARゲームなどでトレーニングして使う機能も提供します。これらのトレーニングされたエージェントは、NPMの振る舞いを制御することを含めて、ゲームの自動テストや、リリース前に異なったゲームのデザインを評価するなど様々な目的で利用できます。

機能

  • PythonからUnityの環境を操作
  • 10以上のUnity用のサンプル
  • 複数の環境設定や、トレーニングシナリオに対応
  • ディープラーニングを使ってメモリーを強化したエージェントのトレーニン
  • 分かりやすい学習シナリオのカリキュラム定義
  • 教師あり学習(supervised learning)のためのエージェントの振る舞いのブロードキャスティング
  • 模倣学習のための組み込みに対応
  • On Demand Decision Making(要求された時に意思決定をする)による自由度の高いエージェント制御
  • 環境内でのネットワークの視覚化
  • Dockerによる環境構築
  • ジム(gym)として学習環境をラッピングする機能

インストール手順

ml-agents/Installation.md at master · Unity-Technologies/ml-agents · GitHubを参考に、Windows10で環境を構築する手順です。簡単にするために、ドキュメント通りではない部分があります。

インストール手順概要

ML-Agentsの環境は以下の通り。

なお、このブログでは以下の環境で確認しました。

  • Windows10
  • Unity2018.2.10とUnity2017.3.0(推奨環境ではありませんが、3DBallは動きました)
  • Anaconda5.1 + Python3.6
  • TensorFlow1.7.1

Unity2017.4以降のインストール

Unity2017.4以降がインストールされていればOKです。Dockerを利用する場合は、Linuxビルドが必要ですが、今回は利用しないので、Linuxビルドは不要です。

ML-Agentsツールキットリポジトリーのダウンロード

gitが利用できる環境であれば、コマンドラインを開いて、リポジトリーをダウンロードしたいフォルダー(ドキュメント -> Unity Projectsフォルダーなど)に移動してから、以下を実行します。

git clone https://github.com/Unity-Technologies/ml-agents.git

Gitの利用が不安な場合は、こちらのページを開いて、以下を実行します。

  • Webページ右の緑色のClone or Downloadボタンをクリックします
  • Download ZIPを選択して、ZIPファイルをダウンロードします
  • ml-agents-master.zipのダウンロードが完了したら、プロジェクトを保存したいフォルダー(ドキュメント -> Unity Projectsフォルダーなど)に全て展開します

以上でml-agents-masterフォルダーに以下のファイルが展開されます。

UnitySDKフォルダー

ML-Agentsを利用したいプロジェクトに追加が必要なUnityアセットが入っています。このフォルダー内には、UnityでML-Agentsを使い慣れるのに便利な多くのサンプル環境も入っています。

ml-agentsフォルダー

AIをトレーニングしたり、UnityとPythonを連携させるためのPython APIなどのPythonパッケージが入っています。

gym-unityフォルダー

ブロック崩しピンボールなどのゲーム向けAIなどを強化学習で教育するためのツールキットであるOpen AI GymをUnityで使うためのインターフェースが入っています。

Pythonとmlagentsパッケージのインストール

以下をインストールしていきます。

注意!

  • 現在、Python3.5や3.7はサポートしていません

インストール手順

ml-agents/Installation-Windows.md at master · Unity-Technologies/ml-agents · GitHubを参考にインストールをします。

ML-AgentsはWindows10のみでテストされています。他のWindowsやBootcampやParallelsのような仮想マシンでも動く可能性はありますが、動作テストはされていません。

Step 1: Anacondaを使ってPythonをインストール する

Windows用のAnacondaをダウンロードしてインストールします。Anacondaは、Pythonの科学計算向けパッケージです。Python自体を含み、複数の独立したPython環境の構築ができます。

ML-Agentsでは、少し古いAnaconda5.1を利用します。Anacondaのサイトからだと、古いバージョンのダウンロードリンクが動いていないようなので、64-bitならこちら32-bitならこちらからダウンロードしてください(ビット数が分からない場合は、とりあえず64ビットのものを使ってみてください)。

オプションはそのままで構わないので、インストールを進めます。

  • VSCodeは不要なので、以下はSkipします

f:id:am1tanaka:20181106162826p:plain

  • 完了したら、特に何も開く必要はないので、チェックを外してFinishをクリックします

f:id:am1tanaka:20181106162911p:plain

インストールが完了したら、仕上げのためにAnaconda Navigationを起動して設定をします。

  • 検索バーに(検索バーが表示されていなければスタートボタンをクリックしてから)anaと入力すると、Anaconda Navigationが検索で表示されるのでクリックします

f:id:am1tanaka:20181106163055p:plain

  • 設定が完了したらアプリが起動するので、OK, and don't show ageinをクリックします

f:id:am1tanaka:20181106163619p:plain

  • 以上が完了したら、アプリを閉じます

f:id:am1tanaka:20181106163626p:plain

コマンドラインからAnacondaを使えるようにするために、環境設定を実行します。

  • 検索バーに環境と入力するなどして、環境変数を編集を開きます

f:id:am1tanaka:20181106165350p:plain

  • Path欄をダブルクリックします

f:id:am1tanaka:20181106165504p:plain

  • 新規ボタンをクリックして、以下の2つのパスを追加して、OKをクリックします
%UserProfile%\Anaconda3\Scripts
%UserProfile%\Anaconda3

以上で、コマンドラインからAnaconda用のコマンドであるcondaが呼び出せるようになりました。

Step 2: 新規にCondaの環境(Conda environment)を設定する

ML-Agentsツールキット用のCondaの環境を作成します。Condaの環境を利用することで、ML-Agentsの動作に必要なパッケージをインストールする影響を限定して、システム全体や他のPython環境に影響を与えるのを防ぎます。Conda環境は、ML-Agentsを実行したい時に必要に応じて有効にすることができます。

Anaconda Promptを使って環境設定をします。

  • 検索バーにanaと入力して、リストアップされるAnaconda Promptをクリックして起動します

f:id:am1tanaka:20181106172435p:plain

  • 以下のコマンドを実行して、Python3.6の環境を、ml-agentsという名前で新規に作成します
conda create -n ml-agents python=3.6

f:id:am1tanaka:20181106172653p:plain

  • yを入力して[Enter]キーを押して、設定を完了させます

f:id:am1tanaka:20181106172839p:plain

Anaconda環境の有効化

ML-Agentsでこの環境を利用する場合は、Anaconda Promptから以下のコマンドを実行します。

activate ml-agents

これで、作成したml-agents環境が有効になり、以下のようにプロンプトが変更されます。

f:id:am1tanaka:20181106173153p:plain

TensorFlow1.7.1のインストール

機械学習ライブラリであるTensorFlowを、作成したConda環境にインストールします。先に開いたAnaconda Promptで、ml-agentsを有効にしたら、以下を実行します。

pip install tensorflow==1.7.1

f:id:am1tanaka:20181106173407p:plain

注意!! ML-Agentsは、TensorFlowの最新版はサポートしません。必ず、指定のバージョン(1.7.1)をインストールしてください。

Step 3: 必要なPythonパッケージのインストール

先にダウンロードしていた、ML-AgentsのGitリポジトリーフォルダーに移動して、インストールコマンドを実行します。Step 2に続けて、Anaconda Promptで作業します。

  • cdコマンドで、先ほどダウンロードして展開したML-Agentsのフォルダー内のml-agentsフォルダーを開きます
    • 展開したのがユーザーのドキュメントフォルダー内のUnity Projectsフォルダーだった場合は、以下のようになります。違うフォルダーに展開した場合は、そのフォルダー内のml-agentsフォルダーを開いて下さい
cd %USERPROFILE%\Documents\UnityProjects\ml-agents-master\ml-agents
pip install .

Step 4のGPUレーニングは使わなくてもよいのでとりあえず飛ばします

以上でインストール完了です。

最初の手引き

以下の内容に沿って、インストールした教育済みモデルの例をUnityで動作させる手順と、どのようにモデルをトレーニングするかの手順を示します。

github.com

UnityでML-Agentsツールキットを準備する

まずは、ML-Agentsツールキットを利用するための設定をして、TensorFlowSharpプラグインを組み込みます。

  • 2017.4以降のUnityを起動します
  • Openをクリックして、ダウンロードして展開したml-agents-masterフォルダー内のUnitySDKフォルダーを開きます

f:id:am1tanaka:20181107145656p:plain

  • バージョン違い確認のダイアログが表示されたら、Continueをクリックして、読み込みを進めます

この段階ではまだエラーが発生します。設定を続けます。

  • プロジェクトが起動したら、Editメニューから、Project Settings > Playerを選択します
  • 以下を、利用したいプラットフォームごとに実施します
    • Other Settingsを開きます
    • Scripting Runtime VersionExperimental、あるいは.NET 4.6 Equivalent、あるいは.NET 4.x Equivalentにします

f:id:am1tanaka:20181107150052p:plain

  • Scripting Defined Symbols欄に、ENABLE_TENSORFLOWを入力して、[Enter]キーを押します

f:id:am1tanaka:20181107150241p:plain

  • FileメニューからSave Projectを選択します

TensorFlowSharpプラグインのインストール

こちらからTensorFlowSharpプラグインをダウンロードします。

ダウンロードが完了したら、ダウンロードしたファイルをダブルクリックするか、UnityのProjectタブにドラッグ&ドロップして、プロジェクトにインポートします。AndroidiOSでの利用の予定がない場合は、インポートするパッケージからチェックを外しておくとよいです(ご指摘下さったHayatoさん、ありがとうございます!)

インポートが成功したら、Assets > ML-Agents > Plugins > Computerフォルダー内に、TensorFlow関連のファイルが確認できます。

f:id:am1tanaka:20181107152720p:plain

以上で、エラーがなくなります。

レーニング済みのモデルを動作確認

3DBallを実行してみましょう。

  • ProejctタブでAssets/ML-Agents/Examples/3DBallフォルダーを開いて、3DBallシーンをダブルクリックして開きます

f:id:am1tanaka:20181107152947p:plain

  • Hierarchyタブで、Ball3DAcademyゲームオブジェクトの子供のBall3DBrainオブジェクトを選択します

f:id:am1tanaka:20181107153107p:plain

f:id:am1tanaka:20181107153220p:plain

  • Projectタブで、Assets/ML-Agents/Examples/3DBall/TFModelsフォルダーを開きます

f:id:am1tanaka:20181107153355p:plain

  • 3DBallモデルをドラッグして、InspectorタブのGraph Model欄にドロップします

f:id:am1tanaka:20181107153526p:plain

以上で設定完了です。Playすると、板の上でボールのバランスをとる様子が確認できます。

f:id:am1tanaka:20181107153610p:plain

強化学習(Reinforcement Learning)のトレーニン

Brainを外部(External)に設定

学習はUnityとPythonを連携させて行うので、Brainを外部にあるPythonとつなげるためにExternalにします。

  • Hierarchyタブで、Ball3DAcademyオブジェクトの子供のBall3DBrainオブジェクトを選択します
  • InspectorタブのBrain Type欄をExternalに変更します

f:id:am1tanaka:20181107155442p:plain

学習の開始

学習は、Anaconda Promptから行います。Anaconda Promptが閉じている状態から開く手順を示します。

  • 検索ボックスにanaと入力して、Anaconda Promptが表示されたらクリックして開きます

f:id:am1tanaka:20181106172435p:plain

  • 以下を実行して、ML-Agents環境に切り替えます
activate ml-agents
  • cdコマンドで、ml-agents-masterフォルダーに切り替えます
  • 以下を入力して、トレーニングの開始を準備します
mlagents-learn config/trainer_config.yaml --run-id=firstRun --train

ツールが起動すると、以下のような画面が表示されて待機状態になります。

f:id:am1tanaka:20181107160032p:plain

  • UnityでPlayします

f:id:am1tanaka:20181107160314p:plain

Unityでボールのバランスを取るトレーニングがはじまります。最初は下手ですが、徐々に上手になっていくのが確認できます。Anaconda Promptの方では、学習の様子が表示されます。

f:id:am1tanaka:20181107160714p:plain

実行が始まらない場合

実行が始まらない場合は、開始するフォルダーを間違えている可能性があります。ML-Agentsのリポジトリーを展開したml-agents-masterフォルダーにいることを確認してください。

また、Anaconda Promptでmlagents-learn config/trainer_config.yaml --run-id=firstRun --trainコマンドを実行したあと、UnityでPlayしないまま1分程度放置すると待機が終わってUnity側でエラーが発生します。Anaconda Promptを実行してUnityでPlayするようメッセージが表示されたら、速やかにUnityの方もPlayしてください(2018/11/9追記)

レーニングを終了する

レーニングは、5万ステップに達したら自動的に終了するような設定になっています。

f:id:am1tanaka:20181107161155p:plain

AIでは過学習といって、学習させすぎるとかえって精度が落ちる場合があります。学習の途中でも十分な成果が出ていると感じたら、[Ctrl]+[C]キーを押して停止することができます。

レーニングしたモデルは、models/<run-identifire>/editor_<academy_name>_<run-identifier>.bytesというファイルに保存されます。

f:id:am1tanaka:20181107161607p:plain

<academy_name>は、現在のUnityシーンでのAcademyゲームオブジェクト名です。このファイルに、最後のチェックポイントの状態のモデルが記録されています。以下の手順で、学習させたモデルをプロジェクト内部に組み込むことができます。

  • 保存された上記の.bytesファイルを、UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/フォルダーにコピーします
  • Unityに切り替えて、3DBallシーンが開いていることを確認します
  • Hierarchyタブから、Ball3DBrainオブジェクトを選択します

f:id:am1tanaka:20181107162002p:plain

  • Inspectorタブで、Brain TypeInternalに切り替えて、読み込んだ学習ファイルをGraph Model欄にドラッグ&ドロップします

f:id:am1tanaka:20181107162242p:plain

以上で設定完了です。UnityでPlayすると、先ほど学習したモデルでの実行を試せます。

本当に学習したモデルなのか?

学習を完全にやると、最初の時と同じなので自分で学習させたのか良く分かりません。そこで、不完全な学習状態のデータを作って、それを設定してみましょう。以下は概略なので、分からない部分があったら、これまでの説明に戻ってください。

  • Brain TypeExternalに切り替えます
  • Anaconda Promptで上カーソルキーを押すと、学習させる時に入力したコマンドを呼び出せるので、[Enter]キーを押します
  • 学習の待機状態になったら、Unityに切り替えてPlayします
  • Anaconda Promptで経過を見て、Stepが5000になったら、[Ctrl]+[C]キーを押して、学習を停止します

f:id:am1tanaka:20181107162651p:plain

  • Unityに切り替えて、Projectタブで先ほど読み込んだ学習ファイルeditor_Ball3DAcademy_firstRun-0.bytesを削除します
  • 学習させなおしたeditor_Ball3DAcademy_firstRun-0.bytesを、ProjectタブのUnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/にドラッグ&ドロップし直します
  • Brain TypeInternalに切り替えます
  • Inspectorタブで、Brain TypeInternalに切り替えて、読み込んだ学習ファイルをGraph Model欄にドラッグ&ドロップします

以上で、今度は全然バランスが取れない状態が確認できます。これで、自分が学習させたファイルだったことが確認できるでしょう。

f:id:am1tanaka:20181107163745g:plain

このあと

このブログでは、以下のドキュメントのInstallation & Set-upの内容を紹介しました。

ml-agents/docs at master · Unity-Technologies/ml-agents · GitHub

新規のUnityプロジェクトにML-Agentsの環境を作って、学習、再生する記事であるml-agents/Learning-Environment-Create-New.md at master · Unity-Technologies/ml-agents · GitHubについて以下にまとめました。続きに是非どうぞ。

am1tanaka.hatenablog.com

あるいは以下のような場合はそれぞれのものを読み進めるとよいでしょう。

www.youtube.com

最後に

1年が経過して、インストール手順も学習方法もシンプルになりました。Jupyter Notebookを使わずにコマンドラインで学習できるようになったのはいいですね。Jupyterも引き続き使えるので、必要になったら使えます。

このブログの内容で、Examplesの中のプロジェクトを一通り試すことはできると思います。サンプルプロジェクトのBrain Typeは、最初はプレイヤーが自分で操作するPlayerモードになっているので、それをInternalに変更すれば学習済みのAIの動きを見ることができます。

自分でモデルを作って学習させる方法は分からないので、引き続きドキュメントを読んでまとめていきます。

参考URL