注:当ブログでは広告を利用しています。

Unity:シーン移動後自動セーブ+ロード基本編

おはよう。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にするなど別個にしておくべし。

出ないと、次の悩みで時間をとられる。

二つとも同じstringで同じ変数じゃないか。
なんで空白エラーが出るんだ?

参照:Unityでゲームデータのセーブ・ロードを行う方法

 

継承monobehaviourが持つ意味

unityでスクリプトを創ると、必ず出てくる単語「monobehaviour」がある、
monobehaviourがあると新しいシーンに入った際、今まであったシーンが全て消える

例えばシーンAとシーンBに共通のゲームオブジェクト「Tool」があるとする。
二つのシーンにある「Tool」はシーンを変えたらそのまま移動ではない

シーンAはシーンAだけの「Tool」が、シーンBはシーンBだけの「Tool」であり、
AとBの「Tool」はmonobehaviourがある限り、そこでしか効果がない。

中身が同じでもmonobehaviourがある限り、すべて更新される。
新しいシーンに入ると、今まであったシーンはすべて更新される。

セーブするならデータ格納ファイルにmonobehaviourをつけてはいけない
monobehaviourを外すからシーンを移動しても消滅しなくなり、変数の中身を保持してくれる。

 

セーブファイルの置き場所

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;//現在のシーン名取得
}

$をつけて””で囲み、変更する変数部分を{}で囲めばいい。
私はシーン名だけだと重なる部分があるため、シーン名の前にステージ番号をつけている。

参照:【Unity入門】$を使って文字列の中に変数を埋め込む方法

 

セーブとロードの基本:アイテムをとったらセーブする

まだまだ基本だ。
セーブとロードの大基本操作は上記動画「セーブロード前編」を見て、
そのままの通りにやればいい。

ここでやるべきセーブとロードとして、

  1. コインをとったら即セーブ
  2. シーン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のままだ。

ここまでが基本だ。次が応用に入る。

お願い

めがびちゃんからお知らせ♪

お知らせ

megabe-0へ訪問した"本当"の理由

まさか記事の書き形一つでこうなるとは…

お願い1

Writer軽い自己紹介

ティラノスクリプトや小説家になろう、ピクシブ他で物語を書きながら、 「私が気になった事件」の裏側を作家の視点で書いているおっさん。

プロフィール画像は自画像でなく、Megabe-0ブログのマスコット、めがびちゃん。

 

雷が苦手で、光を見ると頭が固まる(元から固い)。 月初めは墓参りと神社参拝を行い、賽銭箱へ1万円を入れた際、とても気持ちがすっきりした。

 

■ 簡単な自分史 ■

0歳:釧路のある病院で生まれる。暇さえあれば母乳を吸って、ご飯を4膳食べても体重が落ちるほど、母のダイエットにものすごく貢献したらしい

 

3歳:行方不明になり、全裸で海を泳ごうとしたところ、いとこのお姉さんに発見され、この世へ留まる

 

8歳:自分のお金でおもちゃのカードを初めて買い、経済を知る。なぜか父親に怒られ、家出するがすぐに見つかる。

 

12歳:学校で給食委員長になる。委員長として初めて全校生徒の前にて演説する際、原稿用紙を忘れてアドリブで笑いを誘いながらも何とかやり過ごし、多くの生徒に名前と顔を覚えてもらう。また、運動会の騎馬戦では変なアドリブを行い、多くの笑いを誘った。

 

18歳:初めて好きな人ができたけれど、告白が恥ずかしくてついにできず、別れたことを今でも根に持っている(妻となる人にははっきり言えてよかった)

 

21歳:大学在学中、アルバイトを始める。人手不足かつとても忙しい日々を過ごしながら「どうせなら自分から楽しいことをしていきたいなあ⇒起業って選択肢があるのか」働き方の選択肢を見つける

 

27歳:自分で作った会社がうまくいかず、一度たたんで都落ち。実家でとことん自分を責める日が続く。「何をやっても駄目だな、お前は」など。自分を責めても自殺ができず、体中から毒素があふれ出て苦しい日々を送る。寝るのも怖かった日々。

 

28歳:「このままじゃいけない」決心を決め、小学校からの勉強をやり直す。高校の勉強で躓きながらも、学び直すうちに「自分は何もわかっていなかったんだなあ」大切な教えに気づかされる。 加えて、小説やイラストなど「今までの自分が手を出さなかった分野」に手を伸ばしてみた。

 

29歳:「定義」と「自己肯定」こそが生き方を決めると気づかされ、不安な日々が起きても、心が強くなったと感じる。でも子供の誘惑にはめっぽう弱くなる。

 

35歳:人生初の交通事故(物損)に出会う。冬道の運転で車を上下に大回転(スピンではない)を体型氏、何とか命を取り留め、なぜ生きているのかわからない状態に陥る。

自分の生き方はすべて自分が握っている。わずかな瞬間にしか現れない「自分の真実」を表に引きずり出し、ピンチからチャンスを生み出す発想や視点をブログやメルマガ他で提供中。