57歩目 復活機能を実装! Unityで1日1ステップ!ノンフィールドRPG開発日記

1日1歩開発日記

ふりかえり

前回は、放置報酬機能を実装しました!

「寝て起きたら強くなっている」そんな成長の実感が味わえる要素で、より何度もプレイしたくなるゲームになってきました。

復活機能を実装!

今回は、ゲームオーバー後にプレイヤーが復活できる機能を追加しました。

これまでプレイヤーのHPが0になると、以下のような重いペナルティが即時に適用されていました:

  • 所持金(G)が半分に減る
  • ステージの進行度がリセットされる

これではあまりにも厳しく、モチベーションも下がりかねません。

そこで新たに「復活パネル」を表示する仕組みを導入。

プレイヤーに対して、以下の選択肢を提示するようにしました:

  • 広告を見る → ペナルティなしで復活
  • 広告を見ない → 通常通りペナルティを受ける

広告視聴を選べば、所持Gも進行度もそのままキープ

リトライ性とゲーム体験の柔軟性が大きく向上しました。

スクリプトの変更

復活機能の実装では、以下の2つのスクリプトを作成・拡張しました。

ReviveManager.cs

このスクリプトは復活処理の中心を担うマネージャークラスです。以下の処理を実装しています。

  • プレイヤー死亡時に状態を保存(所持Gとステージ進行)
  • 復活パネルの表示
  • 復活選択に応じて以下の処理を分岐:
    • 広告を見て復活(ペナルティなし)
    • 見ない or キャンセル(ペナルティあり)

プレイヤーのHPを回復したり、エネミーをリスポーンさせたり、UIやログを更新する細かな処理もここに集約しています。

using UnityEngine;

public class ReviveManager : MonoBehaviour
{
    [Header("参照")]
    public PlayerManager playerManager;
    public StageManager stageManager;
    public EnemyManager enemyManager;
    public RevivePanel revivePanel;
    public BattleLogManager battleLogManager;
    public AnimationManager animationManager;
    public AdmobUnitReward admobUnitReward; // 広告ユニットを追加
    public SaveManager saveManager; // SaveManager の参照を追加

    // 復活時の状態保存用
    private int savedGold;
    private int savedStageProgress;
    private int currentStage;

    void Start()
    {
        // 初期化処理
        Debug.Log("復活システムが初期化されました。");
    }

    // プレイヤーが死亡した時の処理
    public void OnPlayerDeath()
    {
        // 現在の状態を保存
        SaveCurrentState();

        // 復活パネルを表示
        if (revivePanel != null)
        {
            revivePanel.ShowRevivePanel();
        }
        else
        {
            Debug.LogWarning("復活パネルが設定されていません。通常の復活処理を実行します。");
            OnReviveCancelled(); // パネルがない場合は通常の復活処理
        }
    }

    // 復活が選択された時の処理
    public void OnReviveAccepted()
    {
        Debug.Log("復活が選択されました。所持金とステージ進行度を保持します。");

        // バトルログに復活記録を追加
        if (battleLogManager != null)
        {
            battleLogManager.AddLog("復活を選択しました!所持金とステージ進行度が保持されます。", Color.green);
        }

        // プレイヤーを復活させる(ペナルティなし)
        RevivePlayerWithoutPenalty();
    }

    // 復活がキャンセルされた時の処理
    public void OnReviveCancelled()
    {
        Debug.Log("復活がキャンセルされました。通常のデスペナルティーを適用します。");

        // バトルログにキャンセル記録を追加
        if (battleLogManager != null)
        {
            battleLogManager.AddLog("復活をキャンセルしました。デスペナルティーが適用されます。", Color.red);
        }

        // 通常のデスペナルティーを適用して復活
        RevivePlayerWithPenalty();
    }

    // 広告視聴で復活
    public void OnReviveWithAd()
    {
        if (admobUnitReward == null)
        {
            Debug.LogWarning("広告ユニットが設定されていません。通常の復活処理を実行します。");
            OnReviveAccepted();
            return;
        }

        if (!admobUnitReward.IsReady)
        {
            Debug.LogWarning("広告が準備できていません。通常の復活処理を実行します。");
            OnReviveAccepted();
            return;
        }

        // 広告を表示
        admobUnitReward.ShowRewardAd((reward) =>
        {
            if (reward != null)
            {
                // 広告視聴成功 - ペナルティーなしで復活
                Debug.Log("広告視聴完了!ペナルティーなしで復活します。");
                OnReviveAccepted();
                // 復活成功後にセーブ
                saveManager?.DebugSave();
            }
            else
            {
                // 広告視聴失敗 - 通常の復活処理
                Debug.Log("広告視聴に失敗しました。通常の復活処理を実行します。");
                OnReviveAccepted();
            }
        });
    }

    // 現在の状態を保存
    private void SaveCurrentState()
    {
        if (playerManager != null && playerManager.player != null)
        {
            savedGold = playerManager.player.gold;
        }

        if (stageManager != null)
        {
            currentStage = stageManager.GetCurrentStage();
            savedStageProgress = stageManager.GetMonstersDefeatedInStage();
        }

        Debug.Log($"現在の状態を保存: 所持金{savedGold}G, ステージ{currentStage}, 敵撃破数{savedStageProgress}");
    }

    // ペナルティなしでプレイヤーを復活
    private void RevivePlayerWithoutPenalty()
    {
        if (playerManager == null) return;

        // プレイヤーのHPを全回復
        playerManager.player.currentHP = playerManager.player.maxHP;

        // 復活アニメーションを再生
        if (animationManager != null)
        {
            animationManager.PlayerReviveAnimation();
        }

        // モンスターを再びリスポーンさせる
        RespawnEnemy();

        // バトルログに復活記録を追加
        if (battleLogManager != null)
        {
            battleLogManager.AddLog($"{playerManager.player.name}が復活しました!", Color.green);
        }

        Debug.Log($"{playerManager.player.name}が復活しました!HP:{playerManager.player.currentHP}/{playerManager.player.maxHP}");
        Debug.Log($"所持金: {playerManager.player.gold}G (保持), ステージ進行度: {savedStageProgress} (保持)");

        // UIを更新
        if (playerManager.playerUIManager != null)
        {
            playerManager.playerUIManager.UpdateUI();
        }
    }

    // ペナルティありでプレイヤーを復活
    private void RevivePlayerWithPenalty()
    {
        if (playerManager == null) return;

        // 所持金を半分にする
        playerManager.player.gold = Mathf.FloorToInt(playerManager.player.gold * 0.5f);

        // ステージの敵撃破数をリセット
        if (stageManager != null)
        {
            stageManager.ResetEnemiesDefeated();
        }

        // プレイヤーのHPを全回復
        playerManager.player.currentHP = playerManager.player.maxHP;

        // 復活アニメーションを再生
        if (animationManager != null)
        {
            animationManager.PlayerReviveAnimation();
        }

        // 新しいモンスターを生成
        RespawnEnemy();

        // バトルログにデスペナルティーを記録
        if (battleLogManager != null)
        {
            int lostGold = savedGold - playerManager.player.gold;
            battleLogManager.AddLog($"デスペナルティー!所持金が{lostGold}G減少しました。", Color.red);
            battleLogManager.AddLog("ステージの敵撃破数がリセットされました。", Color.red);
            battleLogManager.AddLog($"{playerManager.player.name}が復活しました!", Color.green);
        }

        Debug.Log($"デスペナルティー適用: 所持金 {savedGold}G → {playerManager.player.gold}G");
        Debug.Log("ステージの敵撃破数がリセットされました。");
        Debug.Log($"{playerManager.player.name}が復活しました!HP:{playerManager.player.currentHP}/{playerManager.player.maxHP}");

        // UIを更新
        if (playerManager.playerUIManager != null)
        {
            playerManager.playerUIManager.UpdateUI();
        }
    }

    // モンスターを再びリスポーンさせる
    private void RespawnEnemy()
    {
        if (enemyManager != null)
        {
            // 現在のステージレベルを取得
            int stageLevel = (stageManager != null) ? stageManager.GetCurrentStage() : 1;

            // 新しいモンスターを生成
            enemyManager.SetupNewEnemyForStage(stageLevel);

            // 敵のUIも更新
            if (enemyManager.enemyUIManager != null)
            {
                enemyManager.enemyUIManager.InitializeHPBar();
                enemyManager.enemyUIManager.ForceUpdateHPBar();
            }

            Debug.Log($"ステージ{stageLevel}に新しいモンスターを生成しました");
        }
    }

    // 復活システムが利用可能かチェック
    public bool IsReviveSystemAvailable()
    {
        return revivePanel != null && playerManager != null;
    }
}

RevivePanel.cs

こちらはUIまわりを管理するクラスです。

  • 復活パネルの表示/非表示
  • 広告復活/通常復活/キャンセルの各ボタン処理
  • 5秒のカウントダウン後、自動でキャンセルされる仕組み
  • テキストメッセージの表示更新
  • 各ボタンにイベントを割り当て

UI側の責務とゲームロジックをしっかり分けた構成になっています。

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;

public class RevivePanel : MonoBehaviour
{
    [Header("UI要素")]
    public GameObject panelObject; // パネル全体のGameObject
    public TextMeshProUGUI countdownText; // カウントダウン表示
    public TextMeshProUGUI messageText; // メッセージ表示
    public Button reviveButton; // 復活ボタン
    public Button reviveWithAdButton; // 広告視聴で復活ボタン
    public Button cancelButton; // キャンセルボタン

    [Header("参照")]
    public ReviveManager reviveManager;
    public AudioManager audioManager;

    private Coroutine countdownCoroutine;
    private bool isPanelActive = false;

    void Start()
    {
        // 初期状態では非表示
        if (panelObject != null)
        {
            panelObject.SetActive(false);
        }

        // ボタンイベントを設定
        if (reviveButton != null)
        {
            reviveButton.onClick.AddListener(OnReviveButtonClicked);
        }

        if (reviveWithAdButton != null)
        {
            reviveWithAdButton.onClick.AddListener(OnReviveWithAdButtonClicked);
        }

        if (cancelButton != null)
        {
            cancelButton.onClick.AddListener(OnCancelButtonClicked);
        }
    }

    // 復活パネルを表示
    public void ShowRevivePanel()
    {
        isPanelActive = true;

        // パネルを表示
        if (panelObject != null)
        {
            panelObject.SetActive(true);
        }

        // ボタンを有効化
        if (reviveButton != null)
        {
            reviveButton.interactable = true;
        }
        if (reviveWithAdButton != null)
        {
            reviveWithAdButton.interactable = true;
        }
        if (cancelButton != null)
        {
            cancelButton.interactable = true;
        }

        // メッセージを設定
        if (messageText != null)
        {
            messageText.text = "復活しますか?\n復活ボタンを押すと所持金とステージ進行度が保持されます。";
        }

        // カウントダウンを開始
        StartCountdown();

        Debug.Log("復活パネルを表示しました。");
    }

    // カウントダウンを開始
    private void StartCountdown()
    {
        if (countdownCoroutine != null)
        {
            StopCoroutine(countdownCoroutine);
        }
        countdownCoroutine = StartCoroutine(CountdownCoroutine());
    }

    // カウントダウン処理
    private IEnumerator CountdownCoroutine()
    {
        int timeLeft = 5; // 5秒のカウントダウン

        while (timeLeft > 0 && isPanelActive)
        {
            if (countdownText != null)
            {
                countdownText.text = $"自動復活まで {timeLeft}秒";
            }

            yield return new WaitForSeconds(1f);
            timeLeft--;
        }

        // 時間切れの場合は自動でキャンセル
        if (isPanelActive)
        {
            OnCancelButtonClicked();
        }
    }

    // 復活ボタンがクリックされた時
    private void OnReviveButtonClicked()
    {
        if (reviveManager != null)
        {
            reviveManager.OnReviveAccepted();
        }

        // 復活音を再生
        if (audioManager != null)
        {
            audioManager.PlayPowerUpSE(); // 復活音として使用
        }

        ClosePanel();
        Debug.Log("復活を選択しました!");
    }

    // 広告視聴で復活ボタンがクリックされた時
    private void OnReviveWithAdButtonClicked()
    {
        if (reviveManager != null)
        {
            reviveManager.OnReviveWithAd();
        }

        ClosePanel();
    }

    // キャンセルボタンがクリックされた時
    private void OnCancelButtonClicked()
    {
        if (reviveManager != null)
        {
            reviveManager.OnReviveCancelled();
        }

        ClosePanel();
        Debug.Log("復活をキャンセルしました。");
    }

    // パネルを閉じる
    private void ClosePanel()
    {
        isPanelActive = false;

        if (countdownCoroutine != null)
        {
            StopCoroutine(countdownCoroutine);
            countdownCoroutine = null;
        }

        if (panelObject != null)
        {
            panelObject.SetActive(false);
        }

        Debug.Log("復活パネルを閉じました。");
    }

    // 外部からパネルを閉じる
    public void ForceClosePanel()
    {
        ClosePanel();
    }

    // パネルが表示中かチェック
    public bool IsPanelActive()
    {
        return isPanelActive;
    }
}

Unityでの設定

空のGameObjectを生成し、ReviveManager.csをアタッチします。

Inspector上でそれぞれの参照を設定します。

UIのPanelを生成しRevivePanel.csをアタッチします。

それぞれのUI要素と参照を設定します。

動作確認

実際にゲーム内で敵に倒されると復活パネルが表示され、5秒のカウントダウンが開始されます。

その間に広告を視聴すれば、所持Gもステージ進捗も保持された状態で復活できます!

まとめ

今回は、「復活機能」を導入し、プレイヤーが選択できる余地を用意しました。

ただ倒されて終わりではなく、「復活して続けるか」「潔くやり直すか」を選ばせることで、より戦略性や没入感が高まると感じています。

今後のプレイにも「もしもの時の選択肢」があることで、挑戦しやすくなり、ゲームにリズムと深みが出てきたと思います!

進捗

今回は復活機能を追加をしました!

  • (済)オート戦闘:ボタン操作なしでもバトルが進むようにする。
  • (済)オートスキル発動:スキルも自動で発動する機能。
  • (済)UIのブラッシュアップ:デザインとレイアウトを整えて見やすくする。
  • (済)ステージ数の拡充(最低20):遊びごたえを高めるためにボリュームアップ。
  • (済)復活機能(広告視聴):ゲームオーバー時、広告視聴で復活できる仕組み。
  • (済)広告報酬でダイヤ獲得:無課金でもダイヤが手に入る仕組み。
  • (済)G強化・アーティファクト・転生の拡張:各システムにさらなる深みを持たせる。
  • (済)回復薬の追加:ステージを進めやすくする。
  • (済)アニメーション・エフェクトの追加:ゲーム画面にもっと動きを持たせる。
  • (済)レアモンスターの追加:わくわく感を追加する。
  • (済)放置要素の追加:プレイしていない時間も強くなる。
  • (済)メニューの内容追加:ストーリー、記録、ガイドなどプレイヤーが遊びやすくする。

    次回予告

    ついに予定していたすべての要素を実装し終えました!

    次回からはゲーム全体のバランス調整最終ビルド作業に入ります。

    より多くのプレイヤーに楽しんでもらえるよう、細部まで仕上げていきます!

    お楽しみに!

    タイトルとURLをコピーしました