52歩目 ストーリーを実装! Unityで1日1ステップ!ノンフィールドRPG開発日記

1日1歩開発日記

ふりかえり

前回は「レアモンスター」の追加を行いました!

倒すと確定でダイヤや大量の経験値がもらえ、演出も豪華でテンションが上がる仕様です。プレイヤーの強化に直結する、ちょっとしたご褒美のような存在になりました。

ストーリーを実装!

今回は、ついにストーリー機能の実装に着手しました!

これまでのメニューには、

  • ステータス
  • お知らせ
  • 設定
  • クレジット

…といった項目がありましたが、ここに「ストーリー」という新しいボタンを追加しました。

ストーリーで没入感アップ

ゲームの世界観にプレイヤーを引き込むには、物語性の強化がとても効果的です。

そこで、ステージの進行に応じてストーリーが段階的に解放されていく仕組みを導入。

ゲームプレイだけでなく、物語の進行というもう一つの楽しみを提供できるようになります。

スクリプトの変更

StoryManager.cs

以下のコード StoryManager.cs は、「ストーリー閲覧機能」を統括するマネージャークラスです。

主な役割
  • ストーリーボタンを初期化し、押されたら該当ストーリーを表示
  • ストーリーごとの**解放条件(到達ステージ)**を管理
  • 未解放のストーリーはボタンをグレーアウトして押せないようにする
  • ストーリータイトル・本文の表示更新
  • ストーリーのスクロール管理や、ボタンの見た目変更 など
処理の大まかな流れ
  1. 初期化処理
    • 登録されているボタンやストーリーデータを読み込み。
    • RecordManager を通じてプレイヤーの到達ステージ情報を取得。
  2. 解放チェックとUI更新
    • 各ストーリーに設定された requiredStage に到達しているかを確認。
    • 未達成の場合は「???(ステージXで解放)」というテキストを表示し、ボタンを無効化。
  3. 表示処理
    • ストーリーボタンを押すと該当の StoryData を参照し、テキストを更新。
    • 同時にスクロール位置やボタンの色なども更新してユーザビリティを向上。
  4. データ構造
    • StoryData にはタイトルと本文を格納。
    • StoryUnlockCondition では、各ストーリーの解放条件(インデックス・必要ステージ)を管理。
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;

public class StoryManager : MonoBehaviour
{
    [Header("ストーリー選択ボタン")]
    public List<Button> storyButtons = new List<Button>(); // ストーリー選択ボタンのリスト
    public List<string> storyTitles = new List<string>(); // ストーリーのタイトルリスト

    [Header("ストーリー表示UI")]
    public ScrollRect storyScrollRect; // ストーリー表示用のスクロールビュー
    public TextMeshProUGUI storyContentText; // ストーリー内容表示用のテキスト
    public TextMeshProUGUI storyTitleText; // ストーリータイトル表示用のテキスト

    [Header("ストーリーデータ")]
    public List<StoryData> stories = new List<StoryData>(); // ストーリーデータのリスト

    [Header("Manager References")]
    public RecordManager recordManager; // RecordManagerへの参照

    [Header("ストーリー解放設定")]
    [Tooltip("各ストーリーの解放条件を設定")]
    public List<StoryUnlockCondition> storyUnlockConditions = new List<StoryUnlockCondition>();

    private int currentStoryIndex = 0; // 現在選択されているストーリーのインデックス

    void Start()
    {
        InitializeStoryButtons();
        InitializeStories();
        InitializeStoryUnlockConditions();
        UpdateStoryUnlockStatus();
        
        // 最初のストーリーを表示
        if (stories.Count > 0)
        {
            ShowStory(0);
        }
    }

    // ストーリーボタンを初期化
    private void InitializeStoryButtons()
    {
        for (int i = 0; i < storyButtons.Count; i++)
        {
            int index = i; // クロージャーのためにローカル変数を作成
            if (storyButtons[i] != null)
            {
                storyButtons[i].onClick.AddListener(() => ShowStory(index));
                
                // ボタンのテキストを設定
                if (i < storyTitles.Count)
                {
                    TextMeshProUGUI buttonText = storyButtons[i].GetComponentInChildren<TextMeshProUGUI>();
                    if (buttonText != null)
                    {
                        buttonText.text = storyTitles[i];
                    }
                }
            }
        }
    }


    // ボタンの見た目を更新
    private void UpdateButtonAppearance()
    {
        for (int i = 0; i < storyButtons.Count; i++)
        {
            if (storyButtons[i] != null)
            {
                bool isActive = (i == currentStoryIndex);
                UpdateButtonAppearance(storyButtons[i], isActive);
            }
        }
    }

    // 個別のボタンの見た目を更新
    private void UpdateButtonAppearance(Button button, bool isActive)
    {
        if (button == null) return;

        ColorBlock colors = button.colors;
        
        if (isActive)
        {
            // アクティブなボタンは明るく表示
            colors.normalColor = Color.white;
            colors.highlightedColor = Color.white * 1.1f;
            colors.pressedColor = Color.white * 0.9f;
            colors.selectedColor = Color.white;
        }
        else
        {
            // 非アクティブなボタンは暗く表示
            colors.normalColor = Color.gray;
            colors.highlightedColor = Color.gray * 1.1f;
            colors.pressedColor = Color.gray * 0.9f;
            colors.selectedColor = Color.gray;
        }
        
        button.colors = colors;
    }

    // 次のストーリーを表示
    public void ShowNextStory()
    {
        int nextIndex = (currentStoryIndex + 1) % stories.Count;
        ShowStory(nextIndex);
    }

    // 前のストーリーを表示
    public void ShowPreviousStory()
    {
        int previousIndex = (currentStoryIndex - 1 + stories.Count) % stories.Count;
        ShowStory(previousIndex);
    }

    // 現在のストーリーインデックスを取得
    public int GetCurrentStoryIndex()
    {
        return currentStoryIndex;
    }

    // ストーリーの総数を取得
    public int GetStoryCount()
    {
        return stories.Count;
    }

    // ストーリー解放状態を更新
    public void UpdateStoryUnlockStatus()
    {
        if (recordManager == null) return;

        int maxStageReached = recordManager.GetRecords().maxStageReached;

        for (int i = 0; i < storyButtons.Count; i++)
        {
            if (storyButtons[i] != null)
            {
                bool isUnlocked = IsStoryUnlocked(i, maxStageReached);
                UpdateButtonUnlockStatus(storyButtons[i], isUnlocked, i);
            }
        }
    }

    // ストーリー解放条件を初期化
    private void InitializeStoryUnlockConditions()
    {
        // デフォルトの解放条件を設定(Inspectorで設定されていない場合)
        if (storyUnlockConditions.Count == 0)
        {
            storyUnlockConditions.Add(new StoryUnlockCondition { storyIndex = 0, requiredStage = 1, description = "はじまりの物語" });
            storyUnlockConditions.Add(new StoryUnlockCondition { storyIndex = 1, requiredStage = 4, description = "魔法の森の秘密" });
            storyUnlockConditions.Add(new StoryUnlockCondition { storyIndex = 2, requiredStage = 7, description = "闇の王の復活" });
        }
    }

    // 指定されたストーリーが解放されているかチェック
    private bool IsStoryUnlocked(int storyIndex, int maxStageReached)
    {
        // storyUnlockConditionsから該当する条件を検索
        foreach (var condition in storyUnlockConditions)
        {
            if (condition.storyIndex == storyIndex)
            {
                return maxStageReached >= condition.requiredStage;
            }
        }

        // 条件が見つからない場合は解放されていないとする
        return false;
    }

    // ボタンの解放状態を更新
    private void UpdateButtonUnlockStatus(Button button, bool isUnlocked, int storyIndex)
    {
        if (button == null) return;

        // ボタンの有効/無効を設定
        button.interactable = isUnlocked;

        // ボタンのテキストを取得
        TextMeshProUGUI buttonText = button.GetComponentInChildren<TextMeshProUGUI>();
        if (buttonText != null)
        {
            if (isUnlocked)
            {
                // 解放済み:通常のテキスト
                if (storyIndex < storyTitles.Count)
                {
                    buttonText.text = storyTitles[storyIndex];
                }
            }
            else
            {
                // 未解放:解放条件を表示
                int requiredStage = GetRequiredStageForStory(storyIndex);
                buttonText.text = $"??? (ステージ{requiredStage}で解放)";
            }
        }

        // ボタンの色を更新
        ColorBlock colors = button.colors;
        if (isUnlocked)
        {
            // 解放済み:通常の色
            colors.normalColor = Color.white;
            colors.highlightedColor = Color.white * 1.1f;
            colors.pressedColor = Color.white * 0.9f;
            colors.selectedColor = Color.white;
        }
        else
        {
            // 未解放:グレーアウト
            colors.normalColor = Color.gray;
            colors.highlightedColor = Color.gray;
            colors.pressedColor = Color.gray;
            colors.selectedColor = Color.gray;
        }
        button.colors = colors;
    }

    // 指定されたストーリーを表示(解放チェック付き)
    public void ShowStory(int storyIndex)
    {
        if (storyIndex < 0 || storyIndex >= stories.Count)
        {
            Debug.LogWarning($"無効なストーリーインデックス: {storyIndex}");
            return;
        }

        // 解放チェック
        if (recordManager != null)
        {
            int maxStageReached = recordManager.GetRecords().maxStageReached;
            if (!IsStoryUnlocked(storyIndex, maxStageReached))
            {
                int requiredStage = GetRequiredStageForStory(storyIndex);
                Debug.Log($"ストーリー{storyIndex}はまだ解放されていません。ステージ{requiredStage}に到達してください。");
                return;
            }
        }

        currentStoryIndex = storyIndex;
        StoryData story = stories[storyIndex];

        // ストーリー内容を表示
        if (storyContentText != null)
        {
            storyContentText.text = story.content;
        }

        // ストーリータイトルを表示
        if (storyTitleText != null)
        {
            storyTitleText.text = story.title;
        }

        // スクロール位置を一番上に設定
        if (storyScrollRect != null)
        {
            storyScrollRect.verticalNormalizedPosition = 1f;
        }

        // ボタンの見た目を更新
        UpdateButtonAppearance();

        Debug.Log($"ストーリーを表示: {story.title}");
    }

    // 指定されたストーリーの必要ステージ数を取得
    private int GetRequiredStageForStory(int storyIndex)
    {
        foreach (var condition in storyUnlockConditions)
        {
            if (condition.storyIndex == storyIndex)
            {
                return condition.requiredStage;
            }
        }
        return 999; // 条件が見つからない場合は非常に高い値を返す
    }

    // 外部から呼び出し可能なストーリー解放状態更新メソッド
    public void CheckAndUpdateStoryUnlocks()
    {
        UpdateStoryUnlockStatus();
    }
}

// ストーリーデータクラス
[System.Serializable]
public class StoryData
{
    public string title; // ストーリーのタイトル
    public string content; // ストーリーの内容
}

// ストーリー解放条件クラス
[System.Serializable]
public class StoryUnlockCondition
{
    [Tooltip("ストーリーのインデックス(0から開始)")]
    public int storyIndex;
    
    [Tooltip("解放に必要なステージ到達数")]
    public int requiredStage;
    
    [Tooltip("ストーリーの説明(デバッグ用)")]
    public string description;
}

Unityでの設定

Unityエディタ上では以下の手順で簡単に設定できます:

1.空のGameObjectを作成して StoryManager.cs をアタッチ。
2.ボタンを複数作成し、storyButtons リストにアサイン。
3.Scroll View を作成して、本文やタイトルの表示エリアをセット。

    4.StoryData と解放条件をインスペクターで入力。

    ※この構造なら、後からストーリー数を増やしたり、解放条件を調整したりするのも簡単です。

    動作確認

    • ストーリーボタンを押すと正しくストーリーが表示されました!
    • 到達していないステージのストーリーは「???」で表示され、ボタンも無効になっていました。

    これにより、「ストーリーを読みたければ先に進まないと!」という自然な動機づけが実現できました。

    まとめ

    • ストーリー閲覧機能を無事実装!
    • 解放型の構成により、プレイヤーの成長と物語の進行がリンク。
    • ゲーム世界への没入感を強化する大きな一歩となりました。

    ストーリーの中身はこれから本格的に練っていきますが、まずは土台ができたということで大きな前進です。

    進捗

    今回はレアモンスターの追加をしました!

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

    次回予告

    次回は「記録機能」を追加予定です!

    プレイヤーのバトルの履歴や転生回数などを記録していくことで、達成感を深める機能となる予定です。お楽しみに!

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