最終更新:
orika_ex_miyako 2023年05月29日(月) 11:49:51履歴
この手順では、List の情報をセーブ・ロードするための機能を実装します。
以下の内容で実装を進めていきます。
発展18 ーセーブ・ロード機能の実装�◆�
26.クリアしたステージの情報と契約したキャラの情報をセーブ・ロードする機能を実装する
新しい学習内容は、以下の通りです。
・String.IsNullOrEmpty メソッド ・String.Split メソッドと StringSplitOptions 列挙型 ・Linqの機能の実装例�◆ �Aggregate メソッド、ToList メソッド〜 ・Listの使い方�� ーConvertAll メソッドー ・int32.TryParse メソッド、int32.Parse メソッド
前回の手順では int 型のクリアポイントの情報を PlayerPrefs クラスの SetInt メソッドと GetInt メソッドを 活用し、セーブ・ロード機能を実装しました。
この手順では、クリアポイントの他にもセーブ・ロードを行いたい情報を考えて、その変数をセーブ・ロードする機能を実装する設計を考えていきます。
現在ユーザーの情報としてセーブしておきたい情報としては、クリアしたステージの情報と、契約しているキャラの情報があげられます。
どちらも int 型の情報ですが、List を活用して管理をおこなっています。
PlayerPrefs クラスには int 型をセーブ・ロードするメソッドはありますが、List 型をそのままセーブ・ロードする機能はありません。
つまり、List<int> 型そのままの状態ではセーブ・ロードができないため、少し手を加えてあげる必要があります。
このようなときにはいくつかの方法が考えられます。(他にもあります)
<型が異なる際のセーブの方法>
1.List の要素(中身)を1つずつ取り出して、int 型として1つずつ、セーブ・ロードを行う 2.保存したいデータの型の作りをセーブ・ロードが行える型に変更してから、セーブ・ロードを行う
今回は【2】の方法を採用します。
参考までにですが、【1】の方法を解説しておきます。
for 文や foreach 文などを活用して、List の最大値を目標値に繰り返しの処理を作り、処理の中で、List の各要素に順番にアクセスして、それを int 型でセーブを行います。
ロードする場合も同様で、int 型の情報をロードして、それを List に順番に追加することで復元します。これは、List で管理している型がセーブ・ロードできる型である場合にのみ実装出来ます。
ただし、この実装の場合、セーブ・ロード用の Key が各値ごとに必要になります。例えば、List の要素数が 5 個ある場合、5 つのセーブ・ロード用の Key が必要になります。
このような場合は、1つの Key 用の文字列を作成し、文字の最後に通し番号を採番することで各値のセーブ・ロード用の Key を自動的に作成できます。
<for 文を活用して List の情報をセーブ・ロードする場合の実装例>
private const string ENGAGE_CHARA_KEY = "engageCharaNos_"; // セーブ・ロード用の Key(このままだと1つしかセーブできないので、最後に番号を追加する)
// セーブ
public void SaveEngageCharaListToInt() {
// 契約したキャラの List から要素を 1つずつ取り出す
for (int i = 0; i < engageCharaNosList.Count; i++) {
// 1つずつセーブするために Key に採番し、セーブする値を設定
PlayerPrefs.SetInt(ENGAGE_CHARA_KEY + engageCharaNosList[i].ToString(), engageCharaNosList[i]);
// engageCharaNos_0 のような形でセーブされますので、ロードする場合も、engageCharaNos_0 のように、最後に番号を付ける必要があります
}
// SetInt メソッドの情報をすべてセーブ
PlayerPrefs.Save();
}
// ロード
public void LoadEngageCharaListToInt() {
// 登録されているキャラのデータ数を目標値に繰り返す
for (int i = 0; i < DataBaseManager.instane.charaDataSO.charaDataList.Count; i++) {
// セーブされている Key があるか確認
if(PlayerPrefs.HasKey(ENGAGE_CHARA_KEY + i)) {
// ロードして List に追加
engageCharaNosList.Add(PlayerPrefs.GetInt(ENGAGE_CHARA_KEY + i));
}
}
}
【2.保存したいデータの型の作りをセーブ・ロードが行える型に変更してから、セーブ・ロードを行う】方法についての設計の考え方です。
型の作りをセーブ・ロードが行える型に変更する、という部分ですが、PlayerPrefs クラスでは3つの型でのセーブ・ロードが可能です。
List の情報は int 型ですが、この情報を、値(要素)ごとにカンマで区切られた1行の文字列として新しく変数を作成するようにします。
<List<int> をカンマ区切りの string 型にする>
List<int> engageCharaNosList = new List<int>(); engageCharaNosList[0] = 0; engageCharaNosList[1] = 1; engageCharaNosList[2] = 2; engageCharaNosList[3] = 3;
↓
// List<int> を カンマ区切りの 1行の string 型にする string saveStr = "0, 1, 2, 3,";
このように、情報を書き換えてあげることで、PlayerPrefs クラスのセーブ・ロードができる string 型になりますので、
セーブを行う際に string 型にして保存し、ロードを行う際にはこの string 型でロードして、その情報を元の型の情報に戻すことで、
PlayerPrefs クラスを利用してセーブとロードが行えるようにします。
処理の流れをまとめます。
<セーブするとき>
・セーブしたい情報(クラス・型)が PlayerPrefs クラスに対応している型ではないため、そのままでは List<int> 型の変数の値をセーブ・ロードできない。 ↓ ・List<int> 型の情報を1つずつ取り出して、値と値とをカンマで区切った1行の文字列として作成し、セーブしたい情報を string 型の情報に変換する。 ↓ ・この string 型の情報を PlayerPrefs クラスの SetString メソッドと Save メソッドを利用してセーブする。
<ロードするとき>
・string 型でセーブしてある情報を PlayerPrefs クラスの GetString メソッドを利用してロードをする。 ↓ ・ロードした情報(クラス・型)が string 型のため、そのままではセーブする前の情報(今回は List<int> 型)として活用できない。 ↓ ・ロードした情報をカンマの位置で区切って、List<int> 型の値として1つ順番に取り出す。その際に、string 型を int 型に変換して List に追加することで情報を復元する。 ↓ ・セーブする際の情報(クラス)に復元されるので、この情報をまたゲームで利用する
IsNullOrEmpty メソッドは String クラスが持つ機能の1つです。利用するためには、using System の宣言が必要になります。
引数内に指定された文字列が null 、あるいは空の文字列 ("") であるかどうかを判定して真偽値を戻します。
null、あるいは空の文字列 ("") である場合には true 、文字列が null でない場合には false を戻します。
値があるか、ではなく、値がないかに主を置いた判定ですので、真偽値の使い方を間違えないようにしましょう。
つまり、文字列が入っていることを期待するのであれば、false であることを判定値に使う必要があります。
// ロードした文字列がある場合
if (!string.IsNullOrEmpty(engageCharaListString)) {
今回のケースの場合、引数として指定されているのは、engageCharaListString 変数の部分であり、この値が判定の対象です。
engageCharaListString 変数の値が null なのか、あるいは値があるのかを判定して、真偽値を戻す処理になっています。
参考サイト
MicroSoft C# ドキュメント
String.IsNullOrEmpty(String) メソッド
@IT 様
文字列がnull(Nothing)か空文字列かを簡単に判定するには?
Split メソッドは String クラスが持つ機能の1つです。利用するためには、using System の宣言が必要になります。
Split メソッドには引数のオーバーロードが多くありますが、今回は、Split(String[], StringSplitOptions) の書式の引数を利用しています。
参考サイト
MicroSoft
String.Split メソッド
今回の場合の Split メソッドは、第1引数に指定した文字列で、対象となる文字列内を区切って、1つの文字列を分割する場合に使用します。
// カンマの位置で区切って、文字列の配列を作成。その際、最後にできる空白の文字列を削除
string[] strArray = engageCharaListString.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
今回の場合であれば、engageCharaListString が分割の対象となる文字列です。
この文字列を先頭から調査していき、第1引数で指定している , (カンマ)の部分で区切り、分割します。分割後、それを配列として strArray 変数に代入しています。
例えば、engageCharaListString 変数の値が {0, 1, 2} であるならば、これをカンマの位置で区切り、3つの文字列に分割しています。
それを配列として代入しています。
StringSplitOptions 列挙型は、Split メソッドに指定できるオプション機能です。今回の引数の場合は、第2引数として設定できます。
StringSplitOptions 列挙型には3種類の列挙子があり、この機能を活用すると、返された配列からの空の部分文字列を省略するかどうか、
または部分文字列から空白をトリミングするかどうかなどのオプションを指定することができます。
今回は RemoveEmptyEntries の列挙子を指定していますので、文字列を分割していって空白の情報が作成された場合、それを省略(空白を削除)しています。
そのため、配列の要素には空白の情報が代入されないように制御しています。
参考サイト
MicroSoft
StringSplitOptions 列挙型
GameData クラスの Awake メソッド内のデバッグの処理を活用して、デバッグを行います。
デバッグの方法は、前回のクリアポイントの場合と同じです。
Awake メソッド内にセーブ用のメソッドの呼び出し命令とロード用のメソッドの呼び出し命令がコメントアウトされていますので、
それを活用してデバッグを行ってください。
List のセーブとロードの処理のデバッグが無事に終了したら、次は、どのタイミングでセーブとロードを行うのかを考えて、
その部分にセーブとロードのメソッドを実行する処理を実装する必要があります。ロードについては、前回のクリアポイントと同じで
ゲーム開始時に1回だけ実行すればいいので、セーブする部分を考えてみてください。
GameData クラスの SaveEngageCharaList メソッドと LoadEngageCharaList メソッドの処理を戻り値と Linq のメソッドを活用してリファクタリングを行います。
最初に SaveEngageCharaList メソッドからリファクタリングを行います。
戻り値を持つ ConvertListToString メソッドを新しく作成し、そのメソッド内で、
いままで SaveEngageCharaList メソッドにおいて処理していた内容を Linq に用意されているメソッドを利用して実装しています。
<SaveEngageCharaList メソッドのリファクタリング>
using System.Linq; // <= 追加します
(中略)
/// <summary>
/// engageCharaNosList の値をセーブ
/// </summary>
public void SaveEngageCharaList() {
////* ここからコメントアウト *////
// 新しく作成する文字列
//string engageCharaListString = "";
// 契約したキャラの List をカンマ区切りの1行の文字列にする
//for (int i = 0; i < engageCharaNosList.Count; i++) {
// engageCharaListString += engageCharaNosList[i].ToString() + ",";
//}
// 文字列をセットしてセーブ
//PlayerPrefs.SetString(ENGAGE_CHARA_KEY, engageCharaListString);
////* ここまで *////
// 上記の処理を1行で記述します。SetString メソッドの第2引数内では ConvertListToString メソッドを実行し、string 型の戻り値を受け取り、それを第2引数として設定しています
PlayerPrefs.SetString(ENGAGE_CHARA_KEY, ConvertListToString(engageCharaNosList));
PlayerPrefs.Save();
}
/// <summary>
/// int 型の List の値をカンマ区切りの1行の string 型に変換
/// </summary>
/// <param name="listData"></param>
/// <returns></returns>
public string ConvertListToString(List<int> listData) {
// この処理は Linq の機能を利用して記述されています。
// この処理は、GameData クラスの SaveEngageCharaData メソッド内の処理の、List<int> 型の情報を string 型(1行のカンマ区切りの文字列)にする処理と同じものです
return listData.Select(x => x.ToString()).Aggregate((a, b) => a + "," + b);
}
続いて、LoadEngageCharaList メソッドのリファクタリングを行います。
現在は非常に長い処理になっていますが、こちらも、戻り値を持つ ConvertStringToList メソッドを新しく作成して、Linq で用意されているメソッドを活用することにより、
スマートな書式に書き換えることが可能です。
<LoadEngageCharaList メソッドのリファクタリング>
/// <summary>
/// engageCharaNosList の値をロード
/// </summary>
public void LoadEngageCharaList() {
////* ここからコメントアウト *////
// 文字列としてロード
//string engageCharaListString = PlayerPrefs.GetString(ENGAGE_CHARA_KEY, "");
// ロードした文字列がある場合
//if (!string.IsNullOrEmpty(engageCharaListString)) {
// カンマの位置で区切って、文字列の配列を作成。その際、最後にできる空白の文字列を削除
// string[] strArray = engageCharaListString.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);
// Debug.Log(strArray.Length);
// 配列の数だけ契約したキャラの情報があるので
// for (int i = 0; i < strArray.Length; i++) {
// Debug.Log(strArray[i]);
// 配列の文字列の値を int 型に変換して List に追加して、契約キャラの List を復元
// engageCharaNosList.Add(int.Parse(strArray[i]));
// }
//}
////* ここまで *////
// 上記の処理を1行で記述します。 ConvertStringToList メソッドの引数として GetString メソッドを実行し、List<int> 型の戻り値を受け取り、それを engageCharaNosList に設定しています
engageCharaNosList = ConvertStringToList(PlayerPrefs.GetString(ENGAGE_CHARA_KEY, ""));
}
/// <summary>
/// カンマ区切りになっている1行の string 型の値を int 型の List に変換
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public List<int> ConvertStringToList(string str) {
if (string.IsNullOrEmpty(str)) {
Debug.Log($"str 変数の値 : {str}");
return null;
}
// この処理は Linq の機能を利用して記述されています。
// GameData クラスの LoadEngageCharaData メソッド内の処理の、string 型(1行のカンマ区切りの文字列)を List<int> 型にする処理と同じです
return str.Split(',').ToList().ConvertAll(x => int.Parse(x));
}
新しく実装している Linq のメソッドについて順番に説明を行います。
Linq の機能を利用するためには using System.Linq の宣言が必要です。
アグリゲートと読みます。英単語としては、集計という意味を持ちます。
listData.Select(x => x.ToString()).Aggregate((a, b) => a + "," + b);
Select メソッドを利用して int 型の情報が string 型に型変換されていますので、その string 型の情報に対して、Aggregate メソッドが実行されます。
このメソッドは、作用する処理の型の情報を戻り値として持ちます。そのため今回であれば、string 型の戻り値を持つ処理になります。
Aggregate メソッドの引数はラムダ式による記述を行う決まりになっています。
第1引数は a 変数を宣言している部分であり、ここには集計結果が代入されます。
第2引数は b 変数を宣言している部分であり、ここには Select メソッドの処理結果である string 型の List の情報が要素として代入されて、1つずつ順番に処理が行われます。
処理の内容は、=> の右側の部分に設定します。今回は a + "," + b と設定していますので、List の最初の文字列に , (カンマ)を加えてから、List の次の文字列を足します。
これを、List の要素が無くなるまで繰り返し、最終的に string 型の情報が値として作成されます。
例えば、listData 変数の値が {0, 2, 4} であったとした場合、最初に、Select メソッドが実行されて、この3つの値は int 型から string 型に型変換されます。
次に Aggregate メソッドが実行されて、string 型の List の要素が1つず準版に取り出されます。
a + "," + b の部分に実数値を当てはめてみると
a + "," + b 0,2 (a = 0 b = 2) 0,2,4 (a = 0,2 b = 4)
という処理になります。よって、Aggregate メソッドの処理の結果、List の要素が1つの文字列の値になります。
<参考サイト>
MicroSoft
Enumerable.Aggregate メソッド
https://docs.microsoft.com/ja-jp/dotnet/api/system...
.NET Column 様
【C#とLINQ】Aggregateメソッドを使ってデータの集計をする方法
https://www.fenet.jp/dotnet/column/language/2048/
ゲーマーときどきエンジニア 様
【C#】【LINQ】Aggregateメソッドの使い方を解説します
https://www.tairax.com/entry/Csharp/LINQ-Aggregate
配列や、Linq の Select メソッド、Where メソッドの結果を List のインスタンス(実体)として作成します。
Select メソッドなどの戻り値である IEnumerable型の値は抽出しただけで List の状態にはなっていませんので、抽出した情報を List として利用したい場合には
この ToList メソッドの処理を実行することにより List として値が作成されます。
<参考サイト>
MicroSoft
Enumerable.ToList<TSource>(IEnumerable<TSource>) メソッド
https://docs.microsoft.com/ja-jp/dotnet/api/system...
超初心者向けプログラミング入門 様
LINQの主要メソッド一覧
https://programming.pc-note.net/csharp/linq_method...
ヤスノートは今日も考える 様
[C# LINQ] 配列やSelect、Whereした結果をリストに変換する
https://yaspage.com/prog/csharp/cs-linq-tolist/
List の持つメソッドの1つです。Array(配列)にも同名・同機能のメソッドがあります。
ConvertAll メソッドを利用すると、引数に指定した要素をすべて別の型に変換(キャスト)処理を行うことが出来ます。
str.Split(',').ToList().ConvertAll(x => int.Parse(x));
x 変数の部分には List の要素が順番に代入されます。
今回の実装例では、string 型の List のすべての要素を ConvertAll メソッドを利用し、 int.Paruse メソッドによって int 型に型変換する処理を実行しています。
これにより、戻り値の型である List<int> 型の値が作成されています。
public List<int> ConvertStringToList(string str) {
return str.Split(',').ToList().ConvertAll(x => int.Parse(x)); // <= 処理が最後まで完了することで戻り値の型である List<int> 型の値になる
}
なおこの処理は、すでに学習している Linq の Select メソッドを活用することで、同じ機能として実装することが出来ます。
return str.Split(',').ToList().ConvertAll(x => int.Parse(x));
↓
// こちらの処理も同じ機能として動作します
return str.Split(',').Select(x => int.Parse(x)).ToList();
色々な処理の機能と記述方法を覚えていくことで、より幅の広い、異なる処理の記述方法に応用していくことが出来ます。
<参考サイト>
MicroSoft
List<T>.ConvertAll<TOutput>(Converter<T,TOutput>) メソッド
https://docs.microsoft.com/ja-jp/dotnet/api/system...
ConvertAllメソッドを使って文字列型のListから数値型のListに変換する
https://smdn.jp/programming/dotnet-samplecodes/col...
文字列型(string)を数値に変換する方法の1つとして、数値型 (int、long、double など) には TryParse または Parse メソッドが用意されています。
利用するためには、using System の宣言が必要になります。
今回のケースでは、ConvertAll メソッドの引数内において int.Parse メソッドを利用し、分割されて List 化された各文字列を1つずつ順番に整数値に置き換えています。
public List<int> ConvertStringToList(string str) {
return str.Split(',').ToList().ConvertAll(x => int.Parse(x));
}
例えば、x 変数が "1" という文字列である場合には、 int 型の 1 という情報に変換されます。
上記の処理をしっかりと理解した上でになりますが、この処理は、Linq の Select メソッドを活用することで、
int.TryParse メソッドを利用した、より安全な処理にリファクタリング出来ます。
難しい内容であるため、いまはスキップしておいても大丈夫です。
<TryParse に置き換え>
public List<int> ConvertStringToList(string str) {
return str.Split(',')
.Select(x => int.TryParse(x, out int i) ? i : -1)
.ToList();
}
ここでの変更は、
.ConvertAll(x => int.Parse(x))
を
.Select(x => int.TryParse(x, out int i) ? i : -1).ToList()
に置き換えています。
int.TryParse(x, out int i) ? i : -1という表現は、引数内に三項演算子を利用し、int.TryParseを使って文字列を整数に変換しようと試みています。
成功した場合はその整数を、失敗した場合は -1 をリストに追加します。この例では、解析に失敗した場合に-1を返していますが、必要に応じて別のデフォルト値に変更することが可能です。
この記述は、元の文字列が正しい整数値を持っていることが確定していない場合において採用するとよいでしょう。
int.Parseは解析(変換)に失敗すると例外をスローしますが、int.TryParseは例外をスローせずに単にfalseを返すため、エラーが発生して処理が停止することを未然に防ぐことが出来ます。
<参考サイト>
MicroSoft
文字列を数値に変換する方法 (C# プログラミング ガイド)
MicroSoft
Int32.TryParse メソッド
MicroSoft
Int32.Parse メソッド
サムライエンジニアブログ
【C#入門】文字列を数値に、数値を文字列に変換する方法
今回実装している List のセーブ・ロードの機能を応用して、もう1つの List<int> 変数であるクリアしたステージの情報についても
セーブ・ロードする機能の実装に挑戦してみてください。実装方法は、リファクタリング前後のどちらの処理でも構いませんが、
出来るならば、リファクタリング前の処理を実装してから、自分でリファクタリングして、処理をスマートに記述する方法を学習してみてください。
最初からスマートな処理を書くことは難しいので、1つずつ順番に、実装とリファクタリングを繰り返して覚えていくといいでしょう。
実装してデバッグが終了したら、契約キャラのセーブ・ロードと同じように、どのタイミングでセーブ・ロードを行うべきかを考えて、処理の実装にも挑戦してみてください。
以上でこの手順は終了です。
次は 発展19 ーセーブ・ロード機能の実装��− です。




コメントをかく