- 投稿日:2019-12-05T22:02:16+09:00
Unityでピンボールのフリッパーを作る
はじめに
この記事は SLP KBIT Advent Calendar 2019 の21日目の記事です。
SLP KBIT Advent Calendar 2019概要
ピンボールが作りたくなったので、Unityで実装していきたいと思いました。
中でも今回はピンボールのボールをはじく部分をHingeJointを用いてつくっていきます。実行環境
Unity version:2019.2.9f1
参考URL
スクリプトリファレンス
Hinge Jointでシーソーを作る
【Unity】 重力を変更する事前準備
scene上に3Dオブジェクトを置きます。
名前 3D Object Floor Cube Ball Sphere AxisL, R Cylinder FlipperL, R Cube HingeJoint
ヒンジとはドアの接合部に使われているような回転の支点となる部分の事だそうです。
UnityではHingeJointがついている物体をヒンジで接合されているように動かすことができるようです。
今回はHingeJointの中でもSpringという変数をつかってばねの力でボールを弾こうと思います。使ってみます
まずHingeJointComponentを軸になるオブジェクトに加えます。
軸には重力がかかってほしくないのでUseGravityのチェックを外し、
物理の影響を受けてほしくないのでIs Kinematicのチェックをつけます。
ここではAxisRにAdd Componentしています。
HingeJointは2つのRigidbody(剛体)をつなげるので動かす部分にRigidbodyを加えます。
ここではFlipperRにAdd Componentしています
2つのRigidbodyをHingeJointで繋げるために、
AxisRのHingeJoint内のConenected BodyにFlipperRをドラッグアンドドロップします。これをAxisLとFlipperLにも行います。
この後AxisRのHingeJointのEdit Joint Angular LimitsでHingeの向きと方向を編集します。
Axisの値で調整します。
矢印を軸として、円に沿って力が加わるので上向きにします。(反時計回りが正)変更前
変更後
この時点でAxisRのHingeJointの
UseSpringにチェックを入れ、Springに40000、Damperに0、TargeetPositionを-60にすると
このようになりました。(AxisLも変更済み)この時にフリッパーが回転しすぎたりしないようにAxisRのHingeJoint内のUse Limitsにチェックを入れ、
Minに-60、Maxに0を入れます。(AxisLも同様)この後、左クリックで左フリッパー、右クリックで右フリッパーを動かしてみます。
コード
Flipper.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class Flipper : MonoBehaviour { // Inspecterで値を変更する public float spring = 40000; public float openAngle = 60; // 開く角度 public float closeAngle = 0; // 閉じる角度 // Hinge Joint private HingeJoint hjL; // AxisL private HingeJoint hjR; // AxisR // JointSpring private JointSpring jL; // AxisL private JointSpring jR; // AxisR void Start() { // AxisLとAxisRを探す GameObject flipperL = GameObject.Find("AxisL"); GameObject flipperR = GameObject.Find("AxisR"); // HingeJointを受け取る hjL = flipperL.GetComponent<HingeJoint>(); hjR = flipperR.GetComponent<HingeJoint>(); // Springを受け取る jL = hjL.spring; jR = hjR.spring; } void Update() { // 左クリックを押す if (Input.GetMouseButtonDown(0)) { jL.spring = spring; jL.targetPosition = openAngle; hjL.spring = jL; } // 左クリックを離す if (Input.GetMouseButtonUp(0)) { jL.spring = spring; jL.targetPosition = closeAngle; hjL.spring = jL; } // 右クリックを押す if (Input.GetMouseButtonDown(1)) { jR.spring = spring; jR.targetPosition = -openAngle; hjR.spring = jR; } // 右クリックを離す if (Input.GetMouseButtonUp(1)) { jR.spring = spring; jR.targetPosition = closeAngle; hjR.spring = jR; } } }Update関数内でボタンを検知しています。
左クリックをすると40000の力で60度左フリッパーが回転、
右クリックをすると40000の力で-60度右クリックが回転します。実行結果
Edit→Project Settings→PhysicsでGravityの方向を変更します。
フリッパーが重力の影響を受けないようにFlipperL、FlipperRのUseGravityのチェックを外しました。
重力を反映させるためBallにRigidbodyをつけます。
実行するとこんな感じになりました。
カクついていますが何とか動いています。
終わりに
結構ちゃんとフリッパーしてくれていると感じました。
ピンボールはフリッパーだけではできないので他の部分も実装してみたいと思いました。
- 投稿日:2019-12-05T19:43:41+09:00
「MonKey - Productivity Commands」でパラメータを渡す
PONOS Advent Calendar 2019の6日目の記事です。
昨日は私の「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化するでした。
はじめに
MonKey - Productivity Commands - Asset Store本記事はPONOS Advent Calendar 2019の以下の記事の続編となります。
- 「MonKey - Productivity Commands」のコマンド操作でUnity開発を効率化する
- 「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化する
今回はコマンドにパラメータを渡して実行する方法について紹介します。
なお、
- Unity 2019.2.12f1
- MacOS 10.14.6
の環境で動作確認しています。
コマンドのパラメータを指定する
コマンドの実行時にパラメータを追加するためには
MonKey.Command
属性の付与されたstaticメソッドの引数に対してMonKey.CommandParameter
属性を付与します。
MonKey.CommandParameter
属性の第一引数にはパラメータ表示時の説明文を設定してください。using MonKey; public static class SampleCommands { [Command("Sample Parameter Command", QuickName = "SPC", Help = "好きな数字を選んでコンソールへ出力する。" )] public static void SampleParameterCommand( [CommandParameter("好きな数字を選んでください。")] // 「value」をコマンドのパラメータ化する。 int value ) { Debug.Log($"選んだ数字は\"{value}\"です。"); } }このコマンドをコマンドパレットで開くと以下のような表示になります。
パラメータが設定されたコマンドは、コマンド名の左側に吹き出しアイコンが表示されており、パラメータの有無を判別できます。
このコマンドを選択すると、以下のようなパラメータ入力画面が表示されます。
任意のパラメータ(今回はint型のパラメータなので整数値)を入力して実行します。
コマンドに渡されたパラメータの値を使用して、処理が実行されました。利用可能なコマンドパラメータの型
MonKeyがデフォルトでサポートしているパラメータの型は以下の通りです。
これらの型の引数はMonKey.CommandParameter
属性を付与するだけでコマンドパラメータ化が可能です。
- String
- Int
- Float
- Double
- Byte
- Bool
- Char
- Long
- Short
- Enum
- Vector2
- Vector3
- Vector4
- Quaternion
- Color
- Object
- Component
- GameObject
- LayerMask
- Scene
- Type
- Arrays
なお、
MonKey.Editor.Internal.CommandParameterInterprete
を継承したクラスを用意することで、ここにある以外の型もパラメータとして利用することが出来ます。使用例:Rect型のコマンドパラメータを受け取り、コンソールに矩形情報を出力する
今回は使用例として、デフォルトで定義されていない
Rect
型のコマンドパラメータに対応してみましょう。Rect型を入力するルールを決める
MonKeyのコマンド実行に入力されるパラメータは文字列です。
そのままではRect
型のコマンドパラメータとして渡すことはできないので、入力された文字列を適切にRect
型へパースする処理が必要となります。
Rect
型のオブジェクトを作成するためには「x」「y」「width」「height」の4個の数値が必要ですので、今回はこれらの4個の数値をカンマ(,)区切りで入力してもらうルールにします。このルールに従うとコマンドパラメータに
1, 2, 100, 200
という文字列が入力された場合、
x = 1, y = 2, width = 100, height = 200
というRect
オブジェクトが作成されます。Rect型のコマンドパラメータを持つコマンドを作成する
次に
Rect
型をパラメータに指定したコマンドを用意します。[Command("Rect Parameter Command", QuickName = "RPC", Help = "矩形情報を出力する。" )] public static void RectParameterCommand( [CommandParameter] Rect value ) { Debug.Log($"入力された矩形の情報:{value.ToString()}"); }入力された矩形情報をコンソール出力するだけの、シンプルなコマンドです。
さて、試しにこのコマンドを実行してみますが…
パラメータの入力後に「Error」と表示され、コマンドを実行することができませんでした。
Rect
型はデフォルトでサポートされている型ではなく、「文字列を適切にRect
型へパースする処理」もまだ実装していないので、Rect
型のコマンドパラメータをコマンドへ渡せずにエラーとなってしまっているようです。また、コマンド内の以下の処理についても、valueがnullで渡されてしまうため、
NullReferenceException
が発生していました。Debug.Log($"入力された矩形の情報:{value.ToString()}");Rect型に対応するためのCommandParameterInterpreterを作成する
さて、それでは「文字列を適切に
Rect
型へパースする処理」を実装するために専用のCommandParameterInterpreter
クラスを用意しましょう。今回は
Rect
型に対応するので、RectInterpreter
という名前にします。
スクリプトはEditor
以下に設置してください。// Rectの構造体をコマンドパラメータで使用するためのCommandParameterInterpreter。 class RectInterpreter : MonKey.Editor.Internal.CommandParameterInterpreter { [InitializeOnLoadMethod] static void AddInterpreter() { // CommandParameterInterpreterの定義を追加する。 AddInterpreter(new RectInterpreter()); } RectInterpreter() : base(typeof(Rect)) // 対応する型を指定する。今回はRect。 { } // 入力された文字列を、コマンドパラメータの型にパースする。 public override bool TryParse(string text, out object obj, System.Type subType = null) { // カンマ区切りでx, y, width, heightの4個の数値を指定させる。 var parts = text.Split(','); if (parts.Length == 4) { // それぞれの数値をfloatへ変換し、Rectを作成する。 float[] values = new float[4]; for (int i = 0; i < 4; ++i) { float result; if (float.TryParse(parts[i], out result)) { values[i] = result; } } var rect = new Rect(values[0], values[1], values[2], values[3]); obj = rect; // パースした結果はobjに格納する。 return true; } obj = null; return false; } }では、もう一度実行してみましょう。
入力した文字列から矩形情報を作成し、コンソールに出力することができました!おわりに
コマンドパラメータを指定してコマンドを実行することで、柔軟に処理を実行することができます。
MonKeyの優れている点の一つだと思うので、是非活用していきましょう。明日は@karizumaiさんです!
- 投稿日:2019-12-05T17:50:43+09:00
【Unity】Unity Collaborateでリアルタイム通信コンテンツの制作フローを効率化
Unity Collaborate
Unity Collaborate は Unity Teams の一部です。Unity Team は、小さなチームがクラウド環境で Unity のプロジェクトを保存、共有、同期することを可能にします。Collaborate を使用すると、場所や役割に関係なくチーム全体がプロジェクトに貢献できます。
要するに協力しながらの作業が簡単にできちゃうってことですが、今回は少し目的が違います。
今回の目的は1人で2台のPCを使ってリアルタイム通信ゲームの開発を進めることです。
Unity Collaborate
を使うまでは、
Googleドライブに実行ファイルをZip形式に圧縮したものをアップし、
もう1台のPC側で解凍&ダウンロードしてやってました。ただ、そのやり方だと少しの変更を試すだけであっても
そこそこの待ち時間が発生してしまって無駄が多かったです。
一人で楽にリアルタイム通信の検証を行うにはどうすればいいか探していたところ、
下記記事を発見し、Unity Collaborate
での開発を推奨していたので、
試してみたら本当に楽に進められました。ただ、設定が行ったり来たりでややこしかったので、メモを残します。
設定手順
前提として、UnityID(Unityのアカウント)は既に持っていることとします。無料、有料は問いません。
また、今回のやり方だとUnityIDは2つ必要となります。それに伴って、メールアドレスも2つ必要です。追加したいメンバーに招待を送り、共同で開発する権限を与えるようなイメージです。
1台目のPCでプロジェクト作成
まずは1台目のPCでプロジェクトを作成します。
そして、タブのWindow
→Genaral
→Services
を選択します。
Collaborate
を選択し、下記画像の黄色マーカーのリンクに飛びます。Unity Dashboard
ダッシュボードに飛んだら、
Users
を選んで次に進みます。下記画像の箇所をページ内から探してリンクに飛びます。
UnityID
先ほどのリンク先がUnityIDのマイページになっているはずです。
メンバー&グループ
に進みます。そこから招待ページに飛びます。
メールアドレスと役割(Userでいいと思います)を選択して、招待します。
招待される側にメールが届きますが、特に承認のフローなどは必要ありませんでした。再度、Unity Dashboardへ
ここでもう一度
Unity Dashboard
のUsers
に戻ります。
そして、先ほど招待したメンバーを下記画像の箇所から選択し、追加します。2台目のPCでプロジェクト作成
次に2台目のPCでプロジェクトを作成します。
こちらは先ほどのUnityIDとは別のものでログインして作成します。
そして、先程と同様にServices
を選択し下記画面を呼び出します。※ここで上記画面が出ない場合は、
SETTINGS
を選択し、Unlink Project
すればOKです。
あとは招待されたOrganizationを選択し、共同編集したいプロジェクトを選択すれば設定完了です。
途中から導入した場合
今回、
Unity Collaborate
を知るのが遅かったので
片方のPCではプロジェクトはある程度作り終えた状態で、もう片方には全く何もない状態でした。プロジェクトの大きさによってはかなり時間がかかりますが、
GitのCloneのようなことが可能です。
まっさらなプロジェクト側でCollab
を選択し、左下のView History
を選択します
Restore
を押すと「ローカルを置き換えますよ?」という警告が出て、承認するとデータを引っ張てきてくれます。変更点の共有
片方で変更した内容をもう片方に反映させるために、
Publish
という作業を行います。
GitのPush的なやつです。アップロードが完了すると、もう片方のプロジェクトでは矢印が変更されて、
Update
が可能になります。実際に使ってみた感想
情報が無くて大変でしたが、知ってるかどうかの差なので
一度準備のフローを知った以上は二回目以降は超便利システムだと思いました。今回の用途だと片方のプロジェクトしか変更しないので、
コンフリクトなどの問題は心配する必要はなさそうです。もっと良い効率化があれば教えてください。
参考リンク
- 投稿日:2019-12-05T16:34:01+09:00
Adressableのおいしいとこだけ使いたい?Scriptable Build Piplineがあるじゃない
この記事ではAssetBundleの基本的な知識は説明しません。
すでにAssetBundleを利用したことがある人向けの記事になっています。新機能 Adressable Asset System
アセットをビルドから切り出せるAssetBundleをより簡単に扱えるようになるシステムが登場しました。
https://blogs.unity3d.com/jp/2019/07/15/addressable-asset-system/
- 任意の文字列でアセットを簡単にAssetBundleからLoadできる
- AssetBundleの依存関係解決の処理も用意してある
- AssetBunldeのまとめかたを調整できるEditor
- プロファイラで読み込み状況を確認できる
といった便利な機能が用意されているリソース管理のオールインワンといってもいいパッケージです。
今からAssetBundleを導入する必要があるひとは、まずは考えるべき選択肢かとおもいますが
- できて間もないから不安で使いたくない
- すでにある自作ライブラリで運用してる
- 自作ライブラリのほうが便利だ
- Adressableの仕組みが合わない
などの理由でAdressable Asset Systemを使わないという選択肢を取っている人もいると思います。
しかし、Adressable Asset Systemではできるけど、古いUnityではできなかった機能が中にはいくつか存在します。個人的に一番欲しかった機能はBuiltInShaderを分離する機能です。
なぜBuiltInShaderを分離するのか
https://docs.unity3d.com/ja/2017.4/ScriptReference/BuildPipeline.BuildAssetBundles.html
Unity5から導入されたこのメソッドでは、事前にAssetBundle名を割り当てたアセットを収集してAssetBunldeを簡単に作ってくれるのですが、あまり細かな制御はできませんでした。
手軽にできる反面、よく調べないと気が付かない罠に気が付かずにパフォーマンスを悪化させていることがあります。
複数のAssetBundleに同じアセットが重複して保存される
特に考えずにビルドしたときに陥る罠に、いくつかのAssetBundleに同じデータが重複して入ってしまうという問題があります。
(↑ New Render Textureが4つのmaterialのAssetBundleそれぞれに含まれてしまっている様子)
うっかりテクスチャやマテリアルやShaderなど、いくつかのPrefabなどで使いまわすアセットにAssetBundle名を指定し忘れるとこのようなことになります。AssetBundleは依存関係を構築できるので、共通で使うアセットにもちゃんとAssetBundle名を設定してあげれば基本的には回避できます。
ただし、このときどうしてもAssetBundle名を設定できない困ったアセットがあります。それがUnityに組み込まれているBuiltInAssetです。Project Viewに表示されないのでAssetBundle名を指定できず、それを参照しているアセットがあるAssetBundleにはすべて重複して別々のアセットとして保存されてしまいます。
BuiltInShaderはモバイル版の天敵
そんなBuiltInAssetの中でもさらにタチが悪いのがShaderの存在です。Shaderは実行時に初めて使うときにコンパイルされるのですが、上記の問題で重複したShaderは別ものとして扱われるので、毎回コンパイルされてしまいます。特にUI-DefaultやStanderedShaderなどよく使われるShaderは地獄のような状況になる場合があります。(注1)
この問題を回避するために、わざわざDefaultのUIのShaderとマテリアルを残らず差し替えたり、徹底的にBuiltInShaderを使わないなどの辛い対応をした人もいるかと思います。(注2)
前置きがながくなりましたがこの問題を簡単に解決できるのがScriptable Build Piplineになります。
注1) そもそもUnityも最初は数個のパッチファイルを作る程度に考えていて、AssetBundleを滅茶苦茶細かく分けて作ることを想定していなかったのかもしれない。大量のアセットを頻繁にアップデートで細やかに更新するようなソシャゲの運用は想定していなかった可能性がある
注2) もしかしたらスマートな方法がもっとあったかもしれないScriptable Build Pipline (SBP) を使う
正直Scriptable Build PiplineはAddressable Asset Systemを作る上で既存のAssetBundleビルドシステムの限界に気が付いて作られた福産物なのかと思えるほど、Addressableのビルドシステムのために必要な機能が盛り込まれています。
(Assetのロードする際の名前を好きに変えたりできるようになってる)http://tsubakit1.hateblo.jp/entry/2018/12/03/202044
細かい説明はテラシュールブログがとても分かりやすいのですが、簡単に説明するならSBPを使うことで、AssetBundleのビルドの処理にTaskを自由に作ってカスタマイズできるようになります。
DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction
そしてforumの公式の人の回答を見るとすでに待ち望んでいた機能が用意されいるのがわかります。SBPでビルドする処理のサンプルが以下です。
https://forum.unity.com/threads/standard-shader-duplicated-in-asset-bundle-build.593248/
public static class BuildAssetBundlesExample { public static bool BuildAssetBundles(string outputPath, bool useChunkBasedCompression, BuildTarget buildTarget, BuildTargetGroup buildGroup) { var buildContent = new BundleBuildContent(ContentBuildInterface.GenerateAssetBundleBuilds()); var buildParams = new BundleBuildParameters(buildTarget, buildGroup, outputPath); if (useChunkBasedCompression) buildParams.BundleCompression = BuildCompression.DefaultLZ4; IBundleBuildResults results; var tasks = DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction); ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParams, buildContent, out results, tasks); return exitCode == ReturnCode.Success; } }DefaultBuildTasksというPresetを提供する機能の中に AssetBundleBuiltInShaderExtraction なるカスタマイズされたTaskが用意されているのです!
var tasks = DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction);特に重要なのは CreateBuiltInShadersBundle.csに記載されている処理なのですが、BuiltInAssetを探して別のAssetBundleにまとめる処理のサンプルコードが書かれています。
実際にこの方法でビルドするとUnityBuiltInShaders.bundleが生成され、BuiltInShaderの重複ロードが発生しなくなります。
AssetBundleBrawserで確認すると、AssetBundleからBuiltInShaderの参照が切り離されているのが確認できます。
このSBPはAdressableを使っていなくても使えるPackageなので、既存のAssetBundleを管理するライブラリがある人でも導入できる可能性があります。
しかしAssetBundleManifestに対応せず
ただし、悲しいことに、DefaultBuildTasksはAssetBundleManifestに対応していませんでした…
(一部抜粋)DefaultBuildTasks.csbuildTasks.Add(new PostWritingCallback()); // Generate manifest files // TODO: IMPL manifest generation return buildTasks;Manifestを使わないAdressableのために用意した機能なので、Manifestを作る機能が後回しになるのは仕方ないことなのかもしれませんが、Manifestがなければ依存関係のAssetBundleを探せなくなってしまいます。
IBundleBuildResults からManifestの代わりを自作する
実はManifestをロードしていなくても、依存関係のあるAssetBundleを読み込むことは可能です。Manifestはあくまで依存関係のあるAssetBundleを取得するメソッドを用意しているだけなので、そのリストさえ自分で作成してしまえば代わりにすることができます。
ありがたいことにContentPipeline.BuildAssetBundlesの IBundleBuildResultsにビルドの結果が記載されているのでそれを使います。
interface IBundleBuildResults { Dictionary<string, BundleDetails> BundleInfos { get; } } class BundleDetails { public string FileName {get; set;} public uint Crc {get; set;} public Hash128 Hash {get; set;} public string[] Dependencies {get; set;} }このresultからBundleInfosをファイルにシリアライズして保存すれば、AssetBundleManifest.GetAllDependenciesなどの基本的なメソッドの代わりを提供するクラスは自作することができます。
AssetNameがファイルパスになっているのを直す
Manifestを自作することで任意のアセットをロードするのに必要なすべてのAssetBundleを読み込むことができました。しかし、以前のUnityのAssetBundleのLoad時にアセットをロードするのに必要な文字列はアセットのファイル名だったのですが、SBPのデフォルトではファイルパスになってしまっています。
これをもとに戻すにはBundleBuildContentのAddressesを変更する必要があります。
var buildContent = new BundleBuildContent(ContentBuildInterface.GenerateAssetBundleBuilds()); //GC気になる人は適宜直してね foreach (var adress in buildContent.Addresses.Keys.ToList()) { buildContent.Addresses[adress] = System.IO.Path.GetFileName(buildContent.Addresses[adress]); }AddressesのKeyはGUID, Valueがファイルパスになっているので、これをファイル名に書き換えればファイル名でAssetBundle.Loadが利用できるようになります。
ManifestとAssetNameが大きな変更になっていますが、ここまでカスタマイズできるようになれば、既存のAssetBundleのライブラリでもSBPを使えるようになるのではないでしょうか? (注3)
注3) IgnoreTypeTreeChangeが現状未サポートらしいので、それが無理だとキツイかも
まとめ
今回はBuiltInShaderを分離するためだけにSBPを導入してみましたが、DefaultBuildTasksを参考にTaskを自作することでかなりの自由なAssetBundleのビルドパイプラインを構築することができます。
- 差分ビルドで変更のあったAssetBundleを抽出したい
- 別のプロジェクトのAssetBundleと組み合わせて運用したい
- ビルドする前にファイルを差し替えたい
- ビルド直前に暗号化してAssetBundleに入れたい
などはできるのではないかと思っています。
- 投稿日:2019-12-05T15:15:13+09:00
Airtest+Pocoで始めるUnityの自動テスト
はじめに
本記事はQualiArts Advent Calender 2019 6日目の記事です。
皆さんはUnityで開発を行う上で、UIテストの自動化はされてますでしょうか?
運用で開発を進めていくと、既存のUIフローで進行不能になってしまった、仕様と違う挙動になってしまった、表示崩れが発生してしまった、などなど実装によるデグレが発生してしまうというのはよく聞く問題だと思います。
こうした既存のUIフローを回帰的にテストし、問題を検出するというのは大切なことですが、同時に人力でやるにはとても労力のかかることでもあります。
そこで役に立つのがUIテストの自動化です。単純な検証フローであれば、人間の手ではなく機械的にそれらを行うことによって、かかるコストを削減しつつヒューマンエラーを取り除くことができます。本記事ではそんなUnityでのUIテストの自動化を行う上で有用なAirtestというツール及びフレームワークと、それに付随して効果を発揮するPocoというフレームワークについて説明していきます。
Airtest
Airtestは中国のNetEase社より提供されているアプリケーションのテスト用のツールになります。
ゲームを主にターゲットとして作られており、Android、iOS、Webなど、様々なプラットフォームにおけるアプリケーションに対応しています。
使用するスクリプトの言語はpythonで、特にアプリケーション側で用意など要さずにテストを実行することが可能です。AirtestはIDEが提供されており、IDE上で端末の接続、テストスクリプトの構築、実行など様々な作業が快適に行えるようになっています。
上が実際のIDEの画像です。ダーク調な感じで個人的にはとても使いやすいです。
中央のScript Editorが実際のテストのスクリプトを記述する場所です。デフォルトで必要なセットアップのスクリプトが記述されています。
テストのスクリプトを保存する時は.airという形式のプロジェクトとして保存します。テストの実行はこの.air単位で選択を行います。
テストを実行した際には.airの配下に結果などが出力されます。環境の準備
Airtestのスクリプトの説明に入る前に、Airtestを使う上でのテスト環境の準備を行いましょう。
iOS、Android、Webなど様々な環境でテストを行うことが可能ですが、本記事ではAndroid実機をUSB接続しての環境構築について説明していきます。接続されているandroid端末は上の画像のConnection Panelという画面で確認することができます。
端末を接続し、refresh ADBというところを押すと、接続した端末が一覧に表示されます。(シリアル番号は消しています。)後はテストを実行する端末に対してconnectというボタンを押すと接続を行うことができます。簡単ですね。
一覧に端末が表示されない、connectボタンが押せないという場合にはUSB接続周りの設定やデバッグ接続の権限などを見直してみてください。接続が完了すると、接続した端末のキャプチャ画面が表示されるようになります。
以上で接続は完了です。Poco
PocoはAirtestと共に提供されている様々なゲーム開発プラットフォームでのUI周りのテストを記述するためのフレームワークになります。
https://github.com/AirtestProject/PocoPocoを使うことで、iOS/Androidのネイティブアプリ、Cocos-2dx、Unityといった様々なプラットフォームでのUIテストをより実装に沿った形で記述することができます。
Unityであればヒエラルキー情報やTextといった指定のUIコンポーネントのオブジェクトを参照することが可能です。
例えば「決定」という文字のボタンを押す、特定のオブジェクトの子にある「Button」というコンポーネントがついているものを長押しする、など、かなり実装目線でのテストスクリプトを記述することができるので大変便利です。Pocoを使用するには、アプリケーション側でSDKの導入とその対応が必要になります。Unity側の対応については後述しています。
スクリプトの構築
Airtest+Pocoを使ったスクリプトはシンプルで、Python自体に知見がなくとも直感的に記述することが可能です。
基本的に、ユーザーのタップや長押し、UIがあるかどうかの確認といった1アクション単位で記述を行います。スクリプトの例__author__ = "udon" from airtest.core.api import * from poco.drivers.unity3d import UnityPoco poco = UnityPoco() # タップして touch([0.0, 1.0]) #スワイプして swipe([0.0, 1.0], [0.0, 2.0]) # キーポードから文字を入力して keyevent("Test") # 待機して sleep(1.0) # 「ボタン」というオブジェクトをタップ poco("button").click()Airtest API
Airtestの提供するAPIは、端末上でのアクションのテストを直感的に書くためのものです。
import_Pocofrom airtest.core.api import *機能を使用するには上のimportをスクリプトに追加します。
AirtestのAPIを使用することで、タップ、スクリーンショット、テキスト入力など、端末上で行いたいアクションをスクリプトで記述することができます。AirtestAPIの例# 座標を指定してタップ touch([0.0, 1.0]) # テキストの入力 text("hoge") # スクリーンショット snapshot(msg="hogehoge") # 待機 sleep(1.0)スクリプトは直接記述することももちろん可能ですが、IDEのGUIベースでボタンをぽちぽちしながら組み立てることも可能です。
上画像のように、IDEにAirtest AssistantというGUIが用意されており、ここにあるボタンを押すことでスクリプトに処理を加えることができます。
Airtest Assistantは端末を接続することで、タップなど端末のUIに依存したメニューを選択することができます。
そしてAirtestで面白いのが、タッチ操作において画像ベースの処理を直接的にスクリプトに起こすことができることです。例えば上の画像はChromeのロゴと周りのピンクの部分も含めた画像を認識し、その座標をタップするという処理になります。
画像の設定もお手軽で、Airtest Assistantから画像を使用するメニュー(touch、swipe、existsなどUIに関わるもの)を選択し、接続した端末のキャプチャ画面からドラッグ&ドロップで行うことができます。画像ベースでタップしたり、指定のUIがあるかどうか知りたいのような簡単なテストであれば、これだけで組めてしまいます。
他にも様々な機能があるので、詳しく機能が知りたいという方は公式のドキュメントを読むことを推奨します。Airtest APIドキュメント
https://airtest.readthedocs.io/en/latest/all_module/airtest.core.api.htmliOSやandroidなどのプラットフォームごとのAPIも用意されているので、apkのインストールなどのシナリオから組み立てる場合には活用することができます。
Poco API
Pocoは前述の通り、プラットフォームに対応したテストを書くためのフレームワークです。
import_Pocofrom poco.drivers.unity3d import UnityPoco poco = UnityPoco()上のimportをスクリプトに追加することでPocoの機能を利用することができます。
Pocoで使う基本的なUIProxypoco("hoge")Pocoでは上のスクリプトのようにGameObjectの名前を引数としたオブジェクトを多用します。
これをUI Proxyというもので、UI上のオブジェクトをスクリプト上のオブジェクトとして扱えるものです。
UI Proxyには様々な機能が用意してあり、それらを使うことで色んなアクションをそのオブジェクトに対して働きかけることができます。buttonのタップpoco("button").click()例えば上の1行で、「button」という名前のGame Objectを検索し、その位置をタップするという意味のスクリプトになります。
PocoAPIの例#テキストコンポーネントのオブジェクトの取得 poco("hoge", text="huga") #オブジェクトのロングタップ(2秒) poco("hoge").long_click(duration=2) #オブジェクトが出現するのを待つ poco("hoge").wait_for_appearance #オブジェクトが消失するのを待つ poco("hoge").wait_for_disappearance() #オブジェクトの存在フラグ(bool) poco("hoge").exists() #子オブジェクト poco("hoge").child("huga") #子オブジェクトをタップ poco("hoge").child("huga").click()上がUI Proxyの機能の一例になります。UI Proxyを使ったアクションは色々用意されており、様々なUIテストのケースに対応することが可能です。
コンポーネントや階層構造など、Unity上の構造に合わせられるため、少しレイアウトやデザインが変更したことによるテストの破綻も防ぐことができます。
Pocoに関しても、詳しく機能が知りたい方は公式のドキュメントを読んでみてください。Poco APIドキュメント
https://poco.readthedocs.io/en/latest/source/poco.pocofw.html特定のGame Objectが出てきた後に、ボタンのGame Objectを押したい!特定のGame Objectにぶら下がってるTextを押したい!などUnityライクなテストをやるときに本当に便利なフレームワークになっています。
スクリプトのサンプル
APIの説明だけじゃいまいち具体的なスクリプトのイメージがつかないかと思います。そんな時に役立つのが公式のサンプルです。
公式ではそれぞれのAPIの説明とともに、動画付きで具体的なスクリプトの説明を行っています。
実際にスクリプトを書いてみよう!となった方はこちらを参考にしつつ書くのがベターです。(筆者もかなり参考にしました。)Airtest APIを利用したサンプル
https://airtest.readthedocs.io/en/latest/README_MORE.html#examplePoco APIを利用したサンプル
https://poco.readthedocs.io/en/latest/source/README.html#tutorials-and-examplesテストする
AirtestやPocoのスクリプトについて説明したところで、どうやってテストを実行するか説明していきます。
Poco SDKの準備
前述にもある通り、Pocoの機能を使うにはアプリケーション側にPocoのSDKを組み込む必要があります。
Poco SDK自体はGithubで公開されているので、そちらからクローンするかダウンロードをしてください。Poco-SDK
https://github.com/AirtestProject/Poco-SDK落としてきた中に「Unity3D」というフォルダがあるので、それをUnityのプロジェクトに持ってこればSDKの導入は完了です。
コンパイルエラーが起きてしまう場合には使っていないGUIシステム用スクリプトのフォルダを消してやると解決します。
筆者はuGUI環境だったので、「ngui」「fairygui」の2つのフォルダを削除しました。次にPoco SDKがアプリケーション側に適応されるようにする必要がありますが、これについてはシンプルでPocoManagerというスクリプトをシーン上の適当なオブジェクトにアタッチすれば大丈夫です。
テストの実行
IDEの再生ボタンを押すと、Script Editorにあるテストスクリプトが実行されます。
実行中は現在の実行箇所が青くハイライトされ、ログは下のViewerに出力されます。
後はテストが終了するまで、見守れば大丈夫です。ちなみにAirtestはCLIも用意されており、IDEからではなくCLIで実行することも可能です。
CLIはpythonのライブラリなので、使う際にはpipでインストールしてやってください。CLIの導入pip install -U airtestCLIでテストの実行airtest run {.airのパス} --device Android:///テストの実行結果はlogと画像によって出力されます。
1つのコマンドを実行するたびにAirtest側でスクリーンショットをとっており、テストの状況がどうなっていたかを確認することができます。
また、logと画像を元にhtml形式のレポートを出力することも可能です。出力するにはIDE上でCmd+Lを押すと実行されます。出力すると、上の画像のようにいい感じに整形された見た目になります。
画像の参照パスが絶対パスになっているため、Jenkinsなど自動化したルーチンでhtmlを出力する際には注意が必要になります。ちなみにhtmlの出力についてもCLIで実行可能です。
CLIでhtmlの出力airtest report {.airのパス}上記のコマンドを叩くことで、同様のhtmlが出力されます。
まとめ
スクリプトの記述から実行まで説明したのでめちゃくちゃ長くなってしまいましたが、要約するとAirtestはUnityのアプリケーションのテストを行う上でかなり便利なツールということです。
画像やレイアウトベースのテストであれば、前述のように直感的なスクリプトを用意するだけで、いい感じのテストを作ることができます。
また、画像やレイアウトベースのテストではレイアウトの変更によってテストのメンテナンスが必要になるという懸念がありますが、Pocoという存在のおかげでヒエラルキーベースのテストを構成しそのリスクを緩和することができます。
前述にもあるようにCLIも用意されているため、Jenkinsなどのインテグレーションツールと組み合わせることでシステム全体を自動化したりできます。もっと詳細なAirtestの機能が見たい方は、とにかく公式ドキュメントを読んでみてください。
本当に多機能なツールで、ここでは紹介しきれなかった有用なものも多く存在します。
Android以外で活用したい!という方も公式ドキュメントには各プラットフォームの対応が書いてあります。
- 投稿日:2019-12-05T14:12:20+09:00
迷路ゲームにランキング機能をつけるお話
N高アドカレ16日目の記事ですー
データベース一切わかんない人がオンラインランキングの実装を頑張った話になります。はじめに
軽く自己紹介
N高の江坂プロクラの高三の受験生です。
大学に無事AOで合格して時間ができたのでアドカレ参加。
UnityとBlenderでゲーム作ったり色々してます。
Webとサーバーサイドはほとんどわかんないです…ゲームの説明
迷路ゲーム自体は自作キャラがモデリングできたからそれを歩かせてゲームにしたいという思いから作ったもので、シンプルなものです。
敵キャラのゴーストを避けながらゴールを目指すゲームです。
このゲームにオンラインランキング(タイムアタックのハイスコア)をつけるときの話です。どうやってスコアを保存するか
はじめにどうやってクリアタイムを保存させようかと考えました。
ゲームに記憶させるのか、データをサーバーに保存するか…と色々考えていた時に、こちらのUnity + NCMBでスコアランキングを実装するという記事を見つけました。この記事の通りにやってみることにしました。記事を参考に作業
NCMB側の設定
Googleでログインしました。
そしてクラスをHighScoreという名前で作成。
Unity側の設定
UnityにNCMBのパッケージを導入。
NCMBSettingsという名前でオブジェクトを作ってNCMBSettingsというスクリプトをアタッチしました。
アプリケーションキーとクライアントキーをNCMBSettingsのインスペクタにコピペ。
ここまでは事前準備です、次にコードを書いていきます。
クリアタイムの保存
データを保存するためのオブジェクト(箱)を作ってその中にクリアタイムを保存します。
NCMBObject HighScore = new NCMBObject("HighScore");これでデータを入れるオブジェクトを作ります。
HighScore["Score"] = script.totalTime;HighScoreに値を代入。
私は別の時間制御をしているスクリプトのtotalTimeを参照させるようにしています。
HighScore.SaveAsync();そしてNCMB側に保存させます。
私が実際に書いたコードの一部を参考に置いておきます。
void OnTriggerEnter(Collider collision) { if (collision.gameObject.tag == "Player") { gameclear.SetActive(true); TimerUI.SetActive(false); NCMBObject HighScore = new NCMBObject("HighScore"); HighScore["Score"] = script.totalTime; HighScore.SaveAsync(); } }プレイヤーとゴールの判定オブジェクトが衝突判定を起こした時に、gameclearUIの表示、制限時間UIの非表示と同時にクリア時の時間をデータベースに送っています。
クリアタイムの取得
クリアタイムをデータベースから取得して表示させます。
NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("HighScore");取得したデータはNCMBQueryに格納されるのでこれを先に作ります。
query.OrderByAscending("Score"); query.Limit = 5;並び替え&5つだけ取得させる設定
取得設定ができました。
そして実際に取得させようと思い、
query.FindAsync ((List<NCMBObject> objList ,NCMBException e) => { if (e == null) { objList[0]["Score"] = score; objList[0].SaveAsync(); } });このコードを参考にしてコードを書いたところ、エラーでデータの取得ができませんでした。
Scoreデータを関数に代入する方法がわからず、しばらく試行錯誤しました。query.FindAsync((List<NCMBObject> objList, NCMBException e) =>{ if (e == null) { HighScore_1 = System.Convert.ToSingle(objList[0]["Score"]); HighScore_2 = System.Convert.ToSingle(objList[1]["Score"]); HighScore_3 = System.Convert.ToSingle(objList[2]["Score"]); HighScore_4 = System.Convert.ToSingle(objList[3]["Score"]); HighScore_5 = System.Convert.ToSingle(objList[4]["Score"]); }結果このコードでHighScoreにListのScoreを代入することができました…
どうやらSaveAsyncが邪魔してたみたいでした…
ランキング表示
取得して関数に代入した値をランキングのUIとして表示させます。
参考に私が書いたコード置いときます。
using UnityEngine; using UnityEngine.UI; public class Ranking_1 : MonoBehaviour { private Text Ranking1; public GameObject Ranking; ranking script; public GameObject Next; Next script2; int display; float totalTime; int minute; float seconds; void Start() { script = Ranking.GetComponent<ranking>();//HighScoreを代入した関数を参照させる設定 script2 = Next.GetComponent<Next>();//HighScoreを表示させる関数を参照させる設定 Ranking1 = GetComponentInChildren<Text>(); } void Update() { totalTime = script.HighScore_1;//totalTimeにHighScore_1の値を入れる display = script2.rankingdisplay;//HighScoreを表示させるための関数 if (display == 1) { minute = (int)totalTime / 60; seconds = totalTime - minute * 60; Ranking1.text = minute.ToString("00") + ":" + ((int)seconds).ToString("00"); } } }HighScoreの値とHighScoreを表示させる関数の値を参照させる設定をして、HighScoreを表示させる関数が1の時にHighScoreをUIに表示させるようにしています。
ゲームクリア画面からランキング画面の画面推移時に関数を0から1にして表示させることができます。
(もうちょっといいコードがかけそうな気がしたけど、私のコーディング力ではこれぐらいしかかけなかった…)
ゲームクリア画面
Nextボタンを押すと
ランキング表示させることができた!追記
ゲームにランキングが追加できたので、しばらくテストプレイしていると、なぜか大量のAPIリクエストを送っていた…
この日と次の日はすごい数送ってました…その数約5万件…
今は改善して減りましたが…
大量のAPIリクエストの原因は、ゴール後にUpdate処理で1フレームに1回、データベースのデータを取得していたからでした…void Update() { int GameclearJudgment = script.Gameclearfunction; if (GameclearJudgment == 1) { NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("HighScore"); query.OrderByAscending("Score"); query.Limit = 5; query.FindAsync((List<NCMBObject> objList, NCMBException e) =>{ if (e == null) { HighScore_1 = System.Convert.ToSingle(objList[0]["Score"]); HighScore_2 = System.Convert.ToSingle(objList[1]["Score"]); HighScore_3 = System.Convert.ToSingle(objList[2]["Score"]); HighScore_4 = System.Convert.ToSingle(objList[3]["Score"]); HighScore_5 = System.Convert.ToSingle(objList[4]["Score"]); } }); } }問題のコード
ゲームクリアした時に値が1になる関数を参照して、ゲームクリアしていたらデータベースの値を取得するようにする処理をUpdateに書いてしまった。void Update() { int RankingJudgment = script.Rankingfunction; if (RankingJudgment == 1) { NCMBQuery<NCMBObject> query = new NCMBQuery<NCMBObject>("HighScore"); query.OrderByAscending("Score"); query.Limit = 5; query.FindAsync((List<NCMBObject> objList, NCMBException e) =>{ if (e == null) { HighScore_1 = System.Convert.ToSingle(objList[0]["Score"]); HighScore_2 = System.Convert.ToSingle(objList[1]["Score"]); HighScore_3 = System.Convert.ToSingle(objList[2]["Score"]); HighScore_4 = System.Convert.ToSingle(objList[3]["Score"]); HighScore_5 = System.Convert.ToSingle(objList[4]["Score"]); } script.RankingJudgmentReset(); }); } }データベースのデータを取得した後に参照元の関数を0にリセットするように改善。
無事にリクエスト数が減りました。まとめ
データベースを触るのは初めてだったのでわからないことも多かった…
NCNBは細かい設定が必要ないのでわかりやすくて触りやすかった
次はランキングのスコアと同時にuser nameを表示できるようにしたい…
WebGL書き出ししてWeb上で遊べるようにしようか検討中。
- 投稿日:2019-12-05T10:00:16+09:00
DockerでUnity ML-Agentsを動作させてみた(v0.11.0対応)
Unity ML-Agents(v0.9.1)をDocker上で動作させてみました。
UnityやUnity ML-Agentsの環境構築などは下記をご参考ください。
Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033MacでUnity ML-Agentsの環境を構築する(v0.11.0対応) - Qiita
https://qiita.com/kai_kou/items/0d40157cbc303fb10c22手順
v0.10.1まではDockerでの学習方法についてドキュメントがあったのですが、v0.11.0の時点でドキュメントが削除されています。情報としては若干古くなりますが、v0.11.0でもこちらを参考にして動作させることができます。
ml-agents/Using-Docker.md at 0.10.1 · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/0.10.1/docs/Using-Docker.mdDockerのインストール
Dockerがインストールされていない場合、インストールします。
Macでbrewコマンド利用> brew cask install docker (略) > docker --version Docker version 19.03.4, build 9013bf5※Dockerを初回起動すると初期設定のためにパスワード入力が求められます。
UnityにLinuxビルドサポートコンポーネントを追加する
Unity Hubを利用してUnityにLinuxビルドサポートコンポーネントを追加します。
Unityのバージョンは2019.2.10f1
を利用しています。Unity Hubがインストールされていない場合は下記をご参考ください。
Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033
- Unity Hubアプリを起動する
- [インストール] > [Unityの利用するアプリ]右側にある[︙]をクリックして[モジュールを加える]を選択する
![]()
![]()
- [モジュールを加える]ダイアログの[Platforms]にある[Linux Build Support]にチェックを入れて[実行]ボタンをクリックする
![]()
学習用のUnityアプリをダウンロードしてビルドする
ML-Agentsリポジトリに含まれているサンプルを学習できるようにします。
ML-Agentsリポジトリをダウンロード
適当なディレクトリにリポジトリをダウンロードする。
> mkdir 適当なディレクトリ > cd 適当なディレクトリ > git clone https://github.com/Unity-Technologies/ml-agents.gitUnityアプリからサンプルプロジェクトを開く
Unity Hubでアプリを立ち上げます。
ML-Agentsを利用するにはUnityのバージョン2017.4
以上が必要となります。今回は2019.2.10f1
を利用しました。アプリが立ち上がったら「開く」ボタンから
任意のディレクトリ/ml-agents/UnitySDK
フォルダを選択します。Unityエディタのバージョンによっては、アップグレードするかの確認ダイアログが立ち上がります。
「確認」ボタンをクリックして進めます。アップグレード処理に少し時間がかかります。
起動しました。
今回は、サンプルにある[3DBall]Scenesを利用します。
- Unityアプリの下パネルにある[Project]タブから以下のフォルダまで開く
- [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Scenes]
- 開いたら、[3DBall]ファイルがあるので、ダブルクリックして開く
Scenes(シーン)の設定
ML-Agentsで学習させるための設定です。
[Inspector]パネルで以下の設定を確認する
ビルド設定
- Unityアプリの[File]メニューから[Build Settings]を選択して[Build Settings]ダイアログを開く
- [Add Open Scenes]をクリックする
- [Scenes In Build]で[ML-Agents/Examples/3DBall/Scenes/3DBall]にチェックを入れる
- [Platform]で
PC, Mac & Linux Standalone
が選択されていることを確認する- [Target Platform]を
Linux
に変更する- [Architecture]を
x86_64
に変更する- [Server Build]にチェックを入れる
- [Build]ボタンをクリックする
- ファイル保存ダイアログで以下を指定してビルドを開始する
- ファイル名: 3DBall
- フォルダ名: 任意のディレクトリ/ml-agents/unity-volume
すると、
unity-volume
に以下フォルダ・ファイルが出力されます。> ls 任意のディレクトリ/ml-agents/unity-volume 3DBall.x86_64 3DBall_Dataハイパーパラメーターファイルの用意
ハイパーパラメーターファイルを
unity-volume
フォルダにコピーしておきます。> cp ml-agents/config/trainer_config.yaml ml-agents/unity-volume/Dockerイメージを作成する
すでに
Dockerfile
が用意されているので、docker build
するだけ。楽々ですね。
ml-agents-on-docker
はイメージ名となりますので、任意でOKです。> docker build -t ml-agents-on-docker ./ml-agents (略) Removing intermediate container c1384ee9d5a6 ---> 9ff8832e88dc Step 19/20 : EXPOSE 5005 ---> Running in 53253a272fb4 Removing intermediate container 53253a272fb4 ---> f0b43146ad36 Step 20/20 : ENTRYPOINT ["mlagents-learn"] ---> Running in f80f7504b790 Removing intermediate container f80f7504b790 ---> 9caddd5a62b1 Successfully built 9caddd5a62b1 Successfully tagged ml-agents-on-docker:latest > docker images REPOSITORY TAG IMAGE ID CREATED SIZE ml-agents-on-docker latest 9caddd5a62b1 2 minutes ago 1.23GB注意点
ml-agents
ディレクトリ直下でdocker build
コマンドを実行するとエラーになるのでご注意ください。> cd ml-agents > docker build -t ml-agents-on-docker . error checking context: 'file ('/Users/xxx/xxxxx/ml-agents/UnitySDK/Temp') not found or excluded by .dockerignore'.v0.10.0からPython 3.7.xにも対応しましたが、
Dockerfile
を覗いてみるとPyhtonのバージョンは3.6.4
のままでした。Dockerfile一部抜粋ENV PYTHON_VERSION 3.6.4Dockerコンテナの実行
ビルドできたら実行してみます。
bashの場合# ml-agents-3dball: コンテナ名(任意) # ml-agents-on-docker: Dockerでビルド時に付けたイメージ名 # 3DBall: Unityでビルド時に付けたアプリの名前(拡張子なし) > cd ml-agents > docker run -it --rm \ --name ml-agents-3dball \ --mount type=bind,source="$(pwd)"/unity-volume,target=/unity-volume \ -p 5005:5005 \ -p 6006:6006 \ ml-agents-on-docker:latest \ --docker-target-name=unity-volume \ --env=3DBall \ --train \ trainer_config.yaml
mlagents-learn
コマンドの--docker-target-name
オプションはdocker run
コマンドの--workdir(-w)
に置き換えることもできます。> docker run -it --rm \ --name ml-agents-3dball \ --mount type=bind,source="$(pwd)"/unity-volume,target=/unity-volume \ -w /unity-volume \ -p 5005:5005 \ -p 6006:6006 \ ml-agents-on-docker:latest \ --env=3DBall \ --train \ trainer_config.yamlfishシェルで実行する場合は、
"$(pwd)"
を"$PWD"
に置き換えます。fishの場合> cd ml-agents > docker run -it --rm \ --name ml-agents-3dball \ --mount type=bind,source="$PWD"/unity-volume,target=/unity-volume \ -p 5005:5005 \ -p 6006:6006 \ ml-agents-on-docker:latest \ --docker-target-name=unity-volume \ --env=3DBall \ --train \ trainer_config.yaml実行すると、学習が始まります。
trainer_config.yaml
のmax_steps
で指定されているステップ数が完了するか、ctrl
+c
キーで学習が終了します。> docker run (略) ▄▄▄▓▓▓▓ ╓▓▓▓▓▓▓█▓▓▓▓▓ ,▄▄▄m▀▀▀' ,▓▓▓▀▓▓▄ ▓▓▓ ▓▓▌ ▄▓▓▓▀' ▄▓▓▀ ▓▓▓ ▄▄ ▄▄ ,▄▄ ▄▄▄▄ ,▄▄ ▄▓▓▌▄ ▄▄▄ ,▄▄ ▄▓▓▓▀ ▄▓▓▀ ▐▓▓▌ ▓▓▌ ▐▓▓ ▐▓▓▓▀▀▀▓▓▌ ▓▓▓ ▀▓▓▌▀ ^▓▓▌ ╒▓▓▌ ▄▓▓▓▓▓▄▄▄▄▄▄▄▄▓▓▓ ▓▀ ▓▓▌ ▐▓▓ ▐▓▓ ▓▓▓ ▓▓▓ ▓▓▌ ▐▓▓▄ ▓▓▌ ▀▓▓▓▓▀▀▀▀▀▀▀▀▀▀▓▓▄ ▓▓ ▓▓▌ ▐▓▓ ▐▓▓ ▓▓▓ ▓▓▓ ▓▓▌ ▐▓▓▐▓▓ ^█▓▓▓ ▀▓▓▄ ▐▓▓▌ ▓▓▓▓▄▓▓▓▓ ▐▓▓ ▓▓▓ ▓▓▓ ▓▓▓▄ ▓▓▓▓` '▀▓▓▓▄ ^▓▓▓ ▓▓▓ └▀▀▀▀ ▀▀ ^▀▀ `▀▀ `▀▀ '▀▀ ▐▓▓▌ ▀▀▀▀▓▄▄▄ ▓▓▓▓▓▓, ▓▓▓▓▀ `▀█▓▓▓▓▓▓▓▓▓▌ ¬`▀▀▀█▓ INFO:mlagents.trainers:CommandLineOptions(debug=False, num_runs=1, seed=-1, env_path='3DBall', run_id='ppo', load_model=False, train_model=True, save_freq=50000, keep_checkpoints=5, base_port=5005, num_envs=1, curriculum_folder=None, lesson=0, slow=False, no_graphics=False, multi_gpu=False, trainer_config_path='trainer_config.yaml', sampler_file_path=None, docker_target_name='unity-volume', env_args=None, cpu=False) INFO:mlagents.envs: 'Ball3DAcademy' started successfully! Unity Academy name: Ball3DAcademy Number of Training Brains : 0 Reset Parameters : gravity -> 9.8100004196167 scale -> 1.0 mass -> 1.0 (略) 2019-11-07 05:31:38.593118: I tensorflow/core/platform/cpu_feature_guard.cc:142] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA 2019-11-07 05:31:38.604956: I tensorflow/core/platform/profile_utils/cpu_utils.cc:94] CPU Frequency: 2400000000 Hz 2019-11-07 05:31:38.607307: I tensorflow/compiler/xla/service/service.cc:168] XLA service 0x39ccf70 executing computations on platform Host. Devices: 2019-11-07 05:31:38.607465: I tensorflow/compiler/xla/service/service.cc:175] StreamExecutor device (0): <undefined>, <undefined> (略) 2019-11-07 05:31:40.131532: W tensorflow/compiler/jit/mark_for_compilation_pass.cc:1412] (One-time warning): Not using XLA:CPU for cluster because envvar TF_XLA_FLAGS=--tf_xla_cpu_global_jit was not set. If you want XLA:CPU, either set that envvar, or use experimental_jit_scope to enable XLA:CPU. To confirm that XLA is active, pass --vmodule=xla_compilation_cache=1 (as a proper command-line flag, not via TF_XLA_FLAGS) or set the envvar XLA_FLAGS=--xla_hlo_profile. INFO:mlagents.envs:Hyperparameters for the PPOTrainer of brain 3DBall: trainer: ppo batch_size: 64 beta: 0.001 buffer_size: 12000 epsilon: 0.2 hidden_units: 128 lambd: 0.99 learning_rate: 0.0003 learning_rate_schedule: linear max_steps: 5.0e4 memory_size: 256 normalize: True num_epoch: 3 num_layers: 2 time_horizon: 1000 sequence_length: 64 summary_freq: 1000 use_recurrent: False vis_encode_type: simple reward_signals: extrinsic: strength: 1.0 gamma: 0.99 summary_path: /unity-volume/summaries/ppo_3DBall model_path: /unity-volume/models/ppo-0/3DBall keep_checkpoints: 5 WARNING:tensorflow:From /ml-agents/mlagents/trainers/trainer.py:223: The name tf.summary.text is deprecated. Please use tf.compat.v1.summary.text instead. WARNING:tensorflow:From /ml-agents/mlagents/trainers/trainer.py:223: The name tf.summary.text is deprecated. Please use tf.compat.v1.summary.text instead. INFO:mlagents.trainers: ppo: 3DBall: Step: 1000. Time Elapsed: 10.062 s Mean Reward: 1.167. Std of Reward: 0.724. Training. (略) INFO:mlagents.trainers: ppo: 3DBall: Step: 10000. Time Elapsed: 109.367 s Mean Reward: 36.292. Std of Reward: 28.127. Training. (略) INFO:mlagents.trainers: ppo: 3DBall: Step: 49000. Time Elapsed: 520.514 s Mean Reward: 100.000. Std of Reward: 0.000. Training. (略) Converting /unity-volume/models/ppo-0/3DBall/frozen_graph_def.pb to /unity-volume/models/ppo-0/3DBall.nn IGNORED: Cast unknown layer IGNORED: StopGradient unknown layer GLOBALS: 'is_continuous_control', 'version_number', 'memory_size', 'action_output_shape' IN: 'vector_observation': [-1, 1, 1, 8] => 'sub_3' IN: 'epsilon': [-1, 1, 1, 2] => 'mul_1' OUT: 'action', 'action_probs' DONE: wrote /unity-volume/models/ppo-0/3DBall.nn file. INFO:mlagents.trainers:Exported /unity-volume/models/ppo-0/3DBall.nn file INFO:mlagents.envs:Environment shut down with return code 0.
WARNING
がかなり出力されますが、学習できました。
TensorBoard
を利用して学習の進捗を視覚的に確認することもできます。> docker exec -it \ ml-agents-3dball \ tensorboard \ --logdir=/unity-volume/summaries \ --host=0.0.0.0 (略) _np_qint32 = np.dtype([("qint32", np.int32, 1)]) /usr/local/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:525: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'. np_resource = np.dtype([("resource", np.ubyte, 1)]) TensorBoard 1.14.0 at http://0.0.0.0:6006/ (Press CTRL+C to quit)学習結果をアプリに組み込む
学習結果は、
ml-agents/unity-volume
フォルダ内に保存されます。
それをUnityアプリに組み込むことで学習結果をUnityアプリに反映できます。> ls unity-volume/ 3DBall.x86_64 3DBall_Data csharp_timers.json models summaries trainer_config.yaml > tree unity-volume/models/ unity-volume/models/ └── ppo-0 ├── 3DBall │ ├── checkpoint │ ├── frozen_graph_def.pb │ ├── model-50000.cptk.data-00000-of-00001 │ ├── model-50000.cptk.index │ ├── model-50000.cptk.meta │ ├── model-50001.cptk.data-00000-of-00001 │ ├── model-50001.cptk.index │ ├── model-50001.cptk.meta │ └── raw_graph_def.pb └── 3DBall.nn 2 directories, 10 filesUnityアプリの設定
Playerの設定を行います。
- Unityアプリの[Edit]メニューから[Project Settings]を選択する
- [Inspector]ビューの[Other Settings]欄で以下を確認・設定する
学習結果ファイルの取り込み
ターミナルかFinderで学習結果を以下フォルダにコピーします。
- 学習結果ファイル:
models/ppo-0/3DBall.nn
- 保存先:
UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/
※すでに保存先に
3DBall.nn
ファイルが存在していますので、リネームします。> cp models/ppo-0/3DBall.nn ml-agents/UnitySDK/Assets/ML-Agents/Examples/3DBall/TFModels/3DBall_new.nn
- Unityアプリの下パネルにある[Project]タブから以下のフォルダまで開く
- [Assets] > [ML-Agents] > [Examples] > [3DBall] > [Scenes]
- 開いたら、[3DBall]ファイルがあるので、ダブルクリックして開く
- [Hierarchy]パネルから[Agent]を選択する
- Unityアプリの[Project]パネルで以下フォルダを選択する
- [Assets] > [ML-Agents] > [Examples] > [3DBall] > [TFModels]
Unityアプリの[Inspector]パネルにある[Model]という項目に[TFModels]フォルダ内の
3DBall_new.nn
ファイルをドラッグ&ドロップする
Unity上部にある[▶]ボタンをクリックする
これで、学習結果が組み込まれた状態でアプリが起動します。
参考
Macでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033MacでUnity ML-Agentsの環境を構築する(v0.11.0対応) - Qiita
https://qiita.com/kai_kou/items/0d40157cbc303fb10c22ml-agents/Using-Docker.md at 0.10.1 · Unity-Technologies/ml-agents
https://github.com/Unity-Technologies/ml-agents/blob/0.10.1/docs/Using-Docker.mdMacでhomebrewを使ってUnityをインストールする(Unity Hub、日本語化対応)
https://qiita.com/kai_kou/items/445e614fb71f2204e033
- 投稿日:2019-12-05T09:07:51+09:00
Unityライセンスのalfファイルを自動でulfにしたい!
CIのActivateとかでライセンスを自動でActivateさせたい!
CIでUnityを扱う時はJenkinsとかであれば問題ないのですが、CircleCIやGitHub Actionsを使用するときにDockerでのライセンス認証では.ulfファイルというのが必要になってきます。
現在.ulfファイルをコマンドラインから生成することはできません。生成するにはブラウザ経由の一択です。
それを今回Puppeteerというnode.jsのツールを使って自動化してみました。※今回の認証フローはPersonalEdition固定になります。
Puppetterとは、Webブラウザでの操作をソースコードから行えるものになります。
詳しくはこちら
Puppeteer今回のリポジトリはこちら
MizoTake/unity-license-activate実装
npm経由でPuppeteerを入れて以下のjsで実装しました。
今回はライセンスの認証が必要になるので
https://license.unity3d.com/manual
のページで操作を行います。
手元にあるalfファイルから最終的にulfファイルをダウンロードする操作になります。叩くコマンドは以下になります
node activate.js $email $password $alf_file_path
以下が今回のScriptの全容ですが細かく分けてどうなっているのか下で記述します。
activate.jsconst puppeteer = require('puppeteer') const fs = require('fs') ;(async () => { const browser = await puppeteer.launch() const page = await browser.newPage() const downloadPath = process.cwd() const client = await page.target().createCDPSession() await client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadPath }) await page.goto('https://license.unity3d.com/manual') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) const email = `${process.argv[2]}` await page.type('input[type=email]', email) const password = `${process.argv[3]}` await page.type('input[type=password]', password) await page.click('input[name="commit"]') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) const input = await page.$('input[name="licenseFile"]') const alfPath = `${process.argv[4]}` await input.uploadFile(alfPath) await page.click('input[name="commit"]') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) const selectedTypePersonal = 'input[id="type_personal"][value="personal"]' await page.evaluate( s => document.querySelector(s).click(), selectedTypePersonal ) const selectedPersonalCapacity = 'input[id="option3"][name="personal_capacity"]' await page.evaluate( s => document.querySelector(s).click(), selectedPersonalCapacity ) await page.click('input[class="btn mb10"]') await page.waitForNavigation() await page.click('input[name="commit"]') let _ = await (async () => { let ulf do { for (const file of fs.readdirSync(downloadPath)) { ulf |= file.endsWith('.ulf') } await sleep(1000) } while (!ulf) })() function sleep(milliSeconds) { return new Promise((resolve, reject) => { setTimeout(resolve, milliSeconds) }) } await browser.close() })()画面ごとの処理
// ライセンス認証を行うページに行く await page.goto('https://license.unity3d.com/manual') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) // ライセンス認証を行うページに飛ばしたがリダイレクトでUnityのログインページに飛んでいる //コマンド引数からメールアドレスとパスワードをとってくる const email = `${process.argv[2]}` await page.type('input[type=email]', email) const password = `${process.argv[3]}` await page.type('input[type=password]', password) // Sign inのボタンを押す await page.click('input[name="commit"]')↓
const input = await page.$('input[name="licenseFile"]') // コマンドライン引数で指定したpathからfileを添付 const alfPath = `${process.argv[4]}` await input.uploadFile(alfPath) // Nextボタンを押す await page.click('input[name="commit"]')これでファイル添付ができてることがわかります。ファイル添付までできるのすげぇ…Puppeteer…
↓
// Personal Editionの選択 const selectedTypePersonal = 'input[id="type_personal"][value="personal"]' await page.evaluate( s => document.querySelector(s).click(), selectedTypePersonal ) // Personal Edition選択後に出てくるのOptionを選択 const selectedPersonalCapacity = 'input[id="option3"][name="personal_capacity"]' await page.evaluate( s => document.querySelector(s).click(), selectedPersonalCapacity ) // Nextボタンを押す await page.click('input[class="btn mb10"]')↓
// Download license fileボタンを押す await page.click('input[name="commit"]') // ダウンロードが始まるので手元に.ulfファイルができるまで待つ let _ = await (async () => { let ulf do { for (const file of fs.readdirSync(downloadPath)) { ulf |= file.endsWith('.ulf') } await sleep(1000) } while (!ulf) })() function sleep(milliSeconds) { return new Promise((resolve, reject) => { setTimeout(resolve, milliSeconds) }) }以上のような流れになっています。
さいごに
Puppeteer便利!!!!
await page.screenshot( { path: "./example.png" });でScreenShotを撮りつつ実装してました。
ブラウザでしか行えない操作もこれがあればできるので色々と捗るんじゃないかなと思っています。
- 投稿日:2019-12-05T09:07:51+09:00
Unityの.alfファイルから自動で.ulfをダウンロードしたい!
CIのActivateとかでライセンスを自動でActivateさせたい!
CIでUnityを扱う時はJenkinsとかであれば問題ないのですが、CircleCIやGitHub Actionsを使用するときにDockerでのライセンス認証では.ulfファイルというのが必要になってきます。
現在.ulfファイルをコマンドラインから生成することはできません。生成するにはブラウザ経由の一択です。
それを今回Puppeteerというnode.jsのツールを使って自動化してみました。※今回の認証フローはPersonalEdition固定になります。
Puppetterとは、Webブラウザでの操作をソースコードから行えるものになります。
詳しくはこちら
Puppeteer今回のリポジトリはこちら
MizoTake/unity-license-activate実装
npm経由でPuppeteerを入れて以下のjsで実装しました。
今回はライセンスの認証が必要になるので
https://license.unity3d.com/manual
のページで操作を行います。
手元にあるalfファイルから最終的にulfファイルをダウンロードする操作になります。叩くコマンドは以下になります
node activate.js $email $password $alf_file_path
以下が今回のScriptの全容ですが細かく分けてどうなっているのか下で記述します。
activate.jsconst puppeteer = require('puppeteer') const fs = require('fs') ;(async () => { const browser = await puppeteer.launch() const page = await browser.newPage() const downloadPath = process.cwd() const client = await page.target().createCDPSession() await client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadPath }) await page.goto('https://license.unity3d.com/manual') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) const email = `${process.argv[2]}` await page.type('input[type=email]', email) const password = `${process.argv[3]}` await page.type('input[type=password]', password) await page.click('input[name="commit"]') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) const input = await page.$('input[name="licenseFile"]') const alfPath = `${process.argv[4]}` await input.uploadFile(alfPath) await page.click('input[name="commit"]') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) const selectedTypePersonal = 'input[id="type_personal"][value="personal"]' await page.evaluate( s => document.querySelector(s).click(), selectedTypePersonal ) const selectedPersonalCapacity = 'input[id="option3"][name="personal_capacity"]' await page.evaluate( s => document.querySelector(s).click(), selectedPersonalCapacity ) await page.click('input[class="btn mb10"]') await page.waitForNavigation() await page.click('input[name="commit"]') let _ = await (async () => { let ulf do { for (const file of fs.readdirSync(downloadPath)) { ulf |= file.endsWith('.ulf') } await sleep(1000) } while (!ulf) })() function sleep(milliSeconds) { return new Promise((resolve, reject) => { setTimeout(resolve, milliSeconds) }) } await browser.close() })()画面ごとの処理
// ライセンス認証を行うページに行く await page.goto('https://license.unity3d.com/manual') await page.waitForNavigation({ timeout: 60000, waitUntil: 'domcontentloaded' }) // ライセンス認証を行うページに飛ばしたがリダイレクトでUnityのログインページに飛んでいる //コマンド引数からメールアドレスとパスワードをとってくる const email = `${process.argv[2]}` await page.type('input[type=email]', email) const password = `${process.argv[3]}` await page.type('input[type=password]', password) // Sign inのボタンを押す await page.click('input[name="commit"]')↓
const input = await page.$('input[name="licenseFile"]') // コマンドライン引数で指定したpathからfileを添付 const alfPath = `${process.argv[4]}` await input.uploadFile(alfPath) // Nextボタンを押す await page.click('input[name="commit"]')これでファイル添付ができてることがわかります。ファイル添付までできるのすげぇ…Puppeteer…
↓
// Personal Editionの選択 const selectedTypePersonal = 'input[id="type_personal"][value="personal"]' await page.evaluate( s => document.querySelector(s).click(), selectedTypePersonal ) // Personal Edition選択後に出てくるのOptionを選択 const selectedPersonalCapacity = 'input[id="option3"][name="personal_capacity"]' await page.evaluate( s => document.querySelector(s).click(), selectedPersonalCapacity ) // Nextボタンを押す await page.click('input[class="btn mb10"]')↓
// Download license fileボタンを押す await page.click('input[name="commit"]') // ダウンロードが始まるので手元に.ulfファイルができるまで待つ let _ = await (async () => { let ulf do { for (const file of fs.readdirSync(downloadPath)) { ulf |= file.endsWith('.ulf') } await sleep(1000) } while (!ulf) })() function sleep(milliSeconds) { return new Promise((resolve, reject) => { setTimeout(resolve, milliSeconds) }) }以上のような流れになっています。
さいごに
Puppeteer便利!!!!
await page.screenshot( { path: "./example.png" });でScreenShotを撮りつつ実装してました。
ブラウザでしか行えない操作もこれがあればできるので色々と捗るんじゃないかなと思っています。
- 投稿日:2019-12-05T04:09:46+09:00
「MonKey - Productivity Commands」のカスタムコマンドでさらにUnity上の作業を効率化する
PONOS Advent Calendar 2019の5日目の記事です。
昨日は@nimitsuさんのGameLift RealTimeServerで遊んでみよう for Unity(AWS設定編)でした。
はじめに
本記事はPONOS Advent Calendar 2019の2日目の記事である「MonKey - Productivity Commands」のコマンド操作でUnity開発を効率化するの続編となります。
MonKey - Productivity Commands - Asset Store前回はMonKeyを使用してどんな事ができるのか、導入について書かせてもらいました。
今回は少し発展して、カスタムコマンドの作り方についてまとめたいと思います。MonKeyには予め130超もの豊富なコマンドが用意されており、様々な処理をコマンドから実行できますが、それらのコマンドだけではそのプロジェクト特有の処理まではサポートできません。
カスタムコマンドを利用することにより、プロジェクト独自の処理についてもコマンド化し操作時間を短縮することができるので、MonKeyを有効利用したいと思っている方は参考にしてみてください。なお、
- Unity 2019.2.12f1
- MacOS 10.14.6
の環境で動作確認しています。
カスタムコマンド
MonKey.Command
属性をstaticメソッドに付与することで、そのメソッドをコマンド化することができます。
なお、スクリプトはEditor
フォルダ以下に存在している必要がありますのでご注意ください。using MonKey; public static class SampleCommands { [Command("Sample Command 01")] // 引数にMonKey上のコマンド名を指定する static void SampleCommand01() { Debug.Log($"コマンド\"SampleCommand01\"を実行したログです。"); } }
MonKeyのコマンドパレット上に表示されました!
コマンドを実行すると、メソッド内に記述した処理が実行されています。
このように、メソッドをMonKeyのコマンド化するのは非常に簡単です。使用例:選択中のGameObjectの階層パスをコンソールに出力する
さて、ここからはより実用的なカスタムコマンドを作成してみます。
今回作成するのは「選択中のGameObjectの階層パスをコンソールに出力する」コマンドです。
例えば、上のようなHierarchyで実行した場合には「Parent/SecondSon/Grandchild/Great-grandson」とコンソールにログ出力する挙動となります。『SecondSon』の下にある『Grand-grandson』のことを他の人に伝えたいとき、Hierarchyの階層のパスを提示できれば、『EldestSon』の下にいるGrand-grandsonを誤って参照しまうことを防止できます。
コマンドの基本情報を設定する
前項で作成した
Sample Command 01
ですが、他のコマンドと異なり「ヘルプ」や「短縮されたコマンド名」が表示されておらず、実際に使うには不便でした。
今回はそれらコマンドに関する基本情報をちゃんと設定してみましょう。
MonKey.Command
属性には上記の「ヘルプ」や「短縮されたコマンド名」のようなコマンドの基本情報を設定するためのプロパティが用意されています。MonKey.Command属性のプロパティ(抜粋)
プロパティ 用途 Name コマンド名。コマンドの検索に用いられる。 QuickName 短縮されたコマンド名。
3文字以内に収めることを推奨されている。Help コマンド名の下に表示されるヘルプ。
1行より長くなるとフォーマットが完全ではない可能性がある。実際にこれらのプロパティを使用して「ヘルプ」と「短縮されたコマンド名」が設定された
Sample Command 02
を作成します。[Command( "Sample Command 02", QuickName = "SC2", Help = "選択中のGameObjectの階層パスをコンソールに出力する" )] static void SampleCommand02() { }
設定した「ヘルプ」と「短縮されたコマンド名」がコマンドパレット上に表示されていることがわかります。
また、短縮されたコマンド名である「sc2」を入力しただけでSmaple Command 02
が候補として表示されます。コマンドから選択されているGameObjectを参照する
次に、実際に選択中のGameObjectを参照してコンソールに出力する、というコマンドの処理部分を実装してみましょう。
現在選択されているGameObjectを参照するためには、通常エディタ拡張で使用しているUnityEditor.Selection
ではなく、MonKeyから提供されているMonKey.Editor.MonkeyEditorUtils.OrderedSelectedGameObjects
を使用します。(なお、選択されたGameObjectではなく、選択されたTransformを取得することのできる
MonKey.Editor.MonkeyEditorUtils.OrderedSelectedTransform
も用意されています)
IEnumerable<GameObject>
で返ってくるので、foreachで列挙し処理していきます。選択中のGameObjectを取得し、そのパスを出力するコードは以下になります。
using MonKey.Editor; ... // OrderedSelectedGameObjectsから選択中のGameObjectを列挙できる。 foreach (var selectedGameObject in MonkeyEditorUtils.OrderedSelectedGameObjects) { // 親のGameObjectの名前を手前に連結していく。 var pathBuilder = new System.Text.StringBuilder(selectedGameObject.name); var parent = selectedGameObject.transform.parent; while (parent != null) { pathBuilder.Insert(0, parent.name + "/"); parent = parent.transform.parent; } Debug.Log(pathBuilder.ToString()); }
この選択状態でコマンドを実行すると、
このように、各GameObjectの階層をパスとして出力することができました。コマンドを実行できる条件を設定する
現在の
Sample Command 02
は、GameObjectを全く選択していなくてもコマンドを実行することができます。選択していない状態で実行したとしても
MonkeyEditorUtils.OrderedSelectedGameObjects
の要素が0で返ってくるだけなのでエラーが発生することはありませんが、それではコマンドを実行する意味がないため、「GameObjectを選択している状態でのみ」実行できるように、検証条件を設定してみましょう。コマンドの検証条件は
MonKey.Command
属性のDefaultValidation
プロパティで設定します。
DefaultValidation
にはMonKey.DefaultValidation
列挙体の値が使用できます。MonKey.DefaultValidation列挙体(抜粋)
値 用途 AT_LEAST_ONE_GAME_OBJECT 1個以上のGameObjectを選択している。 AT_LEAST_TWO_GAME_OBJECTS 2個以上のGameObjectを選択している。 IN_PLAY_MODE プレイモード中である。 IN_EDIT_MODE プレイモード中ではない。 今回は、「GameObjectを選択している状態でのみ」実行できるようにしたいので、
MonKey.DefaultValidation.AT_LEAST_ONE_GAME_OBJECT
を指定します。[Command( "Sample Command 02", QuickName = "SC2", Help = "選択中のGameObjectの階層パスをコンソールに出力する", DefaultValidation = DefaultValidation.AT_LEAST_ONE_GAME_OBJECT )] static void SampleCommand02() {
GameObjectを選択していない状態でコマンドを選択すると…
ご覧のように「Select at least one GameObject」の警告が表示され、コマンドを実行することができません。コマンドの製作者が自身だけで使用する分には使い方を気をつければいいので、検証条件の定義は必須ではありませんが、他者へ共有する予定がある場合は検証条件を正しく定義して正しい状況で使用してもらえるようにしたいですね。
おわりに
カスタムコマンドを作成してMonKeyが実行できる機能を充実させることで、Unity上の作業を更に効率化することができます。
なお、Unityエディタ上からstaticメソッドを実行するには
UnityEditor.MenuItem
属性でメニューアイテム化する、という方法もありますが、MonKeyなら処理の実行条件の検証を行ったり実行時に引数を与えることができるので、メニューアイテムとして実行するよりも柔軟な処理の実行が可能となります。
そういった用途で実行したい処理がある場合にはMonKeyのカスタムコマンド作成を検討してみてください。さて、PONOS Advent Calendar 2019は6日目も続けて私@e73ryoが担当する予定です。
次回は引数を与えてカスタムコマンドを実行する方法について紹介したいと思います。
- 投稿日:2019-12-05T01:53:40+09:00
【Unity】公式のソースコードを閲覧する。
公式のソースコードはGitHubから閲覧可能。
GitHub
- 投稿日:2019-12-05T00:53:53+09:00
VR剣戟ゲーのための自作当たり判定処理
はじめに
SEKIROみたいな剣の斬り合いがVRでやりたい!!!!
こんにちは、ZeniZeniです。
昨今、Sword Of GargantuaやSword Master VRなど、面白い良VR剣戟ゲームが増えてきました。
それらをプレイしてると、自分の理想の剣戟ゲームというのを作りたい欲がふつふつと湧き上がってきます。
というわけで絶賛開発中です。今回は、VRで剣戟ゲームを作るための第一歩として、剣を高速で振っても、剣の当たり判定と剣同士が交差した座標を取得できる機能を実装しようと思います。
下の動画のようなことができるようになります。この速度で当たり判定とるの苦労したができた!
— ZeniZeni (@ZeniYuki0922) November 13, 2019
めっっっっちゃ楽しい!!!
VR剣戟ゲーはこれくらい斬りあう感じが俺の理想
(音あり) pic.twitter.com/7ZMnJZBgdPこれは、剣が交差した瞬間の座標を取得して、その座標から火花のエフェクトを発生させています。
実装方法
実装方法ですが、コリダーは使わずにやっています。
なぜかというと、高速な物体同士の当たり判定は、コリダーだと簡単にすり抜けてしまうからです。
下の動画くらいの速度が限界でした。SEKIROみたいに火花を散らすとめちゃめちゃ楽しいことに気づいた pic.twitter.com/B4hWueOCrl
— ZeniZeni (@ZeniYuki0922) November 12, 2019プラス、座標を取得するために小さいコリダーを大量に配置していたので、剣を変えるときは設定がめんどくさいですし、パフォーマンスもよろしくなさそうです。
それではコリダーを使わない当たり判定の実装方法を考えていきましょう。
まず、剣と剣がぶつかった判定をどうとるかを考えてみます。
これは三次元空間において、ある線分と線分の距離が一定値以下になったときを考えればよさそうです。剣同士の距離の導出
計算方法
それでは、$点(p_{11}, p_{12})$からなる線分$L_1$と、$点(p_{21}, p_{22})$からなる線分$L_2$の距離$d$を導出していきます。
こちらのサイトを参考にしてみます。
まず線分$L_1$の方向ベクトルを$V_1$、線分$L_2$の方向ベクトルを$V_2$として、$V_1$と$V_2$の外積、すなわち線分$L_1$から線分$L_2$に垂直なベクトル$n$を求めます。
$L_1$上の任意の点$P_1$から$L_2$上の任意の点$P_2$へのベクトルを$V_{12}$とすれば、ベクトル$n$とベクトル$V_{12}$の内積が、そのまま距離$d$となります。
計算がうまくできないとき
上記の導出では、線分同士が同一平面上にあるときには距離$d$は必ず0になり、正確な値が出ません。
ベクトル$V_{12}$とベクトル$n$が垂直になり、内積$(V_{12},n) = 0$となるからです。(垂直なベクトルの内積は0)
実際の所、剣同士をぶんぶん振り回している中で、剣同士が同一平面上になるときなど滅多にないのですが、一応考慮しておきます。剣同士が交差した座標の導出
計算方法
火花を剣同士がぶつかった瞬間にぶつかった場所から発生させたいので、剣同士が交差した座標を導出していきます。
これがちょっとめんどくさいです。
考え方としては、まず線分$L_2$上の点で、線分$L_1$に最も近い点を$P_{min}$とします。その点$P_{min}$上から線分$L_1$への垂線の方向ベクトルの単位ベクトルを$\hat{n}$とします。
剣同士の交点ですが、例えば剣同士が10cmより小さくなったときを剣同士が接触したと考えれば、剣同士の交点は剣同士の互いに最も近い点二つの中点とするのがよさそうです。
ゆえに交点$M$は、剣同士の距離を$d$とすればM = P_{min} + \frac{d}{2} * \hat{n}となります。
それでは次に、$P_{min}$を導出していきます。
まず、線分$L1$上の任意の点$P_1$は、線分$L_1$の始点$P_{11}$の$x$座標を$P_{11x}$、線分$L_1$の方向ベクトル(始点$P_{11} - $ 終点$P_{12}$)の$x$成分を$v_{1x}$のようにあらわすとして、状態変数$t_1$$(0 \leqq t_1 \leqq 1)$を用いれば
\begin{align} P_1 &= (l_{1x},l_{1y},l_{1z}) \\ &= (p_{11x} + t_1v_{1x},p_{11y} + t_1v_{1y},p_{11z} + t_1v_{1z}) \\ \end{align}と表せます。
また線分$L2$上の任意の点$P_2$も同様にして\begin{align} P_2 &= (l_{2x},l_{2y},l_{2z}) \\ &= (p_{21x} + t_2v_{2x},p_{21y} + t_2v_{2y},p_{21z} + t_2v_{2z}) \\ \end{align}と表せます。
すると距離$d$は
\begin{align} d^2 &= (l_{1x} - l_{2x})^2 + (l_{1y} - l_{2y})^2 +(l_{1z} - l_{2z})^2 \\ &= (v_{1x}^2 + v_{1y}^2 + v_{1z}^2)t_1^2 \\ & \quad \quad + 2(v_{1x}v_{2x} + v_{1y}v_{2y} + v_{1z}v_{2z})t_1t_2 \\ & \quad \quad + 2(v_{1x}(p_{11x} - p_{21x}) + v_{1y}(p_{11y} - p_{21y}) + v_{1z}(p_{11z} - p_{21z}))t_1 \\ & \quad \quad + (v_{2x}^2 + v_{2y}^2 + v_{2z}^2)t_2^2 \\ & \quad \quad + 2(v_{2x}(p_{21x} - p_{11x}) + v_{2y}(p_{21y} - p_{11y}) + v_{2z}(p_{21z} - p_{11z}))t_2 \\ & \quad \quad + (p_{11x}-p_{21x})^2 + (p_{11y}-p_{21y})^2 + (p_{11z}-p_{21z})^2 \end{align}というようにあらわせます。
うへぇ…って思いますよね、僕は思いました。
これを次数に注目して、係数は適当な文字に置き換えて、平方完成してみます。\begin{align} d^2 &= At_1^2 + Bt_1 + Ct_1t_2 + Dt_2^2 + Et_2 + F \\ &= A\biggr(t_1 + \frac{C}{2A}t_2 + \frac{B}{2A}\biggr)^2 + \biggr(D - \frac{C^2}{4A}\biggr)\biggr(t_2 + \frac{E - \frac{2BC}{4A}}{2D-\frac{C^2}{4A}}\biggr)^2 -\frac{B^2}{4A} + F \end{align}今求めようとしている点$P_{min}$は、$d$が最小のとき、すなわち平方完成した部分が0になるときなので、
\begin{align} t_2 &= - \frac{E - \frac{2BC}{4A}}{2D-\frac{C^2}{4A}} \\ &= \frac{BC - 2AE}{4AD - C^2} \end{align}のときです。
したがって$P_{min}$は$P_2 = (p_{21x} + t_2v_{2x},p_{21y} + t_2v_{2y},p_{21z} + t_2v_{2z})$の$t_2$に$\frac{BC - 2AE}{4AD - C^2}$を代入したものとなります。絶対に交差しないとき
剣が絶対に交差しない状況のときは上のような計算をするのは無駄なので、そのような状況は早い段階ではじきましょう。
剣が絶対に交差しない状況は、下図のようなときです。
これにz座標の判定も加わります。実際のコード
それでは実際に書いた線分同士の距離と交点を導出するコードがこちらです。
線分同士の距離とその交点を同時に取得したかったので、IntersectionInfoという構造体を作っています。
線分はLineという構造体を作成していて、剣の刃の部分の根本と剣先の2点を設定してください。IntersectionChecker.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using System; [Serializable] public struct Line { public Transform p1; public Transform p2; } public struct IntersectionInfo { public float Distance; public Vector3 MidPoint; } public class IntersectionChecker : MonoBehaviour { public Line l1; public Line l2; public IntersectionInfo info; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { if (Input.GetKeyDown(KeyCode.Space)) { var t = GetIntersectionInfo(l1, l2); Debug.Log("distance is " + t.Distance); Debug.Log("mid point is " + t.MidPoint); } } public IntersectionInfo GetIntersectionInfo(Line line1, Line line2, float dThreshold = 0.5f) { //各平面で交差していない時は排除 if (!CheckIntersectionException(line1, line2)) { Debug.Log("not intersect"); info.Distance = -1; return info; } Debug.Log("intersect!"); var p11 = line1.p1.position; var p12 = line1.p2.position; var p21 = line2.p1.position; var p22 = line2.p2.position; var v1 = p12 - p11; var v2 = p22 - p21; var v12 = p22 - p11; var n = Vector3.Cross(v1, v2).normalized; var d = Mathf.Abs(Vector3.Dot(n, v12)); //線分同士が同一平面上にあるとき if (d == 0) { if (IsInSamePlane(line1, line2)) { Debug.Log("lines are in same plane"); info.Distance = -1; return info; } } //dThresholdより離れている時を排除 if (d > dThreshold) { info.Distance = -1; return info; } info.Distance = d; //線分ががもう一つの線分に対して手前か奥にあるかの判定 var side = (Vector3.Cross(v1, v12).y < 0 ? 1 : -1); var tmpA = v1.x * v1.x + v1.y * v1.y + v1.z * v1.z; var tmpB = 2 * (v1.x * (p11.x - p21.x) + v1.y * (p11.y - p21.y) + v1.z * (p11.z - p21.z) ); var tmpC = 2 * (v1.x * v2.x + v1.y * v2.y + v1.z * v2.z); var tmpD = v2.x * v2.x + v2.y * v2.y + v2.z * v2.z; var tmpE = 2 * ( v2.x * (p21.x - p11.x) + v2.y * (p21.y - p11.y) + v2.z * (p21.z - p11.z) ); //var t2 = -( tmpE - ( (2 * tmpB * tmpC) / (4 * tmpA) ) ) / ( 2 * (tmpD - ( (tmpC * tmpC ) / (4 * tmpA) )) ); var t2 = ( tmpB * tmpC - 2 * tmpA * tmpE) / ( 4 * tmpA * tmpD - tmpC * tmpC); Debug.Log("P min is " + (p21 + (t2 * v2))); info.MidPoint = p21 + (t2 * v2) + ((d/2) * side * n); return info; } public bool IsInSamePlane(Line line1, Line line2) { var p1 = line1.p1.position; var p2 = line1.p2.position; var p3 = line2.p1.position; var p4 = line2.p2.position; var v1 = p2 - p1; var v2 = p3 - p1; var v3 = p4 - p1; var det = (v1.y * v2.z * v3.x) + (v1.z * v2.x * v3.y) + (v1.x * v2.y * v3.z) - (v1.z * v2.y * v3.x) - (v1.x * v2.z * v3.y) - (v1.y * v2.x * v3.z); return det == 0; } public bool CheckIntersectionException(Line line1, Line line2) { var p1 = line1.p1.position; var p2 = line1.p2.position; var p3 = line2.p1.position; var p4 = line2.p2.position; //x座標チェック if (p1.x <= p2.x) { if ((p3.x < p1.x && p4.x < p1.x) || (p2.x < p3.x && p2.x < p4.x)) { return false; } } else { if ((p3.x < p2.x && p4.x < p2.x) || (p1.x < p3.x && p1.x < p4.x)) { return false; } } //y座標チェック if (p1.y <= p2.y) { if ((p3.y < p1.y && p4.y < p1.y) || (p2.y < p3.y && p2.y < p4.y)) { return false; } } else { if ((p3.y < p2.y && p4.y < p2.y) || (p1.y < p3.y && p1.y < p4.y)) { return false; } } //z座標チェック if (p1.z <= p2.z) { if ((p3.z < p1.z && p4.z < p1.z) || (p2.z < p3.z && p2.z < p4.z)) { return false; } } else { if ((p3.z < p2.z && p4.z < p2.z) || (p1.z < p3.z && p1.z < p4.z)) { return false; } } return true; } } }という感じになります。
交差しない場合やいくつかの例外時には、IntersectionInfoのDistanceは-1となります。下図のような感じで設定してください。
後は、GetIntersectionInfo関数を呼んで得られたIntersectionInfoのMidPointで火花等のエフェクトを発生させればよいのです。剣を速く振ったときだけ呼びたい場合、まず剣の振る速度を求める方法を考えると思います。
剣の振る速度は、SteamVR SDKアセットに入っている、VelocityEstimatorというコンポーネントを使うことをお勧めします。開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。
- 投稿日:2019-12-05T00:03:52+09:00
プロキシ環境下でUnity EditorのAsset store�にサインインできなかったのでUnity Hubを使用しないで問題回避した件
前略
会社のプロキシのせいでUnityがまともに動かったので、その対策を書きます。
環境
- Unity: 2019.2.14f1
- Unity Hub: 2.2.1
手順
UnityはUnity Hubからではなく、単体でインストールします。
こちらの「ダウンロード」-「Unityインストーラー」から「UnityDownloadAssistant-2019.2.14f1.exe」をダウンロードし、インストールします。
Fiddlerを導入します。インストール手順はこちらに従いました。プロキシの設定はこちらに従いました。
Fiddlerを導入すると、Unity EditorからAsset storeにアクセスできます。Fiddlerを使用しているので環境変数の設定は不要でした。しかしUnity Hubは通信エラーになります。Fiddlerのログを見ると407エラーが出ていることがわかります。Unity Hubが使えないので、Asset storeにサインインできません。
Unity Hubはアンインストールしておきます。
Unity EditorでAsset storeを開き、右上の「Sing In」ボタンをクリックします。「Create a Unity account」をクリックし、遷移先の画面で「Already have a Unity ID?」をクリックします。サインイン画面に飛ぶのでサインインします。これで認証が通り、Asset storeを使用できます。草々
Unityの中の人、プロキシ環境下でも使えるようにしてください。オナシャッス。