Unity初心者が2Dタワーディフェンスゲームを制作しています。
今回は、複数のステージを実装して、タイトル画面からステージを選択できるようにします。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
ステージマップの作成
このゲームは5つのステージで構成しようと思っています。今のところ、ステージ1のマップしかありませんのでステージ2〜5のマップを作成します。
まず、城オブジェクト(Fortress
)がMapオブジェクトの外にあるので、配下に移動します。

Mapオブジェクトの名前を「Map1」に変更して、ステージ1のマップだとわかるようにします。
Map1オブジェクトを複製して、名前を「Map2」に変更します。これがステージ2のマップになります。

Map1を一旦非表示にして、Map2のタイルを修正してマップを作っていきます。
マップの作り方を完全に忘れてしまっていたので過去記事を参照しながらやりました。
- タイル画像のインポート
- Tile Paletteに画像を登録
- Grid_BaseのTilemapに下地となるタイルを配置
- Grid_WayのTilemapに道や障害物のタイルを配置
- 城(Fortress)の位置を調整
こんな感じのマップができました。ステージ2は敵が2ヶ所から発生するみたいです。

同じように、ステージ3、4、5のマップも作ります。
ステージに関する情報はScriptableObjectで管理しようと思います。で、マップオブジェクトをそのScriptableObjectにアサインしたいのですが、できません。ScriptableObjectにはヒエラルキー上のオブジェクトはアサインできないという制約があるようなのです。
この問題を回避するために、マップオブジェクトをプレハブ化します。

ヒエラルキーのMap1〜Map5は削除します。
マップオブジェクトをプレハブ化したことにより、マップの配下にあるFortressオブジェクトもプレハブになりました。FortressにアタッチしているFortressControllerのhpSliderへのアサインが無効になってしまうので、スクリプトでアサインします。
FortressControllerスクリプトを修正します。
using UnityEngine;
using UnityEngine.UI;
public class FortressController : MonoBehaviour
{
// ...省略...
private void Awake()
{
// シングルトンパターンの実装
if (Instance == null)
{
Instance = this;
}
else if (Instance != this)
{
Destroy(gameObject);
}
// ここから 追加
// HPスライダーをアサイン
if (hpSlider == null)
{
var sliderObj = GameObject.Find("FortressHpSlider");
if (sliderObj != null)
{
hpSlider = sliderObj.GetComponent<Slider>();
}
}
// ここまで
}
// ...省略...
}
敵の移動経路の作成
ステージ2〜5用の敵の移動経路を作成します。
これまた、作り方を忘れていたので過去記事を参照しました。
- 「Assets > Prefabs > Path_1-1」をヒエラルキーにドラッグ&ドロップしてPathオブジェクトを作成
- Pathオブジェクトを右クリックして「Prefab > Unpack Completely」を選択して、プレハブとの紐付けを解除
- Pathオブジェクトの配下にあるPosition*オブジェクトを追加、削除、位置調整する
- インスペクターの「Path Data (Script) > Path Points」にPosition*オブジェクトをアサインして経路を作る
完成したPathオブジェクトを「Assets/Prefabs」にドラッグ&ドロップしてプレハブ化し、名前を「Path_2-1」のように修正します。
ヒエラルキーのPathオブジェクトは削除します。
ウェーブ情報の追加
ステージ2〜5のウェーブ情報を設定します。
「Assets > Data > WaveSetting」のインスペクターでWave Data Listに各ステージのウェーブ情報を追加します。
ステージ選択機能の実装
ステージデータをScriptableObjectで管理する
StageSettingスクリプトを作成します。
using System;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "StageSetting", menuName = "ScriptableObject/Stage Setting")]
public class StageSetting : ScriptableObject
{
// ステージのデータを格納するリスト
[SerializeField] private List<StageData> stageDataList = new List<StageData>();
public List<StageData> StageDataList => stageDataList;
// ステージのデータ構造
[Serializable]
public class StageData
{
[SerializeField] private int stageId; // ステージのID
public int StageId => stageId;
[SerializeField] private GameObject stageMap; // ステージのマップ
public GameObject StageMap => stageMap;
}
}
DBManagerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DBManager : MonoBehaviour
{
// ...省略...
// ここから 追加
// ステージの設定データを保持する変数
[SerializeField] private StageSetting stageSetting;
public StageSetting StageSetting => stageSetting;
// ここまで
void Awake()
{
// ...省略...
}
}
「Assets > Data」下で右クリック「Create > ScriptableObject > Stage Setting」を選択して、StageSettingアセットを作成します。

StageSettingのインスペクターで「Stage Data List」に項目を追加して、ステージ1〜5のStage IdとStage Mapを設定します。

ステージの選択
タイトル画面のステージ選択ボタンを押すと選択したステージが開始するようにします。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
[SerializeField] private TextMeshProUGUI stageText; // ステージIDのテキスト
[SerializeField] private StageSetting stageSetting; // StageSettingの参照
private GameObject currentStageMap; // 現在表示中のマップ
public TurretGenerator turretGenerator; // TurretGeneratorの参照
// ここまで
// ...省略...
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
CurrentState = newState;
switch (newState)
{
case GameState.Title:
// ...省略...
case GameState.Preparing:
// ...省略...
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
Time.timeScale = 1; // 時間を通常速度に戻す
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここから 追加
// ウェーブ開始
var spawner = EnemySpawner.Instance;
if (spawner != null)
{
spawner.StartWaves();
}
// ここまで
break;
case GameState.Paused:
// ...省略...
case GameState.GameOver:
// ...省略...
case GameState.Victory:
// ...省略...
}
}
// ...省略...
/// <summary>
/// ステージを選択する
/// </summary>
/// <param name="stageId">選択するステージのID</param>
public void SelectStage(int stageId)
{
currentStageId = stageId; // ステージIDを設定
Debug.Log($"ステージ {stageId} が選択されました。");
// ここから 追加
// ステージIDを表示
if (stageText != null)
{
stageText.text = stageId.ToString();
}
// 既存のマップを削除
if (currentStageMap != null)
{
Destroy(currentStageMap);
currentStageMap = null;
}
// ステージデータから該当マップを取得して表示
var stageData = stageSetting.StageDataList.Find(data => data.StageId == stageId);
if (stageData != null && stageData.StageMap != null)
{
currentStageMap = Instantiate(stageData.StageMap);
// Gridコンポーネントをセット
var grid = currentStageMap.GetComponentInChildren<Grid>();
if (grid != null && turretGenerator != null)
{
turretGenerator.grid = grid;
}
// 建築不可能なタイルマップの設定
// Grid_Wayオブジェクトを探す
var gridWay = currentStageMap.transform.Find("Grid_Way");
if (gridWay != null)
{
// Grid_Wayの子のTilemapオブジェクトを探す
var tilemapObj = gridWay.transform.Find("Tilemap");
if (tilemapObj != null)
{
var tilemap = tilemapObj.GetComponent<UnityEngine.Tilemaps.Tilemap>();
if (tilemap != null && turretGenerator != null)
{
turretGenerator.unbuildableTilemap = tilemap;
}
}
}
}
else
{
Debug.LogWarning($"ステージID {stageId} に対応するマップが見つかりません。");
}
// EnemySpawnerのウェーブリストを更新
var spawner = EnemySpawner.Instance;
if (spawner != null)
{
spawner.SetStageWaves(stageId);
}
// ここまで
// ゲーム状態を Title から Preparing に変更
if (CurrentState == GameState.Title)
{
ChangeState(GameState.Preparing);
}
}
// ...省略...
}
- 4つの変数を定義しました。
stageText
: ゲーム画面上でステージ番号を表示するTMPオブジェクト。stageSetting
: StageSettingスクリプタブルオブジェクトの参照。currentStageMap
: 現在のマップ。turretGenerator
: TurretGeneratorスクリプトの参照。
ChangeState()
内で、ゲーム状態がPlaying
のときにウェーブを開始する処理を追加しました。SelectStage()
に、以下の処理を追加しました。- ゲーム画面にステージ番号を表示する。
- 直前にプレイしていたステージのマップを削除。
- これからプレイするステージのマップを表示。
- Gridコンポーネント(
turretGenerator.grid
)をセット。 - 建築不可能なタイルマップ(
turretGenerator.unbuildableTilemap
)の設定。
- Gridコンポーネント(
EnemySpawnerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic; // Listを使うために必要
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using TMPro; // TextMeshProを使うために必要
public class EnemySpawner : MonoBehaviour
{
// ...省略...
private void Start()
{
// ここから 削除
// 現在のステージに対応するウェーブをフィルタリング
//currentStageWaves = waveSetting.WaveDataList.FindAll(wave => wave.StageId == gameManager.CurrentStageId);
//if (currentStageWaves.Count == 0)
//{
// Debug.LogError($"ステージID {gameManager.CurrentStageId} に対応するウェーブが見つかりません。");
// return;
//}
// ここまで
// ボタンにクリックイベントを登録
nextWaveButton.onClick.AddListener(SkipToNextWave);
// ここから 削除
//StartCoroutine(ManageWaves());
// ここまで
}
// ...省略...
// ここから 追加
/// <summary>
/// ステージのウェーブを設定する
/// </summary>
/// <param name="stageId">ステージID</param>
public void SetStageWaves(int stageId)
{
currentWaveId = 0;
currentStageWaves = waveSetting.WaveDataList.FindAll(wave => wave.StageId == stageId);
}
/// <summary>
/// ウェーブを開始する
/// </summary>
public void StartWaves()
{
StopAllCoroutines(); // 念のため前のコルーチンを停止
StartCoroutine(ManageWaves());
}
// ここまで
}
Start()
から、以下の処理を削除しました。- 現在のステージに対応するウェーブを探す処理を削除しました。この処理は
SetStageWaves()
に移動します。 ManageWaves()
コルーチンを開始する処理を削除しました。この処理はStartWaves()
に移動します。
- 現在のステージに対応するウェーブを探す処理を削除しました。この処理は
SetStageWaves()
メソッドを追加しました。GameManager.SelectStageCoroutine
から呼び出されます。StartWaves()
メソッドを追加しました。GameManager.ChangeState
でゲーム状態がPlaying
のときに呼び出されます。
TurretGeneratorスクリプトを修正します。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.EventSystems;
public class TurretGenerator : MonoBehaviour
{
// ...省略...
// ここから 修正
//[SerializeField] private Grid grid; // グリッドコンポーネント
[SerializeField] public Grid grid; // グリッドコンポーネント
//[SerializeField] private Tilemap unbuildableTilemap; // 建築不可能なタイルマップ
[SerializeField] public Tilemap unbuildableTilemap; // 建築不可能なタイルマップ
// ここまで
// ...省略...
}
GameManagerからアクセスできるように、grid
とunbuildableTilemap
のアクセス修飾子をprivate
からpublic
に変更しました。
GameManagerのインスペクターで以下の変数にオブジェクトをアサインします。
- 「Game Manager (Script) > Stage Text」にStageTextオブジェクトをアサイン。
- 「Game Manager (Script) > Stage Setting」にStageSettingクリプタブルオブジェクトをアサイン。
- 「Game Manager (Script) > Turret Generator」にTurretGeneratorオブジェクトをアサイン。
ゲームの状態をリセット
現状では、ステージ1をクリアしたあとにステージ2を開始した場合に、マップは更新されますが、それ以外のオブジェクトはステージ1をクリアした直後の状態のまま残ってしまいます。これらを初期化してキレイな状態で次のステージをプレイできるようにします。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
/// <summary>
/// ステージを選択する
/// </summary>
/// <param name="stageId">選択するステージのID</param>
public void SelectStage(int stageId)
{
// ...省略...
// ここから 追加
// ゲーム全体の初期化
InitializeGame();
// ここまで
// ゲーム状態を Title から Preparing に変更
if (CurrentState == GameState.Title)
{
ChangeState(GameState.Preparing);
}
}
// ここから 追加
/// <summary>
/// ゲーム全体の初期化(スコア・ゴールド・城HP・敵・砲台などのリセット)
/// </summary>
private void InitializeGame()
{
// スコア初期化
if (ScoreManager.Instance != null)
ScoreManager.Instance.ResetScore();
// ゴールド初期化
if (GoldManager.Instance != null)
GoldManager.Instance.ResetGold();
// 城HP初期化
if (FortressController.Instance != null)
FortressController.Instance.ResetHp();
// 敵の生成をリセット
var spawner = FindObjectOfType<EnemySpawner>();
if (spawner != null)
spawner.ResetSpawner();
// 砲台の設置情報をリセット
var turretGenerator = FindObjectOfType<TurretGenerator>();
if (turretGenerator != null)
turretGenerator.ResetTurretPlacements();
// tileHighlighterを非表示
if (tileHighlighter != null)
tileHighlighter.SetActive(false);
// マップ上の敵・HPバー・砲台・インジケータ削除
foreach (var enemy in GameObject.FindGameObjectsWithTag("Enemy"))
Destroy(enemy);
foreach (var hpBar in GameObject.FindGameObjectsWithTag("HpBar"))
Destroy(hpBar);
foreach (var turret in GameObject.FindGameObjectsWithTag("Turret"))
Destroy(turret);
foreach (var indicator in GameObject.FindGameObjectsWithTag("Indicator"))
Destroy(indicator);
}
// ここまで
// ...省略...
}
SelectStage()
内で、ゲームの初期化をするメソッドInitializeGame()
を実行します。InitializeGame()
メソッドを追加します。- スコアの初期化
- ゴールドの初期化
- 城のHPの初期化
- 敵の生成の初期化
- 砲台の設置情報の初期化
- タイルの強調表示の初期化
- 敵、敵のHPバー、砲台、砲台のインジケータを削除
EnemySpawnerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic; // Listを使うために必要
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using TMPro; // TextMeshProを使うために必要
public class EnemySpawner : MonoBehaviour
{
// ...省略...
// ここから 追加
/// <summary>
/// 敵の生成をリセットする
/// </summary>
public void ResetSpawner()
{
StopAllCoroutines(); // すべてのコルーチンを停止
currentWaveId = 0; // 現在のウェーブ番号をリセット
aliveEnemyCount = 0; // 生存している敵の数をリセット
timeRemaining = 0f; // 次のウェーブまでの残り時間をリセット
isWaveSkipped = false; // ウェーブをスキップしたかどうかのフラグを初期化
// ウェーブ表示用のテキストが設定されていれば更新
if (waveText != null)
waveText.text = currentStageWaves != null && currentStageWaves.Count > 0
? $"{currentWaveId + 1}/{currentStageWaves.Count}"
: "";
// 次のウェーブまでのカウントダウンテキストが設定されていれば更新
if (timeToNextWaveText != null)
timeToNextWaveText.text = $": {Mathf.CeilToInt(timeRemaining)}s";
// カウントダウン用のコルーチンが動作していれば停止し、参照を解除
if (countdownCoroutine != null)
{
StopCoroutine(countdownCoroutine);
countdownCoroutine = null;
}
}
// ここまで
}
敵の生成関連の変数をリセットするResetSpawner()
メソッドを追加します。
FortressControllerスクリプトを修正します。
using UnityEngine;
using UnityEngine.UI;
public class FortressController : MonoBehaviour
{
// ...省略...
// ここから 追加
/// <summary>
/// 防衛拠点のHPをリセットする
/// </summary>
public void ResetHp()
{
currentHp = maxHp;
UpdateHpSlider();
}
// ここまで
}
城のHPをリセットするResetHp()
メソッドを追加しました。
GoldManagerスクリプトを修正します。
using UnityEngine;
using TMPro; // TextMeshPro を使うために必要
using DG.Tweening; // DOTween を使うために必要
public class GoldManager : MonoBehaviour
{
// ...省略...
// ここから 追加
/// <summary>
/// ゴールドをリセットする
/// </summary>
public void ResetGold()
{
currentGold = initialGold;
UpdateGoldDisplay();
}
// ここまで
}
現在のゴールドをリセットするResetGold()
メソッドを追加しました。
ScoreManagerスクリプトを修正します。
using UnityEngine;
using TMPro; // TextMeshPro を使うために必要
using DG.Tweening; // DOTween を使うために必要
public class ScoreManager : MonoBehaviour
{
// ...省略...
// ここから 追加
/// <summary>
/// スコアをリセットする
/// </summary>
public void ResetScore()
{
currentScore = 0;
UpdateScoreDisplay();
}
// ここまで
}
現在のスコアをリセットするResetScore()
メソッドを追加しました。
TurretGeneratorスクリプトを修正します。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.EventSystems;
public class TurretGenerator : MonoBehaviour
{
// ...省略...
// ここから 追加
/// <summary>
/// すべての砲台設置状態をリセット
/// </summary>
public void ResetTurretPlacements()
{
placedTurretCells.Clear();
}
// ここまで
}
砲台が配置されているセルのリストをリセットするResetTurretPlacements()
メソッドを追加しました。
敵、敵のHPバー、砲台、砲台のインジケータにタグを付けます。
これらはすべてプレハブから生成されるので、「Assets > Prefabs」の中にあるプレハブをダブルクリックして、プレハブの編集モードに入ってからタグを設定します。
プレハブ | 付加するタグ |
---|---|
Enemy | Enemy |
HpBar | HpBar |
Turret | Turret |
UpgradeIndicatorCanvas | Indicator |
Enemyにはすでにタグが付いていたので、他の3つのタグを追加しました。
不具合の修正
以上で完了と思ったのですが、実行してみるといくつか不具合があったので修正していきます。
同じステージを繰り返しプレイすると城が消える
同じステージを繰り返しプレイすると、2回目以降に城が表示されないという問題が発生しました。
自力で解決できそうにないので、AI様に聞くことにしました。私はVSCodeにGitHub Copilotを入れております。
この現象の主な原因は、同じステージを連続して開始した場合、前回生成したFortress(やMapプレハブ)が正しく削除されていないためです。 UnityのDestroy()は即時ではなくフレームの最後でオブジェクトを削除するため、 「同じプレハブをInstantiateしても、まだ前回のオブジェクトが残っている」状態になり、 新しいFortressが生成されないことがよくあります。
GitHub Copilot様のお言葉
なるほど、こんなん自力で解決できるわけありませんね。
ステージ選択ボタンが押されたときの処理をコルーチンにして、マップ削除後に1フレーム待つようにすれば解決するようです。やってみます。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
// ここから 追加
using System.Collections; // IEnumeratorを使用するために必要
// ここまで
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
/// <summary>
/// ステージ選択ボタンが押されたときの処理
/// </summary>
/// <param name="stageId">選択されたステージID</param>
public void SelectStage(int stageId)
{
StartCoroutine(SelectStageCoroutine(stageId));
}
// ここまで
// ここから 修正
/// <summary>
/// ステージを選択する
/// </summary>
/// <param name="stageId">選択するステージのID</param>
//public void SelectStage(int stageId)
private IEnumerator SelectStageCoroutine(int stageId)
// ここまで
{
currentStageId = stageId; // ステージIDを設定
Debug.Log($"ステージ {stageId} が選択されました。");
// ステージIDを表示
if (stageText != null)
{
stageText.text = stageId.ToString();
}
// 既存のマップを削除
if (currentStageMap != null)
{
Destroy(currentStageMap);
currentStageMap = null;
// ここから 追加
yield return null; // 1フレーム待つ
// ここまで
}
// ...省略...
}
// ...省略...
}
- ステージ選択ボタンが押されたときに実行されるメソッド
SelectStage()
を作り直しました。SelectStageCoroutine()
をスタートするだけの簡単なメソッドです。 - 既存の
SelectStage()
メソッドは、SelectStageCoroutine()
に名前を変えてコルーチン化します。- 既存のマップを削除する処理の後ろに
yield return null
を追加して1フレーム待ちます。
- 既存のマップを削除する処理の後ろに
以上でうまくいきました。いえい。
最終ウェーブのあとにウェーブが発生してしまう
最終ウェーブの発生後にゲームをポーズして再開すると、またウェーブが発生してしまうという問題が発生しました。AI先生に聞いてみましょう。
この現象は、GameStateがPausedからPlayingに戻るたびにspawner.StartWaves()が呼ばれているためです。 その結果、最終ウェーブ後でも再度ウェーブが始まってしまいます。
GitHub Copilot様のお言葉
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using System.Collections; // IEnumeratorを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
private GameState previousState = GameState.Title; // 前のゲーム状態を保存するための変数
// ここまで
// ...省略...
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
// ここから 追加
previousState = CurrentState; // 前の状態を保存
// ここまで
CurrentState = newState;
switch (newState)
{
case GameState.Title:
// ...省略...
case GameState.Preparing:
// ...省略...
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
Time.timeScale = 1; // 時間を通常速度に戻す
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここから 修正
// Preparing -> Playing のときだけウェーブ開始
if (previousState == GameState.Preparing) // 追加
{ // 追加
var spawner = EnemySpawner.Instance;
if (spawner != null)
{
spawner.StartWaves();
}
} // 追加
// ここまで
break;
case GameState.Paused:
// ...省略...
case GameState.GameOver:
// ...省略...
case GameState.Victory:
// ...省略...
}
}
// ...省略...
}
ゲーム状態がPlaying
に変わると常にウェーブを開始していたのを、Preparing
からPlaying
に変わったときだけ開始するようにしました。
準備中はボタンを非活性化したい
これは不具合ではないのですが、準備中画面で砲台の強化・削除ボタンが押されると厄介なので、ボタンを押せないようにします。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using System.Collections; // IEnumeratorを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
public static GameManager Instance { get; private set; }
// ここまで
// ここから 追加
// シングルトンパターンを実装
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else if (Instance != this)
{
Destroy(gameObject);
}
}
// ここまで
// ...省略...
}
TurretControllerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TurretController : MonoBehaviour
{
// ...省略...
/// <summary>
/// 砲台がクリックされたときの処理
/// </summary>
public void OnTurretClicked()
{
SideBarManager sideBarManager = FindObjectOfType<SideBarManager>(); // SideBarManagerを取得
// ここから 追加
var state = GameManager.Instance.CurrentState;
bool isPlaying = state == GameManager.GameState.Playing;
bool isPreparing = state == GameManager.GameState.Preparing;
// ここまで
// ここから 修正
// 砲台情報パネルの表示
//sideBarManager.ShowTurretInfo(currentTurretData, currentTurretLevelData, true, this);
// 砲台情報パネルの表示(売却ボタンはプレイ中のみ有効)
sideBarManager.ShowTurretInfo(currentTurretData, currentTurretLevelData, isPlaying, this);
// 次のレベルのデータを取得
TurretSetting.TurretLevelData nextLevelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, currentTurretLevelData.Level + 1);
if (nextLevelData != null) // 次のレベルが存在する場合
{
//sideBarManager.ShowTurretUpgradeInfo(currentTurretData, currentTurretLevelData, true); // 強化情報パネルを表示
// 砲台強化情報パネルの表示
sideBarManager.ShowTurretUpgradeInfo(currentTurretData, currentTurretLevelData, isPlaying);
if (isPreparing)
{
sideBarManager.SetUpgradeButtonInteractable(false); // 準備中は必ず無効化
}
}
// ここまで
// ...省略...
}
}
ステージのアンロック機能の実装
ステージ1をクリアしたらステージ2がアンロックされ、ステージ2をクリアしたらステージ3がアンロックされるようにします。
UserDataスクリプト
まず、各ステージのクリア済みフラグを入れておくリストを作成します。ついでに、各ステージのハイスコアも保存できるようにしました。
新規C#スクリプトを作成し、名前を「UserData」にします。
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 各ステージのハイスコアとクリア情報を格納するデータ構造
/// </summary>
[Serializable]
public class StageUserData
{
public int stageId;
public int highScore;
public bool isCleared;
}
/// <summary>
/// ユーザーの進行状況を管理するクラス
/// </summary>
public class UserData : MonoBehaviour
{
// 各ステージのデータリスト
public List<StageUserData> stageUserDataList = new List<StageUserData>();
public static UserData Instance { get; private set; }
// シングルトンパターンを実装
private void Awake()
{
if (Instance == null)
Instance = this;
else if (Instance != this)
Destroy(gameObject);
}
/// <summary>
/// ステージIDからデータを取得
/// </summary>
public StageUserData GetStageData(int stageId)
{
var data = stageUserDataList.Find(d => d.stageId == stageId);
return data;
}
/// <summary>
/// ハイスコアを更新
/// </summary>
public void UpdateHighScore(int stageId, int score)
{
var data = GetStageData(stageId);
if (score > data.highScore)
data.highScore = score;
}
/// <summary>
/// クリア情報を更新
/// </summary>
public void SetStageCleared(int stageId)
{
var data = GetStageData(stageId);
data.isCleared = true;
}
}
StageUserData
クラスを定義し、各ステージに対して、ステージID、ハイスコア、クリアフラグを持たせます。stageUserDataList
はステージごとのデータを保持するリストです。- シングルトンパターンを実装します。
GetStageData()
は、指定されたステージIDに対応するStageUserData
をリストから探して返すメソッドです。UpdateHighScore()
は、指定ステージのハイスコアを更新するメソッドです。SetStageCleared()
は、指定ステージにクリア済みフラグを設定します。
ヒエラルキーに空オブジェクトを作成して、名前を「UserData」にします。
そして、UserDataオブジェクトにUserDataスクリプトをアタッチします。
UserDataオブジェクトのインスペクターで「User Data (Script) > Stage User Data List」に項目を5つ追加します。
各エレメントの「Stage Id」に1〜5を設定します。

ハイスコアとクリア済みフラグの保存
ステージクリアしたらクリア済みフラグを設定するようにスクリプトを修正します。ゲームオーバーまたはステージクリアしたときにハイスコアを更新する処理も入れます。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using System.Collections; // IEnumeratorを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
previousState = CurrentState; // 前の状態を保存
CurrentState = newState;
switch (newState)
{
case GameState.Title:
// ...省略...
case GameState.Preparing:
// ...省略...
case GameState.Playing:
// ...省略...
case GameState.Paused:
// ...省略...
case GameState.GameOver:
Debug.Log("ゲーム状態: GameOver");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(true); // ゲームオーバーパネルを表示
gameOverPanel.transform.SetAsLastSibling(); // ゲームオーバーパネルを最前面に移動
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここから 追加
// ハイスコア記録
if (UserData.Instance != null && ScoreManager.Instance != null)
{
UserData.Instance.UpdateHighScore(currentStageId, ScoreManager.Instance.CurrentScore);
}
// ここまで
break;
case GameState.Victory:
Debug.Log("ゲーム状態: Victory");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
stageClearPanel.SetActive(true); // ステージクリアパネルを表示
stageClearPanel.transform.SetAsLastSibling(); // ステージクリアパネルを最前面に移動
if (scoreText != null && ScoreManager.Instance != null)
scoreText.text = ScoreManager.Instance.CurrentScore.ToString(); // スコア表示
// ここから 追加
// ハイスコアとクリアフラグ記録
if (UserData.Instance != null && ScoreManager.Instance != null)
{
UserData.Instance.UpdateHighScore(currentStageId, ScoreManager.Instance.CurrentScore);
UserData.Instance.SetStageCleared(currentStageId);
}
// ここまで
break;
}
}
// ...省略...
}
ステージのアンロック
ステージ選択ボタンの有効/無効をクリアフラグに応じて切り替えます。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using System.Collections; // IEnumeratorを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
[SerializeField] private Button[] stageSelectButtons; // ステージ選択ボタン1〜5
[SerializeField] private Sprite stageSelectButtonLockedIcon; // ステージ選択ボタンのロックアイコン
[SerializeField] private Sprite stageSelectButtonUnlockedIcon; // ステージ選択ボタンのアンロックアイコン
// ここまで
// ...省略...
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
previousState = CurrentState; // 前の状態を保存
CurrentState = newState;
switch (newState)
{
case GameState.Title:
Debug.Log("ゲーム状態: Title");
Time.timeScale = 0; // 時間を停止
titlePanel.SetActive(true); // タイトルパネルを表示
titlePanel.transform.SetAsLastSibling(); // タイトルパネルを最前面に移動
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここから 追加
UpdateStageSelectButtons(); // ステージ選択ボタンの有効/無効を更新
// ここまで
break;
case GameState.Preparing:
// ...省略...
case GameState.Playing:
// ...省略...
case GameState.Paused:
// ...省略...
case GameState.GameOver:
// ...省略...
case GameState.Victory:
// ...省略...
}
}
// ...省略...
// ここから 追加
/// <summary>
/// ステージ選択ボタンの有効/無効をクリア情報に応じて切り替える
/// </summary>
private void UpdateStageSelectButtons()
{
if (stageSelectButtons == null || stageSelectButtons.Length == 0) return;
// UserDataの参照をキャッシュ
var userData = UserData.Instance;
for (int i = 0; i < stageSelectButtons.Length; i++)
{
var button = stageSelectButtons[i];
bool interactable = false;
bool unlocked = false;
int highScore = 0;
// ステージIDはボタン配列のインデックス+1
int stageId = i + 1;
// ステージ1は常に選択可能
if (i == 0)
{
interactable = true;
unlocked = true;
}
else if (userData != null)
{
// 前のステージがクリア済みならアンロック
var prevData = userData.GetStageData(stageId - 1);
unlocked = prevData != null && prevData.isCleared;
interactable = unlocked;
}
// ハイスコア取得
if (userData != null)
{
var stageData = userData.GetStageData(stageId);
if (stageData != null)
{
highScore = stageData.highScore;
}
}
button.interactable = interactable;
// LockImageのアイコンと透明度
var lockImage = button.transform.Find("LockImage")?.GetComponent<Image>();
if (lockImage != null)
{
lockImage.sprite = unlocked ? stageSelectButtonUnlockedIcon : stageSelectButtonLockedIcon;
var c = lockImage.color;
c.a = unlocked ? 1f : 0.3f;
lockImage.color = c;
}
// テキスト透明度
var labels = button.GetComponentsInChildren<TextMeshProUGUI>();
foreach (var label in labels)
{
var c = label.color;
c.a = unlocked ? 1f : 0.3f;
label.color = c;
}
// ハイスコア表示
var highScoreText = button.transform.Find("HighScoreText")?.GetComponent<TextMeshProUGUI>();
if (highScoreText != null)
{
highScoreText.text = $"HIGH SCORE {highScore}";
}
}
}
// ここまで
}
- 以下の3つの変数を定義しました。
stageSelectButtons
: ステージ選択ボタンを格納する配列。stageSelectButtonLockedIcon
: ステージ選択ボタンのロックアイコン。stageSelectButtonUnlockedIcon
: ステージ選択ボタンのアンロックアイコン。
ChangeState()
内で、ゲーム状態がTitle
のときに、ステージ選択ボタンの有効/無効を更新する処理を追加しました。UpdateStageSelectButtons()
メソッドを追加しました。- 前のステージのクリア状況に応じて、各ステージ選択ボタンを有効/無効化します。
- 同様にボタンのロックアイコンを切り替えます。
- 同様にボタンのテキストの透明度を切り替えます。
- 各ステージのハイスコアを表示します。
GameManagerのインスペクターで「GameManager (Script) > Stage Select Buttons」に要素を5つ追加して、Stage1Button〜Stage5Buttonをアサインします。
また、「Stage Select Button Locked Icon」と「Stage Select Button Unlocked Icon」にlock画像、unlock画像をアサインします。

さいごに
今回はステージを選択する機能を実装しました。
しかし、クリア済み情報をセーブする機能がないので、ゲームを終了するとステージのアンロックが初期化されてしまいます。次回はこれをなんとかしましょう。
でわでわ
コメント