[UNITY]2DRPG開発日誌 #58 Fungusでラブデリック語を実装する方法

Fungus

Fungusについての基礎知識はこちらの記事を参照してください。

「そもそもラブデリック語とは?」や「UNITYで(Fungusを使わずに)ラブデリック語を実装する方法」についてはこちらの記事で解説しています。

Fungusの音声再生

はじめに、「moon」と比べるとFungusのデフォルト文字表示速度は速めなので、SayDialog内のWriterコンポーネントのWritingSpeedを18~20程度に変更しておきましょう。

続いて、Fungusではデフォルトで文字送りの際にBeep音が流れる仕様ですが、これはSayDialog内のWriterAudioコンポーネントで制御されている機能です。

ここのAudioModeを「Beeps」にして、BeepSoundsリストに好きなだけ音を登録すればとりあえずラブデリック語は完成します。超簡単ですね。

ただし、このままでは「同じセリフを喋った時は同じ音を再生する(ランダムテーブルの固定)」という機能にはなりません。

完璧な「ラブデリック語」にするには少々手順が多いですが、難しくはありません。

事前準備

はじめに、特定のコマンドが開始されたことを検知するため、Flowchartオブジェクト内に適当な名前(私は FlowchartSignals という名前にしました)のスクリプトを作って、中身をこう書きます。

using Fungus;
using UnityEngine;

public class FlowchartSignals : MonoBehaviour
{
    public string ActiveCommandInBlock;

    /// <summary>
    /// このスクリプトがアタッチされたフローチャート内でブロックが始まった際に呼ばれる
    /// </summary>
    void OnEnable()
    {
        BlockSignals.OnCommandExecute += BlockSignals_OnCommandExecute;
        BlockSignals.OnBlockStart += OnBlockStart;
    }

    private void BlockSignals_OnCommandExecute(Block block, Command command, int commandIndex, int maxCommandIndex)
    {
        ActiveCommandInBlock += commandIndex;

        //子オブジェクトを検索
        for (int i = 0; i < this.transform.childCount; i++)
        {
            //WriterAudioコンポーネントに現在のコマンド情報を渡す
            if (this.gameObject.transform.GetChild(i).gameObject.TryGetComponent(out WriterAudio comp))
                comp.isCommandStart = true;
        }
        
        Debug.Log(this + ": ActiveCommandInBlock:" + ActiveCommandInBlock);
    }

    void OnBlockStart(Block _block)
    {
        ActiveCommandInBlock = "BlockName:"+_block.BlockName.ToString()+", BlockID:"+_block.ItemId+", CommandIndex:";
    }
}

次に、Flowchartオブジェクト直下にラブデリック語を使用したいSayDialogオブジェクトを格納します

これは先ほどのスクリプトから子オブジェクトとして検索するためなので、もっと上手な方法があれば適宜工夫してください。

アセットの中身の書き換えかた

ProjectウインドウからFungus -> Scripts -> Components フォルダにある WriterAudio.csを探して、中身を編集します。

はじめに、固定パターンのランダムテーブルを使用するため、神ブログ(:3[kanのメモ帳]で公開されている固定パターンランダムテーブルクラスをまるごとコピペします。スクリプトの上のほうにでも置いておきましょう。

using System;

class MyRandom
    {
        private uint x, y, z, w;

        public MyRandom() : this((uint)DateTime.Now.Ticks) { }

        public MyRandom(uint seed)
        {
            setSeed(seed);
        }

        public void setSeed(uint seed)
        {
            x = seed; y = x * 3266489917U + 1; z = y * 3266489917U + 1; w = z * 3266489917U + 1;
        }

        public uint getNext()
        {
            uint t = x ^ (x << 11);
            x = y;
            y = z;
            z = w;
            w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
            return w;
        }

        public int Range(int min, int max)
        {
            return min + Math.Abs((int)getNext()) % (max - 1);
        }
    }

続いて、BeepSounds音声を再生しているこの箇所を、

        public virtual void OnGlyph()
        {
            if (playingVoiceover)
            {
                return;
            }

            if (playBeeps && beepSounds.Count > 0)
            {
                if (!targetAudioSource.isPlaying)
                {
                    if (nextBeepTime < Time.realtimeSinceStartup)
                    {
                        targetAudioSource.clip = beepSounds[Random.Range(0, beepSounds.Count)];

                        if (targetAudioSource.clip != null)
                        {
                            targetAudioSource.loop = false;
                            targetVolume = volume;
                            targetAudioSource.Play();

                            float extend = targetAudioSource.clip.length;
                            nextBeepTime = Time.realtimeSinceStartup + extend;

                        }
                    }
                }
            }
        }

このように編集します。

        public bool isCommandStart = false;
        string storyText;
        List<int> randomTable;
        int currentTableIndex;
        MyRandom myRandomPattern;

        public virtual void OnGlyph()
        {
            if (playingVoiceover)
            {
                return;
            }

            if (playBeeps && beepSounds.Count > 0)
            {
                //再生中でなければ実行
                if (!targetAudioSource.isPlaying)
                {
                    //再生終了時間に到達していなければ実行
                    if (nextBeepTime < Time.realtimeSinceStartup)
                    {
                        //targetAudioSource.clip = beepSounds[Random.Range(0, beepSounds.Count)];

                        //オーディオクリップが一つしかなければ、それを再生する
                        if (beepSounds.Count == 1)
                            targetAudioSource.clip = beepSounds[0];

                        //オーディオクリップが複数あれば、クリップをランダムに取得する
                        else
                        {
                            //テキストを取得する
                            GameObject g = this.gameObject.transform.GetChild(0).Find("StoryText").gameObject;
                            string s = null;
                            if (g.TryGetComponent<Text>(out Text comp1))
                                s = comp1.text;
                            else if (g.TryGetComponent<TextMeshProUGUI>(out TextMeshProUGUI comp2))
                                s = comp2.text;

                            //最初の再生なら、テーブルの進捗を初期化
                            if (isCommandStart)
                            {
                                isCommandStart = false;
                                currentTableIndex = 0;
                            }

                            //テキストを保存
                            storyText = s;

                            //テキストの文字数をSeed値とした固定パターンを作成する
                            myRandomPattern = new MyRandom((uint)storyText.Length);

                            //ランダム整数テーブルを作成
                            randomTable = new List<int>();
                            for (int i = 0; i < 100; i++)
                                randomTable.Add(myRandomPattern.Range(0, beepSounds.Count));
                            string ss = null;
                            foreach (var item in randomTable)
                                ss += item.ToString();

                            //テーブルが終わっていたらテーブルの進捗を初期化
                            if (currentTableIndex == randomTable.Count)
                                currentTableIndex = 0;

                            //前回と同じ音声の場合
                            if (beepSounds[randomTable[currentTableIndex]] == targetAudioSource.clip)
                            {
                                //現在がテーブルの最後なら進捗をリセット
                                if (++currentTableIndex == randomTable.Count)
                                    currentTableIndex = 0;
                                //途中なら進捗を進める
                                else
                                    ++currentTableIndex;
                            }

                            //オーディオクリップを取得
                            targetAudioSource.clip = beepSounds[randomTable[currentTableIndex]];

                            //テーブルの進捗を一つ進める
                            ++currentTableIndex;
                        }

                        //音声を再生
                        if (targetAudioSource.clip != null)
                        {
                            //クリップごとのループを切る
                            targetAudioSource.loop = false;

                            //音量を設定する
                            targetVolume = volume;

                            //再生する
                            targetAudioSource.Play();

                            //次の再生時間を決定
                            float extend = targetAudioSource.clip.length;
                            nextBeepTime = Time.realtimeSinceStartup + extend;
                        }
                    }
                }
            }
        }

次に、Fungusの初期設定であるフェードイン/フェードアウト機能を無効化する必要がありますので、同スクリプト内の203行目あたりにある

        protected virtual void Update()
        {
            targetAudioSource.volume = Mathf.MoveTowards(targetAudioSource.volume, targetVolume, Time.deltaTime * 5f);
        }

これを

        protected virtual void Update()
        {
            targetAudioSource.volume = targetVolume;
        }

こうします。

このフェードイン/アウト無効化について、最初は「ラブデリック語再生時のみ無効化する」という実装を考えていて、それなら同スクリプト最下にある「public void OnAllWordsWritten()」メソッドでテキスト終了を検知すればOK…と思っていたのですが、
そうするとテキスト送りをスキップしたときにテキスト終了が呼ばれないなど予期せぬバグの原因となるので、特別な事情がなければフェードイン/アウト機能自体を無効化するほうがよいと思います。

 

以上で完成です!

動画でも解説しました。

タイトルとURLをコピーしました