【Unity】ゲームの進行状況をセーブ/ロードする

投稿者: | 2020-09-28

スタート時にはコライダーの赤いCubeだけがあり、それに入るとCubeが消えて秒読みが始まります。3秒立つと左上に球が出現して、ゲームの進行状況がセーブされ、シーンを再読み込みすると、球だけがある状態からはじまります。

球の方を向くと球が消えて緑のCubeが出現するので、そのCubeに入ると2秒後にシーンが再読み込みされます。このロード前に最初の状態でセーブされるので、再スタートするとまた赤いCubeだけがある状態に戻っています。

ゲームの進行管理やセーブ/ロードするスクリプトはゲームの進行を管理するを元にしています。今回はUnityEngine.PlayerPrefsを使わずにXMLファイルにデータを保存しました。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.IO;

public class GameScript : MonoBehaviour
{
    [SerializeField] GameObject[] colliders;
    [SerializeField] Text text;
    [SerializeField] Transform target;

    public static bool onTriggerEnter;

    int chapter;
    int state;  
    float sec;
    Transform playerHead;
    AudioSource audioSource;


    string filename;

    // Start is called before the first frame update
    void Start()
    {
        filename = Application.dataPath + "/testsavedata.xml";

        if (File.Exists(filename))
        {
            var data = XmlUtil.Deserialize<SaveData>(filename);
            chapter = data.chapter;
        }
        else {         
            chapter = 1;
            Save(); // 保存
        }
      

        state = 1;
        text.text = "";
        playerHead = Camera.main.transform;
        target.gameObject.SetActive(false);
        audioSource = GetComponent<AudioSource>();

        // コライダーを非アクティブ
        foreach (GameObject col in colliders)
        {
            col.SetActive(false);
        }
        
        switch (chapter)
        {
            case 1:

                colliders[0].SetActive(true);

                break;

            case 2:
                target.gameObject.SetActive(true);
                break;

        }
    }

    // Update is called once per frame
    void Update()
    {

        if (Input.GetKeyDown(KeyCode.Space))
        {
            // シーンをリロード
            SceneManager.LoadScene("New Scene");
        }

        switch (chapter)
        {
            case 1:

                switch (state)
                {
                    case 1:

                        if(onTriggerEnter)
                        {
                            onTriggerEnter = false;
                            audioSource.Play();

                            colliders[0].SetActive(false); // 赤いのコライダーを非アクティブ
                            state = 2; // 次のステートへ
                        }
                        break;
                    case 2:

                        sec += Time.deltaTime;
                        text.text = sec + "";

                        
                        if (sec >= 3f)
                        {
                            sec = 0f;
                            text.text = "";
                            target.gameObject.SetActive(true);
                            chapter = 2; // 次のチャプター
                            state = 1;
                            Save(); // セーブ
                        }
                        break;

                    
                }

                break;

            case 2:

                switch (state)
                {
                    case 1:

                        RaycastHit hit;
                        Vector3 dir = target.position - playerHead.position;
                        if (Physics.Raycast(playerHead.position, dir, out hit, 20f))
                        {
                            if (hit.collider.tag == "Target")
                            {
                                float dot = Vector3.Dot(playerHead.forward, Vector3.Normalize(dir));

                                // プレイヤーがターゲットの方を見たら
                                if (dot > 0.9f)
                                {
                                    target.gameObject.SetActive(false);
                                    colliders[1].SetActive(true); // 緑のコライダーをアクティブ
                                    state = 2; // 次のステートへ
                                }
                            }
                        }
                        break;

                    case 2:

                        if (onTriggerEnter)
                        {
                            onTriggerEnter = false;
                            audioSource.Play();
                            colliders[1].SetActive(false); // 緑のコライダーを非アクティブ
                            state = 3;
                            text.text = "ゲーム終了";
                        }
                        break;

                    case 3:

                        sec += Time.deltaTime;

                        if (sec >= 2f)
                        {
                            chapter = 1;
                            state = 1;
                            Save();
                            SceneManager.LoadScene("New Scene");
                        }
                        break;
                }
                break;
        }



    }

    void Save()
    {
        SaveData data = new SaveData();
        data.chapter = chapter;
        XmlUtil.Seialize(filename, data);
    }
}

Start()でまずデータの保存場所を指定します。Application.dataPathはUnityエディターではAssetsフォルダのパスです。

filename = Application.dataPath + "/testsavedata.xml";

AssetsフォルダはProjectウィンドウで右クリックからShow in Explorerをクリックするとすぐに見つかります。

指定したファイルがあればデータをロードして、無ければセーブします。先に同じ名前の空のファイルを自分で作っておくと、Root element is missing…とエラーがでます。

using System.IO;

// Start()
if (File.Exists(filename))
{
    var data = XmlUtil.Deserialize<SaveData>(filename);
    chapter = data.chapter;
}
else {         
    chapter = 1;
    Save(); // 保存
}

// ---
void Save()
{
    SaveData data = new SaveData();
    data.chapter = chapter;
    XmlUtil.Seialize(filename, data);
}

データをセーブ/ロードするために、データを入れるクラスとXMLファイルに読み書きするクラスを作っておきます。参考:http://ftvoid.com/blog/post/1061

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class SaveData
{
    public int chapter;
}
using System.IO;
using System.Xml.Serialization;

public class XmlUtil
{
    public static T Seialize<T>(string filename, T data)
    {
        using (var stream = new FileStream(filename, FileMode.Create))
        {
            var serializer = new XmlSerializer(typeof(T));
            serializer.Serialize(stream, data);
        }

        return data;
    }

    public static T Deserialize<T>(string filename)
    {
        using (var stream = new FileStream(filename, FileMode.Open))
        {
            var serializer = new XmlSerializer(typeof(T));
            return (T)serializer.Deserialize(stream);
        }
    }
}

データを保存するときのFileStreamのファイルモードがCreateになっているので、ファイルがあれば上書きし無ければ新規作成します。セーブするときにFile.Createでファイルを作ると、IOException: Sharing violation on path C:\Users…とエラーがでました。

Start()ではその後、このチャプターの値によって、シーンの初期状態を設定しています。

switch (chapter)
{
    case 1:

        colliders[0].SetActive(true);

        break;

    case 2:
        target.gameObject.SetActive(true);
        break;

}

チャプター1のとき赤いCubeだけをアクティブにして、2では球だけをアクティブにしています。

その後、Update()でチャプター1のステート2まで来たときに3秒立つと、チャプターを2ステートを1にしてセーブします。

case 2:

    sec += Time.deltaTime;
    text.text = sec + "";

                        
    if (sec >= 3f)
    {
        sec = 0f;
        text.text = "";
        target.gameObject.SetActive(true);
        chapter = 2; // 次のチャプター
        state = 1;
        Save(); // セーブ
    }
    break;

すると、シーンを再読み込みしたときに、チャプター2から始まるので、Start()で球だけがアクティブになっている状態になり、Update()でもチャプター1の処理は飛ばされます。

緑のCubeに入ってゲーム終了のテキストが表示されると、2秒後にチャプター1ステート1にしてから保存してシーンをロードします。

sec += Time.deltaTime;

if (sec >= 2f)
{
    chapter = 1;
    state = 1;
    Save();
    SceneManager.LoadScene("New Scene");
}
break;

すると、次は一番最初の状態からスタートします。これで直前のセーブポイントから始まるようになりました。

チャプター/セクション/ステート 等と区切りを増やしても良いかもしれません。

コメントを残す

メールアドレスが公開されることはありません。