見出し画像

Unity クエスト、実績システムの作り方

概要

やまだと申します。
Unityを利用したゲーム開発の方法をいくつか投稿したりしてます。

今回は、何かのスコアの変動に対応してクエストを達成し、報酬のゲットができるよくあるシステムを作ってみたので共有します。

Youtube版もあります。

作成するもの

前半は基礎の2つ
・クエストのマスターデータ
・クエスト管理クラス

後半は応用で以下の2つを実装していきます。
・UI
・セーブとロードの実装

マスターデータ

マスターデータにはScriptableObjectを利用します。

※ScriptableObjectがわからない方
ScriptableObjectはUnity Editor上で値を変更できる機能です。
マスターデータやシーン遷移する際のデータの受け渡しに利用されます。

[CreateAssetMenu(fileName = "MasterData", menuName = "QuestSystem/MasterData")]
    public class MasterDataScriptable : ScriptableObject
    {
        public QuestScriptable[] quests;
    }
[CreateAssetMenu(fileName = "Quest", menuName = "QuestSystem/Quest")]
    public class QuestScriptable : ScriptableObject
    {
        public string id;
        public int score = 1;
        public string description; // 詳細 
    }

全てのクエストをまとめて登録しておくMasterDataScriptable
各クエストの詳細情報を登録しておくQuestScriptableを作成しました。

※idとしていますが、重複がありのつもりで作成しています。

id が同じでScoreが異なるクエストが下のように登録できます。

OnClick 1
OnClick 10


クエスト管理クラス

まずはマスターデータからゲームの動作中に値を保存する用のクラスを作成します。
コンストラクタでQuestScriptableの配列を受け取りデータを作成します。
QuestクラスではQuestScriptableの情報に加え達成済みフラグと取得済みフラグを持っています。

[Serializable]はをつけると2つの利点があります。
・Unity Editorで変数としてInspectorで見られる
・セーブデータに変換することができる。
今回は後程セーブデータに変換するのでつけています。

QuestPanelは後程UIを作成した際に利用します。

[Serializable]
    public class QuestData
    {
        public List<Quest> quests;

        public QuestData(QuestScriptable[] questData)
        {
            quests = new List<Quest>();
            foreach (var quest in questData)
            {
                quests.Add(new Quest(quest));
            }
        }

        [Serializable]
        public class Quest
        {
            public QuestPanel questPanel;
            public string id;
            public int score;
            public string description; // 詳細
            public bool isAchieved; // 達成済みフラグ
            public bool isAcquired; // 取得済フラグ
            
            public Quest(QuestScriptable quest)
            {
                id = quest.id;
                score = quest.score;
                description = quest.description;
                isAchieved = false;
                isAcquired = false;
            }
        }
    }


次にクエストを管理するクラスを作成します。

Start関数でマスターデータからQuestDataを作成。
QuestAchieved(string id, int score)関数がクエストをクリアできるか確認する処理です。


    public class QuestManager : MonoBehaviour
    {
        [SerializeField] private MasterDataScriptable masterData;
        
        public QuestData questData;

        public void Start()
        {
            questData = new QuestData(masterData.quests);
        }

        public void QuestAchieved(string id, int score)
        {
            // すべてのquestDataに対し確認する
            foreach (var quest in questData.quests)
            {
                if (quest.id != id) continue; // idが一致する
                if (quest.isAchieved) continue; // 未達成
                if (quest.score > score) continue; // スコアがマスターデータより大きい
                quest.isAchieved = true; // 上の3つの条件を達成した場合にクエスト達成
            }
        }
    }

使用方法は以下のようにします。
QuestManagerのQuestAchieved(string id, int score)を呼びます
マスターデータのOnClick 1とOnClick 10を例にすると
OnClick()がそれぞれ1回と10回呼ばれた際にクエストを達成します。

public class Hoge : MonoBehaviour
{

    public QuestManager questManager;
    public int count;

    public void OnClick()
    {
        count++;
        questManager.QuestAchieved("OnClick", count);
    }
}

お疲れ様です。メインのクエスト部分は完了です。

UIも知りたい方はお付き合いください。

まずはクエスト情報のUIを作成します。
見た目は皆さんのセンスにお任せしますが、
・クエストの詳細TextMeshProUGUI
・達成したクエストの報酬を受け取るボタン
・達成状況、取得状況を表すTextMeshProUGUI
以上の3つがあれば大丈夫です。
親要素にQuestPanelクラスを作成し、各UIを登録、
それをPrefab化しておいてください。

public class QuestPanel : MonoBehaviour
    {
        public TextMeshProUGUI questText;
        public Button achievedButton;
        public TextMeshProUGUI achievedText;
    }


次にクエストを一覧表示するためにScroll Viewを作成します。
今回は横方向の移動が不要なので Horizontalのチェックを外して
QuestManagerクラスをアタッチしてください。
※マスターデータをPrefabの登録は次のスクリプトを変更してからで大丈夫です


次にQuestDataの変化によってUIが変化する部分を作成していきます。


    public class QuestManager : MonoBehaviour
    {
        [SerializeField] private MasterDataScriptable masterData;
        [SerializeField] private QuestPanel questPanelPrefab;
        private ScrollRect _scrollRect;
        
        public QuestData questData;

        // 達成状況を表す文字列、好きな名前に変えて使ってください。
        private const string Achieved = "Achieved";
        private const string Acquired = "Acquired";
        private const string NotAchieved = "Not Achieved";

        private void Awake()
        {
            _scrollRect = GetComponent<ScrollRect>();
        }

        public void Start()
        {
            questData = new QuestData(masterData.quests);
            
            // questDataからQuestPanelを動的に作成していきます。
            foreach (var quest in questData.quests)
            {
                var questPanel = Instantiate(questPanelPrefab, _scrollRect.content);
                quest.questPanel = questPanel;
                questPanel.questText.text = quest.description;

                // quest.isAchieved クエストが達成済み且つ
                // !quest.isAcquired 報酬を未取得の場合に
                // ボタンを押せるようにする(報酬を受け取れるようにする)
                questPanel.achievedButton.interactable = quest.isAchieved && !quest.isAcquired;
                
                // クエストを未達成の場合に            Not Achieved
                // 達成済みで報酬を受け取ってない場合に Achieved
                // 報酬を受け取り済みの場合に          Acquired
                questPanel.achievedText.text = quest.isAcquired ? Acquired : quest.isAcquired ? Achieved : NotAchieved
                
                // ボタンに処理を追加する
                questPanel.achievedButton.onClick.AddListener(() =>
                {
                    // 報酬を取得済みに変更
                    quest.isAcquired = true;
                    // ボタンを押せない様にする。
                    questPanel.achievedButton.interactable = false;
                    questPanel.achievedText.text = Acquired;
                    // ここで報酬を付与する処理を書く
                });
            }
        }

        public void QuestAchieved(string id, int score)
        {
            foreach (var quest in questData.quests)
            {
                if (quest.id != id) continue;
                if (quest.isAchieved) continue;
                if (quest.score > score) continue;
                quest.isAchieved = true;
                // ボタンを押せる(報酬を受け取れる)ようにする
                quest.questPanel.achievedButton.interactable = true;
                quest.questPanel.achievedText.text = Achieved;
            }
        }
    }

以上でUIも完了です。

最後にセーブとロードを実装します。

セーブとロードは最低限を実装して詳細は割愛します。
皆さんのゲームに合わせたものを実装してください。

public class SaveFile
    {
        public string Load(string key)
        {
            var path = GetPath(key);
            Debug.Log(path);
            return File.Exists(path) ? File.ReadAllText(path) : string.Empty;
        }

        public void Save(string key, string value)
        {
            var path = GetPath(key);
            File.WriteAllText(path, value);
        }

        private string GetPath(string key)
        {
            return Path.Combine(Application.persistentDataPath, $"{key}.json");
        }
    }

最後にQuestManagerにセーブとロードを実装して完了です。

public class QuestManager : MonoBehaviour
{
    /*
     *  省略
     */

    public void Start()
    {
        // 最初にロードします。
        questData = LoadQuestData();
        // ロードした結果データがない場合や要素が0の場合のみ新規作成します。
        if (questData == null || questData.quests.Count == 0)
        {
            questData = new QuestData(masterData.quests);
        }
        
        foreach (var quest in questData.quests)
        {
            var questPanel = Instantiate(questPanelPrefab, _scrollRect.content);
            quest.questPanel = questPanel;
            questPanel.questText.text = quest.description;
            questPanel.achievedButton.interactable = quest.isAchieved && !quest.isAcquired;
            questPanel.achievedText.text = quest.isAchieved ? Achieved : quest.isAcquired ? Acquired : NotAchieved;
            
            questPanel.achievedButton.onClick.AddListener(() =>
            {
                quest.isAcquired = true;
                questPanel.achievedButton.interactable = false;
                questPanel.achievedText.text = Acquired;
                SaveQuestData(); // 報酬を受け取った後にセーブ
            });
        }
    }

    public void QuestAchieved(string id, int score)
    {
        foreach (var quest in questData.quests)
        {
            if (quest.id != id) continue;
            if (quest.isAchieved) continue;
            if (quest.score > score) continue;
            quest.isAchieved = true;
            quest.questPanel.achievedButton.interactable = true;
            quest.questPanel.achievedText.text = Achieved;
            SaveQuestData(); // クエストを達成したのでセーブ
        }
    }

    private static QuestData LoadQuestData()
    {
        var saveData = new SaveFile();
        var data = saveData.Load("Quest");
        return JsonUtility.FromJson<QuestData>(data);
    }

    private void SaveQuestData()
    {
        var saveData = new SaveFile();
        var data = JsonUtility.ToJson(questData);
        saveData.Save("Quest", data);
    }
}

お疲れ様です。すべて完了です。


最後に要改善点ですが、マスターデータが後から追加されてもデータに反映されません。
もし後から追加したい場合はidとscoreをキーにマスターデータと比較して……のような処理が必要になります。

この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?