おはよう。Unityの話だ。
現在ゲームを作っており、1か月ほど費やした問題があった。
セーブとロードだ。
簡単なセーブとロードは1日もかけずにできる。
シーンAからBに移動したらすぐさまセーブし、再びBからAに移動したらロードする。
例えばアイテムを取った後にシーンBへ入ったら即セーブしないと、
BからAに戻ったとき、取得したアイテムが普通に出てくる。
アイテムを一度とったら、特殊な状況でない限り二度ととれぬ仕組みにしたい。
一度とったら二度と取れない仕組みへ変えるのに1か月も費やした。
参考になった動画や本などを紹介しつつ、
自動セーブとロードの仕組みがやっとできたので、
同じ悩みを抱いているなら参考にしてほしい。
今回はかなり長くなるので、何回かに分けて書く。
それだけ苦労した……どこにも求める答えがないから。
シーン移動後も保持の基本:public static
始めにシーンをまたいでも値が保持されるpublic staticについて書く。
public static int/float/string/bool/void(関数)は、シーンをまたいでも使用できる。
使用方法はpublic staticを置いたスクリプトをPlayer.csとおき、
使いたいスクリプトをEnemy.csと置くと
Player.cs
public static int hp = 5;
//------------------------------------
Enemy.cs(新しいスクリプト)
private int damage = 2;
void Rife(){
int relife = Player.hp - damage;
//relife = 3(5-2より)
}
public staticを使うと、別のスクリプトからも呼び出せる。
呼び出す際は作ったスクリプト名を先頭に乗せてから、変数を置いて使う。
public staticは主に味方のHPや残り人数、得たコインの枚数などで使う。
残りHPや残り人数などを保存する時、jsonを使う。
セーブとロードの基本:json
UnityにおいてセーブとロードはPrefsとjsonがある。今回jsonしか使わない。
jsonは二つある。
- セーブ:新規クラスなどで作った文字列stringをjsonファイルに変換
- ロード:セーブ時に作ったjsonファイルを呼び出し、文字列に変換
スクリプトは一つでもできるけど二つあったほうがいい。
データ格納作成スクリプトとデータ保存処理スクリプトだ。
上記動画では
userDataがデータ格納に必要な値入力スクリプトであり、
SaveSystemがデータ保存処理スクリプトだ。
上記動画の通りにやれば、とりあえずセーブとロードの仕組みがわかる。
補足としてセーブとロードの形に注目する。
JsonUtility.ToJson(クラス); //セーブ用
JsonUtility.FromJson<クラス>(JSONデータ); //ロード用
二つのクラスは基本同じだが、ややこしさの要因でもある。
実際にスクリプトを見ていただこう。上記動画の通りにセーブファイルを作った。
//UserData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]
public class SaveList{//coinなど
public int currentSceneIndex;
public Vector2 pos = Vector2.zero;
public int coin;
public int key;
public FlagsID[] flagsID ;
}
[Serializable]
public class FlagsID{
public int arrangeID ;
}
//----------------------------------
//SaveManager.cs
public class SaveManager{
private static SaveManager instance = new SaveManager();
public static SaveManager Instance => instance;
private SaveManager(){Load();}
public string Path => Application.dataPath + "/Save/data.json";
public SaveList saveList {get; private set;}
public void Save(){//セーブ用
string jsonData = JsonUtility.ToJson(saveList , true);
//通常はfalse,trueだと整えてくれる。falseは一列に並べる。
//スクリプト君の負担を減らすならfalseにする。
//string jsonData = JsonUtility.ToJson(saveList); でも可
StreamWriter streamWriter = new StreamWriter (Path, false);
//falseは上書き、trueは追記
streamWriter.WriteLine(jsonData);
streamWriter.Flush();
streamWriter.Close();
}
public void Load(){//初回起動+初期化+ロード用
if(!File.Exists(Path)){//セーブファイルがあるか?
saveList = new SaveList();//ないなら新規作成して即座に保存。
Save();
return;
}
//ファイルがあるなら、すでにあるファイルを持ってきてロード
StreamReader streamReader = new StreamReader (Path);
string jsonData = streamReader.ReadToEnd();
saveList = JsonUtility.FromJson<SaveList>(jsonData);
streamReader.Close();
}
}
セーブ関数に書いてあるstring jsonData = JsonUtility.ToJson(saveList , true);と
ロード関数にあるstring jsonData = streamReader.ReadToEnd();は別物だ。
同じstring jsonDataとはいえ、二つは全く異なる中身だ。
だからこそロード・セーブのstringは変数をはっきり分けたほうがいい。
セーブstring jsonDataをjsonDatasaveにして、
ロードstring jsonDataをjsonDataloadにするなど別個にしておくべし。
出ないと、次の悩みで時間をとられる。
なんで空白エラーが出るんだ?
継承monobehaviourが持つ意味
unityでスクリプトを創ると、必ず出てくる単語「monobehaviour」がある、
monobehaviourがあると新しいシーンに入った際、今まであったシーンが全て消える。
例えばシーンAとシーンBに共通のゲームオブジェクト「Tool」があるとする。
二つのシーンにある「Tool」はシーンを変えたらそのまま移動ではない。
シーンAはシーンAだけの「Tool」が、シーンBはシーンBだけの「Tool」であり、
AとBの「Tool」はmonobehaviourがある限り、そこでしか効果がない。
中身が同じでもmonobehaviourがある限り、すべて更新される。
新しいシーンに入ると、今まであったシーンはすべて更新される。
セーブするならデータ格納ファイルにmonobehaviourをつけてはいけない。
monobehaviourを外すからシーンを移動しても消滅しなくなり、変数の中身を保持してくれる。
セーブファイルの置き場所
Unity話。1か月も悩んでいた部屋移動間でのオートセーブ+ロード(移動時にアイテムを取得してたら、二度と取れないようにする)、完璧にできた。
1か月前の自分に「絶対あきらめるな、道は少しずつ開ける。何とか出来たよ、大変だったけど」言ってあげたい。 pic.twitter.com/2p5Jz2xkjy— せんけん (@megabi0) April 24, 2023
public string Path => Application.dataPath + “/Save/data.json”;を見てほしい。
セーブしたファイルをどこに置くか決めるための一文だ。
Application.dataPathはアセット内を示す。
/Save/data.json”は「Saveフォルダ内にdata.jsonという名前で保存する」を示す。
私はステージごとに自動セーブとロードをつけたいため、
data.jsonの「data」を変更できないか模索した。
するとstringは変数であり、現在のシーン名を使えば変更できると分かった。
private sting stagename;
public string Path => Application.dataPath + $"/Save/{stagename}.json";
void start(){
stagename = SceneManager.GetActiveScene().name;//現在のシーン名取得
}
$をつけて””で囲み、変更する変数部分を{}で囲めばいい。
私はシーン名だけだと重なる部分があるため、シーン名の前にステージ番号をつけている。
セーブとロードの基本:アイテムをとったらセーブする
まだまだ基本だ。
セーブとロードの大基本操作は上記動画「セーブロード前編」を見て、
そのままの通りにやればいい。
ここでやるべきセーブとロードとして、
- コインをとったら即セーブ
- シーンBに移動後、再びAに戻ってロード
だからこそやるべき計画として、ロードとセーブに分けると、
- ロード:Startですぐさまロードを起動。ファイルがなければ即新規作成。
- セーブ:アイテムをとったら即セーブ(1ファイルのみ)
セーブ保存スクリプト
//SaveData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;//これを入れる
[Serializable]
public class SaveList{
public bool getcoines;//コインのみの保存
}
まずはセーブデータを作る。
[Serializable]と書いている部分はusing Systemがないなら、[System.Serializable]と書けばいい。
ロードのやり方
//SaveStage.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;//StreamReader,StreamWriter使用時に必要
using UnityEngine.SceneManagement;//シーン変更時に必要
public static SaveList save ;//シングルトン
public string PathS => Application.dataPath + "/Save/hozon.json";//セーブファイルの置き場所と名前
public bool getCoin = false;
void Start(){
if(!File.Exists(PathS)){//初めて訪れるなら、その場で新規作成
save = new SaveList();//newが新規クラス作成
string jsonData = JsonUtility.ToJson(save);
StreamWriter streamWriter = new StreamWriter(PathS, false); //falseは上書き、trueは追記
streamWriter.WriteLine(jsonData);
streamWriter.Flush();
streamWriter.Close();
return;
} else {//すでにあるならロードしておく。
StreamReader streamReader = new StreamReader(PathS);
string jsonData1 = streamReader.ReadToEnd();
save = JsonUtility.FromJson<SaveList>(jsonData1);
this.getCoin = save.getcoines;//ロードしたファイルから代入
streamReader.Close();
}
}//Start
//この後セーブへ
if(!File.Exists(PathS)){}はアセットにセーブしたデータがない場合を示す。
セーブしたファイルがない場合、新規クラスnew()を作成する。
save = new SaveLists();
あるいは save = new();だけでもいい
新規クラスを作った時点で空っぽだ。空っぽの状態で即銭セーブを行い、
とりあえずアセットにjsonファイルだけを作ってしまう。
セーブの作り方
ここでは接触したキャラのタグをPlayerと置く。
またアイテム取得時、GameManager.coin (int関数)が増えるとする。
//GameManager.coinはGameManager.csにて
public static int coin; //と置いた。
以下、//SaveStage.csの続きから入る。
//SaveStage.cs続き
void Start(){}//ロードで書いたので省略
void OnTriggerEnter2D(Collider2D nol) { //直で触れたときの処理
if (nol.gameObject.CompareTag("Player"))
{GetItem();}//下のGetItem()関数へ
}//OnTriggerEnter2
public void GetItem(){
GameManager.coin ++;
this.getCoin = true;//thisは省略可。
save = new SaveLists();//新規作成
save.getcoines = getCoin;//値はtrue
string jsonDatas2 = JsonUtility.ToJson(save);//jsonに書き込み
StreamWriter streamWriter = new StreamWriter(PathS, false);
streamWriter.WriteLine(jsonDatas2);
streamWriter.Flush();
streamWriter.Close();
}
これでコインはすでに取得したので、
jsonファイルの中身を消さない限り、コイン取得はtrueのままだ。
ここまでが基本だ。次が応用に入る。