ADX for Unityでインタラクティブミュージック（ブロック再生編）

# はじめに

ゲーム中の展開に応じてBGMがシームレスに変化していくのを「インタラクティブミュージック」といいます。これにより、ゲームプレイと音楽を連動させ盛り上がりなどの演出をより強化することができます。

過去の記事では、ADXを利用したインタラクティブミュージックの基礎的な実装方法を紹介しました。

https://qiita.com/nimushiki/items/4475710c1b6b3644fb96

これらがトラックのパラメータを調整してBGMを遷移させているのに対し、今回はADXの「ブロック再生」機能により、「曲の再生位置をダイナミックに変化させ、BGMの展開を変えてしまう」方法を取ります。

![IMブロック再生例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/e8ed0a02-5f24-437b-685f-6e7bad706ccb.png)

普通にBGMを再生するとAメロ→Bメロ→サビといった通常の進行をしますが、Aメロ→サビ（何度かループ）→Aメロ→Bメロといったように変則的な進行が可能になります。

なお、Aメロやサビといった区分は音楽用語でブロックやセクションなどと呼ばれます。

この記事は「ADX2 for UE4でインタラクティブミュージック（ブロック再生編）」のUnity版です。元記事作者のSigさん ( [https://qiita.com/SigRem](https://qiita.com/SigRem) )の許可を得て作成しています。

https://qiita.com/SigRem/items/696ab6f1b4e533eebdbf

### 動作確認環境

Windows 10 Home 21H2

Unity 2021.3.15f1

ADX LE Unity SDK　3.07.02

CRI ADX LE Tools for Windows　3.47.01

### 前提記事

CRI ADX導入方法や基本的な操作については以下の記事を参照して下さい。

https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377

# 実装

## AtomCraftを使った工程

まずは、再生する音データの作成を行います。ADXにおいては、再生用のデータはUnityとは別のツール「AtomCraft」を使います。その手順については、元の記事（UE4向け記事）の「AtomCraftでBGMを構成する」と同じになりますので省略しています。

本記事の手順を試す場合は、まず元の記事の[該当項目](https://qiita.com/SigRem/items/696ab6f1b4e533eebdbf#atomcraft%E3%81%A7bgm%E3%82%92%E6%A7%8B%E6%88%90%E3%81%99%E3%82%8B)を進めてから本記事を進めてください。

UE4版との違いとして1点、「Atomキューシートバイナリのビルド」画面で、一番左下のオプション「UnityAssets出力」にチェックを入れるのを忘れないようにして下さい。

https://qiita.com/SigRem/items/696ab6f1b4e533eebdbf

またUnityへのインポート手順は以下の記事、「Unity側のセットアップ」の項を参照してください。

https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377#unity%E5%81%B4%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97

## Unityでインタラクティブミュージックを実装する

## Unityでの実装

まずはPlayerAnchorタグのついたプレイヤーキャラクターを用意し、レベルを作成します。

![ブロック再生テストに必要なオブジェクト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/3e5bc1cd-d3d2-1bd6-4134-36e22eb44706.png)

インタラクティブミュージックを管理するためのオブジェクト（IMBlock）を用意し、コンポーネント（IMBlockController.cs）をアタッチします。

その子に、前述の管理コンポーネントへプレイヤーとの接触判定を通知するオブジェクト（Intro、Sabi、Outro）を作成します。それぞれ通知用コンポーネント（IMBlockTrigger.cs）とコライダーをアタッチします。

機能上必要ありませんが、作例では再生中でも判定が見やすいように同サイズのPlaneもアタッチしました。

各コライダーはそれぞれ以下の画像のような役割を持ちます。

![各コライダーの役割](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/ba665fc6-ae80-932d-f5d6-646ffbeabb7b.png)

## ブロック再生の方法

ブロック再生をする上で必要な概念はBlockIndexです。

AtomCraftでCueに対し設定したブロックですが、先頭を0として1ずつ増えるBlockIndexがそれぞれに付与されています。（なお、恐らくですがそのインデックスの値をエディタ上から確認することはできません）

このインデックスを元に、UnityのC#上で再生位置をコントロールします。

![ブロックインデックス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/8deec493-279b-3b6c-e703-3bfaa6e53e95.png)


## ブロック再生の実装

実装例を見てみましょう。

```IMBlockTrigger.cs
public class IMBlockTrigger : MonoBehaviour
{
    private IMBlockController iMBlockController;
    [SerializeField] private IMBlockName _IMBlockName;

    void Awake()
    {
        iMBlockController = GetComponentInParent<IMBlockController>();
    }

    private void OnTriggerEnter(Collider other)
    {
        if(other.tag == "PlayerAnchor") iMBlockController.SetBlock(_IMBlockName);
    }
}
```

一緒にアタッチされたBoxColliderがどのエリアのものかを、_IMBlockNameによって設定しておきます。そしてプレイヤーとの接触判定を受け、IMBlockControllerへ通知を行います。

```IMBlockController.cs
public class IMBlockController : MonoBehaviour
{
    [SerializeField] private CriAtomSource bgmCriAtomSource;
    [SerializeField] private IMBlockName iMBlockName;
    [SerializeField] private int blockIndex = 0;
    private CriAtomExPlayback playback;

    private void Update()
    {
        switch (iMBlockName)
        {
            case IMBlockName.Intro:
                if(playback.GetCurrentBlockIndex() == 1) {
                    blockIndex = 2;
                }
                else{
                    blockIndex = 1;
                }
                break;
            case IMBlockName.Sabi:
                if (playback.GetCurrentBlockIndex() == 3)
                {
                    blockIndex = 4;
                }
                else
                {
                    blockIndex = 3;
                }
                break;
            case IMBlockName.Outro:
                blockIndex = 5;
                break;
            default:
                break;
        }
        playback.SetNextBlockIndex(blockIndex);
    }

    public void SetBlock(IMBlockName _IMBlockName)
    {
        iMBlockName = _IMBlockName;
        switch (_IMBlockName)
        {
            case IMBlockName.Intro:
                if (bgmCriAtomSource.status == CriAtomSourceBase.Status.Playing) return;
                playback = bgmCriAtomSource.Play();
                blockIndex = 1;
                break;
            case IMBlockName.Sabi:
                if (bgmCriAtomSource.status == CriAtomSourceBase.Status.Playing)
                {
                    blockIndex = 3;
                }
                break;
            case IMBlockName.Outro:
                if (bgmCriAtomSource.status == CriAtomSourceBase.Status.Playing)
                {
                    blockIndex = 5;
                }
                break;
            default:
                break;
        }
        playback.SetNextBlockIndex(blockIndex);
    }
}

public enum IMBlockName
{
    Intro,
    Sabi,
    Outro,
}
```

ここが一番重要な部分です。GetCurrentBlockIndexを用いて現在再生中のBlockIndexを取得、SetNextBlockIndexを用いて次に再生しするBlockIndexを指定することができます。

IMBlockTriggerからSetBlockでプレイヤーがどのエリアにいるかを受け取って保存し、曲の再生とブロック再生位置の設定を行います。

Update内では、エリア情報とブロック再生位置を比較し、同一エリア内にいる場合は特定のブロックをループさせています。

## ブロック再生実装例の挙動

ここまでで実装は完成です。間違いがなければ、以下のような挙動が確認できます。

1. Introエリアに入ると音楽が再生される
    1. そのままIntroエリアに居続けるとAメロBメロをループする
2. Sabiエリアに移動すると、再生中のブロックが終わり次第サビAが再生される
    1. そのままSabiエリアに居続けるとサビAサビBをループする
3. Outroエリアに移動すると、再生中のブロックが終わり次第アウトロが再生される

![実装例のループ構造](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2576800/3f80cc84-6f09-e298-abf3-15adfed95870.png)    

# 最後に

サンプルでは、視覚的にわかりやすいように床とコライダーを設置し、その接触判定をトリガーとしました。しかしこのままでは自由に遷移をコントロールできてしまうため、アウトロ→イントロといったやや不自然な遷移を起こせてしまいます。

そのため実際に運用する場合は、自由に遷移しても問題ないように条件やループを見直したり、そもそも自由にはさせない設計にする、といった改良が必要になってきます。

例えばボス戦といった戦闘曲で、敵の形態変化や戦況に合わせてループの位置を変える、最後の一撃を加えた時にアウトロに移行する、といった実装が考えられます。