もちっとメモ

もちっとメモ

もぐりのエンジニアが日々の中で試してみたことを気が向いたときに書き連ねていきます

Unityで外部テキストを読んで表示するノベルが作りたい

手っ取り早くUnityでノベルゲームを作りたい時は宴とかを使えばいいんでしょうが、お試しで軽くノベルゲーム的なのを作って見たいだけなので今回は見送り。

madnesslabo.net

じゃあデフォルトのAssetだけで作れんもんかと調べたらありました。やはり先人は偉大です。下記の記事にしたがって組んで見たら、おお!!すごい。テキストファイルに書いた内容がノベルゲームっぽく表示されてる!!しかも、ちゃんと@brでテキストが区切られていて//でコメントアウトされている。感動です。早速お借りさせていただきます。

tsubakit1.hateblo.jp

基本はこちらに書かれている通りに進めればできますが、個人的につまづいた事を備忘録的に記しておきます。

  1. Q.作ったスクリプトは何にアタッチすればいいの?
    A.スクリプトを動かしたいシーン上に適当に空のオブジェクトを作成して`ScenarioManager.cs`をアタッチします。すると自動的に`TextController.cs`も付随してアタッチされるはずです。あとはInspectorビューで`ScenarioManager.cs`のLoadFileNameにシナリオを書いたテキストのファイル名(拡張子は不要)を入力します。`TextController.cs`にはUiTextをアタッチします。これはその1と同じです。

    tsubakit1.hateblo.jp

  2. Q.`ScenarioManager.cs`のUpdate関数の`m_commandController.PreloadCommand (m_scenarios [m_currentLine])`あたりで`NullReferenceException`が出て動かないんだけど。(その2のコメントにもあったやつです)
    A.commandControllerのインスタンスが見つからない的な事を言っていると思われます。なので`commandController.cs`をシーン上のオブジェクトにアタッチして見たら動きました。アタッチするオブジェクトは何でもいいんだと思いますが私は先ほどの`ScenarioManager.cs`をアタッチしたのと同じオブジェクトにアタッチしました。

せっかくなのでもう一工夫いじらせていただきます。我流魔改造なので出来具合はご愛嬌という事で。

メッセージが表示されるとポポポポというSEを鳴らす

kan-kikuchi.hatenablog.com

オーディオ制御のコアの部分はこちらで紹介されているAudioManagerを利用させていただいてます。こちらのAudioManagerを使って`TextController.cs`のテキストが送られる部分をこんな感じに改造します。


    public void SetNextLine(string text){
		AudioManager.Instance.StopChargeSE ();//すでにポポポポがなっていたらの停止
		AudioManager.Instance.PlayChargeSE ("serifSE");//新しくポポポポを鳴らす
		currentText = text;
		timeUntilDisplay = currentText.Length * intervalForCharactorDisplay;
		timeElapsed = Time.time;
		lastUpdateCharacter = -1;
	}

上で紹介されているAudioManagerではSEの途中停止はなかったのでStopBGMを参考にStopSEを自作しています。ほぼBGMをまるっとコピーして作っています。つまりStop可能なSEは一つだけですね。以下`AudioManager.cs`に足したStop可能SE部分を抜粋(名前がChargeSEになっているのは、はじめに作った時にチャージショット様に作ったためです。stopableSEとかにしておけばよかったですね)


    //ChargeSEのフェードアウト時間	
	public const float CHARGE_SE_FADE_SPEED_RATE_MAX = 0.9f;
	public const float CHARGE_SE_FADE_SPEED_RATE_MIN = 0.3f;
	//BGM用とSE用それぞれのオーディオソース
	private AudioSource _chargeSESource;
	//全オーディオを保持
	private SortedDictionary<string, AudioClip> _bgmDic, _seDic, _chargeSEDic;
	//作成したAudioSourceを取得し、変数にセットする。ボリュームもセットする
    for (int i = 0; i < audioSourceArray.Length; i++) {
		audioSourceArray [i].playOnAwake = false;

		if (i == 0) {
			audioSourceArray [i].loop = true;
			_bgmSource = audioSourceArray [i];
			_bgmSource.volume = PlayerPrefs.GetFloat (BGM_VOLUME_KEY, BGM_VOLUME_DEFAULT);
		} else if (i == 1) {
			_chargeSESource = audioSourceArray [i];
			_chargeSESource.volume = PlayerPrefs.GetFloat (SE_VOLUME_KEY, SE_VOLUME_DEFAULT);
		} else {
			_seSourceList.Add (audioSourceArray [i]);
			audioSourceArray [i].volume = PlayerPrefs.GetFloat (SE_VOLUME_KEY, SE_VOLUME_DEFAULT);
		}
	}

	//リソースフォルダから全てのBGM,SEを読み込み、セットする
	_chargeSEDic = new SortedDictionary<string, AudioClip> ();
	object[] chargeSEList = Resources.LoadAll (SE_PATH);

	/*
	* ChargeSE
	* 指定したファイル名のChargeSEを流す
	* ただし、すでに流れている場合は前の曲をフェードアウトさせてから流す
	* 第二引数のfadeSpeedRateにフェードアウトスピードの割合を入れる
	*/ 
	public void PlayChargeSE(string chargeSEName,float fadeSpeedRate = CHARGE_SE_FADE_SPEED_RATE_MAX){
		if (!_chargeSEDic.ContainsKey (chargeSEName)) {
			Debug.Log (chargeSEName + " can not be found");
			return;
		}
		//現在、再生されていない場合はそのまま再生
		if (!_chargeSESource.isPlaying) {
			_chargeSESource.clip = _chargeSEDic [chargeSEName] as AudioClip;
			_chargeSESource.Play ();
		}
		//違うChargeSEが流れているときは、流れているChargeSEをフェードアウトさせてから次を再生
		//同じChargeSEが再生中はpass
		else if (_chargeSESource.clip.name != chargeSEName) {
			FadeOutChargeSE (fadeSpeedRate);
		}
	}

	/*
	 * ChargeSEの停止
	 */ 
	public void StopChargeSE(){
		_chargeSESource.Stop ();
	}

	/*
	 * 現在再生中のSEをフェードアウト
	 * fadeSpeedrateでスピードを調整
	 */ 
	public void FadeOutChargeSE(float fadeSpeedRate = CHARGE_SE_FADE_SPEED_RATE_MIN){
		_isFadeOut = true;
	}

	/*
	 * 音量調整
	 * BGMとSEのボリュームを別々に変更and保存
	 */ 
	public void ChangeVolume(float BGMVolume,float SEVolume){
		_chargeSESource.volume = SEVolume;
	}

	public void ChangeSEVolume(float SEVolume){
		_chargeSESource.volume = SEVolume;
	}

これでメッセージと一緒にSEが流れる様になったのですが、テキストが表示され切った時点でSEを止めたいのでもう一手間加えます。テキストが表示され切ったかは`ScenarioManager.cs`の`if(m_textController.IsCompletaDisPlayText)`の部分で判定しているのでここに先ほどのStopSEを挟んでやります。


    void Update(){
	if (m_textController.IsCompleteDisplayText) {
		AudioManager.Instance.StopChargeSE ();
		if (m_currentLine < m_scenarios.Length) {
			if (!m_isCallPreload) {
    (以下略)

これでテキストが表示され切った時点でSEが止まる様になりました。

おまけでBGMも鳴らしちゃいましょう。BGMはシーンのスタート時に鳴らします。bgmはpublicで定義しておいて指定すればいいのですがたくさんシーンを作ると毎回打つのが面倒なので中に埋め込んじゃいます。その代わり辞書型で定義しておいてシーン名に応じて切り替えられる様にしました。


    private Dictionary<string,string> bgmList = new Dictionary<string, string> () {
		{ "Story1", "BGM1" },
		{ "Story2", "BGM2" },
		{ "Story3", "BGM3" }
	};

    void PlayBGM(){
		AudioManager.Instance.StopBGM ();
		AudioManager.Instance.PlayBGM (bgmList [SceneManager.GetActiveScene ().name]);
	}

    void Start(){
		PlayBGM ();
		m_textController = GetComponent ();
		m_commandController = GetComponent ();

		UpdateLines (LoadFileName);
		RequestNextLine ();
	}

シーンの切り替えエフェクトをつける

以前紹介したこちらを使ってやります。この記事の元ネタもテラシュールブログさんでしたね。いつもお世話になっています。

t-n-clark.hatenadiary.jp

tsubakit1.hateblo.jp

以前と同じ様に`ScenarioManager.cs`に`Fadeout`用の関数を作ります。 遷移時のSEはInspectorViewから指定できる様にpublicで定義しておきます。


	[SerializeField] Fade fade = null;
    public string changeSE;

    public void Fadeout(){
		AudioManager.Instance.PlaySE (changeSE);
		fade.FadeIn (1, () => {
			Invoke ("ChangeNextScene", 0.5f);
		});
	}
	
	public string nextScene;//次のシーンの名前

	void ChangeNextScene(){
		SceneManager.LoadScene (nextScene);
	}

そして、テキストファイルの中身が全て読み終わった時点で次のシーンに飛ばします。


    void Update(){
		if (m_textController.IsCompleteDisplayText) {
			AudioManager.Instance.StopChargeSE ();
			if (m_currentLine < m_scenarios.Length) {
				if (!m_isCallPreload) {
					m_commandController.PreloadCommand (m_scenarios [m_currentLine]);
					m_isCallPreload = true;
				}
				if (Input.GetKeyDown (KeyCode.N)) {
					RequestNextLine ();
				}
			} else {//テキストの中身が全て読みこまれていて、かつNのキーが押された時
				if (Input.GetKeyDown(KeyCode.N)) {
					Fadeout ();
				}
			}
		} else {
			if(Input.GetKeyDown(KeyCode.N)){
				m_textController.ForceCompleteDisplayText ();
			}
		}
	}

これでテキストが全て読み終わった後にフェードアウトして次のシーンに遷移できる様になりました。 これでだいぶノベルゲームっぽくなりました。RPGのシナリオシーンにも使えるんじゃないかな。