スタート時にはコライダーの赤い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;
すると、次は一番最初の状態からスタートします。これで直前のセーブポイントから始まるようになりました。
チャプター/セクション/ステート 等と区切りを増やしても良いかもしれません。