ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

【Unity】ミクゆにのセーブ方法について

書き手:肥田野

セーブとロード

15.03.27現在のセーブ方式です。

必要なスクリプト

□Save.cs
□DeadLine.cs
□Goal.cs

■Save.cs

using UnityEngine;
using System.Collections;
using System.IO;

public class Save : MonoBehaviour {

    public int savePoint;

    void OnTriggerEnter(Collider col){
        if(col.tag == "Player"){
            if (File.Exists("save.bin")){
                File.Delete("save.bin");
            }
            FileStream BinaryFile = new FileStream("save.bin", FileMode.Create, FileAccess.Write);
            BinaryWriter Writer = new BinaryWriter(BinaryFile);
            if(Application.loadedLevelName.Equals("Stage1")){
                Writer.Write(1);
            }else if(Application.loadedLevelName.Equals("Stage2")){
                Writer.Write(2);
            }else if(Application.loadedLevelName.Equals("Stage3")){
                Writer.Write(3);
            }else if(Application.loadedLevelName.Equals("Stage4")){
                Writer.Write(4);
            }
            Writer.Write(savePoint);
            BinaryFile.Close();
        }
    }
}

各チェックポイントへ動的に取り込むため、このスクリプトはプロジェクトに入れておくだけにします。
取り込む方法はDeadLine.cs参照。
チェックポイントにはBoxColliderが必要ですが、DeadLine.csがこのスクリプトを動的に取り込む際、一緒にBoxColliderも設置されるので、エディターから設定する必要はありません。
また、バイナリファイルを扱うためにSystem.IOを使用しています。

Playerタグの付いたオブジェクトが侵入すると、save.binファイルを探し、もし存在すれば削除します。
そして新しいsabe.binファイルを書き込み形式で新規作成し、現在ロードされているSceneファイル名からステージ番号を、チェックポイント自身が保持するsavePoint変数からチェックポイントの番号を書き込みます。

■DeadLine.cs
※残機を表示する処理が含まれます

using UnityEngine;
using System.Collections;
using System.IO;

public class DeadLine : MonoBehaviour {

    public static Vector3 StartPosition;
    public GameObject negi;

    [HideInInspector]

    public static int startPoint;
    public static Vector3 playerRotate;
    public static GameObject Player;
    public static GameObject[] CheckPoint;
    public static GameObject lifenegi;

    void OnTriggerEnter(Collider col){
        if(col.gameObject.tag == "Player"){
            for(int i=0;i<MikuMove.life;i++){
                GameObject.Destroy(lifenegi);
            }
            MikuMove.life--;
            if(MikuMove.life>=0){
                Load ();
                Player.transform.Rotate (playerRotate-Player.transform.rotation.eulerAngles);
            }
        }
    }

    void Start () {
        CheckPoint = GameObject.FindGameObjectsWithTag ("CheckPoint");
        for(int i=0;i<CheckPoint.Length;i++){
            Save component = CheckPoint[i].AddComponent<Save>();
            component.savePoint = i;
            BoxCollider pos = CheckPoint[i].AddComponent<BoxCollider>();
            pos.isTrigger = true;
            pos.center = new Vector3(0.0f,0.6f,0.0f);
        }
        Load ();
        for(int i=0;i<MikuMove.life;i++){
            lifenegi = Instantiate(negi,Player.transform.position,Quaternion.identity)as GameObject;
            lifenegi.transform.parent = Player.gameObject.transform;
            lifenegi.transform.Rotate(-90.0f,0.0f,0.0f);
            lifenegi.transform.localPosition = new Vector3(-1.7f+0.3f*i,1.3f,0.0f);
        }
        playerRotate = Player.transform.rotation.eulerAngles;
        
    }

    void Update () {
        if(MikuMove.life<=0){
            if(File.Exists("save.bin")){
                File.Delete("save.bin");
            }
            MikuMove.life = 3;
            Load ();
        }
    }

    void Load(){
        Player = GameObject.FindGameObjectWithTag("Player");
        GameObject stpos = GameObject.Find ("StartPosition");
        StartPosition = stpos.transform.position;
        if (File.Exists("save.bin")){
            FileStream BinaryFile = new FileStream("save.bin", FileMode.Open, FileAccess.Read);
            BinaryReader Reader = new BinaryReader(BinaryFile);
            Reader.ReadInt32();
            startPoint = Reader.ReadInt32();
            if(startPoint == -1){
                Player.transform.position = StartPosition;
            }else{
                Player.transform.position = CheckPoint[startPoint].transform.position;
            }
            BinaryFile.Close();
        }else{
            Player.transform.position = StartPosition;
        }
    }
}

まずStart()を解説します。
GameObject型のメンバ変数配列「CheckPoint」に、FindGameObjectsWithTag関数で「CheckPointタグ」の付いたオブジェクトを一つずつ代入していきます。
C#の配列はC++と異なり、「型[] 識別名」と宣言します。要素の数は動的に変動するので、宣言時に添字は不要になります。
また、その配列の要素数は「配列名.Length」で取得できます。
次のfor文内が変則的ですが、変数「component」の型「Save」は、スクリプトのファイル名です。
Save型の変数componentに先ほど用意したCheckPointオブジェクトの一つを代入し、AddComponent関数でSave.csを設置します。
新たに変数componentを用意しないと、CheckPointオブジェクトのメンバ変数にアクセスができませんでした。理由は不明です。ご存知の方はコメント等で指摘していただけると嬉しいです。
同様にBoxCollider型の変数posを宣言し、CheckPointオブジェクトにBoxColliderを追加します。
ついでにBoxColliderの設定を変更し、衝突判定をなくしてセンターの位置を少し上にずらしています。通常はエディタ画面で人力で行う作業ですが、このようにスクリプトで自動的に行うことで、思わぬ設定忘れを防ぎ、作業量を減らすことができます。

Load()の後に続くfor文は、残機を表示するための処理なので解説は省略します。

次にLoad()を解説します。
この関数が実行されると、ゲームオブジェクト「Player」に、該当タグの付いたオブジェクトを探して保持させます。
また、GameObject型変数「stpos」(スタートポジションの略)を宣言し、StartPositionタグのついたオブジェクトを探して代入します。
次に「save.bin(バイナリファイル)」ファイルがフォルダ内に存在するかを調べて、存在すれば読み取り形式で開き、専用の変数に格納します(if文2,3行目)。
バイナリファイルの最初の文字はステージ番号なので、読み飛ばします(Save.cs参照)。
変数「startPoint」に、2文字目を格納します。2文字目は、Start()で各チェックポイントに与えられた番号が記録されています。
もしどこのチェックポイントも通過していなければ、バイナリファイルの2文字目には「-1」が記録されるので、その場合stposの座標をプレイヤーに代入します。
バイナリファイルの2文字目が-1以外のときは、その番号を持つCheckPointの座標を代入しています。

OnTrrigetEnter()は、プレイヤーが接触した時にライフを1減らし、Load()を実行するだけです。

Update()は、ライフが0以下になった時にセーブファイルを消去(=通過したセーブポイントの情報を消去)するので、ライフが全快してスタート地点に戻されます。

■Goal.cs

using UnityEngine;
using System.Collections;
using System.IO;

public class Goal : MonoBehaviour {

    void OnCollisionEnter(Collision col){
        if(col.gameObject.Tag == "Player"){
            if(Application.loadedLevelName.Equals("Stage1")){
                Application.LoadLevel("Stage2");
            }else if(Application.loadedLevelName.Equals("Stage2")){
                Application.LoadLevel("Stage3");
            }else if(Application.loadedLevelName.Equals("Stage3")){
                Application.LoadLevel("Stage4");
            }
        }
    }
}

こちらはシンプルです。
プレイヤーが浸入した時、現在ロードされているSceneファイル名を参照して、次のステージのデータをロードしています。

ざっくりと説明しましたが、自分でも読み返すと(分かりにくいスクリプトだなぁ……(-_-;))と感じたので、その内改良版を作るかもしれません。
現状はこのスクリプトで動いていますので、挙動が気になった場合は参考にしてください。