【ゲーム開発のためのC#入門講座・応用強化編】データをJSON形式でセーブ&ロードしよう(後編)【#5】

6.0_C#応用強化編

ファイルの読み取り方法を学習しよう

前編ではファイルを出力する方法について学習しました。

続いて、ファイルの読み取り方法も学習しましょう。

という訳で、早速実例です。

using System.Text;
​
string folderPath = @"C:\Users\kaza_\Desktop\test";
string fileName = "csharpTest.txt";
​
string filePath = Path.Combine(folderPath, fileName);
​
using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Read))
{
    using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8))
    {
        while (!streamReader.EndOfStream)
        {
            Console.WriteLine(streamReader.ReadLine());
        }
    }
}
コンソール画面

hoge

前回書き込みを行った内容が無事、コンソール画面に出力できていれば成功です。

こちらはファイルの書き込みと構造がほとんど同じなので、書き込み処理を前編できちんと押さえておくことができていれば、理解はそれほど難しくないかなと思います。

そのため、相違点に着目して解説を行いますね。

using System.Text;
​
string folderPath = @"C:\Users\kaza_\Desktop\test";
string fileName = "csharpTest.txt";
​
string filePath = Path.Combine(folderPath, fileName);
​
// 相違点①:FileStreamクラスのコンストラクタ引数
using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Read))
{
    // 相違点②:StreamWriterではなくStreamReaderクラス
    using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8))
    {
        // 相違点③:プロパティ「EndOfStream」
        while (!streamReader.EndOfStream)
        {
            // 相違点④:ReadLine
            Console.WriteLine(streamReader.ReadLine());
        }
    }
}

相違点①FileStreamクラスのコンストラクタ引数

using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Read))

書き込みの際は、三番目の引数が「FileAccess.Write」でしたが、読み取りの際は「FileAccess.Read」になっています。アクセス方法の指定なので、読み取りなら「Read」になるというのは直感的にご理解いただけるかなと思います。

相違点②StreamWriterではなくStreamReaderクラス

using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8))

StreamReaderクラスはデータの読み取りを行うためのクラスです。

ひとつ目の引数に指定されたFileStreamクラスのインスタンスを対象とし、ふたつ目の引数で指定された文字コードタイプで読み取りを行います。

コンストラクタの引数はStreamWriterクラスと全く一緒なので、混乱することはなさそうですね。

また、こちらも使い終わったら閉じる必要があるため、原則usingとセットで使います。

相違点③EndOfStreamと相違点④ReadLine

インスタンス生成方法はStreamWriterと全く一緒だったStreamReaderクラスですが、扱い方は少し異なります。

StreamReaderクラスは内部的に読み取り位置を持っていて、読み取り完了する度に読み取り位置を進めていく構造になっています。

例えば今回使っているメソッド「ReadLine」を使うと、1行単位で読み取りを行い、読み取り位置は次の行の先頭に移動します。

hoge <=ReadLineすると、この1行を読み取り結果として返した上で、
fuga <=次の行の先頭に読み取り位置を移動します

そのため、もう一度「ReadLine」メソッドを実行すると、今度は2行目を読み取り結果として返した上で、さらに次の行の先頭に読み取り位置を移動しようとします。

ですが、上記例でいえば3行目は存在しませんね。

このように「ファイルの一番最後まで到達した=もう読み取りできる情報がない」となった時にプロパティ「EndOfStream」にtrueが設定されます。

そのため、下記のプログラムはコメントに記載の意味となります。

// ↓読み取りできる情報がなくなるまで繰り返し
while (!streamReader.EndOfStream)
{
    // ↓1行ずつ読み取り
    Console.WriteLine(streamReader.ReadLine());
}

今回はわかりやすいので1行ずつ読み取る「ReadLine」を選択しました。

が、頭から最後までまとめて読み取るメソッド「ReadToEnd」や、読み取りはするものの読み取り位置の変更は行わないメソッド「Peek」など、利用者の使いたい用途に応じて様々なメソッドが用意されています。

必ずしも1行ずつでなければ読み取れない訳ではないので、使い方に応じて変更してみてくださいね。

改めてコードを見てみよう

using System.Text;
​
string folderPath = @"C:\Users\kaza_\Desktop\test";
string fileName = "csharpTest.txt";
​
string filePath = Path.Combine(folderPath, fileName);
​
using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Read))
{
    using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8))
    {
        while (!streamReader.EndOfStream)
        {
            Console.WriteLine(streamReader.ReadLine());
        }
    }
}

このコードをひとつずつ追いかけてみると、

  1. 対象となるファイルパスを生成
  2. 対象ファイルを扱うFileStreamクラスのインスタンスを作成(using付)
  3. 対象ファイルを読み取るStreamReaderクラスのインスタンスを作成(using付)
  4. すべて読み取りが終わるまで、対象ファイルを1行ずつ読み取り
  5. usingによって対象ファイルが自動的に保存・閉じられる

このような流れになっています。

無事、テキストファイルを扱った書き込み処理と読み取り処理、どちらも理解できていれば幸いです。

それでは今度はJSON形式の文字列を扱うことにしましょう。

インスタンスをJSON形式の文字列へ変換する方法

下記の新しいクラスを追加してください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
// ↓名前空間は任意です。ご自由に変更してください
namespace Csharp6_5
{
    internal class SystemData
    {
        public float MusicVolume { get; set; } = 0.2f;
        public float SoundVolume { get; set; } = 0.2f;
        public int ClearLevel { get; set; } = 1;
        public int HighScore { get; set; } = 10;
​
        private string PlayerName { get; set; } = string.Empty;
​
        public float VoiceVolume = 0f;
​
        public SystemData() { }
​
        public SystemData(string playerName)
        {
            PlayerName = playerName;
        }
    }
}

それでは、このクラス「SystemData」のインスタンスを丸ごとJSON形式の文字列に変換してみましょう。

C#ではJSONを手軽に扱うためのクラス「JsonSerializer」が用意されています。

このクラスのstaticメソッドを利用することで、簡単にインスタンスをJSON形式のテキストデータに変換・復元できるようになっています。

早速試してみましょう。

名前空間「System.Text.Json」に属しているため、usingを使って名前空間を省略するのも忘れずに!

using Csharp6_5;
using System.Text.Json;
​
SystemData systemData = new SystemData("hoge");
systemData.MusicVolume = 0.5f;
systemData.VoiceVolume = 0.5f;
​
string jsonTextData = JsonSerializer.Serialize(systemData);
​
Console.WriteLine(jsonTextData);
コンソール画面

{“MusicVolume":0.5,"SoundVolume":0.2,"ClearLevel":1,"HighScore":10}

無事、インスタンスのデータをJSON形式の文字列に変換できました。

このように「JsonSerializer」クラスを利用すると、クラスにまとめたデータを外部に保存可能なテキスト形式に変換することができます。

// ↓インスタンスのデータをJSON形式の文字列に変換
string jsonTextData = JsonSerializer.Serialize(systemData);

メソッドひとつであっという間でしたね!

見ていただくとわかりますが、この「JsonSerializer」クラスで変換できるのは「publicなプロパティのみ」です。それ以外の「privateなプロパティ」や「メンバー変数」は対象外です。

ただし、Unityの専用変換クラス「JsonUtility 」の場合はそれらのものも変換対象とすることができます。なのでこれはあくまでMicrosoftの変換クラス「JsonSerializer」を利用するなら、というルールであることをご理解下さい。

さて、それでは今度はJSON形式の文字列をインスタンスへ変換してみましょう。

JSON形式の文字列をインスタンスへ変換する方法

もうお察しの方もいるかもしれませんが、こちらも実はメソッドで一発です。

using Csharp6_5;
using System.Text.Json;
​
SystemData systemData = new SystemData();
systemData.MusicVolume = 0.5f;
systemData.VoiceVolume = 0.5f;
​
string jsonTextData = JsonSerializer.Serialize(systemData);
​
// ↓ここから復元処理
systemData = JsonSerializer.Deserialize<SystemData>(jsonTextData);
Console.WriteLine(systemData.MusicVolume);
コンソール画面

0.5

無事、JSON形式の文字列をインスタンスへ変換することができましたね。

// ↓JSON形式の文字列をインスタンスへ変換。
//  メソッド「Deserialize」を実行する際は、
//  変換対象のクラスを<>の中で指定する必要がある点に注意
systemData = JsonSerializer.Deserialize<SystemData>(jsonTextData);

この処理、一見すると元のインスタンスを復元しているようにも見えますが、

  1. 全く新しいインスタンスを生成
  2. 紐づくプロパティデータをJSON形式の文字列から読み取って設定

しているだけなので、実際には別物になります。

そのため、変換対象のクラスには新しいインスタンスを生成するための「引数のないコンストラクタ」が必要になる点にご注意下さい。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
namespace Csharp6_5
{
    internal class SystemData
    {
        public float MusicVolume { get; set; } = 0.2f;
        public float SoundVolume { get; set; } = 0.2f;
        public int ClearLevel { get; set; } = 1;
        public int HighScore { get; set; } = 10;
​
        private string PlayerName { get; set; } = string.Empty;
​
        public float VoiceVolume = 0f;
​
        // ↓ここで空っぽのコンストラクタを用意していたのはそのため!
        public SystemData() { }
​
        public SystemData(string playerName)
        {
            PlayerName = playerName;
        }
    }
}

データをJSON形式でセーブ&ロードしよう

それでは、ファイルの書き込み・読み取り処理とJSON形式への変換・復元処理の合わせ技で、「SystemData」を外部にセーブ&ロードしてみましょう。

少し長いコードになりますが、ひとつずつ処理を追いかけてみてください。

using Csharp6_5;
using System.Text;
using System.Text.Json;
​
// システムデータの作成
SystemData systemData = new SystemData();
systemData.MusicVolume = 0.5f;
systemData.VoiceVolume = 0.5f;
​
// 保存ファイルパスの生成
string folderPath = @"C:\Users\kaza_\Desktop\test";
string fileName = "csharpTest.txt";
string filePath = Path.Combine(folderPath, fileName);
​
// システムデータのセーブ
Save(systemData, filePath);
​
// システムデータのロード
systemData = Load(filePath);
Console.WriteLine(systemData.MusicVolume);
Console.WriteLine(systemData.VoiceVolume);
​
// セーブ処理。書き込み処理とJSON形式への変換の合わせ技
static void Save(SystemData systemData, string filePath)
{
    using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write))
    {
        using (StreamWriter streamWriter = new StreamWriter(fileStream, Encoding.UTF8))
        {
            string jsonTextData = JsonSerializer.Serialize(systemData);
            streamWriter.WriteLine(jsonTextData);
        }
    }
}
​
// ロード処理。読み取り処理とJSON形式からの変換の合わせ技
static SystemData Load(string filePath)
{
    using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Read))
    {
        using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8))
        {
            return JsonSerializer.Deserialize<SystemData>(streamReader.ReadToEnd());
        }
    }
}

無事、プログラムが完了したら、保存先のファイルを覗いてみましょう。

きちんとJSON形式の文字列が書き込まれていればバッチリOKです。

こんな風にゲームデータをクラスにまとめて、そのクラスデータをJSON形式に変換して外部に書き込み・読み取りするというのが、Unityでよく行われているセーブ&ロードの実装方法です。

そのままだと悪意あるプレイヤーに書き換えられてしまいますので、必要に応じて暗号化処理が必要になりますが、それはまた別の機会に。

まとめ

  • FileStreamクラスとStreamReaderクラスを利用することで、外部のテキストデータを任意の単位で読み取ることができる
  • JsonSerializerクラスを利用することで、インスタンスを丸ごとJSON形式の文字列に変換することができる。また、変換した文字列を再度インスタンスに変換することもできる
  • ただし、UnityではよりUnityに特化した便利な変換クラス「JsonUtility 」があるので、そっちを使った方がいい
  • ファイルの読み書きと、クラスデータを外部に保存可能な文字列に変換したり復元したりする技術の合わせ技で、Unityではセーブ&ロード機能を実装することが多い

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by 夕目紅