前回、曲をボタンで切り替えるBgmControllerとシステム時間を取得表示するClockを作成しました。これをいいかんじにいじって、特定時間帯に特定の曲を流してくれるようにしていったので、やったことを記録しておきます。
何時にどの曲を流すのか設定する場所をつくる
現在、インスペクターに曲を入れる枠をつくって曲をぶちこんでいますが、何時に再生するのかを入力する枠をつくりたいです。

BgmControllerに追記しようと思ったけど、コードが長くなってよくわからなくなってきたので、新しいスクリプトをつくることにしました。ScriptsフォルダにBgmSchedulerスクリプトを新規作成。

とりあえず曲を入れたいので、先にクラスと変数を作ることにします。時&分&曲を入れられるBgmScheduleItemクラスを作って、その3点セットをいっぱい入れられるbgmScheduleItems変数を錬成しています。
using UnityEngine;
public class BgmScheduler : MonoBehaviour
{
[System.Serializable]
public class BgmScheduleItem // スケジュールつき曲用クラス
{
public int hour; // 時
public int minute; // 分
public AudioClip clip; // オーディオクリップ
}
[SerializeField] private BgmScheduleItem[] _bgmScheduleItems; // Bgmのスケジュールリストできたら上書き保存。
Unityに戻って、ヒエラルキーでBGMを選択した状態でインスペクターの下のほうにあるAdd Component(コンポーネントを追加)→BgmSchedulerを検索してクリックで追加します。
追加されたBgmSchedulerの中にBgm Schedule Itemsという項目が表示されているので、右側の枠(たぶん最初は0って入力されてる)に4と入力。Itemsのトグルを全部開くとHour・Minute・Clipの3つがセットになっている枠が4つ表示されています。
Clipの各枠にプロジェクトビューのSoundsフォルダにある曲をドラッグ&ドロップし、その曲を流したい時間をHour(時)とMinute(分)に入力。

時間と曲のセットを入力できました!
自動再生と手動再生を切り替えるボタンを作る
再生方法の切り替えボタンを作成します。ヒエラルキーのCanvasを右クリックしてUI→Buttonを選択。ボタンのオブジェクトができたらオブジェクト名を変更します。ボタンはManualPlayButton、中のテキストオブジェクトはManualPlayButtonTextにしました。
ManualPlayButtonを選択し、ボタンの位置とサイズを変更します。Rect Transformコンポーネントの左の四角いやつ?をクリックしてright topを選択。変更できたらサイズを60×60に、座標をX-40、Y-40にします。

中に表示されている内容を変更します。ManualPlayButtonTextを選択し、TextMeshPro – Text(UI)コンポーネントの中のtext Inputに Manual(改行)OFF と入力。文字がぱっつんぱっつんにならないようフォントサイズを13にし、文字の配置を上下左右ともに中央寄せにしました。Alignmentって日本語版どうなってるんだ?

ゲームビューの右端にボタンができてればOK。

TimeManagerオブジェクトを作る
前回、clockコンポーネントをclockText(時間表示するテキストオブジェクト)に直接アタッチしたけど、BgmControllerもclockコンポーネントを使いたいので、独立したオブジェクトを作ってアタッチしなおします。
ヒエラルキーで右クリック→Create Emptyで空のオブジェクトを作成。

名前はclockにしようと思っていたけど、時計の3Dモデルとか置いたらなんなのかわからんくなるやんけーと気づいたので、TimeManagerにしました。

そうすると今度はclockコンポーネントの名前もTimeManagerにしたほうがいい気がしてきました。スクリプトファイルは後から名前を変更すると良くないらしいので、プロジェクトタブ右クリック→作成→MonoBehaviour Scriptで新規スクリプトを作成し、名前をTimeManagerに変更します。

できたらファイルを開いて、中身を書きます。
using System;
using System.Collections;
using UnityEngine;
public class TimeManager : MonoBehaviour
{
public static event Action<DateTime> OnTimeChanged;
private Coroutine _clockRoutine;
private DateTime _now;
void Start()
{
_clockRoutine = StartCoroutine(UpdateClock()); // コルーチン開始
}
void OnDestroy()
{
if (_clockRoutine != null) StopCoroutine(_clockRoutine); // コルーチン停止
}
// 時刻を更新するコルーチン
IEnumerator UpdateClock()
{
while (true)
{
_now = DateTime.Now;
OnTimeChanged?.Invoke(_now); // 時刻を通知
yield return new WaitForSecondsRealtime(1f); // 1秒待機してもっかい取得から ※WaitForSecondsRealtimeは現実時間
}
}
}clockのスクリプトをコピペして、少しだけ変更しました。OnDestroyでオブジェクトが消えたときにコルーチンが止まるようにしたのと、時計のUI表示切替をしていたのを消して時刻を通知(OnTimeChanged?.Invoke(_now);)するようにしています。UIの表示切替は別のスクリプトでやろうかなーと思います。
スクリプトを書いたら、TimeManagerオブジェクトを選択してAdd ConponentからTimeManagerスクリプトを追加。

ClockTextオブジェクトにアタッチしていたClockコンポーネントは削除しておきます。コンポーネント名の右にあるおだんごアイコンから削除できるよ。

ついでにUIManagerオブジェクトも作る
先ほど時計のUI表示切替するコードを消しました。UIの表示切替をするスクリプトを別途作ります。
ひとまずUIのオブジェクトをぶち込める枠がほしいので、変数だけのスクリプトを書きます。プロジェクトを右クリック→作成→MonoBehaviour Scripを選択し、ファイル名をUIManagerにします。中身はこんな感じ。
UIManager.cs
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIManager : MonoBehaviour
{
[SerializeField] private Button _playButton;
[SerializeField] private Button _nextButton;
[SerializeField] private Button _prevButton;
[SerializeField] private TextMeshProUGUI _clockText;
[SerializeField] private TextMeshProUGUI _playButtonText;
[SerializeField] private TextMeshProUGUI _manualPlayButtonText;
}できたら保存。ヒエラルキー右クリックで作成→空のオブジェクトを選択し、作成されたオブジェクトの名前をUIManagerに変更します。UIManagerを選択した状態でインスペクターのAdd conponentからUIManagerを検索して追加します。

UIManagerコンポーネントの中にある枠に、ヒエラルキーの対応するオブジェクトをドラッグ&ドロップします。名前が同じやつを入れればいいんだけど、ややこしくて発狂するのでお茶を飲みながら入れます。こういうのダメなんですよね

スクリプトを書く
設定した時間に沿って曲が流れるようにスクリプトを書き替えます。
こういうイメージでうごかしたいです↓
⏰TimeManager「9:00になったよー」
🖥️UIManager「9:00になったらしい…時計の表示変えよ」
😽BgmController「9:00になったらしい…9:00に流す曲ってどれ?」
📝BgmScheduler「これだよ💁🎵」
😽BgmController「この曲を再生するよー!」\🎵/
😽BgmController「はっ一時停止ボタンが押された!曲を一時停止するよー」
🖥️UIManager「一時停止したらしい…ボタンの表示変えよ」
BgmScheduler.cs (時間を教えたら流すべき曲を教えてくれる)
using System.Linq;
using UnityEngine;
public class BgmScheduler : MonoBehaviour
{
[System.Serializable]
public class BgmScheduleItem // スケジュールつき曲用クラス
{
public int hour; // 時
public int minute; // 分
public AudioClip clip; // オーディオクリップ
}
[SerializeField] private BgmScheduleItem[] _bgmScheduleItems; // Bgmのスケジュールリスト
void Awake()
{
// bgmScheduleItemsが空の場合は中断
if (IsBgmScheduleItemsEmpty)
{
Debug.LogWarning($"BgmScheduler.Awake : _bgmScheduleItemsがありません");
return;
}
// bgmScheduleItemsの時刻不正チェック
foreach (var Item in _bgmScheduleItems)
{
if (!IsValidTime(Item.hour, Item.minute)) Debug.LogWarning($"bgmScheduleItemsに不正な時刻の曲があります: {Item.hour}:{Item.minute} {Item.clip?.name}");
}
// bgmScheduleItemsの時刻重複チェック
var duplicates = _bgmScheduleItems
.GroupBy(s => (s.hour, s.minute))
.Where(g => g.Count() > 1);
// 重複コンソールログ
foreach (var group in duplicates)
{
Debug.LogWarning($"BgmScheduleに同時刻の曲が複数あります: {group.Key.hour:D2}:{group.Key.minute:D2} 件数: {group.Count()}");
foreach (var item in group)
{
Debug.LogWarning($" - {item.clip?.name}");
}
}
// bgmScheduleItemsを正しい時刻のみで昇順ソート(不正時刻は除外、配列は上書き)
_bgmScheduleItems = _bgmScheduleItems
.Where(s => IsValidTime(s.hour, s.minute))
.OrderBy(s => s.hour)
.ThenBy(s => s.minute)
.ToArray();
}
// 指定した時刻に再生すべきオーディオクリップを返却
public AudioClip GetClipAtTime(int hour,int minute)
{
// 最後に一致したものを返却してます。完全一致じゃない
AudioClip result = null; // result初期化
if (IsBgmScheduleItemsEmpty) return result; // BgmScheduleItemsが空の場合はnullを返却
int targetMinutes = hour * 60 + minute; // 引数の時刻を分数に変換(1:30→90)
// bgmScheduleItemsの中身を順番にチェック
foreach (var item in _bgmScheduleItems)
{
int itemMinutes = item.hour * 60 + item.minute; // bgmScheduleItems内の時刻を分数に変換
if (itemMinutes <= targetMinutes)
{
result = item.clip; // bgmScheduleItems側の時刻が引数の未来じゃなければ、クリップをresultに入れる
}
else
{
break; // foreachを抜ける(ソート済みなので、elseになったら未来の時刻)
}
}
return result; // resultを返却
}
// 時刻正常フラグ
private bool IsValidTime(int hour, int minute)
{
return hour >= 0 && hour < 24 &&
minute >= 0 && minute < 60;
}
// 存在チェック
private bool IsBgmScheduleItemsEmpty => _bgmScheduleItems == null || _bgmScheduleItems.Length == 0; // bgmScheduleItemsが空
}BgmController.cs(曲を再生する)
using System;
using UnityEngine;
public enum BgmMode{ Auto, UserSelect } // BGMモード
public enum BgmState{ Playing, Paused } // BGM再生状態
public class BgmController : MonoBehaviour
{
// 曲情報クラス
[System.Serializable]
public class TrackData
{
public string title; // 曲名
public AudioClip clip; // クリップ
}
// コンポーネント
[SerializeField] private AudioSource _audioSource;
[SerializeField] private BgmScheduler _scheduler;
// 曲情報
[SerializeField] private TrackData[] _tracks;
// イベント
public static event Action<BgmState> OnBgmStateChanged; // BGM再生状態変更イベント
public static event Action<BgmMode> OnBgmModeChanged; // BGMモード切替イベント
private BgmMode _bgmMode; // BGMモード
private int _currentTrackIndex; // 現在の曲のインデックス番号
// Awakeはゲーム開始前に実行される。自分の準備におすすめ
void Awake()
{
// コンポーネント取得
if (!_audioSource) _audioSource = GetComponent<AudioSource>();
if (!_scheduler) _scheduler = GetComponent<BgmScheduler>();
// 初期値設定
_bgmMode = BgmMode.Auto; // BGMモード初期値
_currentTrackIndex = 0; // 現在の曲のインデックス番号
}
void Start()
{
// 存在チェック
if (!_audioSource) Debug.LogWarning($"BgmController.Start : _audioSourceがありません");
if (!_scheduler) Debug.LogWarning($"BgmController.Start : _schedulerがありません");
if (IsTracksEmpty) Debug.LogWarning($"BgmController.Start : _tracksがありません");
// 最初の曲をセット
if (!IsTracksEmpty)
{
_audioSource.clip = _tracks[_currentTrackIndex].clip;
if (IsBgmModeAuto) HandleTimeChanged(DateTime.Now); // Autoモードなら、今の時間の曲(最初の判定だけ自分で)
}
// BGMモード通知
OnBgmModeChanged?.Invoke(_bgmMode);
// 再生(再生状態通知もここで)
PlayAndNotify();
}
private void OnEnable()
{
// イベントに自分の関数を登録
TimeManager.OnTimeChanged += HandleTimeChanged; // 時間の通知
}
void OnDestroy()
{
// イベントの登録解除
TimeManager.OnTimeChanged -= HandleTimeChanged;
}
// BgmMode切替
public void ToggleBgmMode()
{
BgmMode nextMode = (_bgmMode == BgmMode.UserSelect) ? BgmMode.Auto : BgmMode.UserSelect;
_bgmMode = nextMode; // モード変更
if (nextMode == BgmMode.Auto) HandleTimeChanged(DateTime.Now); // Autoモードなら曲を変更
OnBgmModeChanged?.Invoke(_bgmMode); // モード変更を通知
}
// 再生・一時停止切替
public void TogglePlayPause()
{
// 再生中の場合は一時停止、一時停止中の場合は再生する
if (_audioSource.isPlaying)
PauseAndNotify();
else
PlayAndNotify();
}
// 次の曲へ
public void ToNextTrack()
{
if (!IsBgmModeSelect) return; // UserSelectモード以外なら中断
AudioClip clip = GetRelativeClip(1); // 次の曲を取得
ShiftTrack(clip);
}
// 前の曲へ
public void ToPrevTrack()
{
if (!IsBgmModeSelect) return; // UserSelectモード以外なら中断
AudioClip clip = GetRelativeClip(-1); // 次の曲を取得
ShiftTrack(clip); // 前の曲へ
}
// 再生
private void PlayAndNotify()
{
_audioSource.Play(); // 再生
OnBgmStateChanged?.Invoke(BgmState.Playing); // 再生を通知
}
// 一時停止
private void PauseAndNotify()
{
_audioSource.Pause(); // 一時停止
OnBgmStateChanged?.Invoke(BgmState.Paused); // 一時停止を通知
}
// 時間が変わった時
private void HandleTimeChanged(DateTime now)
{
if (!IsBgmModeAuto) return; // Autoモード以外の場合は中断
AudioClip clip = _scheduler.GetClipAtTime(now.Hour, now.Minute); // 再生すべき曲を取得
if (clip != null && clip != _audioSource.clip) ShiftTrack(clip); // 取得Clipが現在の曲と相違している場合は、曲を変更
}
// 曲を変更
private void ShiftTrack(AudioClip clip)
{
bool wasPlaying = _audioSource.isPlaying; // 再生中だったかどうかチェック
_audioSource.clip = clip; // 曲を変更
_currentTrackIndex = GetCurrentTrackIndex(); // インデックス番号取得
if (_currentTrackIndex < 0)
{
Debug.LogWarning($"BgmController.ShiftTrack : clip {clip?.name} が_tracksにありません");
_currentTrackIndex = 0;
}
if (wasPlaying) PlayAndNotify(); // 再生中だった場合は再生
}
// tracksのインデックス順に曲を進めてオーディオクリップを返却
private AudioClip GetRelativeClip(int offset)
{
if (IsTracksEmpty) return null; // _tracksに1曲もなければ中断
int index = _currentTrackIndex += offset; // offsetを加算
index = (index + _tracks.Length) % _tracks.Length; // 循環インデックス処理 ※indexをtracksの範囲内にする
AudioClip clip = _tracks[index].clip; // 該当の曲を取得
return clip;
}
// 現在の曲のインデックス番号を返却
private int GetCurrentTrackIndex()
{
int i = GetTrackIndex(_audioSource.clip);
return i;
}
// _tracks内でのインデックス番号を返却
private int GetTrackIndex(AudioClip clip)
{
if (IsTracksEmpty) return -1; // _tracksが空なら-1を返却
// _tracksの中をチェックして、clipと一致があればインデックス番号を返却
for (int i = 0; i < _tracks.Length; i++)
{
if (_tracks[i].clip == clip)
return i;
}
return -1; // 見つからなかった場合、-1を返却
}
// BgmModeチェック
private bool IsBgmModeSelect => _bgmMode == BgmMode.UserSelect;
private bool IsBgmModeAuto => _bgmMode == BgmMode.Auto;
// 存在チェック
private bool IsTracksEmpty => _tracks == null || _tracks.Length == 0;
}
UIManager.cs(UIを変更する)
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
[SerializeField] private Button _playButton;
[SerializeField] private Button _nextButton;
[SerializeField] private Button _prevButton;
[SerializeField] private TextMeshProUGUI _clockText;
[SerializeField] private TextMeshProUGUI _playButtonText;
[SerializeField] private TextMeshProUGUI _manualPlayButtonText;
private void Awake()
{
// 存在チェック
if (!_playButton) Debug.LogWarning("UIManager.Awake : _playButtonがありません。");
if (!_nextButton) Debug.LogWarning("UIManager.Awake : _nextButtonがありません。");
if (!_prevButton) Debug.LogWarning("UIManager.Awake : _prevButtonがありません。");
if (!_clockText) Debug.LogWarning("UIManager.Awake : _clockTextがありません。");
if (!_playButtonText) Debug.LogWarning("UIManager.Awake : _playButtonTextがありません。");
if (!_manualPlayButtonText) Debug.LogWarning("UIManager.Awake : _manualPlayButtonTextがありません。");
}
private void OnEnable()
{
// イベントに自分の関数を登録
TimeManager.OnTimeChanged += HandleTimeChanged; // 時刻変更
BgmController.OnBgmStateChanged += HandleBgmStateChanged; // BGM再生状態変更
BgmController.OnBgmModeChanged += HandleBgmModeChanged; // BGMモード変更
}
private void OnDisable()
{
// イベントの登録解除
TimeManager.OnTimeChanged -= HandleTimeChanged; // 時刻変更
BgmController.OnBgmStateChanged -= HandleBgmStateChanged; // BGM再生
BgmController.OnBgmModeChanged -= HandleBgmModeChanged; // BGMモード変更
}
// 時刻変更時
private void HandleTimeChanged(DateTime time)
{
if (!_clockText) return;
_clockText.text = DateTime2StrHHmm(time);
}
// BGM再生状態変更時
private void HandleBgmStateChanged(BgmState state)
{
if (!_playButtonText)
{
Debug.LogWarning("UIManager.HandleBgmStateChanged : _playButtonTextがありません。");
return;
}
if (state == BgmState.Playing)_playButtonText.text = "Pause"; // 再生中の場合はPauseと表示
if (state == BgmState.Paused)_playButtonText.text = "Play"; // 一時停止した場合はPlayと表示
}
// BGMモード変更時
private void HandleBgmModeChanged(BgmMode mode)
{
if (!_manualPlayButtonText) Debug.LogWarning("UIManager.HandleBgmModeChanged : _manualPlayButtonTextがありません。");
if (!_nextButton) Debug.LogWarning("UIManager.HandleBgmModeChanged : _nextButtonがありません。");
if (!_prevButton) Debug.LogWarning("UIManager.HandleBgmModeChanged : _prevButtonがありません。");
// BGMモードのボタンのUI
if (!_manualPlayButtonText) return;
if (mode == BgmMode.Auto) _manualPlayButtonText.text = "Manual\nON"; // AutoモードのときはManual ONと表示
if (mode == BgmMode.UserSelect) _manualPlayButtonText.text = "Manual\nOFF"; // UserSelectモードのときはManual OFFと表示
// UserSelectのときだけ曲送りのボタンを有効化
bool isActive = (mode == BgmMode.UserSelect);
if (_nextButton != null) _nextButton.interactable = isActive;
if (_prevButton != null) _prevButton.interactable = isActive;
}
// 時刻を文字列に変換
public static string DateTime2StrHHmm (DateTime time)
{
string str = time.ToString("HH:mm");
return str;
}
}ボタンクリック時にスクリプトが呼ばれるようにする
スクリプトいじってるうちに関数名とかもめっちゃ書き換えたので、すべてのボタンのクリック時イベントをチェックします。ボタン4つしかないから大丈夫だけど、ボタンが5つ以上あるプロジェクトでは関数名をめちゃくちゃ考えて書かないと後々めんどくさいことになりそう。
ヒエラルキーのPlayButtonを選択し、クリック時イベントの左下の枠にBGMオブジェクトをドラッグ&ドロップ。右のプルダウンでBgmController.TogglePlayPauseを選択します。

NextButtonはBgmController.ToNextTrack、PrevButtonはBgmController.ToPrevTrack、ManualPlayButtonにはBgmController.ToggleBgmModeを設定します。
これでOK。ゲームを再生すると、ゲーム開始時点から曲が流れます。時間に応じて自動で選曲され、曲送りのボタンはグレーアウトしていて使えなくなっています。Pauseボタンで音楽を一時停止できます。

Manual ONボタンを押すとボタンのグレーアウトが解除されて、曲送りも普通にできるようになります。ボタンの表示がややこしいけど下の画像がManualの状態。気が向けばボタンは画像にしてわかりやすくしたらよいです。やり方知らんけどButtonオブジェクトを見たところImageとかいうコンポーネントがくっついていたのでたぶんできるでしょう……。

ゲームを再生した後は忘れずに停止します。
最後に、Manualのボタン右上だとサムネ見切れそうだなと思ったので適当に真ん中に移動してスクショをとって完成!

気が向けば、次は画面をかわいくしていけたらいいなと思います。ボタンを先にいじってもいいし、テーブルとか椅子とかのモデルを作って置けたらなおよいですね。
