【Unityゲーム開発】演出でコメント表示とキャラクター画像、ボイス再生を行う機能の実装方法【備忘録】

この記事では、Unityを使用してコメントとそのキャラクター画像を画面に表示し、対応するボイスを再生する機能を実装する手順を初心者向けに解説します。

この機能は、特にゲーム開発でキャラクターの台詞やコメントを表示し、ボイスを再生したい場合に役立ちます。

この記事は演出としての表示方法なので、ノベルゲームなどのメッセージ表示とは異なります

メッセージ表示に関する情報が知りたい方は有料のアセットの解説ですが以下の記事で紹介しています。

目次

構成ファイル

このプロジェクトは以下の構成になっています。

  • MainManager.cs: 全体の管理を行うスクリプト。
  • CommentSystem.cs: コメントと画像の表示を管理するスクリプト。
  • SoundManager.cs: ボイスや効果音を管理するスクリプト。
  • CommentEntity.cs: コメントのデータを管理するクラス。
  • SoundEntity.cs: 音声データを管理するクラス。
  • GameConst.cs: ゲーム内で使用する定数を管理するクラス。

データクラスの準備

コメントと音声データを保持するためのクラスを用意します。

CommentEntity.cs

CommentEntity.csは、コメント情報を保持するクラスです。

using UnityEngine;

[System.Serializable]
public class CommentEntity
{
    /// <summary>
    /// GameConst.CHARA_SPRITEのキャラクター名
    /// </summary>
    public string character;

    /// <summary>
    /// テキスト
    /// </summary>
    public string text;

    /// <summary>
    /// GameConst.COMMENT_POSITIONの位置
    /// </summary>
    public string position;

    /// <summary>
    /// 次の処理までの待機時間(ms)
    /// </summary>
    public float waitTime;

    /// <summary>
    /// 声ID
    /// </summary>
    public SoundManager.VOICE_ID? VoiceID;
}

このクラスは、表示するコメントの内容、キャラクター画像、表示位置、再生するボイスのIDなどを保持します。

SoundEntity.cs

SoundEntity.csは、音声データを管理するクラスです。

using UnityEngine;

[System.Serializable]
public class SoundEntity
{
	public int indexNo;

	public string resourcePath; // Resourcesフォルダ内のパス
	public float volume = 1.0f;
	public float duration = 0.0f; // 0.0の場合、クリップの長さ通りに再生

	[System.NonSerialized]
	public AudioClip clip; // 実際のオーディオクリップ(非シリアライズ)
}

音声ファイルのパスや再生時の音量、クリップの再生時間などを保持します。

サウンドマネージャーの設定

次に、ボイスや効果音の再生を管理するSoundManagerを実装します。

SoundManagerは、ボイスをロードして再生する機能を提供します。

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

public class SoundManager : MonoBehaviour
{
    public static SoundManager instance = null;

    /// <summary>
    /// 効果音のAudioSource
    /// </summary>
    public AudioSource sfxSource;

    [HideInInspector]
    public List<SoundEntity> voiceEntities;

    public enum VOICE_ID
    {
        voice01,
        voice02,
        voice03,
        voice04,
        voice05,
        voice06,
        voice07,
        voice08,
    }


    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (instance != this)
        {
            Destroy(gameObject);
        }

        voiceEntities = new List<SoundEntity>();

        LoadAudioClips();
    }

    // リソースからオーディオクリップをロードする
    private void LoadAudioClips()
    {
        initSetSound();

        foreach (var entity in voiceEntities)
        {
            entity.clip = Resources.Load<AudioClip>(entity.resourcePath);
        }

    }

    private void initSetSound()
    {
        const string VOICE_PATH = "sound/voice/";

        //# voiceのセット
        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "a_1_みこちってさなんでにぇって",
            volume = 1f,
            duration = 4.459f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "a_2_みこはなんでにぇっていうの",
            volume = 1f,
            duration = 4.18f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "a_3_なんでにぇっていうのかな",
            volume = 1f,
            duration = 5.878f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "b_1_がらがら",
            volume = 0.5f,
            duration = 3.42f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "b_2_みこはばかじゃない",
            volume = 1f,
            duration = 1.829f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "b_3_ぽんなだけだ",
            volume = 0.5f,
            duration = 2.873f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "はじまるー!",
            volume = 0.5f,
            duration = 1.5f
        });

        voiceEntities.Add(new SoundEntity
        {
            indexNo = voiceEntities.Count + 1,
            resourcePath = VOICE_PATH + "リーチ",
            volume = 0.5f,
            duration = 1.5f
        });

    }

    // ボイス再生
    public void PlayVoice(SoundManager.VOICE_ID clipIndexID)
    {
        int clipIndex = (int)clipIndexID;

        if (clipIndex < voiceEntities.Count && voiceEntities[clipIndex].clip != null)
        {
            sfxSource.volume = voiceEntities[clipIndex].volume;
            sfxSource.PlayOneShot(voiceEntities[clipIndex].clip);

            if (voiceEntities[clipIndex].duration > 0.0f)
            {
                StartCoroutine(StopSFXAfterDuration(voiceEntities[clipIndex].clip, voiceEntities[clipIndex].duration));
            }
        }
    }

    // 効果音停止
    public void StopSFX()
    {
        sfxSource.Stop();
    }

    private IEnumerator StopSFXAfterDuration(AudioClip clip, float duration)
    {
        yield return new WaitForSeconds(duration);
        if (sfxSource.clip == clip)
        {
            sfxSource.Stop();
        }
    }

    public IEnumerator PlaySoundAndWait(VOICE_ID? voiceID)
    {

        if (voiceID == null) yield break;

        //ボイスIDからクリップを取得する
        AudioClip clip = voiceEntities[(int)voiceID].clip;

        if (clip == null) yield break;

        sfxSource.clip = voiceEntities[(int)voiceID].clip;

        sfxSource.volume = voiceEntities[(int)voiceID].volume;
        sfxSource.PlayOneShot(voiceEntities[(int)voiceID].clip);

        while (sfxSource.isPlaying)
        {
            yield return null;
        }
    }
}

SoundManagerクラスは、MonoBehaviourを継承しており、ゲーム全体で音声管理を一元化する役割を担います。このクラスはシングルトンパターンを採用しており、音声の再生や管理を一箇所で行うことができます。

変数の定義

  • instance: SoundManagerのシングルトンインスタンスです。シーンを跨いで音声管理を一貫させるために使用します。
  • sfxSource: 効果音を再生するためのAudioSourceコンポーネントです。音声クリップを再生する際に使用します。
  • voiceEntities: SoundEntityのリストで、音声クリップの情報を保持します。このリストには、音声のファイルパスや音量などの情報が含まれています。

VOICE_IDの定義

VOICE_IDは、音声クリップを識別するための列挙型です。これにより、特定の音声を簡単に指定して再生することができます。

LoadAudioClipsメソッド

リソースからの音声クリップのロード: voiceEntitiesに含まれる各SoundEntityresourcePathに基づいて、Resources.Loadメソッドを使用して音声クリップをロードします。この方法により、音声ファイルをResourcesフォルダから簡単にロードし、再生できるようになります。

PlaySoundAndWaitメソッド

音声の再生: PlaySoundAndWaitメソッドは、指定されたvoiceIDに対応する音声クリップを再生し、その音声が再生され終わるまで待機します。IEnumerator型のメソッドなので、StartCoroutineで呼び出す必要があります。

クリップの取得と再生: voiceIDに基づいてvoiceEntitiesから音声クリップを取得し、sfxSourceを使って再生します。PlayOneShotメソッドで音声を再生し、その音声が再生されている間、コルーチンが待機します。

Hierarchyの設定

空のオブジェクトを作成し、SoundManagerという名前にして、”SoundManager.cs”をコンポーネント弐追加します。さらにその配下に空のオブジェクトを追加してAudio Sourceコンポーネントを追加しています。

コメントシステムの実装

次に、コメントとキャラクター画像を表示するCommentSystemを実装します。

CommentSystemは、コメントと画像を表示し、必要に応じてボイスを再生します。

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

public class CommentSystem : MonoBehaviour
{
    /// <summary>
    /// コメントを表示するキャンバス
    /// </summary>
    public GameObject SetCanvas;

    /// <summary>
    /// アイコンが右にあるコメントのプレハブ
    /// </summary>
    public GameObject IconRightPrefab;

    /// <summary>
    /// アイコンが左にあるコメントのプレハブ
    /// </summary>
    public GameObject IconLeftPrefab;

    /// <summary>
    /// サウンドマネージャー
    /// </summary>
    [HideInInspector]
    public SoundManager soundM;

    /// <summary>
    /// ノーマルみこアイコン
    /// </summary>
    public Sprite SakuraMikoNormal;

    /// <summary>
    /// ノーマルすいせいアイコン
    /// </summary>
    public Sprite SuiseiNormal;
    
    /// <summary>
    /// ノーマル35Pアイコン
    /// </summary>
    public Sprite S35pNormal;

    /// <summary>
    /// グラサン35Pアイコン
    /// </summary>
    public Sprite S35pGurasan;

    /// <summary>
    /// ノーマル金時アイコン
    /// </summary>
    public Sprite KintokiNormal;
    private List<GameObject> lstComments = new List<GameObject>();
    
    /// <summary>
    /// コメントを作成する
    /// </summary>
    public IEnumerator CreateComment(CommentEntity entComment)
    {
        //comment.
        Sprite charaSprite = getCharaSprite(entComment.character);

        if (charaSprite == null)
        {
            Debug.LogError("キャラクターの画像が見つかりませんでした。");
            yield break;
        }

        //プレハブからインスタンスを生成
        GameObject comment;
        Vector3 pnt = GetCommentPosition(entComment.position);

        switch (entComment.position)
        {
            case GameConst.COMMENT_POSITION.TOP_LEFT:
            case GameConst.COMMENT_POSITION.CENTER_LEFT:
            case GameConst.COMMENT_POSITION.BOTTOM_LEFT:
                comment = Instantiate(IconLeftPrefab, pnt, Quaternion.identity);
                break;
            default:
                comment = Instantiate(IconRightPrefab, pnt, Quaternion.identity);
                break;
        }


        // 親オブジェクトを設定(ローカル座標を維持)
        comment.transform.SetParent(SetCanvas.transform, false);

        // ローカル座標を設定
        comment.GetComponent<RectTransform>().localPosition = pnt;

        //生成したインスタンスの名称をつける
        comment.name = "Comments" + lstComments.Count.ToString();

        //インスタンスのIconの画像を変更する
        // 階層内のIconオブジェクトを見つける
        Transform iconTransform = comment.transform.Find("Icon");
        if (iconTransform != null)
        {
            // IconオブジェクトのImageコンポーネントを取得
            Image iconImage = iconTransform.GetComponent<Image>();
            if (iconImage != null)
            {
                // ImageコンポーネントのSourceImageを新しいImageに変更
                iconImage.sprite = charaSprite;
            }
            else
            {
                Debug.LogError("Imageコンポーネントが見つかりませんでした。");
            }
        }
        else
        {
            Debug.LogError("Iconオブジェクトが見つかりませんでした。");
        }

        //コメントを反映させる
        comment.GetComponentInChildren<Text>().text = entComment.text;

        //音声がある場合は音声を再生する
        if (entComment.VoiceID != null)
        {
            // BGMかSFXかを判断して再生
            yield return soundM.PlaySoundAndWait(entComment.VoiceID);
        }

        yield return new WaitForSeconds(entComment.waitTime);

        yield break;
    }

    /// <summary>
    /// キャラクタ画像を取得する
    /// </summary>
    /// <param name="chara"></param>
    /// <returns></returns>
    private Sprite getCharaSprite(string chara)
    {
        Sprite charaSprite = null;
        
        switch (chara)
        {
            case GameConst.CHARA_SPRITE.SAKURA_MIKO_NORMAL:
                charaSprite = SakuraMikoNormal;
                break;
            case GameConst.CHARA_SPRITE.S_35P_NORMAL:
                charaSprite = S35pNormal;
                break;
            case GameConst.CHARA_SPRITE.SUISEI_NORMAL:
                charaSprite = SuiseiNormal;
                break;

            case GameConst.CHARA_SPRITE.S_KINTOKI_NORMAL:
                charaSprite = KintokiNormal;
                break;
            default:
                break;  
        }
        return charaSprite;
    }

    public IEnumerator DestroyComments()
    {
        //コメントを全て削除する
        GameObject[] reels = GameObject.FindGameObjectsWithTag("comment"); 
        foreach (GameObject i in reels)//各reelsのオブジェクトに対して以下を行う
        {
            Destroy(i);//reelsの各オブジェクトを破壊する
        }
        lstComments = new List<GameObject>();
        yield break;
    }


    /// <summary>
    /// コメントを表示する位置座標を取得する
    /// </summary>
    /// <param name="comment_posisiton"></param>
    /// <returns></returns>
    public static Vector3 GetCommentPosition(string comment_posisiton)
    {
        Vector3 pnt;

        switch (comment_posisiton)
        {
            case GameConst.COMMENT_POSITION.TOP_LEFT:
                pnt = new Vector3(-200f, 330f, 0);
                break;
            case GameConst.COMMENT_POSITION.TOP_RIGHT:
                pnt = new Vector3(200f, 330f, 0);
                break;
            case GameConst.COMMENT_POSITION.CENTER_LEFT:
                pnt = new Vector3(-200f, 30f, 0);
                break;
            case GameConst.COMMENT_POSITION.CENTER_RIGHT:
                pnt = new Vector3(200f, 30f, 0);
                break;
            case GameConst.COMMENT_POSITION.BOTTOM_LEFT:
                pnt = new Vector3(-200f, -230f, 0);
                break;
            case GameConst.COMMENT_POSITION.BOTTOM_RIGHT:
                pnt = new Vector3(200f, -230f, 0);
                break;
            default:
                pnt = new Vector3(0, 0, 0);
                break;
        }
        return pnt;
    }

}

CommentSystemクラスは、Unityでコメントとキャラクター画像を画面に表示するためのスクリプトです。このクラスは、コメントの生成、配置、削除、そして必要に応じて音声の再生を行います。

変数の定義

  • SetCanvas: コメントを表示するキャンバスを指定します。このキャンバスの中に、コメントとキャラクターのアイコンが表示されます。
  • IconRightPrefab: キャラクターアイコンが右側に配置されたコメントのプレハブを指定します。(詳細は後述)
  • IconLeftPrefab: キャラクターアイコンが左側に配置されたコメントのプレハブを指定します。(詳細は後述)
  • soundM: SoundManagerのインスタンスを保持し、音声再生の管理を行います。
  • Sprite変数: SakuraMikoNormalなどの変数は、キャラクターのアイコン画像を保持します。
  • lstComments: 画面に表示されているコメントのリストです。

CreateCommentメソッド

  • キャラクター画像の取得: getCharaSpriteメソッドを使って、entComment.characterに基づいたキャラクターのスプライトを取得します。もしスプライトが見つからない場合はエラーメッセージを出力し、メソッドを終了します。
  • コメントオブジェクトの生成: Instantiateメソッドを使って、IconLeftPrefabまたはIconRightPrefabを元にコメントオブジェクトを生成します。生成されたコメントオブジェクトの位置は、GetCommentPositionメソッドで計算されます。
  • コメントオブジェクトの設定: 生成されたコメントオブジェクトはSetCanvasの子オブジェクトとして追加され、RectTransformを使って位置が設定されます。その後、コメントオブジェクト内のIconオブジェクトにキャラクター画像を設定し、コメントテキストを反映します。
  • 音声の再生: entComment.VoiceIDが指定されている場合、soundM.PlaySoundAndWaitメソッドを呼び出して音声を再生します。
  • 待機時間: 最後に、entComment.waitTimeに指定された時間だけ待機します。

DestroyCommentsメソッド

DestroyCommentsメソッドは、画面上に表示されているすべてのコメントオブジェクトを削除します。

  • コメントオブジェクトの検索と削除: GameObject.FindGameObjectsWithTagメソッドを使って、タグが"comment"になっているすべてのオブジェクトを取得し、それらを順番に削除します。
  • リストのリセット: コメントオブジェクトが格納されていたリストlstCommentsもリセットします。

IconRightPrefabとIconLeftPrefabの構成について

アイコンプレハブは、以下のような構成になっています。

画像が横に並ぶように”FukidashiRightPrefab”には、”Content Size Filter”と”Horizontal Layout Group”を追加してサイズの自動調整と配下の部品が横に並ぶように設定しています。(このあたり試行錯誤でうまくいったので詳細はあまり理解できていません。)

コメント部分は 背景画像>吹き出し画像>テキストオブジェクトを子として配置し、表示順を調整しています。

また、文字数によって、自動で範囲が広がるように背景画像と吹き出し画像には”Horizontal Layout Group”とContetnt Size Filter”を設定します。

吹き出し画像にも”Content Size Filter”と”Vertical Layout Group”を設定しています。

一番下の子オブジェクトにテキストを配置します。

さらに、”FukidashiRightPrefab”配下に、Imageオブジェクトを配置することで、”Horizontal Layout Group”によって自動で右側に配置されます。

Leftバージョンは配下の順番を入れ替えるだけです。

Hierarchyの設定

表示したいキャンバス、先程解説したアイコンプレハブを初期設定しています。

また、表示したい対応する画像をセットしています。今回あまり表示する数がなかったのでそのまま設定していますが、数が多い場合は配列やResourceからの読み込みにしたほうが良いかもしれません。

メインマネージャーの設定

最後に、全体の管理を行うMainManagerを実装し、コメントシステムとサウンドマネージャーを連携させます。

MainManagerは、コメント表示とボイス再生をトリガーします。

using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
using UnityEngine;


public class MainManager : MonoBehaviour
{

    #region public 変数

    /// <summary>
    /// メインマネージャーのインスタンス
    /// </summary>
    public static MainManager instance;

    /// <summary>
    /// サウンドマネージャー
    /// </summary>
    public GameObject soundManager;

    /// <summary>
    /// コメントシステム
    /// </summary>
    public GameObject commentSystem;



    #endregion

    #region private 変数

    private CommentSystem commentS;

    private SoundManager soundM;

    #endregion

    #region unityのイベント処理

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {

        //各コンポーネントの取得
        soundM = soundManager.GetComponent<SoundManager>();
        commentS = commentSystem.GetComponent<CommentSystem>();

        //各コンポーネントの設定
        commentS.soundM = soundM;

    }

    #endregion

    #region デバッグ処理

    public void debug()
    {
        StartCoroutine(CommentDisplay());

    }

    private IEnumerator CommentDisplay()
    {
        List<CommentEntity> lstComment = new List<CommentEntity>();

        CommentEntity comment1 = new CommentEntity();
        comment1.character = GameConst.CHARA_SPRITE.S_KINTOKI_NORMAL;
        comment1.text = "テストコメント";
        comment1.position = GameConst.COMMENT_POSITION.TOP_LEFT;
        comment1.waitTime = 0;
        comment1.VoiceID = SoundManager.VOICE_ID.voice01;
        lstComment.Add(comment1);

        CommentEntity comment2 = new CommentEntity();
        comment2.character = GameConst.CHARA_SPRITE.S_35P_NORMAL;
        comment2.text = "テストコメント2";
        comment2.position = GameConst.COMMENT_POSITION.CENTER_RIGHT;
        comment2.waitTime = 0;
        comment2.VoiceID = SoundManager.VOICE_ID.voice02;
        lstComment.Add(comment2);

        foreach (var comment in lstComment)
        {
            yield return StartCoroutine(commentS.CreateComment(comment));
        }

        yield return StartCoroutine(commentS.DestroyComments());
        
        yield break;
    }

    #endregion

}



MainManagerクラスは、プロジェクト全体の管理者として、他のシステム(コメントシステムやサウンドマネージャーなど)を統括します。このクラスを通じて、ゲーム内のさまざまな機能を統合し、システム間の連携を容易にします。

変数の定義

  • instance: MainManagerクラスのシングルトンインスタンスです。この変数を使って、どのシーンからでもMainManagerにアクセスできるようにします。
  • soundManager: サウンド管理を担当するSoundManagerコンポーネントをアタッチしたゲームオブジェクトを指定します。
  • commentSystem: コメント表示を担当するCommentSystemコンポーネントをアタッチしたゲームオブジェクトを指定します。

CommentDisplayメソッド

  • コメントリストの作成: CommentDisplayメソッドでは、CommentEntityのリストlstCommentを作成し、2つのコメントエンティティを追加します。これらのエンティティには、キャラクター、テキスト、位置、音声IDが設定されています。
  • コメントの表示: リスト内の各コメントについて、CommentSystemCreateCommentメソッドをコルーチンとして呼び出し、コメントの表示と音声の再生を行います。
  • コメントの削除: 全てのコメントが表示された後、DestroyCommentsメソッドを呼び出して表示されたコメントを削除します。

Hierarchyの設定

SoundManagerとCommentSystemを設定しています。

まとめ

今回解説したUnityプロジェクトでは、コメント表示とキャラクター画像、そして音声再生の機能を実装するために、いくつかの重要なスクリプトが連携して動作しています。

  1. SoundManager.cs:
    • 音声クリップのロード、再生、管理を行うクラスです。シングルトンパターンを用いてシーンを跨いだ音声管理を可能にし、特定の音声を識別して再生するためのVOICE_IDを利用します。このクラスを使うことで、ゲーム全体のサウンドエフェクトやボイスを一元的に制御できます。
  2. CommentSystem.cs:
    • コメントとキャラクター画像を画面に表示し、必要に応じて音声を再生するクラスです。CreateCommentメソッドでコメントオブジェクトを生成し、SoundManagerとの連携によって音声を再生します。また、DestroyCommentsメソッドを使って、表示されたコメントをまとめて削除することができます。このシステムにより、対話形式のUIやメッセージ表示が簡単に実装可能です。
  3. MainManager.cs:
    • プロジェクト全体を統括するマネージャークラスで、他のシステム(CommentSystemSoundManager)との連携を管理します。シングルトンパターンを採用しており、どのシーンからでもアクセス可能です。デバッグ用のdebugメソッドでは、簡単にコメント表示と音声再生のテストが行えます。

これらのスクリプトは、それぞれが独立しつつも、相互に連携して動作します。

このプロジェクトの構成を理解することで、Unityを使ったより複雑なUIやサウンド機能を構築するための基礎を固めることができます。これらのスクリプトをベースに、さらに多機能なシステムを構築していくことも可能ですので、ぜひ挑戦してみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次