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

# はじめに
この記事は、「[【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る　パート１](https://qiita.com/kousaku-maron/items/42f801df2b87e37b1d91)」の続きです。

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

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

[【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る　パート１](https://qiita.com/kousaku-maron/items/42f801df2b87e37b1d91)
[【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る　パート3](https://qiita.com/kousaku-maron/items/3887e57c5c5519abb46d)


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

# 前回のおさらい

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

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

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

# 曲の再生

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

ミュージック自作は流石に自作できないので、**完全商用フリーで使用できる音楽素材が集まっている[魔王魂](https://maoudamashii.jokersounds.com/)より、曲を拝借します**。

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

<img width="662" alt="スクリーンショット 2019-02-06 20.27.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/dc7b357f-d264-b419-902e-38f64749caa4.png">

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


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

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

<img width="397" alt="スクリーンショット 2019-02-06 20.33.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/be239c81-1828-f832-6110-99b974fca874.png">

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

### GameManagerから曲を再生させる

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

```c#:GameManager
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にセットする**。

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

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

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

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

    ...
}
```

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

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

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

<img width="418" alt="スクリーンショット 2019-02-06 20.54.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/8f72a030-a655-4ffc-0f3e-b9a26a4c0dc8.png">


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

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

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

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


```c#:GameManager.cs
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)以内なら良(成功)になります**。

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

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

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

```c#
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も作る**。

```c#
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()`は処理が重いため使用しません。

```c#
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座標を少し後ろにずらしてください**。

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

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

<img width="983" alt="スクリーンショット 2019-02-07 20.39.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/a48d8352-6e84-cee7-b250-d7ecea1307d4.png">



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

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

### 効果音の準備

効果音も自作できないので、**[魔王魂](https://maoudamashii.jokersounds.com/)より、似た効果音を拝借します**。

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

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

<img width="335" alt="スクリーンショット 2019-02-05 20.06.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/cc52f74a-4bea-b9ea-453b-0ed3948f197b.png">

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

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

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

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

```c#
Subject<Unit> SampleSubject = new Subject<Unit>();

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

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

```c#
// 通知
SampleSubject.OnNext(Unit.default);

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

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

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

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

```c#:GameManager.cs
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を作成する**。

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

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

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

```c#
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にアタッチします_。

```c#:SoundEffectManager.cs
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()`関数を作成する。

```c#
void donPlay() {
  DonPlayer.Stop();
  DonPlayer.Play();
}

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

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

```c#
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を作成してください。

<img width="248" alt="スクリーンショット 2019-02-06 21.29.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/2f30a17a-86c5-51ba-ee2d-f37e6eb26c30.png">

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

<img width="411" alt="スクリーンショット 2019-02-06 21.30.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/a6236d1d-0860-2dda-1a77-1d5a123f8b85.png">


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

<img width="413" alt="スクリーンショット 2019-02-06 21.31.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/76d6edca-ea06-2e7f-5845-ad0dbc61803b.png">

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

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

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

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

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

```c#:GameManager.cs
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にアタッチします_。

```c#:MessageEffectManager.cs
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)後に自動的に非表示にさせているところ**です。

```c#
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を作成してください。

<img width="236" alt="スクリーンショット 2019-02-07 21.28.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/90b92287-3171-b794-e6d3-ca8e6029dde2.png">

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

※_Splite Renderコンポーネントで画像は貼れます_。

<img width="412" alt="スクリーンショット 2019-02-07 21.30.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/1e1989b0-892d-bee3-1a06-173851aed992.png">

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

<img width="412" alt="スクリーンショット 2019-02-07 21.32.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/189146/ef27f623-524f-b613-39eb-349247955177.png">



# 動きを確認する

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

![taiko2.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/189146/5a8f0d46-27b6-45e9-75c2-65f7845767cc.gif)

# 最後に

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

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