Unityでゲームを作成している者です。主に2Dのゲームを作成しています。
普段はUnityのライブラリを使っていろいろ作ってみた系の記事を掲載しています。今回の記事はUnityと題名につけたものの、Uniryライブラリ関係ないので、実質C#で作ったソース紹介記事ですね。
紹介内容は自作の「iniファイル読み書き」プログラムです。
標準ライブラリを使わずに自作した経緯
本筋とはそれると思いますが、自作でiniを読み書きするソースを作った経緯を記載しておきます。
iniファイルを使用したいと思った経緯
今まではゲーム内で使用する定数について、ソース上に直接Constを使って宣言をしていました。
ただゲームを作る場合、ユーザーが自由に変更できる設定なども作りたいため、それは定数とは別のクラスで管理していました。(例えば音量とか、キーコンフィグとか)
よく考えると、ユーザーが変更できる設定値も定数もまとめてiniファイルからロードするようにしておけば一元管理できるので、iniファイルで管理する方式を採用することにしました。
なぜ自作したのか
そこでC#でiniファイルを操作する方法について調べると、ほとんどのサイトでWindows標準のGetPrivateProfileStringなどを使う方法が紹介されていました。
例えば以下のサイトがわかりやすかったです。
GetPrivateProfileStringはWindowsのkernel32.dllに格納されているもので、昔からあるため今後のWindowsでも必ず梱包されると思います。
ただ、外部DLLに依存した状態で配布すると、環境依存のバグが発生して困った経験が多いので、なるべく使いたくないという気持ちがありました。
また、GetPrivateProfileStringはKeyNameが必須なのでロードするたびにiniにアクセスが発生してしまいます。
「ファイルアクセスを減らすために、一括で読み込んでメモリ上に確保しておきたい。」と思ったのでいろいろカスタマイズできる用に自作することにしました。
作成したソース
以下が作成したC#のコードになります。
using System.Collections.Generic;
using System;
using System.IO;
using System.Text;
using System.Reflection;
// iniファイルを読み込んだり書き込んだりするクラス
public static class IniFileUtils
{
// ファイルごとの連想配列
private static Dictionary<string, Dictionary<string, Dictionary<string, string>>> fileDic = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>();
// iniファイルから設定値を読み込む、該当の設定値がない場合はdefaultStrが返却される
public static string read(string filePath, string section, string param, string defaultStr, Encoding enc)
{
// セクションごとの連想配列
Dictionary<string, Dictionary<string, string>> sectionDic;
// パラメータ(変数)ごとの連想配列
Dictionary<string, string> paramDic;
if (!fileDic.ContainsKey(filePath))
{
// まだファイルを読み込んでいない場合、読みこんで変数fileDicに格納する
addFileDic(filePath, enc);
}
if (!fileDic.ContainsKey(filePath))
{
// ファイルが見つからなかった場合
return defaultStr;
}
sectionDic = fileDic[filePath];
if (!sectionDic.ContainsKey(section))
{
// セクションが見つからなかった場合
return defaultStr;
}
paramDic = sectionDic[section];
if (!paramDic.ContainsKey(param))
{
// パラメータが見つからなかった場合
return defaultStr;
}
return paramDic[param];
}
public static string read(string filePath, string section, string param)
{
return read(filePath, section, param, "", Encoding.GetEncoding("UTF-8"));
}
// 読み込んだファイルをクリアする
public static void clear(string filePath)
{
if (fileDic.ContainsKey(filePath))
{
fileDic.Remove(filePath);
}
}
// 読み込んだファイルを再読み込みする
public static void reload(string filePath)
{
clear(filePath);
addFileDic(filePath, Encoding.GetEncoding("UTF-8"));
}
// iniファイルに設定値を書き込む
public static void write(string filePath, string section, string param, string value, Encoding enc)
{
// ファイルを開いて読み込む
reload(filePath);
StreamReader sr = new StreamReader(filePath, enc);
List<string> writeLine = new List<string>();
// ファイルを1行ずつ読み込む
// 現在読み込んでいるセクション名
string curSection = "";
bool isSectionExist = fileDic[filePath].ContainsKey(section);
bool isParamExist = isSectionExist && fileDic[filePath][section].ContainsKey(param);
string escapedValue = value;
if (value != null && value.Contains("="))
{
// イコールがescapedValue含まれている場合はダブルクォーテーションで囲む
escapedValue = "\"" + value + "\"";
}
while (sr.Peek() >= 0)
{
string buf = sr.ReadLine();
string trimed = buf.Trim();
if (trimed.StartsWith(";") || trimed.StartsWith("#"))
{
// コメント行の場合、そのまま書き込み
writeLine.Add(buf);
}
else if (trimed.StartsWith("[") && trimed.EndsWith("]"))
{
string newSection = buf.Substring(1, buf.Length - 2);
if (!isParamExist && curSection == section && newSection != curSection)
{
// セクション変更時、新規パラメータなら書き込み
writeLine.Add(param + "=" + escapedValue);
}
// セクションの場合、そのまま書き込み
curSection = newSection;
writeLine.Add(buf);
}
else
{
// 設定値の場合
if (curSection != section)
{
// 別セクションの場合、そのまま書き込み
writeLine.Add(buf);
}
else
{
string[] bufs = buf.Split('=');
if (bufs.Length < 2)
{
// key=valueの形式以外は何もしない
}
else
{
// パラメータ名
string key = bufs[0].Trim();
if (key == param)
{
// 同設定値の場合、値を書き換え
writeLine.Add(param + "=" + escapedValue);
}
else
{
// 別設定値の場合、そのまま書き込み
writeLine.Add(buf);
}
}
}
}
}
// 新規セクションの場合
if (!isSectionExist)
{
writeLine.Add("[" + section + "]");
}
if (!isParamExist)
{
// 新規パラメータ場合
writeLine.Add(param + "=" + escapedValue);
}
sr.Close();
// ファイル書き込み
StreamWriter sw = new StreamWriter(filePath, false, enc);
for (int i = 0; i < writeLine.Count; i++)
{
if (i != writeLine.Count - 1 || writeLine[i] != "")
{
// 最終行は空白なら出力しない
sw.WriteLine(writeLine[i]);
}
}
sw.Close();
// 書き込んだファイルを再読込する
reload(filePath);
}
public static void write(string filePath, string section, string param, string value)
{
write(filePath, section, param, value, Encoding.GetEncoding("UTF-8"));
}
// ファイルを読み込んで変数fileDicに格納する
private static void addFileDic(string filePath, Encoding enc)
{
clear(filePath);
// 読み込み中のデータを格納しておく一時変数
Dictionary<string, Dictionary<string, string>> tmpSecDic = new Dictionary<string, Dictionary<string, string>>();
// ファイルを開いて読み込む
StreamReader sr = new StreamReader(filePath, enc);
// 現在読み込んでいるセクション名
string curSection = "";
while (sr.Peek() >= 0)
{
// ファイルを 1 行ずつ読み込む
string buf = sr.ReadLine().Trim();
if (buf.StartsWith(";") || buf.StartsWith("#"))
{
// コメント行の場合何もしない
}
else if (buf.StartsWith("[") && buf.EndsWith("]"))
{
// セクションの場合
curSection = buf.Substring(1, buf.Length - 2);
}
else
{
string[] bufs = buf.Split('=');
if (bufs.Length < 2)
{
// key=valueの形式以外は何もしない
}
else
{
// パラメータ名
string key = bufs[0].Trim();
// 設定値
string value = string.Join("=", bufs, 1, bufs.Length - 1);
if (value.StartsWith("\"") && value.EndsWith("\"") && value.Length > 1)
{
// ダブルクォーテーションで囲まれている場合は、ダブルクォーテーションを外してvalueを読む
value = value.Substring(1, value.Length - 2);
}
Dictionary<string, string> targetParamDic;
if (tmpSecDic.ContainsKey(curSection))
{
// 読み込み済みのセクションの場合
targetParamDic = tmpSecDic[curSection];
}
else
{
// 新規のセクションの場合
targetParamDic = new Dictionary<string, string>();
tmpSecDic.Add(curSection, targetParamDic);
}
if (targetParamDic.ContainsKey(key))
{
// 設定値が重複していた場合は後勝ちにする
targetParamDic.Remove(filePath);
}
targetParamDic.Add(key, value);
}
}
}
fileDic.Add(filePath, tmpSecDic);
sr.Close();
}
}
前述しましたが、読み込みを行うたびにファイルのアクセスを行うのを防ぎたかったので、一度読みこんだファイルの内容はfileDicという変数に格納して保持しています。
代わりにメモリ使用量は上がりますが、メモリ10ギガバイトとかが当たり前になってきている現代で、テキストファイルを丸ごと保存しておいて問題になると思わなかったのでファイル読み込みを減らす方がメリットがあると考えました。
再読み込みしたい場合は、clearというメソッドで保持しているデータを開放します。
読み込みの使用例
readというメソッドでiniファイルの値を読み込みます。
例) C:/work/test.iniのGAMEセクションのtestという項目を取得する場合
String val = IniFileUtils.read("C:/work/test.ini", "GAME", "test", "", Encoding.GetEncoding("UTF-8"));- 第1引数:iniファイルのパス
- 第2引数:セクション名
- 第3引数:項目名
- 第4引数:見つからなかった場合に返却される文字列
- 第5引数:エンコード
書き込みの使用例
例) C:/work/test.iniのGAMEセクションにtest=100という項目を追加する場合
String val = IniFileUtils.write("C:/work/test.ini", "GAME", "test", "100", Encoding.GetEncoding("UTF-8"));- 第1引数:iniファイルのパス
- 第2引数:セクション名
- 第3引数:項目名
- 第4引数:設定値
- 第5引数:エンコード
一括で読み込んで定数クラスに格納しておきたい
個人的に、項目を毎回Iniファイル操作用のクラスからとってくるのも使い勝手が悪い気がしたので、一括で定数用のクラスにロードするメソッドも作りました。
前述のIniFileUtilsに下記のメソッドを追加します。
// 引数で渡したクラスにiniファイルの内容を上書きして返却する
public static T mapToObj<T>(T target, string filePath, string section)
{
// 構造体に対応するため、インスタンスをボックス化する
object boxed = target;
// フィールド一覧を取得
Type type = boxed.GetType();
// 格納するクラスのフィールドに応じて格納方法を変更する
foreach (FieldInfo fieldInfo in type.GetFields())
{
Type fieldType = fieldInfo.FieldType;
string fieldName = fieldInfo.Name;
string value = read(filePath, section, fieldName);
if (value != "")
{
switch (fieldType.Name)
{
// intの場合
case "Int32":
fieldInfo.SetValue(boxed, Convert.ToInt32(value));
break;
// stringの場合
case "String":
fieldInfo.SetValue(boxed, value);
break;
default:
break;
}
}
}
// ボックス化を解除する
return (T)boxed;
}リフレクションを使ってしまっていますが、引数で渡した変数のフィールドと同名の項目がiniファイルにあれば、その設定値で上書きしてくれるメソッドです。
使い方は以下の通りです。
public struct SystemConfig
{
public string test;
}
public class TEST{
public getIniFileData(){
SystemConfig conf = new SystemConfig();
conf = IniFileUtils.mapToObj<SystemConfig>(conf, "C:/work/test.ini", "GAME");
}
}- 第1引数:iniファイルの設定値を格納したいクラス、構造体
- 第2引数:ファイル名
- 第3引数:セクション名
上記を実行した場合、conf.testに「C:/work/test.iniのGAMEセクションのtestという項目の設定値」が格納されます。


コメント
こちら使わせてもらいました。非常に便利で助かります。
[Section]
Key=”a=b”
のようなvalueの中に”=”が含まれると正常に動作しません。
すみません。ブログ最近確認していなかったもので、返信が完全に遅れてしまいました><
valueの中に”=”が含まれる場合、valueをダブルクォーテーションで囲むルールなのですね。この仕様自体知らなかったです。
記事に記載したソースを対応できるようなソースに変更しておきました。
ありがとうございます!!