Unity

【脱出ゲーム制作11】セーブ機能を実装する

セーブ機能の実装

前回は、新規アイテムの作成とアイテム合成機能を実装しました。

今回は ゲームの進行状況を保存する「セーブ機能」 を実装します。
このセーブ機能は、プレイヤーが任意のタイミングで手動セーブするものではなく、 アイテムの取得・使用状況やギミックのクリア状況を自動で記録 する仕組みになっています。

セーブ機能を実装することで、アプリを閉じても続きから再開することが可能になります。

セーブの仕組み

今回の実装では、JsonデータとPlayerPrefsを活用してセーブを行います。

具体的に保存する情報は、以下の2つです。

  • アイテムの取得・使用状況(どのアイテムを手に入れ、どれを使用したか)
  • ギミックのクリア状況(どの謎を解いたか)

SaveManagerを用意する

まずはスクリプトとゲームオブジェクトを作成します。

HierarchyでCreateEmptyを選択し、名前を『SaveManager』にする。
AssetのScriptsフォルダに『SaveManager.cs』を作成。

作成したオブジェクトにスクリプトをアタッチします。

SaveManager.cs
using System;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    const string SAVE_KEY = "SaveData";
    public SaveData saveData;

    private void Start()
    {
        Load();
    }
    public void Save()
    {
        string json = JsonUtility.ToJson(saveData);
        PlayerPrefs.SetString(SAVE_KEY, json);
    }

    public void Load()
    {
        saveData = new SaveData();  // 新しいインスタンスを作成
        if (PlayerPrefs.HasKey(SAVE_KEY) == true)
        {
            string json = PlayerPrefs.GetString(SAVE_KEY);
            saveData = JsonUtility.FromJson<SaveData>(json);  // 既存のデータがあれば上書き
        }
        foreach (bool flag in saveData.getItems)
        {
            Debug.Log(flag);
        }
    }
}

[Serializable]
public class SaveData
{
    public bool[] getItems = new bool[(int)Item.Type.Max];  // アイテム取得状況を管理
    public bool[] useItems = new bool[(int)Item.Type.Max];  // アイテム使用状況を管理
}

const string SAVE_KEY = “SaveData”;

セーブデータを保存・取得するためのキー(識別子)const を使うことで、誤って変更されるのを防ぎ、プログラム内で統一したキーを使用できるようにする。

Saveメソッド

JsonUtility.ToJson(saveData) を使用して、ゲームの進行状態をJSON形式の文字列に変換し、それを PlayerPrefs.SetString(SAVE_KEY, json) で保存する。
これにより、ゲームを再起動してもデータを復元できるようになる。

Loadメソッド

ゲーム開始時に Load() を実行し、セーブデータがあるかを確認する。

  • セーブデータがない場合 → 新しい SaveData を作成し、初期化された状態を使用する。
  • セーブデータがある場合PlayerPrefs からデータを取得し、JsonUtility.FromJson<SaveData>(json) を使って saveData に復元する。

SaveDataの役割

SaveDataクラスは、アイテムの取得状態や使用状態を管理するデータコンテナです。

具体的には、bool[]型の配列を使用して、各アイテムが取得済みか・使用済みかを管理しています。

配列のサイズには、Typeの総数を指定する必要がありbool[] getItems = new bool[(int)Item.Type.MAX]; のように記述します。

(int)Item.Type.MAX を使用する理由については、後述します。

Item.csのTypeにMAXを追加

Item.cs
using UnityEngine;
using System;

[Serializable]
public class Item
{
    public enum Type
    {
        Cube,
        Sphere,
        Key,
        RedTile,
        BlueTile,
        YellowTile,
        PurpleTile,
        Max,  // 追加
    }

    public Type type;
    public Sprite sprite;
    public GameObject zoomPrefab;

    public Item(Item item)
    {
        this.type = item.type;
        this.sprite = item.sprite;
    }
}

Item.csに『Max』 を追加する理由

SaveManager.cs
bool[] getItems = new bool[(int)Item.Type.Max]

本来、bool[] getItems = new bool[7];のようにアイテムの総数を直接数値で指定すれば、配列を正しく管理できます。

しかし、この方法では新しいアイテムを追加した際に数値を手動で修正する必要があり、修正漏れによるエラーが発生する可能性があります。

そこで、TypeにMaxを追加し、bool[] getItems = new bool[(int)Item.Type.Max];のように記述することで、アイテムの種類が増えても自動的に正しい配列サイズを確保できます。これにより、修正の手間を減らし、エラーを防ぐことができます。

アイテムを取得情報をセーブする

まずはアイテムの取得情報をセーブするようにします。
画面内のオブジェクトをクリックしたらアイテムが取得できるようになっています。

SaveManager.csをシングルトン化し、SetGetItemFlagメソッドを追加

SaveManager.cs
using System;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    // シングルトン化
    public static SaveManager instance;
    private void Awake()
    {
        instance = this;
    }

    const string SAVE_KEY = "SaveData";
    public SaveData saveData;

    private void Start()
    {
        Load();
    }
    public void Save()
    {
        string json = JsonUtility.ToJson(saveData);
        PlayerPrefs.SetString(SAVE_KEY, json);
    }

    public void Load()
    {
        saveData = new SaveData();
        if (PlayerPrefs.HasKey(SAVE_KEY) == true)
        {
            string json = PlayerPrefs.GetString(SAVE_KEY);
            saveData = JsonUtility.FromJson<SaveData>(json);
        }
        foreach (bool flag in saveData.getItems)
        {
            Debug.Log(flag);
        }
    }

    // 取得したアイテムをtrueにする
    public void SetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        saveData.getItems[index] = true;
    }
}

[Serializable]
public class SaveData
{
    public bool[] getItems = new bool[(int)Item.Type.Max];
    public bool[] useItems = new bool[(int)Item.Type.Max];
}

PickUpObj.cs
using UnityEngine;

public class PickUpObj : MonoBehaviour
{
    public Item.Type type;

    public void OnClick()
    {
        // アイテム取得フラグを保存
        SaveManager.instance.SetGetItemFlag(type);
        
        Item item = ItemDatabase.instance.Spawn(type);
        ItemBox.instance.SetItem(item);
        gameObject.SetActive(false);
    }
}

これでアイテムのオブジェクトをクリックしたらセーブを行うようになりました。

実行

オブジェクトのSaveManagerのInspectorを確認すると、SaveDataにチェックボックスが表示されています。

アイテムを取得するとチェックが入ります。

アイテムを取得したらチェックマークがついた

セーブデータの初期化方法

Unityのプロジェクトでセーブ機能が正しく実装されているか確認し、セーブデータを消して確認したい場合初期化する必要があります。

Edit→「Clear All PlayerPrefas」セーブデータの初期化が行えます。

最初から確認したい場合は初期化しよう

取得済みのアイテムを非表示にする

アイテムを取得しセーブすることはできました。
次はゲームを再開した時、取得したアイテムが画面上に残っているのはおかしいのでゲーム再開時にオブジェクトが非表示になるようにします。

SaveManager.cs
using System;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    public static SaveManager instance;
    private void Awake()
    {
        instance = this;
    }

    const string SAVE_KEY = "SaveData";
    public SaveData saveData;

    private void Start()
    {
        Load();
    }

    public void Save()
    {
        string json = JsonUtility.ToJson(saveData);
        PlayerPrefs.SetString(SAVE_KEY, json);
    }

    public void Load()
    {
        saveData = new SaveData();
        if (PlayerPrefs.HasKey(SAVE_KEY) == true)
        {
            string json = PlayerPrefs.GetString(SAVE_KEY);
            saveData = JsonUtility.FromJson<SaveData>(json);
        }
        foreach (bool flag in saveData.getItems)
        {
            Debug.Log(flag);
        }
    }

    public void SetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        saveData.getItems[index] = true;
        Save();
    }

    // 追加:セーブデータから指定したアイテムの取得フラグを取得
    public bool GetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        return saveData.getItems[index];
    }
}

[Serializable]
public class SaveData
{
    public bool[] getItems = new bool[(int)Item.Type.Max];
    public bool[] useItems = new bool[(int)Item.Type.Max];
}

GetGetItemFlagメソッドを追加

PickUpObj.cs
using UnityEngine;

public class PickUpObj : MonoBehaviour
{
    public Item.Type type;

    // アイテムを取得済みならスロットに追加し、オブジェクトを非表示にする
    private void Start()
    {
        // セーブデータからアイテムの取得状況を確認
        bool hasItem = SaveManager.instance.GetGetItemFlag(type);
        if (hasItem == true)
        {
            SetToItemBox();
        }
    }
    
    public void OnClick()
    {
        SaveManager.instance.SetGetItemFlag(type);
        SetToItemBox();
    }
    
    // 新たに関数を作成
    public void SetToItemBox()
    {
        Item item = ItemDatabase.instance.Spawn(type);
        ItemBox.instance.SetItem(item);
        gameObject.SetActive(false);
    }
}

新たにSetToItemBoxメソッドを追加しアイテムSlotにアイテムを生成し、オブジェクトを非表示にする。
OnClickはセーブとSetToItemBoxを実行するだけのものに変更。
Start関数を追加し、開始時にセーブデータを確認し、アイテムが取得状態(true)ならSetToItemBoxを実行する。

実行

アイテムを取得(セーブ)し、ゲームを終了。
もう一度ゲームを実行するとセーブがロードされ、アイテム取得情報をもとにオブジェクトが消える。

よく見るとCubeのオブジェクトは非表示になったが、他のオブジェクトは非表示にならなかった。
これは、ゲーム開始時にセーブデータのロード(Load)が行われる前に、各PickUpObjのStart関数が実行されてしまうためのようだ。

エラーの修正

Loadメソッドを『Start』から『Awake』に移し、ゲーム開始時にAwakeで実行されるように変更

これにより、他のスクリプトのStart関数が実行される前にLoadが完了し、正しい順番で処理が行われるようになる。

SaveManager.cs
using System;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    public static SaveManager instance;
    private void Awake()
    {
        instance = this;
        
        // こちらに移す
        Load();
    }

    const string SAVE_KEY = "SaveData";
    public SaveData saveData;

    public void Save()
    {
        string json = JsonUtility.ToJson(saveData);
        PlayerPrefs.SetString(SAVE_KEY, json);
    }

    public void Load()
    {
        saveData = new SaveData();
        if (PlayerPrefs.HasKey(SAVE_KEY) == true)
        {
            string json = PlayerPrefs.GetString(SAVE_KEY);
            saveData = JsonUtility.FromJson<SaveData>(json);
        }
        foreach (bool flag in saveData.getItems)
        {
            Debug.Log(flag);
        }
    }

    public void SetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        saveData.getItems[index] = true;
        Save();
    }

    public bool GetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        return saveData.getItems[index];
    }
}

[Serializable]
public class SaveData
{
    public bool[] getItems = new bool[(int)Item.Type.Max];
    public bool[] useItems = new bool[(int)Item.Type.Max];
}

実行

ちゃんと取得したアイテムが全て消えた

ギミックのフラグを作成

「ドアが開いた」「パスワードを解除した」などのゲームの進行状態もセーブで管理する必要があります。

新たにギミックフラグを作成し、アイテムと同じようにbool型で管理します。

SaveManager.cs
using System;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    public static SaveManager instance;
    private void Awake()
    {
        instance = this;
        Load();
    }

    const string SAVE_KEY = "SaveData";
    public SaveData saveData;

    // ギミックフラグの列挙を作成
    public enum Flag
    {
        // 木箱が開いてるかどうかのフラグ
        OpenedWoodenBox01,
        // ドアが開いているかのフラグ
        OpenedDoor01,
        Max,
    }

    public void Save()
    {
        string json = JsonUtility.ToJson(saveData);
        PlayerPrefs.SetString(SAVE_KEY, json);
    }

    public void Load()
    {
        saveData = new SaveData();
        if (PlayerPrefs.HasKey(SAVE_KEY) == true)
        {
            string json = PlayerPrefs.GetString(SAVE_KEY);
            saveData = JsonUtility.FromJson<SaveData>(json);
        }
        foreach (bool flag in saveData.getItems)
        {
            Debug.Log(flag);
        }
    }

    public void SetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        saveData.getItems[index] = true;
        Save();
    }

    public bool GetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        return saveData.getItems[index];
    }
    // ギミックのセーブ
    public void SetGimmickFlag(Flag flag)
    {
        int index = (int)flag;
        saveData.gimmick[index] = true;
        Save();
    }
    // セーブデータから指定したギミックのフラグを取得
    public bool GetGimmickFlag(Flag flag)
    {
        int index = (int)flag;
        return saveData.gimmick[index];
    }
}

[Serializable]
public class SaveData
{
    public bool[] getItems = new bool[(int)Item.Type.Max];
    public bool[] useItems = new bool[(int)Item.Type.Max];
    
    // ギミックセーブデータの管理
    public bool[] gimmick = new bool[(int)SaveManager.Flag.Max];
}

ギミックフラグの列挙を作成。
SetGimmickFlagメソッド、GetGimmickFlagメソッドの追加。
SaveDataにgimmickを追加。

ギミックのセーブと進行状態の確認 – 木箱

木箱を例にセーブと開始時の確認を行います。パスワードをクリアしたらOpenが実行されるスクリプトに処理を追加します。

WoodenBox.cs
using UnityEngine;

public class WoodenBox : MonoBehaviour
{
    [SerializeField] Animator animator;

    private void Start()
    {
        // すでにクリアしているなら木箱を開ける
        bool clearGimmick = SaveManager.instance.GetGimmickFlag(SaveManager.Flag.OpenedWoodenBox01);
        if (clearGimmick == true)
        {
            Open();
        }
    }

    public void Open()
    {
        // 開けたらセーブ
        SaveManager.instance.SetGimmickFlag(SaveManager.Flag.OpenedWoodenBox01);
        
        animator.Play("OpenWoodenBox");
    }
}

アイテムと同じように、Openで木箱を開けたらSetGimmickFlagでセーブを行い、ゲーム再開時にはStart関数でGetGimmickFlagを実行し、フラグが立っていたらOpenを実行するようにする。

Clear All PlayerPrefasで初期化してから実行

再度実行

ちゃんと開いた状態でスタートできた

正しいコードにする

現在のコードでは、ゲーム再開時に既に開いている木箱に対して再度セーブを実行してしまいます。これは不要な処理なので、セーブ処理を行うメソッドアニメーションを実行するメソッドを分けるとよいでしょう。

WoodenBox.cs
using UnityEngine;

public class WoodenBox : MonoBehaviour
{
    [SerializeField] Animator animator;

    private void Start()
    {
        // すでにクリアしているなら木箱を開ける(セーブはしない)
        bool clearGimmick = SaveManager.instance.GetGimmickFlag(SaveManager.Flag.OpenedWoodenBox01);
        if (clearGimmick == true)
        {
            PlayOpenAnimation();
        }
    }

    public void Open()
    {
        // 開けたらセーブしてアニメーション再生
        SaveManager.instance.SetGimmickFlag(SaveManager.Flag.OpenedWoodenBox01);
        PlayOpenAnimation();
    }
    
    // 追加:アニメーションを再生
    private void PlayOpenAnimation()
    {
        animator.Play("OpenWoodenBox");
    }
}

ギミックのセーブと進行状態の確認 – ドア

次は鍵を使用してドアを開けるギミックでのセーブ機能の実装です。アイテムSlotの鍵を選択した状態でドアをクリックしたらドアが開くスクリプトになっています。

まずは木箱と同じようにドアが開いたらセーブする処理を追加してみます。

Door.cs
using UnityEngine;

public class Door : MonoBehaviour
{
    [SerializeField] Animator animator;

    private void Start()
    {
        // すでにクリアしているならドアを開ける
        bool clearGimmick = SaveManager.instance.GetGimmickFlag(SaveManager.Flag.OpenedDoor01);
        if (clearGimmick == true)
        {
            Open();
        }
    }

    public void OnClickObj()
    {
        Open();
    }

    void Open()
    {
        // Keyアイテムを選択したら
        if (ItemBox.instance.CheckSelectItem(Item.Type.Key))
        {
            ItemBox.instance.UseSelectItem();

            // ドアが開いたらセーブ
            SaveManager.instance.SetGimmickFlag(SaveManager.Flag.OpenedDoor01);

            animator.Play("OpenDoor");
        }
    }
}

セーブはできるのですが、これだとゲーム再開時にOpenを実行してもKeyのアイテムを選択していないのでドアが開きません。解決するためには関数を分ける必要がありそうです。

Door.cs
using UnityEngine;

public class Door : MonoBehaviour
{
    [SerializeField] Animator animator;

    private void Start()
    {
        // すでに開けたことがあるなら、アニメーションのみ実行
        if (SaveManager.instance.GetGimmickFlag(SaveManager.Flag.OpenedDoor01))
        {
            PlayOpenAnimation();
        }
    }

    public void OnClickObj()
    {
        Open();
    }

    private void Open()
    {
        // Keyアイテムを選択していたら開く
        if (ItemBox.instance.CheckSelectItem(Item.Type.Key))
        {
            ItemBox.instance.UseSelectItem();

            // ドアが開いたらセーブ
            SaveManager.instance.SetGimmickFlag(SaveManager.Flag.OpenedDoor01);

            PlayOpenAnimation();
        }
    }

    private void PlayOpenAnimation()
    {
        animator.Play("OpenDoor");
    }
}

実行

鍵を取ってドアを開けていったんゲームを終了

再度実行

鍵を持っているか確認する処理と、アニメーションする処理を分けて解決できました。

まだ修正する箇所があります。
使用したはずの鍵がアイテムSlotに残ってしまっています。

使用したアイテム情報のセーブ

アイテムの取得とは別に、アイテムの使用情報のセーブを行い、ゲーム再開時に使用した情報があった場合、アイテムSlotに追加しないようにします。
これで使用したはずのアイテムがSlotに追加される問題が解決できます。

すでに作成してあるuseItemsを使用します。

SaveManager.cs
using System;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    public static SaveManager instance;
    private void Awake()
    {
        instance = this;
        Load();
    }

    const string SAVE_KEY = "SaveData";
    public SaveData saveData;

    public enum Flag
    {
        OpenedWoodenBox01,
        OpenedDoor01,
        Max,
    }

    public void Save()
    {
        string json = JsonUtility.ToJson(saveData);
        PlayerPrefs.SetString(SAVE_KEY, json);
    }

    public void Load()
    {
        saveData = new SaveData();
        if (PlayerPrefs.HasKey(SAVE_KEY) == true)
        {
            string json = PlayerPrefs.GetString(SAVE_KEY);
            saveData = JsonUtility.FromJson<SaveData>(json);
        }
        foreach (bool flag in saveData.getItems)
        {
            Debug.Log(flag);
        }
    }

    public void SetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        saveData.getItems[index] = true;
        Save();
    }
    
    public bool GetGetItemFlag(Item.Type type)
    {
        int index = (int)type;
        return saveData.getItems[index];
    }

    // 追加:アイテムを使用したらtypeを受け取りtrueにする
    public void SetUseItemFlag(Item.Type type)
    {
        int index = (int)type;
        saveData.useItems[index] = true;
        Save();
    }

    // 追加:セーブデータがあればそのtypeを返す
    public bool GetUseItemFlag(Item.Type type)
    {
        int index = (int)type;
        return saveData.useItems[index];
    }

    public void SetGimmickFlag(Flag flag)
    {
        int index = (int)flag;
        saveData.gimmick[index] = true;
        Save();
    }

    public bool GetGimmickFlag(Flag flag)
    {
        int index = (int)flag;
        return saveData.gimmick[index];
    }
}

[Serializable]
public class SaveData
{
    public bool[] getItems = new bool[(int)Item.Type.Max];
    public bool[] useItems = new bool[(int)Item.Type.Max];
    public bool[] gimmick = new bool[(int)SaveManager.Flag.Max];
}

SetUseItemFlagメソッドとGetUseItemFlagメソッドを追加します。

Door.cs
using UnityEngine;

public class Door : MonoBehaviour
{
    [SerializeField] Animator animator;

    private void Start()
    {
        if (SaveManager.instance.GetGimmickFlag(SaveManager.Flag.OpenedDoor01))
        {
            PlayOpenAnimation();
        }
    }

    public void OnClickObj()
    {
        Open();
    }

    private void Open()
    {
        if (ItemBox.instance.CheckSelectItem(Item.Type.Key))
        {
            ItemBox.instance.UseSelectItem();

            // 追加:アイテムの使用情報のセーブ処理を行う
            SaveManager.instance.SetUseItemFlag(Item.Type.Key);

            SaveManager.instance.SetGimmickFlag(SaveManager.Flag.OpenedDoor01);

            PlayOpenAnimation();
        }
    }

    private void PlayOpenAnimation()
    {
        animator.Play("OpenDoor");
    }
}

アイテムを使用した際にセーブする処理を行います。

この書き方でもいいのですが、CheckSelectItemとSetUseItemFlagの2か所に(Item.Type.Key)が使われています。

他のギミックにコードを使いまわす時に変更する部分が多く手間がかかります。
なるべく手間を減らしたいのでコードを修正します。

Door.cs
using UnityEngine;

public class Door : MonoBehaviour
{
    [SerializeField] Animator animator;

    private void Start()
    {
        if (SaveManager.instance.GetGimmickFlag(SaveManager.Flag.OpenedDoor01))
        {
            PlayOpenAnimation();
        }
    }

    public void OnClickObj()
    {
        Open();
    }

    private void Open()
    {
        if (ItemBox.instance.CheckSelectItem(Item.Type.Key))
        {
            // 使用するアイテムの種類を取得
            Item selectedItem = ItemBox.instance.GetSelectItem();
            if (selectedItem != null)
            {
                ItemBox.instance.UseSelectItem();
                SaveManager.instance.SetUseItemFlag(selectedItem.type);  // 引数を変更
            }
            
            SaveManager.instance.SetGimmickFlag(SaveManager.Flag.OpenedDoor01);
            PlayOpenAnimation();
        }
    }

    private void PlayOpenAnimation()
    {
        animator.Play("OpenDoor");
    }
}

これにより複製した際に修正する箇所を1つ減らすことができました。

使用したアイテムをSlotに追加しないようにする

次はゲーム再開時に使用したアイテムを、アイテムSlotに追加しないようにします。

今のままではgetItemsの情報から取得したアイテムを全部Slotに追加してしまいます。
useItemsが「true」のものはアイテムSlotに追加しないようコードを修正します。

PickUpObj.cs
using UnityEngine;

public class PickUpObj : MonoBehaviour
{
    public Item.Type type;

    private void Start()
    {
        bool hasItem = SaveManager.instance.GetGetItemFlag(type);

        // 追加
        bool usedItem = SaveManager.instance.GetUseItemFlag(type);

        // 条件を追加
        if (usedItem == true)
        {
            // アイテムを使用していたらオブジェクトを非表示にする
            gameObject.SetActive(false);
        }
        // アイテムを使用していない、かつアイテムを取得していたらSlotに追加
        else if (hasItem == true)
        {
            SetToItemBox();
        }
    }

    public void OnClick()
    {
        SaveManager.instance.SetGetItemFlag(type);
        SetToItemBox();
    }

    public void SetToItemBox()
    {
        Item item = ItemDatabase.instance.Spawn(type);
        ItemBox.instance.SetItem(item);
        gameObject.SetActive(false);
    }
}

StartメソッドでhasItemとusedItemを取得

『アイテムを使用していたら』Slotに追加せずにオブジェクトを非表示にする。

『アイテムを使用していない』&『アイテムを取得』していた場合に、Slotにアイテムを追加しオブジェクトを非表示にする。

実行

Clear All PlayerPrefasで初期化してから実行

再度実行

使用したアイテムがSlotに表示されることはなくなりました。

合成アイテムのセーブ

次は合成したアイテムの情報もセーブするようにします。

異なる2つのアイテムを使用し、新しくアイテムを生成しています。
赤いタイルを拡大表示し、その状態で青いタイルを選択し、拡大中の赤いタイルをクリックしたら新しく紫タイルが生成されるという仕組みです。

通常のアイテムの使用の処理と取得の処理とは別のメソッドになっているのでそちらにセーブする処理を追加します。

ZoomItem.cs
using UnityEngine;

public class ZoomItem : MonoBehaviour
{
    public void OnClickObj()
    {
        ItemBox.instance.Fusion(Item.Type.BlueTile, Item.Type.RedTile, Item.Type.PurpleTile);
    }
}

実行するメソッドはFusionで引数はこのようになっています。

ItemBox.cs
using UnityEngine;

public class ItemBox : MonoBehaviour
{
    [SerializeField] Slot[] slots;

    public static ItemBox instance;

    Slot selectSlot;
    Slot showSlot;

    private void Awake()
    {
        instance = this;
    }

    public void SetItem(Item item)
    {
        for (int i = 0; i < slots.Length; i++)
        {
            Slot slot = slots[i];
            if (slot.IsEmpty())
            {
                slot.Set(item);
                break;
            }
        }
    }

    public void OnSlotClick(int position)
    {
        for (int i = 0; i < slots.Length; i++)
        {
            slots[i].HideBackPanel();
        }
        slots[position].OnSelect();
        selectSlot = slots[position];
    }
    public bool CheckSelectItem(Item.Type useItemType)
    {
        if (selectSlot == null)
        {
            return false;
        }
        if (selectSlot.GetItem().type == useItemType)
        {
            return true;
        }
        return false;
    }

    public bool CheckShowItem(Item.Type useItemType)
    {
        if (showSlot == null)
        {
            return false;
        }
        if (showSlot.GetItem().type == useItemType)
        {
            return true;
        }
        return false;
    }

    public void UseSelectItem()
    {
        selectSlot.RemoveItem();
        selectSlot = null;
    }

    public void UseShowItem()
    {
        showSlot.RemoveItem();
        showSlot = null;
    }

    public Item GetSelectItem()
    {
        if (selectSlot == null)
        {
            return null;
        }
        return selectSlot.GetItem();
    }

    public void SetShowSlot()
    {
        showSlot = selectSlot;
    }

    public void Fusion(Item.Type item0, Item.Type item1, Item.Type spawnItem)
    {
        if ((CheckShowItem(item0)) && CheckSelectItem(item1)
    || (CheckShowItem(item1)) && CheckSelectItem(item0)
    )
        {
            UseSelectItem();
            UseShowItem();
            Item newItem = ItemDatabase.instance.Spawn(spawnItem);
            SetItem(newItem);
            ZoomPanel.instance.ShowItem(newItem);
        }
    }
}

使用アイテムと生成アイテムのセーブ処理

Fusionメソッドにセーブする処理を追加します。

    public void Fusion(Item.Type item0, Item.Type item1, Item.Type spawnItem)
    {
        if ((CheckShowItem(item0)) && CheckSelectItem(item1)
    || (CheckShowItem(item1)) && CheckSelectItem(item0)
    )
        {
            UseSelectItem();
            UseShowItem();
            
            // 追加:使用したアイテムをセーブ
            SaveManager.instance.SetUseItemFlag(item0);
            SaveManager.instance.SetUseItemFlag(item1);

            // 追加:紫タイルの取得をセーブ
            SaveManager.instance.SetGetItemFlag(spawnItem);

            Item newItem = ItemDatabase.instance.Spawn(spawnItem);
            SetItem(newItem);
            ZoomPanel.instance.ShowItem(newItem);
        }
    }

引数の1番目の「item0」と2番目の「item1」が合成に使用するアイテムなので「SetUseItemFlag」でそれぞれセーブを行います。

引数の3番目の「spawnItem」が合成結果の「PurpleTile」なので「spawnItem」を使用しセーブを行います。

実行

実行し紫タイルを生成し一旦終了。

再度実行

合成に使用した赤タイルと青タイルは正しく処理できました。ですがアイテムSlotに紫タイルが追加されていません。
原因はPickUpObjをセットした紫タイルのオブジェクトが存在していないため取得した情報があっても生成が実行されないためです。

解決方法としては紫タイルのTypeを持つ、空のオブジェクトを作成したらよさそうです。

紫タイルの作成

RedTileを複製し、「PurpleTile」に名前変えます。
Prefabも解除しておきましょう。

Inspectorから不要な部分を削除します。
CubeとBoxColliderとEventTriggerを削除

TypeをPurpleTileにします

実行

無事、紫タイルがアイテムSlotに生成できました。

まとめ

今回の実装では、セーブ機能の実装を行いました。

最初は複雑に感じましたが、仕組みさえ理解してしまえばそこまで難しくないように思います。
セーブ機能は短めなゲームなら必要ないかもしれませんが、脱出ゲームにおいて進行状況のセーブは欠かせないため、しっかり使いこなしたいです。

また、フラグを活用することで、ギミックやアイテムの使用状態を簡潔に管理できると分かりました。
今後はロード時の適切な初期化やバグの発生有無をさらに検証し、より安定したセーブシステムを目指したいです。
今回の方法を応用すれば、他のオブジェクトの状態管理にも活用できそうなので、さらに発展させていきたいと思います。

とりあえず今回はここまで

参考にさせていただきました。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です