【Unity】サウンドテスト画面を作る - その1

f:id:amayoshitqs:20200922000729p:plain Unityでサウンドテストを作っていきます。
ゲームパッドやキーボード操作を前提とした、昔ながらのスタイルで実装します。
今回は実装のしやすさを優先し、オーディオファイルのロードで「Resources」を使用します。
アレルギー体質の方はご注意ください。

また、初心者でも分かるように書いてるつもりですがUnity(特にuGUI)の知識がある程度ある人を対象にしています。
ちょっとしたタイトル画面とかを作ったことがあるぐらいで大丈夫です。

サウンドテストとは?

本来はアーケードゲームなどで音楽や効果音が鳴るか、ボリュームが適切かどうか、といったまさに「テスト」の役割を持っていたこの機能。
家庭用ゲームでもオプションやクリア特典などで用いられているため、それなりに親しまれているゲームシステムの1つと言えるでしょう。

f:id:amayoshitqs:20200921220119j:plain

サウンドテストの内容はゲームによって様々であり、ファミコン「ラフワールド」ではこのようにコンティニュー回数の変更と一緒になった隠しオプションとして登場します。
サウンドを番号で選んで再生する、という特に簡素な例になりますね。
ファミコンの容量というより、そもそも昔はゲームのBGMに名前を付けたりする文化がそこまで浸透してなかったのも事情としてあるでしょうね。

f:id:amayoshitqs:20200921220116j:plain

最近だとスマブラSPなんかは特に優れていますね。
BGMだけでなくボイスなども含まれています。

各種スキップ、ランダム再生、果てはプレイリストを作れたりと、タイトルによって機能の充実度も異なります。
個人的に好きなのはナムコミュージアムDS」です。

昨今のゲーム事情としてはサウンドトラック販売を見越して、ゲーム内には実装されないケースも増えてきています。
仕方ないのはその通りなのですが、タイトルごとの特色があって楽しみにしていたので残念ではあります。

ブレイブルーなど、「シリーズ初期にはあったのに!」というケースも珍しくありません。

簡単な仕様書

  1. メニューには「BGM」「EXIT」の2つだけ
  2. デフォルトで「BGM」を選択している
  3. 「BGM」選択中に上か下を押すと「EXIT」を選択(逆も然り)
  4. 「BGM」選択中に左右を押すと曲を切り替える
  5. 「BGM」選択中に決定キーを押すと曲を再生
  6. 「EXIT」選択中に決定キーを押すと別シーンに移動

今回は一旦これだけです。
ラフワールドからコンティニュー増減を無くしたものです。

UI配置

f:id:amayoshitqs:20200922000729p:plain f:id:amayoshitqs:20200922001135p:plain

  • Title -> 「SOUND TEST」という大文字
  • BGMText -> 「BGM」という文字
  • BGMNumberText -> 「01」という番号
  • ExitText -> 「EXIT」という文字

という感じです、もちろん全部Textコンポーネントで実装します。
Textの値はColorのみスクリプトから弄るので変更しない方が賢明です。
2つのスライダーは後で説明します。

Audio配置

f:id:amayoshitqs:20200922002256p:plain

こんな感じでResources以下に置いてください。
ファイル名はなんでも良いですが、今回はEnumで同じものを定義するのでタイプミスしにくいものだと良いと思います。
素材はフリー配布サイトなどで用意して下さい、wavで。

Sliderについて

f:id:amayoshitqs:20200922005121p:plain

シーンビュー画像の上部に浮かんでいるSlider2つについて説明します。

今回はゲームパッドやキーボードで操作できるサウンドテストを作ります。
キー操作をデフォルトの「Navigation」を使って実装するために「Sliderコンポーネントを使用します。

キーボードなどによる選択・決定を簡単に実装するには「Selectableクラス」を継承しているコンポーネントが必要で、
それがUGUIでは「Button」「Slider」なのですが、今回はオブジェクト数を削減するためにSliderを採用しています。
これを使うと簡単に選択と決定ができるだけでなく、一定時間押しっぱなしにした時の挙動なんかも用意されているのでオススメです。

参考:
[Unity][uGUI] UIをゲームパッドやキーボードで操作する : うえすと開発メモ

f:id:amayoshitqs:20200922004405p:plain

各Sliderコンポーネントの設定です。

  • TransitionをNoneにする(映らないので見た目は関係なし)
  • NavigationをExplicitにして、各方向キーを押したときに選択するものを設定する
  • WholeNumbersにチェックを入れる

以上です。
今回は2つしか項目が無いので上を押しても下を押しても交互に選択されるようになってます。
OnValueChangedの設定は後で説明します。

SoundList

Resourcesファイルに入れたオーディオファイルの名前と同じenumを作ります。

public enum SoundList
{
    BGM00,
    BGM01,
    BGM02
}

ここの名前をパスとして読み込むので完全に同じ名前にしてください。
本来ならenum自動生成したりしたいところですが今回は我慢してください。

SoundManagerオブジェクト

f:id:amayoshitqs:20200922010446p:plain

SoundManagerスクリプトと、「AudioSourceコンポーネントを付けてください。
AudioSourceのPlayOnAwakeをOFFにします。
Loopはお好みで。

SoundManagerスクリプト

ここから実装に入ります。
まずはメンバ変数。

public class SoundManager : MonoBehaviour
{
    [SerializeField] EventSystem eventSystem;
    [SerializeField] Slider bgm;
    [SerializeField] Text bgmText;
    [SerializeField] Text bgmNumberText;
    [SerializeField] Slider exit;
    [SerializeField] Text exitText;
    [SerializeField] AudioSource audioSource;
    int bgmLength;
    GameObject go;
}

eventSystemはボタンとか作ったら自動生成されるやつ。
bgm ~ exitTextは先ほど書いたもの。
audioSourceもついさっき付けたもの、簡単ですね。

お次はStartメソッド。

void Start()
{
    bgmLength = Enum.GetNames(typeof(SoundList)).Length;
    bgm.maxValue = bgmLength;
    bgm.minValue = -1;

    OnChangeBGM(0);

    eventSystem.SetSelectedGameObject(bgm.gameObject);
}

メンバ変数のbgmLengthに曲の総数を代入します。
bgmスライダーの最大値・最小値をそれぞれ定義します。
eventSystemの選択中オブジェクトはbgmゲームオブジェクトとします。

OnChangeBGM()はこちらです。

public void OnChangeBGM(float value)
{
    if(value == bgmLength)
    {
        bgm.value = 0;
    }
    if(value == -1)
    {
        bgm.value = bgmLength-1;
    }

    bgmNumberText.text = bgm.value.ToString("00");
}

スライダーの値が最大・最小値になったらループするようにしています。
また、bgm番号の値を更新しています。

そしてこちらの関数をBGMSliderオブジェクトのSliderコンポーネントOnvalueChangedに登録しておくことをお忘れなく。
そのためのpublicです。

続いてUpdateメソッド。

void Update()
{
    var current = eventSystem.currentSelectedGameObject;

    if(current != go)
    {
        OnChangeMenu(current);
        go = eventSystem.currentSelectedGameObject;
    }

    if(Input.GetKeyDown(KeyCode.Z))
    {
        if(current == bgm.gameObject)
        {
            SoundList music = (SoundList)bgm.value;
            audioSource.clip = Resources.Load<AudioClip>("Audio/" + music.ToString());
            audioSource.Play();
        }
        else if(current == exit.gameObject)
        {
            audioSource.Stop();
            SceneManager.LoadScene("Title");
        }
    }
}

前半部分でどのオブジェクトを選択中かを取得し、前のフレームから変化していればOnChangeMenu(後述)を呼ぶようにしています。
今回の場合はBGMからEXITに切り替えた時などに実行されます。

後半部分では決定キー(今回はZキー)を押した時の挙動を書いてます。
BGM選択中なら曲の再生。
EXIT選択中ならタイトルシーンに戻る。
といった感じですね。

曲の再生機能ではBGMスライダーの値をSoundListに変換し、そのenum名を元にResourcesフォルダからオーディオファイルをロードしてくる。
そしてロードしたものをaudioSourceのclipに設定して再生、という感じです。

メニュー切り替え時に呼ばれるOnChangeMenu()はこの通り。

void OnChangeMenu(GameObject current)
{
    bgmText.color = current == bgm.gameObject? new Color(0, 0, 0, 1) : new Color(0, 0, 0, 0.5f);
    bgmNumberText.color = current == bgm.gameObject? new Color(0, 0, 0, 1) : new Color(0, 0, 0, 0.5f);
    exitText.color = current == exit.gameObject? new Color(0, 0, 0, 1) : new Color(0, 0, 0, 0.5f);
}

f:id:amayoshitqs:20200922012941p:plain f:id:amayoshitqs:20200922012944p:plain

このように選択している方を色濃く表示し、選択していない方は薄くする、という機能です。
項目が増えてきたら工夫が必要そうですね...

完成

f:id:amayoshitqs:20200922000729p:plain

かなりシンプルですがこのようなサウンドテスト画面が作れます。
再生中の曲名とか、ループ再生ON/OFFとか、考えれば色々機能が浮かんできますね。

機能追加も良いですが、今回の実装においての懸念点もあります。
挙げればキリが無いですが、やはりResourcesのロードでしょうか。
せめてロードしたものはこのシーン中ではキャッシュしておくぐらいはやっておきたいですね。

f:id:amayoshitqs:20200922013651p:plain

ちなみに参考までに、過去に私が作ったサウンドテストがこちらです。
曲名表示やループ切り替え、SE再生にボリューム調整(AudioMixer調節)などが追加されてますね。
あとオーディオビジュアライザを背景を表示しています、ちゃんと曲に合わせてピョコピョコします。
こちらのようにBGMとSEを分けようとすると今回ButtonではなくSliderにした理由がなんとなく分かると思います。

これを再現する気はありませんが、次回はもう少し機能拡張してみましょう。

余談

サウンドテストの実装には直接関係なかったので省きましたが、
Navigationを使ってキーボード操作を実現する際に厄介になるのがマウス操作です。

今回例えば最初にBGMを選択状態にする機能を入れましたが、そんなことをしても後からマウスで画面のどこかしらをクリックするとすぐに無選択状態になってしまいます。
しかも一度そうなってしまうとキーボードだけで選択し直すのは困難ですし、その為にスクリプトで回避させてあげるのも面倒です。

なので完全にマウスを使わない、キーボード操作に終始するようにしたければ下記を参考にして自作InputModuleを作ってみてください。
分からなければコピペでも多分大丈夫です。

uGUIのButtonでキー操作(パッド操作)オンリーのUI構築メモ · GitHub

更に余談ですが、タイトルやメニューはマウス操作なのにインゲームはキーボードやゲームパッド、みたいなPCインディーゲーム多いですよね?
Unityなら確かにそうした方が遥かに実装が楽ですが、やっぱりコンシューマー育ちにとってはパッドで終始したくないですか?
私はそんな人間なので日頃から脱・マウスを掲げて試行錯誤しています。
今後もPCスタンドアローンに特化した(例外あり)記事を書いていきたい所存です。

今回のソースコード

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public enum SoundList
{
    BGM00,
    BGM01,
    BGM02
}

public class SoundManager : MonoBehaviour
{
    [SerializeField] EventSystem eventSystem;
    [SerializeField] Slider bgm;
    [SerializeField] Text bgmText;
    [SerializeField] Text bgmNumberText;
    [SerializeField] Slider exit;
    [SerializeField] Text exitText;
    [SerializeField] AudioSource audioSource;
    int bgmLength;
    GameObject go;

    void Start()
    {
        bgmLength = Enum.GetNames(typeof(SoundList)).Length;
        bgm.maxValue = bgmLength;
        bgm.minValue = -1;

        OnChangeBGM(0);

        eventSystem.SetSelectedGameObject(bgm.gameObject);
    }

    void Update()
    {
        var current = eventSystem.currentSelectedGameObject;

        if(current != go)
        {
            OnChangeMenu(current);
            go = eventSystem.currentSelectedGameObject;
        }

        if(Input.GetKeyDown(KeyCode.Z))
        {
            if(current == bgm.gameObject)
            {
                SoundList music = (SoundList)bgm.value;
                audioSource.clip = Resources.Load<AudioClip>("Audio/" + music.ToString());
                audioSource.Play();
            }
            else if(current == exit.gameObject)
            {
                audioSource.Stop();
                SceneManager.LoadScene("Title");
            }
        }
    }

    void OnChangeMenu(GameObject current)
    {
        bgmText.color = current == bgm.gameObject? new Color(0, 0, 0, 1) : new Color(0, 0, 0, 0.5f);
        bgmNumberText.color = current == bgm.gameObject? new Color(0, 0, 0, 1) : new Color(0, 0, 0, 0.5f);
        exitText.color = current == exit.gameObject? new Color(0, 0, 0, 1) : new Color(0, 0, 0, 0.5f);
    }

    public void OnChangeBGM(float value)
    {
        if(value == bgmLength)
        {
            bgm.value = 0;
        }
        if(value == -1)
        {
            bgm.value = bgmLength-1;
        }

        bgmNumberText.text = bgm.value.ToString("00");
    }
}