56歩目 放置報酬を実装! Unityで1日1ステップ!ノンフィールドRPG開発日記

1日1歩開発日記

ふりかえり

前回は「攻撃エフェクト」を実装しました!

戦闘シーンはプレイヤーが最もよく目にする場面の一つなので、視覚的に派手な演出を加えることで、プレイヤーの興味をより強く惹きつけられるようになりました。

放置報酬を実装!

今回はついに「放置報酬」機能を導入しました!

モバイルゲームといえば、やはり放置報酬!

個人的な印象ですが、現代のスマホゲームにおいて放置要素は“ほぼ必須”と言ってもいいほど重要な仕組みだと思っています。

そこで今回は、ゲームを遊んでいない間にも報酬がもらえる仕組みを実装しました!

放置報酬の内容

現在のステージによって、放置報酬の内容が変化します。

獲得できる内容は以下の通りです:

  • ゴールド(G): 5 × 現在のステージ数
  • 経験値(EXP): 10 × 現在のステージ数

この報酬は10分ごとに1回分加算されます。

例えば、ステージ10に滞在していた状態で、63分間放置していた場合、

  • ゴールド:5 × 10 × 6 = 300
  • 経験値:10 × 10 × 6 = 600

という報酬を獲得できます!(※10分単位で切り捨て)

獲得タイミングと表示

ゲームを再起動したときに、最初に表示されるパネルで以下の情報が確認できます:

  • 放置していた時間
  • 獲得したゴールド
  • 獲得した経験値
  • 倒した敵の数(想定)

また、「獲得」ボタンを押すことで報酬がプレイヤーに付与されます!

スクリプトの変更

今回の実装では以下の2つの新規スクリプトを作成しました:

  • OfflineRewardManager.cs:報酬の計算と管理を行うロジッククラス
  • OfflineRewardPanel.cs:放置報酬パネルの表示やUI操作を担当

OfflineRewardManager.cs

目的

ゲームを終了してから再起動したときに、どれだけの時間が経過したかを計算し、その時間に応じた報酬(ゴールドと経験値)をプレイヤーに提供する処理を行います。

主な処理の流れ

  1. アプリ起動時に、保存された終了時間とステージ情報を取得
  2. 放置時間を計算
  3. 10分単位で敵を倒したと仮定し、ステージに応じた報酬を計算
  4. 報酬をUIパネルで表示
  5. 「獲得」ボタンで報酬をプレイヤーに加算
  6. 再度終了時間を保存

プレイヤーの強化要素(G増加系・EXP増加系)にも対応しており、アーティファクトや転生効果による倍率補正も反映されます。

using UnityEngine;
using System;

public class OfflineRewardManager : MonoBehaviour
{
    [Header("参照")]
    public PlayerManager playerManager;
    public StageManager stageManager;
    public EnemyManager enemyManager;
    public SaveManager saveManager;
    public OfflineRewardPanel offlineRewardPanel;

    [Header("放置報酬設定")]
    public float rewardIntervalMinutes = 10f; // 報酬間隔(分)
    public int maxRewardHours = 24; // 最大報酬時間(時間)

    private const string LAST_QUIT_TIME_KEY = "LastQuitTime";
    private const string LAST_STAGE_KEY = "LastStage";

    void Start()
    {
        // ゲーム開始時に放置報酬をチェック
        CheckOfflineRewards();
    }

    // 放置報酬をチェック
    public void CheckOfflineRewards()
    {
        if (!HasOfflineRewards())
        {
            Debug.Log("放置報酬はありません。");
            return;
        }

        OfflineRewardData rewardData = CalculateOfflineRewards();
        
        if (rewardData.totalGold > 0 || rewardData.totalExp > 0)
        {
            Debug.Log($"放置報酬を発見!時間: {rewardData.offlineHours:F1}時間, ゴールド: {rewardData.totalGold}, 経験値: {rewardData.totalExp}");
            
            // 報酬パネルを表示
            if (offlineRewardPanel != null)
            {
                offlineRewardPanel.ShowRewardPanel(rewardData);
            }
        }
    }

    // 放置報酬があるかチェック
    private bool HasOfflineRewards()
    {
        if (!ES3.KeyExists(LAST_QUIT_TIME_KEY))
        {
            return false;
        }

        DateTime lastQuitTime = ES3.Load<DateTime>(LAST_QUIT_TIME_KEY);
        DateTime currentTime = DateTime.Now;
        TimeSpan offlineTime = currentTime - lastQuitTime;

        // 最低10分間放置している必要がある
        return offlineTime.TotalMinutes >= rewardIntervalMinutes;
    }

    // 放置報酬を計算
    private OfflineRewardData CalculateOfflineRewards()
    {
        DateTime lastQuitTime = ES3.Load<DateTime>(LAST_QUIT_TIME_KEY);
        DateTime currentTime = DateTime.Now;
        TimeSpan offlineTime = currentTime - lastQuitTime;

        // 最大報酬時間を制限
        float maxOfflineHours = maxRewardHours;
        float actualOfflineHours = Mathf.Min((float)offlineTime.TotalHours, maxOfflineHours);

        // 最後にいたステージを取得
        int lastStage = ES3.KeyExists(LAST_STAGE_KEY) ? ES3.Load<int>(LAST_STAGE_KEY) : 1;

        // 10分間隔で敵を倒した回数を計算
        int enemiesDefeated = Mathf.FloorToInt(actualOfflineHours * 60f / rewardIntervalMinutes);

        // ステージの敵の報酬を取得
        var stageReward = GetStageReward(lastStage);

        // 総報酬を計算
        int totalGold = enemiesDefeated * stageReward.goldReward;
        int totalExp = enemiesDefeated * stageReward.expReward;

        return new OfflineRewardData
        {
            offlineHours = actualOfflineHours,
            enemiesDefeated = enemiesDefeated,
            totalGold = totalGold,
            totalExp = totalExp,
            stageLevel = lastStage
        };
    }

    // ステージの敵の報酬を取得
    private (int goldReward, int expReward) GetStageReward(int stageLevel)
    {
        // ステージレベルに応じた基本報酬を計算
        int baseGold = 5 * stageLevel; // 基本5G × ステージ
        int baseExp = 10 * stageLevel; // 基本10EXP × ステージ

        // プレイヤーの獲得強化を適用
        float goldMultiplier = 1f;
        float expMultiplier = 1f;

        if (playerManager != null && playerManager.player != null)
        {
            goldMultiplier += playerManager.player.artifactPowerUpGoldGain + playerManager.player.prestigePowerUpGoldGain;
            expMultiplier += playerManager.player.artifactPowerUpExpGain + playerManager.player.prestigePowerUpExpGain;
        }

        int finalGold = Mathf.RoundToInt(baseGold * goldMultiplier);
        int finalExp = Mathf.RoundToInt(baseExp * expMultiplier);

        return (finalGold, finalExp);
    }

    // 報酬を獲得
    public void ClaimRewards(OfflineRewardData rewardData)
    {
        if (playerManager != null && playerManager.player != null)
        {
            // ゴールドと経験値を付与
            playerManager.player.gold += rewardData.totalGold;
            playerManager.player.exp += rewardData.totalExp;

            // レベルアップチェック
            CheckLevelUp();

            Debug.Log($"放置報酬を獲得!ゴールド: +{rewardData.totalGold}, 経験値: +{rewardData.totalExp}");
        }

        // 最後の終了時間を更新
        SaveLastQuitTime();
    }

    // ゲーム終了時に現在のステージと時間を保存
    public void SaveLastQuitTime()
    {
        DateTime currentTime = DateTime.Now;
        ES3.Save(LAST_QUIT_TIME_KEY, currentTime);

        // 現在のステージも保存
        if (stageManager != null)
        {
            ES3.Save(LAST_STAGE_KEY, stageManager.GetCurrentStage());
        }

        Debug.Log($"ゲーム終了時間を保存: {currentTime}");
    }

    // アプリケーション終了時に呼び出し
    void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            SaveLastQuitTime();
        }
    }

    void OnApplicationQuit()
    {
        SaveLastQuitTime();
    }

    // デバッグ用:放置時間をリセット
    [ContextMenu("Reset Offline Time")]
    public void ResetOfflineTime()
    {
        ES3.DeleteKey(LAST_QUIT_TIME_KEY);
        ES3.DeleteKey(LAST_STAGE_KEY);
        Debug.Log("放置時間をリセットしました。");
    }

    // レベルアップチェック
    private void CheckLevelUp()
    {
        if (playerManager != null && playerManager.player != null)
        {
            // 複数回レベルアップできる場合も考慮
            while (playerManager.player.exp >= playerManager.player.expToNextLevel)
            {
                LevelUp();
            }
        }
    }

    // レベルアップ処理
    private void LevelUp()
    {
        if (playerManager == null || playerManager.player == null) return;

        var player = playerManager.player;
        player.level++;
        
        // 基礎ステータスをレベルアップ
        player.baseMaxHP += 20; // HPを20増加
        player.baseAttack += 5; // 攻撃力を5増加
        player.baseDefense += 1; // 防御力を1増加
        
        player.currentHP = player.maxHP; // HPを全回復
        
        player.exp -= player.expToNextLevel; // 溢れた分を次のレベルに持ち越し
        player.expToNextLevel += 10; // 次のレベルに必要な経験値を10増加

        Debug.Log($"{player.name}がレベルアップ!レベル{player.level}になりました!");
        Debug.Log($"HP: {player.maxHP}, 攻撃力: {player.attack}, 防御力: {player.defense}");
    }

    // デバッグ用:強制的に放置報酬を表示
    [ContextMenu("Test Offline Rewards")]
    public void TestOfflineRewards()
    {
        // テスト用に24時間前の時間を設定
        DateTime testTime = DateTime.Now.AddHours(-24);
        ES3.Save(LAST_QUIT_TIME_KEY, testTime);
        ES3.Save(LAST_STAGE_KEY, 5); // ステージ5でテスト

        CheckOfflineRewards();
    }
}

// 放置報酬データ
[System.Serializable]
public class OfflineRewardData
{
    public float offlineHours;      // 放置時間(時間)
    public int enemiesDefeated;     // 倒した敵の数
    public int totalGold;           // 総ゴールド
    public int totalExp;            // 総経験値
    public int stageLevel;          // ステージレベル
}

OfflineRewardPanel.cs

目的

放置報酬を視覚的にわかりやすくプレイヤーに伝えるためのパネル表示・ボタン処理を行います。

主な機能

  • 放置時間を「◯時間」または「◯分」として表示
  • ゴールド、経験値、倒した敵数を表示
  • 「獲得」ボタンを押すと、報酬を加算し、パネルを閉じる
  • 「閉じる」ボタンでキャンセルも可能
  • 獲得ボタンを押すと、音声演出付きで報酬が付与される
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class OfflineRewardPanel : MonoBehaviour
{
    [Header("UI要素")]
    public GameObject panelObject; // パネル全体のGameObject
    public TextMeshProUGUI timeText; // 放置時間表示
    public TextMeshProUGUI goldText; // ゴールド報酬表示
    public TextMeshProUGUI expText; // 経験値報酬表示
    public TextMeshProUGUI enemiesText; // 倒した敵数表示
    public Button claimButton; // 獲得ボタン
    public Button closeButton; // 閉じるボタン

    [Header("参照")]
    public OfflineRewardManager offlineRewardManager;
    public AudioManager audioManager;

    private OfflineRewardData currentRewardData;

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

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

        if (closeButton != null)
        {
            closeButton.onClick.AddListener(OnCloseButtonClicked);
        }
    }

    // 報酬パネルを表示
    public void ShowRewardPanel(OfflineRewardData rewardData)
    {
        currentRewardData = rewardData;

        // UIを更新
        UpdateUI(rewardData);

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

        // 獲得ボタンを有効化
        if (claimButton != null)
        {
            claimButton.interactable = true;
        }

        Debug.Log("放置報酬パネルを表示しました。");
    }

    // UIを更新
    private void UpdateUI(OfflineRewardData rewardData)
    {
        // 放置時間を表示
        if (timeText != null)
        {
            if (rewardData.offlineHours >= 1f)
            {
                timeText.text = $"{rewardData.offlineHours:F1}時間放置していました";
            }
            else
            {
                int minutes = Mathf.RoundToInt(rewardData.offlineHours * 60f);
                timeText.text = $"{minutes}分間放置していました";
            }
        }

        // ゴールド報酬を表示
        if (goldText != null)
        {
            goldText.text = $"+{rewardData.totalGold}G";
        }

        // 経験値報酬を表示
        if (expText != null)
        {
            expText.text = $"+{rewardData.totalExp}EXP";
        }

        // 倒した敵数を表示
        if (enemiesText != null)
        {
            enemiesText.text = $"ステージ{rewardData.stageLevel}の敵を{rewardData.enemiesDefeated}匹倒しました";
        }
    }

    // 獲得ボタンがクリックされた時
    private void OnClaimButtonClicked()
    {
        if (currentRewardData == null)
        {
            Debug.LogWarning("報酬データがありません。");
            return;
        }

        // 報酬を獲得
        if (offlineRewardManager != null)
        {
            offlineRewardManager.ClaimRewards(currentRewardData);
        }

        // 獲得ボタンを無効化
        if (claimButton != null)
        {
            claimButton.interactable = false;
        }

        // 獲得音を再生
        if (audioManager != null)
        {
            audioManager.PlayPowerUpSE();
        }

        // 少し遅れてパネルを閉じる
        Invoke("ClosePanel", 1.0f);

        Debug.Log("放置報酬を獲得しました!");
    }

    // 閉じるボタンがクリックされた時
    private void OnCloseButtonClicked()
    {
        ClosePanel();
    }

    // パネルを閉じる
    private void ClosePanel()
    {
        if (panelObject != null)
        {
            panelObject.SetActive(false);
        }

        // 報酬データをクリア
        currentRewardData = null;

        Debug.Log("放置報酬パネルを閉じました。");
    }

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

    // パネルが表示中かチェック
    public bool IsPanelActive()
    {
        return panelObject != null && panelObject.activeInHierarchy;
    }
}

Unityでの設定

空のGameObjectにOfflineRewardManager.csをアタッチします

OffrineRewardManagerのInspector上で必要なGameObjectを参照し、何分毎に報酬を獲得するかと、最大何時間まで報酬を貯めれるかを設定します。

Canvasに放置報酬表示用のPanelを作成します。
そのパネルにOfflineRewardPanel.csをアタッチします。
それぞれのUI要素と必要なGameObjectをアタッチします。

動作確認

瞬く放置してゲームを起動すると以下のようなパネルが表示され放置時間と放置報酬が表示されました!

ボタンを押すことで報酬をしっかり獲得することができました!

まとめ

今回のアップデートで、モバイルゲームらしい「放置報酬」機能をしっかり実装することができました。

放置報酬があることで、

「しばらく放置しても成長できる」

「再びゲームを開くモチベーションになる」

というように、プレイヤーのリテンション(継続率)を高める効果が期待できます!

進捗

今回は放置報酬を追加をしました!

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

次回予告

次回は、プレイヤーが倒れた際に広告視聴で復活できる仕組みを導入していく予定です!

「せっかく育てたのに…」という悔しさを和らげつつ、広告を見ることでリカバリーできる、現代モバイルゲームらしい設計にしていきたいと思います!

どうぞお楽しみに!

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