Unityやってるワン

Unityでのゲーム開発を備忘録として記事にしていきます

Unityでモバイル録音機能を実装

モバイル実機で動作する録音機能をUnityで実装してみました。
f:id:nsdeveloperman:20200620021020p:plain

UnityのMicrophon機能を使用することで、デバイスが使用可能なマイクを確認して音声の録音を行うことができます。
UnityEngine.Microphone - Unity スクリプトリファレンス

今回はこのMicrophonの機能を利用したスクリプトを作成してモバイル実機上で動かしてみました。
スクリプト内容は以下になります。

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(AudioSource))]
public class MobileSoundRecorder : MonoBehaviour
{
	[SerializeField] int m_frequency = 44100;
	[SerializeField, Range(1, 200)] int m_recordingTime = 10;
	[SerializeField] Button m_btnRecording = default;
	[SerializeField] Button m_btnPlay = default;

	private AudioClip m_clip = null;
	private AudioSource m_source = null;
	private string m_deviceName = string.Empty;

	private void Awake() => m_source =
		gameObject.GetComponent<AudioSource>();

	private void Start()
	{
		if (Microphone.devices.Length <= 0)
		{
			m_btnRecording.interactable = false;
			m_btnPlay.interactable = false;
			Debug.LogError("使用可能なマイクがありません");
		}
		else
		{
			m_deviceName = Microphone.devices[0];
		}
	}

	private void Update()
	{
		if (string.IsNullOrEmpty(m_deviceName)) { return; }
		m_btnRecording.interactable = !m_source.isPlaying;
		m_btnPlay.interactable = !Microphone.IsRecording(m_deviceName);
	}

	public void RecordingStart() => m_clip = Microphone.Start(
		m_deviceName, false, m_recordingTime, m_frequency);

	public void RecordingPlay()
	{
		if (m_clip == null) { return; }
		m_source.clip = m_clip;
		m_source.Play();
	}
}

使用できるマイクがデバイス上にない場合は、エラーログを表示して処理を行わないようにしています。
今回はマイクが複数ある場合、最初に検知したマイクで録音を行うようにします。

上記スクリプトを作成したら、シーン内の適当なオブジェクトにアタッチします。
録音と再生用のボタンを配置して作成したスクリプトに設定すれば、実装は完了です。
f:id:nsdeveloperman:20200620025519p:plain

スクリプトとシーンを作成して準備ができたら、最後にプロジェクトのPlayerSettingsでConfiguration項目のMicrophone Usage Descriptionにカメラを使用する際の同意テキストを入力します。
ここが未入力だと実機上でマイク機能を使用する際にアプリが落ちることがあるので忘れずに記入しましょう。
同意を求めるテキストは初回のみ表示されるようです。
f:id:nsdeveloperman:20200620025140p:plain
(とりあえず動かしたいときはテキスト内容は何でも良さそうです)

あとはビルドした内容を実機に転送して動かせば、モバイルの録音と再生機能が確認できます。
Unity側の実装のみでモバイル機能を利用できるのはありがたいですね。

UnityでARゲーム開発

UnityでARが簡単に導入できるようなので実際にモバイル実機上で動かしてみました。
f:id:nsdeveloperman:20200614085643p:plain


ARのモバイルアプリを開発する際はiOSではARKit、AndroidではARCoreというARプラグインをそれぞれ導入して開発する必要があるみたいです。
ですが、最近ではUnityのパッケージにそれぞれ対応してくれるARFoundationというものがあります。
Unity の AR Foundation フレームワーク | クロスプラットフォーム対応の拡張現実(AR)開発ソフトウェア | Unity


ARFoundationで実装すれば、OSごとに処理を分ける必要もないので便利ですね。
ただし、ARKitとARCoreの全ての機能が使えるわけではないようなので注意が必要です。
平面の検知やマーカー検知くらいであれば、ARFoundationで両OSに対応可能でした。


今回はモバイル実機で平面検知を行ってみようと思います。
それではUnityでの導入手順を順に説明します。


1:Unityの3Dプロジェクトを作成
2:エディタ上部メニューのWindowからPackageManagerを開く
3:PackageManagerからARCoreXRPluginとARKitXRPluginをインストール
4:PackageManagerからARFoundationをインストール(ARSubSystemなどAR開発に必要なパッケージも自動でインストールされます)
f:id:nsdeveloperman:20200614082920p:plain
5:シーンを作成してCameraオブジェクトを削除
6:エディタ上部メニューのGameObjectにXRの項目が追加されているので、ARSessionとARSessionOriginをそれぞれシーンに配置
f:id:nsdeveloperman:20200614083010p:plain
7:ARSessionOriginを選択してInspectorのAddCompornentからARPlaneManagerとARRayCastManagerをアタッチ
8:エディタ上部メニューのGameObjectのXRからARDefaultPlaneを選択してシーンに作成
9:作成したARDefaultPlaneをProjectウィンドウにドラッグ&ドロップしてPrefab化する
10:Prefab化したARDefaultPlaneをARPlaneManagerのPlanePrefabにセットする(検知した平面を可視化します)
f:id:nsdeveloperman:20200614083027p:plain


これでAR平面検知を行うためのシーンのセットアップは完了です。
あとはビルドすればモバイル実機で確認できるのですが、ビルド前にiOS/Androidでそれぞれ設定が必要です。


AndroidでのPlayerSettings設定
GraphicsAPIのAutoGraphicsAPIにチェックを入れる
MinimumAPILevelをAndroid7.0Nougatにする


iOSでのPlayerSettings設定
CameraUsageDescriptionに端末のカメラ使用を許可を求める文言を入力
(とりあえず動かしたい場合はなんでもいいみたいです)
ArchitectureをARM64に設定
Target minimum iOS Versionを11.0にする


これでUnityエディタ上での必要な設定は完了しました。
あとはビルドすればモバイル実機上で平面検知が確認ができます。
平面検知の精度は端末のスペックによって差が出る(?)ようですが、少ない手順で簡単にARが実装できるのはありがたいですね。

Unityプロジェクトをローカルでバージョン管理

Unityで個人的に何か作る際は、とりあえずローカルでバージョン管理してます。
試行錯誤した結果うまくいかず、ソースコードを戻したい... なんて場面で便利だし、WinMergeなどのマージツールがあればコミットする際に差分も比べやすくて便利!


Windowsで作業する際は、TortoiseSVNでローカルにリポジトリを作成してバージョン管理していました。
自分のPC上の好きな場所にローカルリポジトリを作成して、チェックアウト、Unityプロジェクトの作成、無視設定 etc...
最初に少しだけ面倒な手順がありますが、無料で使えるしTortoiseSVN簡単!


ところがMacだとsvnクライアントとしてTortoiseSVNが使えない...
最近はMacBookで作業するので、簡単に使えるものがないか探してましたが、GitHubDesktopで管理するのがいい感じ!
GitHubDesktopで管理する際も最初に少しだけ必要な設定があるのですが、プロジェクトを作成する度に毎回調べている気がするので、自分用に手順をまとめました。


【GitHubDesktopでローカルバージョン管理するとき】

1:GitHubDesktopでローカルリポジトリを作成
2:リポジトリ作成時にGitignore設定でUnityを指定
3:リポジトリを作成したら、ブランチを作成
4:リポジトリに指定したGitのフォルダにUnityプロジェクトを追加して一旦閉じる
5:ignoreファイルを適当なテキストエディタで開いて、設定を一部変更する
Library/
Temp/
Obj/
Build/
Builds/
Logs/
MemoryCaptures/
6:ignoreに指定したファイルが追加されていないことを確認してプッシュ

これでバージョン管理がMacでも簡単にできそう。
日本語翻訳版が今のところ無いみたいですが、慣れればあまり困ることはなさそう。
GitHubDesktopは割と前からあるので、今更感がありますが...

UnityでMagicaVoxelのモデルを動かす

10日間のGWがあっという間に終了してしまいました。
そして長らく放置していたブログをふと思い出して、久々に確認してみると最後に記事を書いたのが1年半以上前...
(最後の記事で「1週間に1記事ペースで書いていきたい」とか言ってるのに、それ以降全く更新してないのが恥ずかしい...)
週1ペースは自分には無理そうなので、気が向いたタイミングでUnityでのゲーム開発記事を書いていくことにします w

GWは10日間の休日にも関わらず、特に予定が無なかったため「とりあえず何かゲーム作ろう」と思いました。
ゲームを作る時は「MagicaVoxel」でモデルを作成して「Mixamo」でアニメーションをつけてfbxで出力してUnityにインポートしています。
モデリングのスキルが無い自分にとって、こういったツールの存在は本当にありがたいです。
たまに手順を忘れて時間がかかってしまう時があるので、備忘録としてUnityにインポートするまでの手順をまとめておこうと思います。

1:MagicaVoxelでモデルを作成
f:id:nsdeveloperman:20190506222432p:plain
2:作成したモデルをobj形式でエクスポート、出力した内容をzipでまとめて圧縮する
3:ブラウザでMixamoにログインし、zip形式でまとめたファイルをアップロードする
f:id:nsdeveloperman:20190506222545p:plain
4:アニメーションを選択して、fbx形式でエクスポート
5:出力したfbxをUnityのプロジェクトにインポートする

あとはUnity上でAnimatorなどでアニメーションを制御すれば完了です。

個人でゲームを作る時、アセット関連で困ることが多いですが、便利なツールがたくさんあるので活用していきたいですね。
謎解き要素があるゲームが好きなので、そんな感じのゲーム作ろうと思います。
f:id:nsdeveloperman:20190506222707g:plain
(そのうちリリースできたらいいなぁ...)

音声を管理・制御するクラスを作ってみた

Unityで勉強したことを備忘録として記事にしていきます
とか言いつつ、しばらくサボってしまいました…
これからは週1回くらいのペースで、Unityで勉強したことを記事にしていきたいです。


今回はUnityでの音声関連の制御について勉強して、スクリプトを作ったので記事にしてみました。まずは音声の再生や停止などを制御するSoundPlayer.csです。



SoundPlayer.cs

using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class SoundPlayer : MonoBehaviour
{
    /// <summary>サウンドID</summary>
    private int m_soundID = 0;

    /// <summary>オーディオソース</summary>
    private AudioSource m_source;

    /// <summary>再生時間</summary>
    private float m_playTime = 0.0f;

    /// <summary>一時停止フラグ</summary>
    private bool m_isPauseNow = false;



    /// <summary>
    /// オブジェクト生成時
    /// </summary>
    private void Awake ()
    {
        m_source  = gameObject.GetComponent<AudioSource>();
        m_soundID = SoundManager.Instance.AddPlayer(this);
    }

    /// <summary>
    /// オブジェクト破棄時
    /// </summary>
    private void OnDestroy ()
    {
        SoundManager.Instance.RemovePlayer(m_soundID);
    }

    /// <summary>
    /// 再生
    /// </summary>
    public void Play ()
    {
        if (m_source.clip != null)
        {
            if (m_isPauseNow)
            {
                m_source.time = m_playTime;
                m_source.UnPause();
            }
            else
            {
                m_source.Stop();
                m_source.Play();
            }
            m_isPauseNow = false;
        }
    }

    /// <summary>
    /// 停止
    /// </summary>
    public void Stop ()
    {
        if (!m_source.isPlaying)
        {
            m_source.Stop();
            m_isPauseNow = false;
        }
    }

    /// <summary>
    /// 一時停止
    /// </summary>
    public void Pause ()
    {
        m_source.Pause();
        m_playTime = m_source.time;
        m_isPauseNow = true;
    }

    /// <summary>
    /// 音量セット
    /// </summary>
    /// <param name="_volume">音量</param>
    public void SetVolume (float _volume)
    {
        m_source.volume = _volume;
    }

    /// <summary>
    /// 再生速度セット
    /// </summary>
    /// <param name="_pitch">再生速度</param>
    public void SetPitch (float _pitch)
    {
        m_source.pitch  = _pitch;
    }
}

作ったとはいうものの、ほとんどAudioSourceが再生、停止などをやってくれるので自力でやってる感はあまりないです…
AudioSourceの処理以外に、Awakeで実態生成時にSoundPlayerのIDを取得して、SoundPlayerが破棄された時にOnDestroyでIDも破棄するようにしています。
このIDは以下のSoundManager.csでSoundPlayerを管理する際に使用しています。
他にはRequireComponentで必ずAudioSourceがアタッチされるようにしています。



SoundManager.cs

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

public class SoundManager : SingletonClass<SoundManager>
{
    /// <summary>サウンドリスト</summary>
    public Dictionary<int, SoundPlayer> m_soundList = new Dictionary<int, SoundPlayer>();



    /// <summary>
    /// 実体生成時
    /// </summary>
    private void Awake ()
    {
        DontDestroyOnLoad(this);
    }

    /// <summary>
    /// プレイヤー追加
    /// </summary>
    /// <param name="_soundPlayer">サウンドプレイヤー</param>
    /// <returns>未使用ID</returns>
    public int AddPlayer (SoundPlayer _soundPlayer)
    {
        //使用されていないIDをサーチ
        int id = 0;
        while (true)
        {
            if (!m_soundList.ContainsKey(id))
            {
                m_soundList.Add(id, _soundPlayer);
                return id;
            }
            id++;
        }
    }

    /// <summary>
    /// プレイヤー破棄
    /// </summary>
    /// <param name="_id">サウンドID</param>
    public void RemovePlayer (int _id)
    {
        if (m_soundList.ContainsKey(_id))
        {
            m_soundList.Remove(_id);
        }
    }

    /// <summary>
    /// リスト破棄
    /// </summary>
    public void Clear ()
    {
        m_soundList.Clear();
    }

    /// <summary>
    /// 再生
    /// </summary>
    public void Play ()
    {
        foreach (SoundPlayer soundPlayer in m_soundList.Values)
        {
            soundPlayer.Play();
        }
    }

    /// <summary>
    /// 停止
    /// </summary>
    public void Stop ()
    {
        foreach (SoundPlayer soundPlayer in m_soundList.Values)
        {
            soundPlayer.Stop();
        }
    }

    /// <summary>
    /// 一時停止
    /// </summary>
    public void Pause ()
    {
        foreach (SoundPlayer soundPlayer in m_soundList.Values)
        {
            soundPlayer.Pause();
        }
    }

    /// <summary>
    /// 音量設定
    /// </summary>
    /// <param name="_volume">音量</param>
    public void SetVolume (float _volume)
    {
        foreach (SoundPlayer soundPlayer in m_soundList.Values)
        {
            soundPlayer.SetVolume(_volume);
        }
    }
}

SoundManagerではシーン内のSoundPlayerを一括管理します。
以前、記事に書いたSingletonClassを継承しているため、シーンが切り替わっても破棄されずに残ります。シーンの切り替え時にフェードイン・フェードアウトで全サウンドの音量を徐々に変えたい場合やポーズ画面で全サウンドを一時停止させたい場合に、SoundManagerで制御ができます。


学生の頃、DirectXC言語でサウンドの制御関連の処理を作った時にかなり苦戦した記憶がありますが、Unityだとあっという間に作れました。ゲームエンジンの恩恵ですね..


次回は入力関連について勉強して記事にまとめてみたいと思います。

継承することでシングルトンパターンで実装できるクラスを作ってみた

前回シングルトンパターンで管理系のクラスを作成する記事を書きました。
シングルトンでマネージャーっぽいクラスを作ってみた - Unityやってるワン


でも、シングルトンのパターンで実装するたびにstaticでインスタンスを保持する処理を用意するのはちょっと面倒です。
なので継承することでシングルトンクラスとして実装することができるクラスを作ってみました。
このクラスを継承すれば、シングルトンのパターンを毎回用意する必要がなくなりそうです。


SingletonClass.cs

using UnityEngine;

public abstract class Singleton<TSingleton> : MonoBehaviour
{
    private static TSingleton m_instance;

    public static TSingleton Instance
    {
        get
        {
            if (m_instance == null)
            {
                // シングルトンオブジェクト生成
                var components = new[]{ typeof( TSingleton ) };
                GameObject obj = new GameObject(typeof(TSingleton).Name, components);
                m_instance = obj.GetComponent<TSingleton>();
                // シーン遷移時に破棄させないようにする
                DontDestroyOnLoad(obj);
            }
            return m_instance;
        }
    }

    protected Singleton () { }
}

シングルトンパターンのクラスSingletonClass.csを作ってみました。
ジェネリックでシングルトンパターンのクラスを宣言しています。
実際に使用する場合は以下のような形になります。


PointManager.cs

using UnityEngine;

public class PointManager : Singleton<PointManager>
{
    private int m_point = 0;

    private PointManager () { }

    public void AddCount (int _point = 1)
    {
        m_point += _point;
    }

    public int Count { get { return m_point; } }
}

Singletonクラスを継承して得点を管理するPointManager.csを作りました。
これで、毎回シングルトンで実装する手間が省けそうですね。


PointManagerは以下のような形で利用できます。

PointManager.Instance.AddCount();
PointManager.Instance.AddCount(10);
Debug.Log("現在の得点:" + PointManager.Instance.Count);

f:id:nsdeveloperman:20170708164929p:plain

シングルトンでマネージャーっぽいクラスを作ってみた

Unityで入力や音声などを管理する複数存在することがないクラスは、シングルトンのパターンで実装することが多いようです。
シングルトンとはインスタンスが1つしか生成されないようにする実装パターンです。

ManagerClass.cs

using UnityEngine;

public class ManagerClass : MonoBehaviour
{
    private static ManagerClass m_instance;

    public static ManagerClass Instance
    {
        get
        {
            if (m_instance == null)
            {
                // オブジェクト生成
                GameObject managerObject = new GameObject("ManagerClass");
                m_instance = managerObject.AddComponent<ManagerClass>();
                // シーン遷移時に破棄させないようにする
                DontDestroyOnLoad(managerObject);
            }
            return m_instance;
        }
    }
  
    protected ManagerClass () { }
}

実体にアクセスするメソッドが呼ばれた際に、実体が存在しない場合はインスタンスを生成します。
コンストラクタをprivateで宣言して、外部でインスタンスを生成させないようにしています。

GameObjectとして生成してからDontDestroyOnLoadでシーン遷移時に破棄させないようにしています。
これで入力や音声データなどを制御するManagerっぽいクラスが実装できそうですね。