Kosaku Kurino

Kosaku Kurino

【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る パート2

はじめに

この記事は、「【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る パート1」の続きです。

読んでない方は、先にパート1から読んでください。

パート1、パート3はこちら

【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る パート1 【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る パート3

今回は、曲の再生、ノーツをタイミングよく弾いたかチェックするロジック、ノーツを弾いたときのエフェクトを実装していきます

前回のおさらい

パート1では、譜面データ、GameManager、NoteControllerを作りました。 そして、ノーツを出現させるロジックとノーツを動かすロジックを実装しました。

GameManager ・・・ 譜面からノーツを出現させるロジック、ノーツを動かすロジックの一部分(ノーツを動かし始めるタイミングの管理) NoteManager ・・・ ノーツを動かすロジック

忘れてしまった方は、さらっとパート1を復習しましょう。

曲の再生

曲データと譜面データの準備

ミュージック自作は流石に自作できないので、完全商用フリーで使用できる音楽素材が集まっている魔王魂より、曲を拝借します

自分はシャイニングスターという曲を使用しました。

スクリーンショット 2019-02-06 20.27.20.png

調査の結果、bpmは158ということだったので379.746835443038(ms)おきにノーツを配置しないといけませんね。

曲をダウンロードし、「Assets → Resources → Charts → [ダウンロードした曲名]」にAudioClip.mp3という名前で保存しておきましょう。

また、新しく曲にあった譜面データを作成し、同じフォルダにChart.jsonという名前で保存しましょう。

スクリーンショット 2019-02-06 20.33.58.png

細かすぎるので、Nodejsで譜面自動作成スクリプトを書いてjsonファイルを作りました

GameManagerから曲を再生させる

SetChartボタンプッシュ時に、AudioClipをセットし、Playボタンプッシュ時に再生させます。 両ボタンのイベント検知はGameManager(Script)で管理してたので、GameManager(Script)に追加します。

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

public class GameManager : MonoBehaviour {

  [SerializeField] string FilePath;
  [SerializeField] string ClipPath; // 追加

  [SerializeField] Button Play;
  [SerializeField] Button SetChart;

  [SerializeField] GameObject Don;
  [SerializeField] GameObject Ka;

  [SerializeField] Transform SpawnPoint;
  [SerializeField] Transform BeatPoint;

  AudioSource Music; // 追加

  float PlayTime;
  float Distance;
  float During;
  bool isPlaying;
  int GoIndex;

  string Title;
  int BPM;
  List<GameObject> Notes;

  void OnEnable() {
    Music = this.GetComponent<AudioSource>(); // 追加

    Distance = Math.Abs(BeatPoint.position.x - SpawnPoint.position.x);
    During = 2 * 1000;
    isPlaying = false;
    GoIndex = 0;

    Play.onClick
      .AsObservable()
      .Subscribe(_ => play());

    SetChart.onClick
      .AsObservable()
      .Subscribe(_ => loadChart());

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Notes.Count > GoIndex)
      .Where(_ => Notes[GoIndex].GetComponent<NoteController>().getTiming() <= ((Time.time * 1000 - PlayTime) + During))
      .Subscribe(_ => {
        Notes[GoIndex].GetComponent<NoteController>().go(Distance, During);
        GoIndex++;
      });
  }

  void loadChart() {
    Notes = new List<GameObject>();

    string jsonText = Resources.Load<TextAsset>(FilePath).ToString();
    Music.clip = (AudioClip)Resources.Load(ClipPath); // 追加

    JsonNode json = JsonNode.Parse(jsonText);
    Title = json["title"].Get<string>();
    BPM = int.Parse(json["bpm"].Get<string>());

    foreach(var note in json["notes"]) {
      string type = note["type"].Get<string>();
      float timing = float.Parse(note["timing"].Get<string>());

      GameObject Note;
      if (type == "don") {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity);
      } else if (type == "ka") {
        Note = Instantiate(Ka, SpawnPoint.position, Quaternion.identity);
      } else {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity); // default don
      }

      Note.GetComponent<NoteController>().setParameter(type, timing);

      Notes.Add(Note);
    }
  }

  void play() {
    Music.Stop(); // 追加
    Music.Play(); // 追加
    PlayTime = Time.time * 1000; 
    isPlaying = true;
    Debug.Log("Game Start!");
  }
}

ヒエラルキーから値をセットできるように、ClipPathを作成する

ClipPath ・・・ 曲データのパス

スクリプトから曲を再生できるように、Musicを作成する

Music ・・・ 後でGameManagerにアタッチするAudioSource

GameManager読み込み時に、アタッチされているAudioSourceを取ってきて、Musicにセットする

Music = this.GetComponent<AudioSource>(); // 追加

SetChartボタンプッシュ時実行されるLoadChart()関数で、Music.clipに再生させる曲のAudioClipをセットさせる

void loadChart() {
    Notes = new List<GameObject>();

    string jsonText = Resources.Load<TextAsset>(FilePath).ToString();
    Music.clip = (AudioClip)Resources.Load(ClipPath); // 追加

    ...
}

Playボタンプッシュ時実行されるplay()関数で、曲を再生させる

void play() {
    Music.Stop(); // 追加
    Music.Play(); // 追加
    PlayTime = Time.time * 1000; 
    isPlaying = true;
    Debug.Log("Game Start!");
}

次はGameManagerにAudioSourceコンポーネントをアタッチしましょう。 アタッチできたら、Play On Awakeのチェックを外し、FilePathClipPathを書き換えましょう。

スクリーンショット 2019-02-06 20.54.35.png

ノーツをタイミングよく弾いたかチェックするロジック

GameManagerでノーツをタイミングよく弾いたかチェックさせますまた、「Dキー」でdonを「Kキー」でkaを弾けるようにします

タイミングよく弾けたかチェックするためには、キーを押したときにノーツをチェックするべき盤面なのかそれともチェックしなくていい盤面なのかを決めます。次に、チェックする盤面においてどのノーツが対象なのかを決めます。最後にタイミングのずれ幅に応じて、 不可(失敗)もしくは良(成功)なのかを決めます。

つまり、キーを押したタイミングで一番弾くべきタイミングが近いノーツを見つけ、タイミングのずれ幅によって処理を切り分けていきます

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

public class GameManager : MonoBehaviour {

  [SerializeField] string FilePath;
  [SerializeField] string ClipPath;

  [SerializeField] Button Play;
  [SerializeField] Button SetChart;

  [SerializeField] GameObject Don;
  [SerializeField] GameObject Ka;

  [SerializeField] Transform SpawnPoint;
  [SerializeField] Transform BeatPoint;

  float PlayTime;
  float Distance;
  float During;
  bool isPlaying;
  int GoIndex;

  float CheckRange;
  float BeatRange;
  List<float> NoteTimings; // 追加

  AudioSource Music;

  string Title;
  int BPM;
  List<GameObject> Notes;

  void OnEnable() {
    Music = this.GetComponent<AudioSource>();

    Distance = Math.Abs(BeatPoint.position.x - SpawnPoint.position.x);
    During = 2 * 1000;
    isPlaying = false;
    GoIndex = 0;

    CheckRange = 120; // 追加
    BeatRange = 80; // 追加

    Play.onClick
      .AsObservable()
      .Subscribe(_ => play());

    SetChart.onClick
      .AsObservable()
      .Subscribe(_ => loadChart());

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Notes.Count > GoIndex)
      .Where(_ => Notes[GoIndex].GetComponent<NoteController>().getTiming() <= ((Time.time * 1000 - PlayTime) + During))
      .Subscribe(_ => {
        Notes[GoIndex].GetComponent<NoteController>().go(Distance, During);
        GoIndex++;
      });

    // 追加
    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Input.GetKeyDown(KeyCode.D))
      .Subscribe(_ => {
        beat("don", Time.time * 1000 - PlayTime);
      });

    // 追加
    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Input.GetKeyDown(KeyCode.K))
      .Subscribe(_ => {
        beat("ka", Time.time * 1000 - PlayTime);
      });
  }

  void loadChart() {
    Notes = new List<GameObject>();
    NoteTimings = new List<float>(); // 追加

    string jsonText = Resources.Load<TextAsset>(FilePath).ToString();
    Music.clip = (AudioClip)Resources.Load(ClipPath);

    JsonNode json = JsonNode.Parse(jsonText);
    Title = json["title"].Get<string>();
    BPM = int.Parse(json["bpm"].Get<string>());

    foreach(var note in json["notes"]) {
      string type = note["type"].Get<string>();
      float timing = float.Parse(note["timing"].Get<string>());

      GameObject Note;
      if (type == "don") {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity);
      } else if (type == "ka") {
        Note = Instantiate(Ka, SpawnPoint.position, Quaternion.identity);
      } else {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity); // default don
      }

      Note.GetComponent<NoteController>().setParameter(type, timing);

      Notes.Add(Note);
      NoteTimings.Add(timing); // 追加
    }
  }

  void play() {
    Music.Stop();
    Music.Play();
    PlayTime = Time.time * 1000; 
    isPlaying = true;
    Debug.Log("Game Start!");
  }

  // 追加
  void beat(string type, float timing) {
    float minDiff = -1;
    int minDiffIndex = -1;

    for (int i = 0; i < NoteTimings.Count; i++) {
      if(NoteTimings[i] > 0) {
        float diff = Math.Abs(NoteTimings[i] - timing);
        if(minDiff == -1 || minDiff > diff) {
          minDiff = diff;
          minDiffIndex = i;
        }
      }
    }

    if(minDiff != -1 & minDiff < CheckRange) {
      if(minDiff < BeatRange & Notes[minDiffIndex].GetComponent<NoteController>().getType() == type) {
        NoteTimings[minDiffIndex] = -1;
        Notes[minDiffIndex].SetActive(false);
        Debug.Log("beat " + type + " success.");
      }
      else {
        NoteTimings[minDiffIndex] = -1;
        Notes[minDiffIndex].SetActive(false);
        Debug.Log("beat " + type + " failure.");
      }
    }
    else {
      Debug.Log("through");
    }
  }
}

ノーツをタイミングよく弾いたかチェックするために、CheckRange、BeatRange、NoteTimingsを作成する

CheckRange ・・・ 良(成功)もしくは不可(失敗)になる領域 BeatRange ・・・ 良(成功)になる領域 NoteTimings ・・・ ノーツのタイミングのみ入った配列

GameManager読み込み時、CheckRangeとBeatRangeをセットします。 キーを押したタイミングとノーツの弾くべきタイミングが80(ms)より大きく120(ms)以内なら不可(失敗)、80(ms)以内なら良(成功)になります

void OnEnable() {
  CheckRange = 120;
  BeatRange = 80;
}

「Dキー」と「Kキー」を押したとき、ノーツをタイミングよく弾いたかチェックするbeat関数を実行させるようにします

キーの入力受付はthis.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.[キー])).Subscribe(_ => 実行したい関数)で、キーを押した際に関数を実行してくれます。 `

void OnEnable() {
  this.UpdateAsObservable()
    .Where(_ => isPlaying)
    .Where(_ => Input.GetKeyDown(KeyCode.D))
    .Subscribe(_ => {
      beat("don", Time.time * 1000 - PlayTime);
    });

  this.UpdateAsObservable()
    .Where(_ => isPlaying)
    .Where(_ => Input.GetKeyDown(KeyCode.K))
    .Subscribe(_ => {
      beat("ka", Time.time * 1000 - PlayTime);
    });
}

譜面データを読み込むとき、Notesを作成するとともにNoteTimingsも作る

void loadChart() {
  Notes = new List<GameObject>();
  NoteTimings = new List<float>(); // 追加
  ...
  foreach(var note in json["notes"]) {
  ...
    Notes.Add(Note);
    NoteTimings.Add(timing); // 追加
  }
}

ノーツをタイミングよく弾いたかチェックするbeat関数を作る

キーを押したタイミングと一番近いタイミングをNoteTimingから取得します。 一番近いタイミングとの差がによって、何もしないのか、不可(失敗)にするのか、良(成功)にするのか分岐させています。

不可(失敗)もしくは良(可)であれば、SetActive(false)でノーツを非表示にするようにしています。

Destroy()でノーツのゲームオブジェクト自体を削除できますが、Destroy()は処理が重いため使用しません。

void beat(string type, float timing) {
  float minDiff = -1;
  int minDiffIndex = -1;

  for (int i = 0; i < NoteTimings.Count; i++) {
    if(NoteTimings[i] > 0) {
      float diff = Math.Abs(NoteTimings[i] - timing);
      if(minDiff == -1 || minDiff > diff) {
        minDiff = diff;
        minDiffIndex = i;
      }
    }
  }

  if(minDiff != -1 & minDiff < CheckRange) {
    if(minDiff < BeatRange & Notes[minDiffIndex].GetComponent<NoteController>().getType() == type) {
      NoteTimings[minDiffIndex] = -1;
      Notes[minDiffIndex].SetActive(false);
      Debug.Log("beat " + type + " success.");
    }
    else {
      NoteTimings[minDiffIndex] = -1;
      Notes[minDiffIndex].SetActive(false);
      Debug.Log("beat " + type + " failure.");
    }
  }
  else {
    Debug.Log("through");
  }
}

最後に、BeatPointの位置がわかるようにエイムを追加しましょう

BeatPointと同じ位置にBeatPointViewという名前でGameObjectを作成し、エイム画像をつけましょう。BeatPointVewのz座標を少し後ろにずらしてください

ノーツが流れてきたとき、ノーツが隠れないようにするためです。

背景が寂しいので、背景画像を用意して入れもいいかもしれません。私の背景画像はアウトですので真似は決してしないでください。

スクリーンショット 2019-02-07 20.39.15.png

ノーツを弾いたときのエフェクト 〜効果音〜

キーを押した時、「ドン」と「カッ」の効果音が再生されるようにします

効果音の準備

効果音も自作できないので、魔王魂より、似た効果音を拝借します

フリー効果音素材の戦闘効果音の戦闘7(どんっ)と戦闘15(ビンタ)が「ドン」と「カッ」近いので採用します。

戦闘7(ドン)と戦闘15(ビンタ)をダウンロードし、「Assets → AudioClip」にdon.mp3、ka.mp3という名前で保存しておきましょう

スクリーンショット 2019-02-05 20.06.30.png

UniRxのサブジェクトとオブザーバーについて

実装を始める前に、UniRxのサブジェクトとオブザーバーについて説明しておきます。

UniRxのサブジェクトとオブザーバーは、簡単に何かのタイミングでメッセージを通知し、別の場所に書いた処理を実行させることができます

メッセージを通知するサブジェクトとメッセージを受け取るオブザーバーの作り方は下記です。

Subject<Unit> SampleSubject = new Subject<Unit>();

IObservable<Unit> OnSampleChatch {
   get { return SampleSubject; }
}

メッセージの通知とメッセージの受け取り方は下記です。

// 通知
SampleSubject.OnNext(Unit.default);

// 受け取り
.OnSampleChatch
    .Subscribe(_ => Debug.Log("catch event"));

それでは、サブジェクトとオブザーバーを使って効果音を鳴らしていきましょう。

GameManager(Script)から効果音再生イベントを通知させる

UniRxのサブジェクトとオブザーバーを実装します。

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

public class GameManager : MonoBehaviour {

  [SerializeField] string FilePath;

  [SerializeField] Button Play;
  [SerializeField] Button SetChart;

  [SerializeField] GameObject Don;
  [SerializeField] GameObject Ka;

  [SerializeField] Transform SpawnPoint;
  [SerializeField] Transform BeatPoint;

  float PlayTime;
  float Distance;
  float During;
  bool isPlaying;
  int GoIndex;

  float CheckRange;
  float BeatRange;
  List<float> NoteTimings;

  string Title;
  int BPM;
  List<GameObject> Notes;

  // イベントを通知するサブジェクトを追加
  Subject<string> SoundEffectSubject = new Subject<string>();

  // イベントを検知するオブザーバーを追加
  public IObservable<string> OnSoundEffect {
     get { return SoundEffectSubject; }
  }

  void OnEnable() {
    Distance = Math.Abs(BeatPoint.position.x - SpawnPoint.position.x);
    During = 2 * 1000;
    isPlaying = false;
    GoIndex = 0;

    CheckRange = 120;
    BeatRange = 80;

    Debug.Log(Distance);

    Play.onClick
      .AsObservable()
      .Subscribe(_ => play());

    SetChart.onClick
      .AsObservable()
      .Subscribe(_ => loadChart());

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Notes.Count > GoIndex)
      .Where(_ => Notes[GoIndex].GetComponent<NoteController>().getTiming() <= ((Time.time * 1000 - PlayTime) + During))
      .Subscribe(_ => {
        Notes[GoIndex].GetComponent<NoteController>().go(Distance, During);
        GoIndex++;
      });

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Input.GetKeyDown(KeyCode.D))
      .Subscribe(_ => {
        beat("don", Time.time * 1000 - PlayTime);
        SoundEffectSubject.OnNext("don"); // イベントを通知
      });

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Input.GetKeyDown(KeyCode.K))
      .Subscribe(_ => {
        beat("ka", Time.time * 1000 - PlayTime);
        SoundEffectSubject.OnNext("ka"); // イベントを通知
      });
  }

  void loadChart() {
    Notes = new List<GameObject>();
    NoteTimings = new List<float>();

    string jsonText = Resources.Load<TextAsset>(FilePath).ToString();

    JsonNode json = JsonNode.Parse(jsonText);
    Title = json["title"].Get<string>();
    BPM = int.Parse(json["bpm"].Get<string>());

    foreach(var note in json["notes"]) {
      string type = note["type"].Get<string>();
      float timing = float.Parse(note["timing"].Get<string>());

      GameObject Note;
      if (type == "don") {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity);
      } else if (type == "ka") {
        Note = Instantiate(Ka, SpawnPoint.position, Quaternion.identity);
      } else {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity); // default don
      }

      Note.GetComponent<NoteController>().setParameter(type, timing);

      Notes.Add(Note);
      NoteTimings.Add(timing);
    }
  }

  void play() {
    PlayTime = Time.time * 1000; 
    isPlaying = true;
    Debug.Log("Game Start!");
  }

  void beat(string type, float timing) {
    float minDiff = -1;
    int minDiffIndex = -1;

    for (int i = 0; i < NoteTimings.Count; i++) {
      if(NoteTimings[i] > 0) {
        float diff = Math.Abs(NoteTimings[i] - timing);
        if(minDiff == -1 || minDiff > diff) {
          minDiff = diff;
          minDiffIndex = i;
        }
      }
    }

    if(minDiff != -1 & minDiff < CheckRange) {
      if(minDiff < BeatRange & Notes[minDiffIndex].GetComponent<NoteController>().getType() == type) {
        NoteTimings[minDiffIndex] = -1;
        Notes[minDiffIndex].SetActive(false);
        Debug.Log("beat " + type + " success.");
      }
      else {
        NoteTimings[minDiffIndex] = -1;
        Notes[minDiffIndex].SetActive(false);
        Debug.Log("beat " + type + " failure.");
      }
    }
    else {
      Debug.Log("through");
    }
  }
}

効果音を再生するイベントを通知・検知できるようにするため、SoundEffectSubject、OnSoundEffectを作成する

// イベントを通知するサブジェクトを追加
Subject<string> SoundEffectSubject = new Subject<string>();

// イベントを検知するオブザーバーを追加
public IObservable<string> OnSoundEffect {
  get { return SoundEffectSubject; }
}

キーが押されたタイミングで、OnNext(ノーツのタイプ)により、効果音再生のイベントを通知させる

void OnEnable() {
  this.UpdateAsObservable()
    .Where(_ => isPlaying)
    .Where(_ => Input.GetKeyDown(KeyCode.D))
    .Subscribe(_ => {
      beat("don", Time.time * 1000 - PlayTime);
      SoundEffectSubject.OnNext("don"); // イベントを通知
    });

  this.UpdateAsObservable()
    .Where(_ => isPlaying)
    .Where(_ => Input.GetKeyDown(KeyCode.K))
    .Subscribe(_ => {
      beat("ka", Time.time * 1000 - PlayTime);
      SoundEffectSubject.OnNext("ka"); // イベントを通知
    });
}

ここで使用していないオブザーバーのOnSoundEffectは、SoundEffectManagerで使います。

SoundEffectManager(Script)から効果音を再生させる

後で作成するSoundEffectManagerで効果音再生の管理をさせます

「Assets → Scripts」にSoundEffectManager.csを作成します。

後でSoundEffectManagerにアタッチします

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

public class SoundEffectManager : MonoBehaviour {

  [SerializeField] GameManager GameManager;
  [SerializeField] AudioSource DonPlayer;
  [SerializeField] AudioSource KaPlayer;

  void OnEnable() {
    GameManager
      .OnSoundEffect
      .Where(type => type == "don")
      .Subscribe(type => donPlay());

    GameManager
      .OnSoundEffect
      .Where(type => type == "ka")
      .Subscribe(type => kaPlay());
  }

  void donPlay() {
    DonPlayer.Stop();
    DonPlayer.Play();
  }

  void kaPlay() {
    KaPlayer.Stop();
    KaPlayer.Play();
  }
}

「ドン」と「カッ」の効果音を再生をさせるために、GameManage、DonPlayer、KaPlayerを作成する

GameManager ・・・ ゲームを管理しているコンポーネント DonPlayer ・・・ 「ドン」を再生できるAudioSource KaPlayer ・・・ 「カッ」を再生できるAudioSource

「ドン」と「カッ」のオーディオを再生させるdonPlay()kaPlay()関数を作成する。

void donPlay() {
  DonPlayer.Stop();
  DonPlayer.Play();
}

void kaPlay() {
  KaPlayer.Stop();
  KaPlayer.Play();
}

効果音再生イベントを検知し、先ほど作成した関数を実行できるようにする。

void OnEnable() {
  GameManager
    .OnSoundEffect
    .Where(type => type == "don")
    .Subscribe(type => donPlay());

  GameManager
    .OnSoundEffect
    .Where(type => type == "ka")
    .Subscribe(type => kaPlay());
  }

次に、SoundEffectManagerを作成します。

SoundEffectManagerという名前でGameObjectを作成し、SoundEffectManager(Scripts)をアタッチしてください

そして子要素にDonPlayerとKaPlayerという名前でGameObjectを作成してください。

スクリーンショット 2019-02-06 21.29.37.png

DonPlayerとKaPlayerにAudioSourceを追加し、先ほどダウンロードした効果音をAudioClipにセットしてください。

スクリーンショット 2019-02-06 21.30.55.png

最後に、SoundEffectManagerのSoundEffectManager(Script)の変数をセットしましょう。 ドラッグアンドドロップで登録していってください。

スクリーンショット 2019-02-06 21.31.53.png

ノーツを弾いたときのエフェクト 〜メッセージ〜

ノーツを弾いた時、「良」と「不可」のメッセージが表示されるようにします

GameManager(Script)からメッセージ表示イベントを通知させる

サブジェクトとオブザーバーを作り、「良」、「不可」を判定する箇所で、OnNext()で通知です。

効果音を実装した時とほぼ同じなので説明省きます

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

public class GameManager : MonoBehaviour {

  [SerializeField] string FilePath;
  [SerializeField] string ClipPath;

  [SerializeField] Button Play;
  [SerializeField] Button SetChart;

  [SerializeField] GameObject Don;
  [SerializeField] GameObject Ka;

  [SerializeField] Transform SpawnPoint;
  [SerializeField] Transform BeatPoint;

  AudioSource Music;

  float PlayTime;
  float Distance;
  float During;
  bool isPlaying;
  int GoIndex;

  float CheckRange;
  float BeatRange;
  List<float> NoteTimings;

  string Title;
  int BPM;
  List<GameObject> Notes;

  Subject<string> SoundEffectSubject = new Subject<string>();

  public IObservable<string> OnSoundEffect {
     get { return SoundEffectSubject; }
  }

  // イベントを通知するサブジェクトを追加
  Subject<string> MessageEffectSubject = new Subject<string>();

  // イベントを検知するオブザーバーを追加
  public IObservable<string> OnMessageEffect {
     get { return MessageEffectSubject; }
  }

  void OnEnable() {
    Music = this.GetComponent<AudioSource>();

    Distance = Math.Abs(BeatPoint.position.x - SpawnPoint.position.x);
    During = 2 * 1000;
    isPlaying = false;
    GoIndex = 0;

    CheckRange = 120;
    BeatRange = 80;

    Play.onClick
      .AsObservable()
      .Subscribe(_ => play());

    SetChart.onClick
      .AsObservable()
      .Subscribe(_ => loadChart());

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Notes.Count > GoIndex)
      .Where(_ => Notes[GoIndex].GetComponent<NoteController>().getTiming() <= ((Time.time * 1000 - PlayTime) + During))
      .Subscribe(_ => {
        Notes[GoIndex].GetComponent<NoteController>().go(Distance, During);
        GoIndex++;
      });

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Input.GetKeyDown(KeyCode.D))
      .Subscribe(_ => {
        beat("don", Time.time * 1000 - PlayTime);
        SoundEffectSubject.OnNext("don");
      });

    this.UpdateAsObservable()
      .Where(_ => isPlaying)
      .Where(_ => Input.GetKeyDown(KeyCode.K))
      .Subscribe(_ => {
        beat("ka", Time.time * 1000 - PlayTime);
        SoundEffectSubject.OnNext("ka");
      });
  }

  void loadChart() {
    Notes = new List<GameObject>();
    NoteTimings = new List<float>();

    string jsonText = Resources.Load<TextAsset>(FilePath).ToString();
    Music.clip = (AudioClip)Resources.Load(ClipPath);

    JsonNode json = JsonNode.Parse(jsonText);
    Title = json["title"].Get<string>();
    BPM = int.Parse(json["bpm"].Get<string>());

    foreach(var note in json["notes"]) {
      string type = note["type"].Get<string>();
      float timing = float.Parse(note["timing"].Get<string>());

      GameObject Note;
      if (type == "don") {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity);
      } else if (type == "ka") {
        Note = Instantiate(Ka, SpawnPoint.position, Quaternion.identity);
      } else {
        Note = Instantiate(Don, SpawnPoint.position, Quaternion.identity); // default don
      }

      Note.GetComponent<NoteController>().setParameter(type, timing);

      Notes.Add(Note);
      NoteTimings.Add(timing);
    }
  }

  void play() {
    Music.Stop();
    Music.Play();
    PlayTime = Time.time * 1000; 
    isPlaying = true;
    Debug.Log("Game Start!");
  }

  void beat(string type, float timing) {
    float minDiff = -1;
    int minDiffIndex = -1;

    for (int i = 0; i < Notes.Count; i++) {
      if(NoteTimings[i] > 0) {
        float diff = Math.Abs(NoteTimings[i] - timing);
        if(minDiff == -1 || minDiff > diff) {
          minDiff = diff;
          minDiffIndex = i;
        }
      }
    }

    if(minDiff != -1 & minDiff < CheckRange) {
      if(minDiff < BeatRange & Notes[minDiffIndex].GetComponent<NoteController>().getType() == type) {
        NoteTimings[minDiffIndex] = -1;
        Notes[minDiffIndex].SetActive(false);

        MessageEffectSubject.OnNext("good"); // イベントを通知
        Debug.Log("beat " + type + " success.");
      }
      else {
        NoteTimings[minDiffIndex] = -1;
        Notes[minDiffIndex].SetActive(false);

        MessageEffectSubject.OnNext("failure"); // イベントを通知
        Debug.Log("beat " + type + " failure.");
      }
    }
    else {
      Debug.Log("through");
    }
  }
}

MessageEffectManager(Script)からメッセージを表示させる

後で作成するMessageEffectManagerでメッセージ表示の管理をさせます

「Assets → Scripts」にMessageEffectManager.csを作成します。

後でMessageEffectManagerにアタッチします

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

public class MessageEffectManager : MonoBehaviour {

  [SerializeField] GameManager GameManager;
  [SerializeField] GameObject Good;
  [SerializeField] GameObject Failure;

  void OnEnable() {
    GameManager
      .OnMessageEffect
      .Where(result => result == "good")
      .Subscribe(result => goodShow());

    GameManager
      .OnMessageEffect
      .Where(result => result == "failure")
      .Subscribe(result => failureShow());
  }

  void goodShow() {
    Good.SetActive(false);
    Good.SetActive(true);

    Observable.Timer(TimeSpan.FromMilliseconds(200))
      .Subscribe(_ => Good.SetActive(false));
  }

  void failureShow() {
    Failure.SetActive(false);
    Failure.SetActive(true);

    Observable.Timer(TimeSpan.FromMilliseconds(200))
      .Subscribe(_ => Failure.SetActive(false));
  }
}

基本的に、効果音を実装した時と同じです。

違いとしては、効果音再生の箇所がオブジェクトの表示に変わったところと、200(ms)後に自動的に非表示にさせているところです。

void goodShow() {
  Good.SetActive(false);
  Good.SetActive(true);

  Observable.Timer(TimeSpan.FromMilliseconds(200))
    .Subscribe(_ => Good.SetActive(false));
  }

void failureShow() {
  Failure.SetActive(false);
  Failure.SetActive(true);

  Observable.Timer(TimeSpan.FromMilliseconds(200))
    .Subscribe(_ => Failure.SetActive(false));
}

次に、MessageEffectManagerを作成します。

MessageEffectManagerという名前でGameObjectを作成し、MessageEffectManager(Scripts)をアタッチしてください。

そして子要素にGoodとFailureという名前でGameObjectを作成してください。

スクリーンショット 2019-02-07 21.28.22.png

GoodとFailureに表示させたいメッセージ画像を貼りましょう。 そして、GoodとFailureは非アクティブ状態にしておいてください

Splite Renderコンポーネントで画像は貼れます

スクリーンショット 2019-02-07 21.30.49.png

最後に、MessageEffectManagerのMessageEffectManager(Script)の変数をセットしましょう。 ドラッグアンドドロップで登録していってください。

スクリーンショット 2019-02-07 21.32.34.png

動きを確認する

ここまで実装できれば、動かしてみましょう。 曲を聴きながらノーツを弾を弾けるようになっているはずです。

taiko2.mov.gif

最後に

ついに音ゲーがまともにプレイできるようになりました。

次回はラスト、スコア(コンボ等)を実装します。