アセットを買い漁ってみる

11月11日は...

ポッキーの日」だったはずだが、中華圏的には独身を祝う日だという。

とりあえず極東限定で

こんなセールをされては黙ってはいられない。

サイバーマンデーが来るまで虎視眈々と狙ってた、アレとかアレをこの機会に抑えておこうというわけで、予算だけ決めて買えるだけ買うことにした。

晒してみる

で、今回購入したのは以下の11点。

すぐに役に立ちそうなもの

assetstore.unity.com

assetstore.unity.com

assetstore.unity.com

まずはメカアクション的なモノを作るには必要と思われるものを揃えたつもり。

assetstore.unity.com

地味にキーアサイン設定画面も必要だしね。

幾何模様なので描くだけなら自分でもできるが、見栄えと手間を考えると買った方が良いと判断。

assetstore.unity.com

assetstore.unity.com

モジュラー形式のレベルデザインパックの購入も検討したが、デザインの幅が狭まるので、モデルは自作することにして、マテリアルだけ手を借りることにした。

念願の...

まぁ、何より欲しかったのはコレである。

assetstore.unity.com

人体とか人体じゃないのとかのモーションをUnity上で手付けする為のアセットで、ここであらためて紹介するのがアホらしいほどUnity界隈では有名なヤツである。

そして、

assetstore.unity.com

これも定番。

アタマに仕込めばツインテが揺れ、ケツに仕込めばしっぽが揺れ、胸に仕込めばチチが揺れる。ガリアンソードを作るなら必須である。

さらに、

assetstore.unity.com

コレにも手を出そうと思ったが、さすがに予算切れで諦めた。

というか、過去に70%OFFとかになっていたので、サイバーマンデーまで様子見である。

究極の選択

予算切れの主な原因はこいつらである。

assetstore.unity.com

assetstore.unity.com

共に動的IK制御用アセットである。

基本機能はほぼ同じである模様。
FINAL IKがすでに製品版であるのに対して、Bone controllerがベータ版なので安い。

機能に大きな差異がないのであれば、どちらかを選択しなければならない。
どちらにするか?

両方買いました。

Bone controllerは将来性を見込んで、FINAL IKは保険として抑えた。
当面はFINAL IKをメインに使っていくことにする。

そして、ついでにコレも買った。

assetstore.unity.com

今すぐ必要な局面はないと思うが、高額アセットなのでここで抑えていて損はなかろう。

諦めたもの

VertExmotion Proもそうだが、これも欲しかった。

assetstore.unity.com

他にも諦めたものはあったが、いっぺんに買っても使いこなせないだろうから、お楽しみはサイバーマンデーまで取っておくことにする。

次回予告

買ったら使わにゃならんよね?ということで、ゴリゴリいじっていきたい。
まずは本命であるCamera Controllerから。

多脚戦車から弾と共に発砲煙を出してみる

おおっと!EXPLOSION

一週遅れとなってしまったが、今回こそ発砲煙を吹いてみたい。

assetstore.unity.com

というわけで、↑これの、"DustExplosion"を使用する。

とはいえ、やることはたかが知れている。

docs.unity3d.com

↑の通り、ParticleSystemをぶら下げたGameObjectをInstantiatメソッドで生成するだけである。

ただし、そのままだと延々とシーン上に残り続けるので、これも↑の通り、GameObjectには生成時に破棄する(つまり、どういうことだってばよ)Script Componentを追加する。

例のごとく、オリジナルは維持しておきたいので、"DustExplosion"から複製した"GunFireSmoke"を発砲煙GameObjectとして新たにPrefab化することにした。

今回のソース

生成時に破棄するとは、こういう事だぁー!!

GunFireSmoke.cs

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

public class GunFireSmoke : MonoBehaviour
{
    // Use this for initialization
    void Start()
    {
        ParticleSystem partcleSystem = GetComponent<ParticleSystem>();
        Destroy(gameObject, (float)partcleSystem.main.duration);
    }

    // Update is called once per frame
    void Update()
    {

    }
}

Destorメソッドは、実行遅延時間が設定できるので、こういう芸当ができる。
コーディングも少なく済み、管理も楽ではある。

ついでに、出したら出しっ放しだった弾も、同じ対応を加えた。

Bullet.cs

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

public class Bullet : MonoBehaviour {

	// Use this for initialization
	void Start () {
        Destroy(gameObject, 5.0f);
    }

    // Update is called once per frame
    void Update () {
		
	}
}

GameObjectを生成するところは、弾のときと同じなので説明は割愛する。
強いて注意点を挙げれば、InspectorでPrefabの割り当てを忘れんようにすることだろうか。

二週間ぶりにいじったら、二週間前に自分がどういう設計していたか忘れており、エラーメッセージに出ているにもかかわらず、Prefabの割り当て忘れに気付くのに数時間かかった。アホス。

...さて、気を取り直して撃ってみよう。

ヒヤッホォォォウ!最高だぜぇぇぇぇ!!

vimeo.com

最後っ屁みたいなモノが出てるのはご愛嬌。
再生開始タイミングによっては、最初のフレームをお洩らししてるな。

ParticleSystemのLoopingチェック外してもコレなのでGameObject破棄時間をちょっと調整する必要がありそう。

まぁとりあえず、らしい感じにはなったな。わりと他力本願だけど。

次回予告

的に当てて爆発粉砕したいところだが、今回の応用ですぐできそうな気もするので、次回はカメラを定点から三人称視点に移してみようと思う。

もう定点は飽きたのさ。

多脚戦車から弾を出してみる

 さまよえる蒼い弾丸

珍しく予告通り、弾を出します。

しかし、ただ弾を出すだけでは芸がない。
幸い、前回から使用している↓コレには射撃時モーションが同梱されているので、これを活用したい。

assetstore.unity.com

用意するモノ

まずは前回参照。今回はこれの変更である。

尚、使用するUnityは2018.2.11f1にした。
常に最新を使うスタイル。

himatsubushi-industry.hatenablog.com

尚、今回は以下のサイトを参考にした。

https://uni.gas.mixh.jp/unity/prefab.html

アパム!弾もってこい!アパーム!

弾モデルをPrefab化してみる

弾を出すには、当然だが弾のモデルを用意せねばならない。
しかし弾をイチから作るのも面倒だし、Sphereあたりでお茶を濁すのも、画の統一感を著しく欠くので頂けない。

幸いにして、このモデルには弾がちゃんと用意されている。
薬莢とセットであるが。

弾頭だけ用意するのは難易度が上がるし、薬莢付いたまま飛ばしたところでまともに見えないし、気にするのはミリオタぐらいだろうという舐めた判断の元、薬莢付きで飛ばすことにした。

まず、Hierarchyの"Player/ASC17/root/baseBone/base"を選択し"CTRL+D"でGameObjectを複製。
マテリアルの設定その他一切合切をそのまま流用する為である。
名前は"Bullet"とでもしておこう。

次に"Bullet"のMesh Filerを"sh1"に置き換え、"Rigidbody"と"Capsule Collider"を追加し、適当にパラメータを与えた後、"Bullet"をAssets上にDnDすればPrefab化完了である。
"Rigidbody"の"Use Gravity"だけは忘れず指定しておこう。

尚、Hierarchy上の"Bullet"は必要ないので削除する。

弾を動的生成してみる

 あとは、ボタンを押したタイミングで"Instantiate"メソッドを呼び出し、"Bullet"の実体(以下、"Bullets"とする)を動的生成すれば良いだけだが、問題は何処に生成してどう飛ばすかである。

というわけで、前述のサイトを参考にして発射点"Mazzle"を空のGameObjectとして生成し、砲身の先端に配置。

尚、発射元である多脚戦車の位置に追従せねばならんので、Hierarchyの"Player/ASC17/root/baseBone/headBonde/head/gun001"下に"Mazzle"を配置する。

しかし、弾の座標系的に弾頭がX軸方向に向いているため、そのままでは弾が横向きに飛んでしまう。

事前に"Mazzle"をグローバル座標系に置いて、Y軸を270度転回させておこう。

"Bullets"生成時に"Mazzle"の平行移動量と転回量を反映すれば、希望の位置に弾が現れることになる。

当然、これだけでは弾は地面に落ちるだけである。
"Bullets"に力を加え、飛ばす必要がある。

やり方はRoll-a-ballの時と同じく、"Bullets"のRigidbodyにAddForceメソッドで文字通り力を加えるのだが、中途半端な力では小便以下の飛距離となるのでガツンとデカい値を入れておこう。

himatsubushi-industry.hatenablog.com

また、力を加えるベクトルについて、"Mazzle"の座標軸的にX軸が弾の進行方向であるため、"forward"プロパティでベクトルを求めるとZ軸方向、すなわち砲身から見て左向きに弾が飛ぶので、"right"プロパティを使用しよう。

狙い撃つぜ!(ネライウツゼ!ネライウツゼ!)

Animation Controllerを複製してみる

さて、これで弾は飛ぶようになったので、Animationの割り付けに移ろう。

アセット同梱のAnimation Controllerは各Animationが排他、すなわちいずれかのAnimationが一つだけしか再生されない。
そのため、弾を撃っている間は歩行モーションが使えないため、静止しなければならない...ではあまりにもお粗末である。

幸いUnityにはAnimationを階層化して、上書きしたり追加したりする機能が備わっている。

docs.unity3d.com

尚、これを行うためには、Animation Controllerをいじる必要があるが、オリジナルの状態は残しておきたいので、"CTRL+D"で複製を作成することとした。
名前は"ACS_Anim_Custom"である(ジムスナイパーカスタム的な)。

弾を撃つアニメーションを別階層に作ってみる

さて、"ACS_Anim_Custom"を選択した状態でAnimatorでAnimation Controllerを編集しよう。

前述リンク先記述に従い"Addtive Layer"を追加し、Blendingを"Addtive"に設定する。
Weightは"1"を指定しよう。

1+1で200だ!10倍だぞ10倍。

弾を撃つアニメーション遷移を構築してみる

次にAnimator右側の方眼図がステートマシン図になるので、これにStateを生成して配置し、相互に遷移線で結ぶ。

今回は何もしない"ACS_NoAction"と、弾を撃つ"ACS_Attack_OneShot"をステートマシン図に置き、Entryから"ACS_NoAction"への遷移と、"ACS_NoAction"と"ACS_Attack_OneShot"間の相互遷移を設けた。

docs.unity3d.com

で、各stateの設定を行うことになるのだが、"ACS_NoAction"のMotionは何も指定せんでもええやろと"none"のままにすると、ループもしてくれない。

というわけで、空のAnimation”ACS_NoAction.anim”を生成し、InspectorでLoop Timeのチェックを入れ、これを"ACS_NoAction"のMotionに割り当てる。

一方、"ACS_Attack_OneShot"は"ACS_Attack.anim"を使用することになるが、こちらはループされては困るので、"ACS_Attack.anim"を"CTRL+D"で複製。
InspectorでLoop Timeのチェックを外し、これを"ACS_Attack_OneShot"のMotionに割り当てる。

名前は"ACS_Attack_OneShot.anim"としておこう。

stateを配置し、それぞれを遷移線で結び終えたならば、次は各stateを結ぶ遷移線の設定を行う。
各遷移線を選択し、Inspector上で遷移時のAnimation切り替えタイミングや、過渡的なAnimationのブレンドの調整を行ったりする。

"ACS_Attack_OneShot"への遷移はボタン押下で直ちに反応させたいので、Has Exit Timeのチェックを外す。

一方、"ACS_NoAction"への遷移は最後まで装填モーションを出し切りたいのでHas Exit Timeのチェックは入れ、Exit Timeは"1"を指定する。

Trandition DurationとTrandition Offsetは今回"0"で良い。
混ざり合うAnimationがないからね。

弾を撃つアニメーションをボタンで開始してみる

最後に、ボタン押下をトリガに、"ACS_Attack_OneShot"へ遷移を行う仕組みを設ける。
AnimatorのParameterタブを選択し、リスト右上の+アイコンからTriggerを選択し、新規にパラメータを生成。"ACS_Attack_OneShot"としておこう。

再び、"ACS_Attack_OneShot"への遷移線を選択し、InspectorのConditionsに"ACS_Attack_OneShot"を追加。これで下準備は完了である。

Hierarchy上の”Player/ACS17”を選択し、"Animator"のcontrollerを"ACS_Anim_Custom"に置き換え、Scriptから"ACS_Attack_OneShot"を"true"にすれば射撃モーションが開始する。

今回のソース

設定編集は色々やったがコード上の変更箇所は少ないので、差分箇所だけ紹介する。

        public GameObject Bullet;
        public Transform Muzzle;
        public float speed = 1000.0f;

発射する弾と発射位置、弾速は設定変更可能とするため、前述の情報参考元と同じくクラスメンバとしてPublic定義し、Inspectorから編集可能としている。

実行時にはそれぞれ割り当てを行う必要があるので注意しよう。

"Bullet”は、ProjetctよりPrefab化した"Bullet"を、"Muzzle"は、Hierarchy上の空GameObjectの"Muzzle"を指定する。

            if (controls[5].WasPressed)
            {
                GameObject Bullets = Instantiate(Bullet) as GameObject;
                Bullets.transform.position = Muzzle.position;
                Bullets.transform.rotation = Muzzle.rotation;
                Vector3 force = Muzzle.right * speed;
                Bullets.GetComponent<Rigidbody>().AddForce(force);
                animator.SetBool("ACS_Attack_OneShot", true);
            }

弾の生成と射撃モーションの開始は、ボタン押下時の処理にこれだけ記述すれば良い。

ステップ数にしてたった9ステップの改造結果がこちらである。

vimeo.com

ふむ、なかなかに良いではないか(自画自賛)。

尚、発射した弾が残り続け、リソースを食いつぶすのは見なかったことにしよう。

次回予告

弾が出るなら的に当てたいし、的に当てれば爆発もさせたい。発砲炎も出したい。
というわけで、パーティクルに手を出したいと思う。

多脚戦車と戯れてみる

 守られない次回予告

コントローラ操作の習得がメインやねんから別にユニティちゃんじゃなくてもええやろ!(キレ芸)

というわけで、どうせ当面のゴールはメカものゲーム制作なんだから、今からメカで色々するのも悪くはなかろうということで、今回からこれで遊ぶことにした。

assetstore.unity.com

フチコマだったりタチコマだったりウチコマだったり。

余談だけど、オリジナルであるフチコマの由来が、日本書紀に出てくる天斑駒に由来するとか、さすがは博識な士郎正宗氏である。

尚、ナメクジの交尾について知ったのも攻殻機動隊(コミック)が初めてである。

能書きはいいからさっさと動かそう

モノを用意

今回必要なものは(デフォルト以外では)以下の通り。
ちなみに今回から新規プロジェクトで作業を始める。
使用するUnityは2018.2.10f1である。

  • InControl(神アセット)
  • Plane(母なる大地)
  • Player(空のGameObject)
  • Rigidbody("Player"のComponent)
  • Box Collider("Player"のComponent)
  • Mech Control("Player"のScript Component)
  • ACS17(”Assets/ACS-17/Prefab/ACS17"の実体、"Player"の子に配置)

InControl必須であるので、入れ忘れの無いように。セットアップも忘れるなヨ!

assetstore.unity.com

各種パラメータは適当で。デフォルトで一向に構わんということだ。

ただ、Colliderを"ACS17"を納めるサイズにしたり、実行時に"ACS17"が"Plane"上に乗るぐらいの配慮は期待したいゾ。

ソースの中身

いい加減GitHab立ち上げて貼ったほうが良いような気もするが、とりあえず今回も直接貼ります。

MechControl.cs

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

namespace InControl
{
    public class MechControl : MonoBehaviour {

        private enum Direction{ Idle, Forward, Backward, Left, Right, TurnLeft, TurnRight};
        private Direction prevDirection = Direction.Idle;

        // Use this for initialization
        void Start() {

        }

        // Update is called once per frame
        void Update() {

        }

        private void FixedUpdate()
        {
            Rigidbody rigidBody = GetComponent<Rigidbody>();
            GameObject gameObject = transform.Find("ACS17").gameObject;
            Actions script = gameObject.GetComponent<Actions>();
            InputControl[] controls = new InputControl[16];
            float horizontal = 0.0f;
            float vertical = 0.0f;
            float turn = 0.0f;
            Direction currentDirection;
            Direction[] directions = { Direction.Idle, Direction.Idle, Direction.Idle };
            Vector3 velocity;

            controls[0] = InputManager.ActiveDevice.GetControl(InputControlType.LeftStickX);
            controls[1] = InputManager.ActiveDevice.GetControl(InputControlType.LeftStickY);
            controls[2] = InputManager.ActiveDevice.GetControl(InputControlType.RightStickX);
            controls[3] = InputManager.ActiveDevice.GetControl(InputControlType.RightStickY);
            controls[4] = InputManager.ActiveDevice.GetControl(InputControlType.LeftStickButton);
            controls[5] = InputManager.ActiveDevice.GetControl(InputControlType.Action1);

            /* Horizontal move */
            if (controls[0].Value <= -0.1f)
            {
                horizontal = controls[0].Value * 10.0f;
                directions[0] = Direction.Left;
            }
            else if (controls[0].Value >= 0.1f)
            {
                horizontal = controls[0].Value * 10.0f;
                directions[0] = Direction.Right;
            }
            /* Vertical move */
            if (controls[1].Value <= -0.1f)
            {
                vertical = controls[1].Value * 10.0f;
                directions[1] = Direction.Backward;
            }
            else if (controls[1].Value >= 0.1f)
            {
                vertical = controls[1].Value * 15.0f;
                directions[1] = Direction.Forward;
            }
            /* Turn */
            if (controls[2].Value <= -0.1f)
            {
                turn = controls[2].Value * 3.0f;
                directions[2] = Direction.TurnLeft;
            }
            else if (controls[2].Value >= 0.1f)
            {
                turn = controls[2].Value * 3.0f;
                directions[2] = Direction.TurnRight;
            }
            velocity = new Vector3(horizontal, 0.0f, vertical);
            transform.localPosition += transform.TransformDirection(velocity) * Time.fixedDeltaTime;
            transform.Rotate(0.0f, turn, 0.0f);

            if ((Mathf.Abs(vertical) >= 1.0f) &&
                Mathf.Abs(vertical) >= Mathf.Abs(horizontal))
            {
                currentDirection = directions[1];
            }
            else if (Mathf.Abs(horizontal) >= 1.0f)
            {
                currentDirection = directions[0];
            }
            else
            {
                currentDirection = directions[2];
            }

            if(prevDirection != currentDirection)
            {
                switch (currentDirection)
                {
                    case Direction.Idle:
                        script.Idle();
                        break;
                    case Direction.Forward:
                        script.WalkForwad2();
                        break;
                    case Direction.Backward:
                        script.WalkBack();
                        break;
                    case Direction.Left:
                        script.StrafeLeft();
                        break;
                    case Direction.Right:
                        script.StrafeRight();
                        break;
                    case Direction.TurnLeft:
                        script.TurnLeft();
                        break;
                    case Direction.TurnRight:
                        script.TurnRight();
                        break;
                }
                prevDirection = currentDirection;
            }

            if (controls[5].WasPressed)
            {
                script.Dead4();
            }

        }
    }
}

InControl影響下に置くClass(今回は"MechControl" class)を"InControl" namespaceに置かねばならんというところが注意点ではあるが、それ以外はユニティちゃんを動かしていた時と大差ない。

入力用のエントリが16個あるのは、将来拡張用なので気にしないで欲しい。

簡単に仕様を説明すると、左スティック入力に応じて進行ベクトルを、右スティック水平入力に応じて旋回量をそれぞれ算出し、"Rigidbody"に反映している。

入力にはそれぞれ、ゼロ点付近の入力誤差を考慮して0.1f程度の遊びを設けている。
操作感によって各自調整すればいいだろう。

尚、移動させるだけでは面白くないので、モーションも付けてみた。

Z軸方向ベクトル>=X軸方向ベクトルで前進/後退モーションに、Z軸方向ベクトル<X軸方向ベクトルで左右移動モーションに、右スティックのみ入力で左右旋回モーションに、未入力状態ではアイドルモーションに、それぞれ切り替えている。

死にたいときはアクションボタンを押せ(CV:青野武

アニメーション切り替えは、”ACS17”の"Action" Scriptの各種メソッドを使用しているが、"Action" Scriptの中身も”ACS17”下の"Animator"のフラグを叩いてるだけなので直接叩いてもいいと思う。

動かしてみた

f:id:himatsubushi-industry:20180930080508g:plain

移動量と歩幅が合ってないとか色々とアラはあるが、それなりに見えるので良しとしよう。

次回予告

戦車なので弾を出してみたいと思う。
また、このモデル、ローラーダッシュが出来るので、ローラー移動も組み込みたい。

あと、いい加減カメラを定点から外したい。

尚、来週が予告通りになるかは保証しない。

InControlと戯れてみる

 予定変更

さて前回、プレイヤー機能を拡充させると宣言したが、

himatsubushi-industry.hatenablog.com

上梓した直後に「標準で機能備えとるワ、このスットコドッコイ(大幅に脚色アリ)」とコメントで指摘があったため(その節はありがとうございます(滝汗)、多少の抵抗はしてみたもののどうあがいても標準機能には勝てないと理解したため、素直に諦めることにした。

まぁ、エディタ拡張について見識を深められたので良しとしよう(負け惜しみ)。

InControlの導入

いい加減本来の目的に軌道修正をせねばと考え、入力系の検討に戻る。

しかし、挿すコントローラによってスティックやボタンの割り当てが異なるのを解決せねば、このまま制作を続けても意味は無い。

himatsubushi-industry.hatenablog.com

てなわけで、

assetstore.unity.com

6月のセールでいくつかアセット買えば無料でもらえた...のだが、6月セールを見逃したため、定価で買いました。

本当はもっと後でセールの時に買いたかったのだが、我慢できんかったワ。

使い方

公式に「はじめてのInControl」は用意されているが、

InControl: Getting Started - Gallant Games

↓こっち読んだ方が早いだろう。

baba-s.hatenablog.com

というわけで、手持ちのDUALSHOCK4とJC-U4013Sを繋いで、TestInputManagerを実行してみたところ、少なくとも両者の共通箇所に関してはそれぞれ同じシンボル名が割り当てられた。

ちなみに、ふたつ接続して個別に認識されていたので、対戦プレーもできるゾ!(多分)

次回予告

入力の準備が整ったので、次回こそユニティちゃんを動かしてみたいと思う(尚、来週)。

Editor拡張で悶えてみる

 こんなんできましたけどー

f:id:himatsubushi-industry:20180916194856p:plain

何が出来たかというと、前回グダグダ言うてたプレイヤーである。

himatsubushi-industry.hatenablog.com

”Universal Sound FX"専用であったり、手抜き感満載のUIだったりと、まだまだ改善の余地はあるが、クッソ多いサウンドを手軽に選んで聴くという最低限度の目的は達せられたので、ちょっと紹介してみたい。

というわけで、今週もユニティちゃんは動きません。

調べなならん技術範囲はわりと多い

深くはないがね。

簡単に挙げると、こんな感じ。

  • エディタ拡張
  • ファイル操作
  • 音声再生

ややこしい処理は殆どフレームワーク任せなので、コーディング量はそれほどでもないが、約束事が多いので初心者にはハードルが高い内容だと思う。

プログラミングに明るくない人がいきなり見てもワケワカランと思うが、とりあえず一つずつ挙げていこう。

Editorフォルダに突っ込むとエディタ拡張スクリプトになるという仕様

"Project"において、"Assets"フォルダ下であれば、どんなにネストしてても最終的に"Editor"と名付けたフォルダに配置されたスクリプトはエディタ拡張として機能する。らしい。

らしいというのは、どこまでエディタ拡張として認識してくれるかがワカランからだが、普通に考えてアホみたいにネストした先に"Editor"フォルダ置くアホもおらんやろと思うので、考えないこととする。

逆に言えば、そんなフォルダ構成はプロジェクトの透明性を損ねるのでやめれ。

さて、単純にスクリプトを置けばいいかと言えば、そんなわけでもない。
エディタ拡張用に記述すべき作法がある。

では具体的にどう記述すれば良いのかというと、書くと長くなるのでこちらを参照されたい。

qiita.com

UIとかは、

anchan828.github.io

↑ここの、第6章 EditorGUI (EdirotGUILayout) - エディター拡張入門を読めばまぁ分かるだろう。

ファイルは何処にあるねん!

"Eeditor" 以外にもフォルダ構成のルールはいくつかある。

qiita.com

目的は"Assets"下の”Universal Sound FX"を漁る事なので、このための手段が必要だ。

"Assets"以下を漁る場合は、"AssetDatabase"という静的クラスが存在するので、これを使えば"Assets"以下のファイルは何でも読み放題である。

docs.unity3d.com

尚、"Resources"以下のフォルダに置かれたファイルアクセスのための"Resources"という静的クラスも存在する。
こちらは、一括読み出しとかいうなかなか豪快な機能がついている。

docs.unity3d.com

しかし、上記の通りランタイム用の機能であるので、今回はこれ以上触れない。
ゲーム制作時のお楽しみである。

したがって、"AssetDatabase"をいじくることになるのだが、残念ながら"AssetDatabase"に全フォルダ、全ファイルを取得する手段はない。

ではどうするのか?

"Application"静的クラスの”dataPath”メンバに"Assets"(Editor時)までの絶対パスが返されるので、これを使って.NETでフォルダを走査することとなる。
普通にWindowsアプリである。

docs.unity3d.com

ちなみに使用するのは"DirectoryInfo"クラスと"FileInfo"クラスである。

あるフォルダ以下にあるサブフォルダをすべて取得する - .NET Tips (VB.NET,C#...)

あるフォルダ以下にあるファイルをすべて取得する - .NET Tips (VB.NET,C#...)

これで、フォルダツリーを走査しつつ、ドロップダウンリストに表示すべきネタを登録していく。

" AudioUtility.cs"とかいう神スクリプト

色々説明を端折ったが、あとは音声を再生するだけである。
しかし、エディタ拡張に対して音声再生のための機能はオープンにされていない。

で、オープンにされてないとこじ開ける人もいる。

baba-s.hatenablog.com

内部APIを無理やり呼び出す感じである。

tsubakit1.hateblo.jp

しかし、ループ再生したり、停止させたり、再生停止を認識させたりと、色々やるにはこれだけでは情報が不十分。

てなわけで、さらに情報を漁っていたら、こんなん見つけた。

https://forum.unity.com/threads/reflected-audioutil-class-for-making-audio-based-editor-extensions.308133/

オリジナルの作者は、

assetstore.unity.com

で、

assetstore.unity.com

とか作ってる人であるが、この際それは置いておこう。
前述のワガママを全て満たしてくれるAPIラッパーを用意してくれているのだ。

ここはありがたく使わせていただこう。ドーモ。ロゴ ディジタル=サン。

ソースコード大公開

今回作成したソースコードは以下の通りである。

SoundSelector.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;

[Serializable]
public class SoundSelecter : ScriptableObject
{
    [SerializeField]

    [NonSerialized]
    private List<string> _categories = new List<string>();
    private List<string> _clips = new List<string>();
    private int _selectCategory = -1;
    private int _selectClip = -1;
    private string _soundRoot;

    public List<string> Categories { get { return _categories; } }
    public int SelectCategory
    {
        set
        {
            if ((value < 0) || (_categories.Count < value))
            {
                _selectCategory = -1;
                _clips.Clear();
            }
            else
            {
                _selectCategory = value;
                DirectoryInfo clipFolder = new DirectoryInfo(_soundRoot + "/" + _categories[_selectCategory]);
                FileInfo[] clips = clipFolder.GetFiles("*.wav");
                _clips.Clear();
                foreach (FileInfo clip in clips)
                {
                    _clips.Add(clip.Name);
                }
            }
        }
        get { return _selectCategory; }
    }
    public List<string> Clips { get { return _clips; } }
    public int SelectClip
    {
        set
        {
            _selectClip = ((value < 0) || (_clips.Count < value)) ? -1 : value;
        }
        get { return _selectClip; }
    }
    public string Path
    {
        get
        {
            return ((_selectCategory == -1) || (_selectClip == -1)) ? null :
                _soundRoot + "/" + _categories[_selectCategory] + "/" + _clips[_selectClip];
        }
    }
    public string AssetPath
    {
        get
        {
            return ((_selectCategory == -1) || (_selectClip == -1)) ? null :
                "Assets/Universal Sound FX/" + _categories[_selectCategory] + "/" + _clips[_selectClip];
        }
    }

    private void OnEnable()
    {
        _soundRoot = Application.dataPath + "/Universal Sound FX";
        DirectoryInfo root = new DirectoryInfo(_soundRoot);
        DirectoryInfo[] categories = root.GetDirectories("*");

        foreach (DirectoryInfo category in categories)
        {
            if (category.Name != "#Game Genre References")
            {
                getCategory(category, "");
            }
        }
    }

    private void getCategory(DirectoryInfo category, string parentName)
    {
        DirectoryInfo[] subCategories = category.GetDirectories("*");
        if (subCategories.Length <= 0)
        {
            _categories.Add(parentName + category.Name);
        }
        else
        {
            foreach (DirectoryInfo subCategory in subCategories)
            {
                getCategory(subCategory, parentName + category.Name + "/");
            }
        }
    }
}

UniversalSoundFXPlay.cs

using System;
using System.Reflection;
using UnityEngine;
using UnityEditor;

public class EditorWindowSample : EditorWindow
{
    private SoundSelecter selector = null;
    private string prevPath = "";
    private AudioClip clip;
    private bool loop;
    private bool playRequest;
    private bool playing;

    [MenuItem("Extend/Universal Sound FX Play")]
    private static void Create()
    {
        GetWindow<EditorWindowSample>("Universal Sound FX Play");
    }

    private void OnGUI()
    {
        if (selector == null) {
            selector = ScriptableObject.CreateInstance<SoundSelecter>();
        }
        selector.SelectCategory = EditorGUILayout.Popup("Category", selector.SelectCategory, selector.Categories.ToArray());
        selector.SelectClip = EditorGUILayout.Popup("Clip", selector.SelectClip, selector.Clips.ToArray());
        if (selector.AssetPath != null) {
            if (selector.AssetPath != prevPath) {
                clip = AssetDatabase.LoadAssetAtPath<AudioClip>(selector.AssetPath);
                prevPath = selector.AssetPath;
            }
        }
        EditorGUILayout.BeginHorizontal();
        loop = GUILayout.Toggle(loop, "Loop");
        playRequest = GUILayout.Toggle(playRequest, playRequest ? "Stop" : "Play", "button");
        if (clip == null)
        {
            playRequest = false;
        }
        EditorGUILayout.EndHorizontal();

        if (playRequest == true)
        {
            if (playing == false)
            {
                playing = true;
                AudioUtility.PlayClip(clip, 0, loop);
            }
            else if (AudioUtility.IsClipPlaying(clip) == false)
            {
                playing = false;
                playRequest = false;
            }
        }
        else
        {
            if(playing == true) {
                AudioUtility.StopClip(clip);
            }
            playing = false;
        }
    }
}

精査はしてないので、ゴミがあったらご容赦のほど。
あと、動作保証もサポートもしないので悪しからず。

次回予告

思いのほか使い勝手がいいので、もうちょっと機能拡張してみたいと思う。