Hatena::Groupgamehell

ゲームプログラムめも日記 このページをアンテナに追加 RSSフィード

けんもほろろ  / オススメゲームプログラム本  / 作ったもの置き場  
ゲーム開発のデザパタまとめ  / めも日記まとめ  

2007-09-20

[]INIファイル読み込みクラス 11:40 INIファイル読み込みクラス - ゲームプログラムめも日記 を含むブックマーク はてなブックマーク - INIファイル読み込みクラス - ゲームプログラムめも日記 INIファイル読み込みクラス - ゲームプログラムめも日記 のブックマークコメント

ふと思い立って、INIファイルの読み込みクラスを作ってみた。

private import std.ctype;
private import std.file;
private import std.string;

/**
 * INIファイル読み込みクラス
 */
class INILoader
{
private:
	char[][char[]] _data; // データ(キーと値)
public:
	this() {}
	/**
	 * INIファイル読み込み
	 * @param filepath ファイルパス
	 */
	void load(char[] filepath)
	{
		auto file = cast(char[])read(filepath);
		foreach(i, b; split(file, "\n"))
		{
			int idx = find(b, '#');       // コメント文字を検索
			if(idx >= 0) b = b[0..idx];   // コメントを除去
			b = strip(b);
			if(b.length == 0) continue;
			if(find(b, '=') < 0) throw new Error(format("INILoader.load(): invalid syntax -> '%s'(%d)", filepath, i+1));
			
			char[][] tmp = split(b, "="); // キーと値で分割
			if(tmp.length < 2) continue;
			char[] key = strip(tmp[0]);
			char[] val = strip(tmp[1]);
			_data[key] = val;
		}
		_data.rehash;
	}
	char[] get(char[] key)        { checkKey(key);         return _data[key];             } // 文字列で取得
	int    getint(char[] key)     { char[] val = get(key); return checkInt(key, val);     } // 整数で取得
	float  getfloat(char[] key)   { char[] val = get(key); return checkFloat(key, val);   } // 浮動小数で取得
	bool   getboolean(char[] key) { char[] val = get(key); return checkBoolean(key, val); } // 真偽で取得
	void dump()
	{
		printf("{\n");
		foreach(k; _data.keys)
		{
			printf("  \"%.*s\" : \"%.*s\",\n", k, _data[k]);
		}
		printf("}\n");
	}
private:
	void checkKey(char[] key)
	{
		if(key in _data) return;
		throw new Error(format("INILoader.checkKey(): not exist key ... '%s'", key));
	}
	int checkInt(char[] key, char[] val)
	{
		foreach(c; val)
		{
			if(!isdigit(c)) throw new Error(format("INILoader.checkInt(): not digit value ... '%s' -> '%s'", key, val));
		}
		return atoi(val);
	}
	float checkFloat(char[] key, char[] val)
	{
		foreach(c; val)
		{
			if(!isdigit(c) && c != '.') throw new Error(format("INILoader.checkFloat(): not digit value ... '%s' -> '%s'", key, val));
		}
		return atof(val);
	}
	bool checkBoolean(char[] key, char[] val)
	{
		switch(val)
		{
		case "1", "yes", "true",  "on":  return true;
		case "0", "no",  "false", "off": return false;
		default: throw new Error(format("INILoader.checkBoolean(): not boolean value ... '%s' -> '%s'", key, val));
		}
	}
}

やっていることは、たいしたことではないです。

例えば、こんなINIファイルを、、

# 自機
PLAYER_DAMAGE_SPEED = 8.0  # ダメージ時に吹き飛ばされる移動量
PLAYER_DECAY_SPEED  = 0.97 # 移動の減衰量
PLAYER_DAMAGE_TIMER = 120  # ダメージ時間(2sec)
PLAYER_COUNT_TIMER  = 40   # 体当たりでの打ち返し演出タイマ

キー=値

という書式をハッシュテーブルに突っ込むだけです。

「#」以降はコメントとして扱います。

(※セクションは使えません)

 

読み込むINIファイルを「game.ini」とすると、使い方はこんな感じ。

/**
 * システム定数管理
 */
class GH
{
public
	// プレイヤー
	static float PLAYER_DAMAGE_SPEED; // ダメージ時に吹き飛ばされる移動量
	static float PLAYER_DECAY_SPEED;  // 移動の減衰量
	static int   PLAYER_DAMAGE_TIMER; // ダメージタイマ
	static int   PLAYER_COUNT_TIMER;  // 体当たりでの打ち返し演出タイマ
	static void load()
	{
		scope ini = new INILoader();
		ini.load("game.ini");
		PLAYER_DAMAGE_SPEED = ini.getfloat("PLAYER_DAMAGE_SPEED");
		PLAYER_DECAY_SPEED  = ini.getfloat("PLAYER_DECAY_SPEED");
		PLAYER_DAMAGE_TIMER = ini.getint("PLAYER_DAMAGE_TIMER");
		PLAYER_COUNT_TIMER  = ini.getint("PLAYER_COUNT_TIMER");
	}
}

ゲーム起動(開始)時に、設定を読みにいって、staticに全部入れてしまうわけです。

で、この定数(正確には違うけど)を参照して、パラメータを決定するようにします。

 

 

まあ、この仕組み自体はたいしたことではないのですが、

ゲームって、パラメータの調整で、面白さが大きく変わってしまうことが良くあります。

 

そういうときに、コンパイルなしで設定ファイルをちょこちょこいじるだけで、

パラメータを調整できると、

 

「もうすこし反動を大きくしてみるかな、、」

とか、簡単に確認できますし、

「これで面白くなるかどうかわかんないけど、この値を大きくしてみよう!」

適当にやってみたら、意外に面白い動きになってしまった!!

 

なんて嬉しい効果があったります。

 

 

ということで、INIファイルゲームの設定ファイルとして使うと、

組み込みも簡単だし、調整も楽だよ!

という話でした。

[]INIファイル読み込みクラス17:50 INIファイル読み込みクラス2 - ゲームプログラムめも日記 を含むブックマーク はてなブックマーク - INIファイル読み込みクラス2 - ゲームプログラムめも日記 INIファイル読み込みクラス2 - ゲームプログラムめも日記 のブックマークコメント

Outputもできると汎用部品になりそう

ということで作ってみた。

 

private import std.ctype;
private import std.file;
private import std.string;

/**
 * INIファイル読み込みクラス
 */
class ConfigParser
{
private:
	int            _index;    // 現在のデータ番号
	char[][]       _indices;  // データ番号
	char[][char[]] _data;     // データ(キーと値)
	char[]         _filepath; // ファイルパス
public:
	this() {}
	void init()
	{
		// 初期化
		_index          = 0;
		_indices.length = 0;
		_data           = null;
		//foreach(k; _data) _data.remove(k);
	}
	void load(char[] filepath)
	{
		init();
		_filepath = filepath.dup; // ファイル名コピー
		// 読み込み
		auto file = cast(char[])read(filepath);
		// データを入れる
		foreach(b; split(file, "\n"))
		{
			int idx = find(b, '#');       // コメント文字を検索
			if(idx >= 0) b = b[0..idx];   // コメントを除去
			char[][] tmp = split(b, "="); // キーと値で分割
			if(tmp is null)    continue;
			if(tmp.length < 2) continue;
			char[] key = strip(tmp[0]);
			char[] val = strip(tmp[1]);
			set(key, val);
		}
		_data.rehash;
	}
	char[] get(char[] key)        { checkKey(key);         return _data[key];             } // 文字列で取得
	int    getint(char[] key)     { char[] val = get(key); return checkInt(key, val);     } // 整数で取得
	float  getfloat(char[] key)   { char[] val = get(key); return checkFloat(key, val);   } // 浮動小数で取得
	bool   getboolean(char[] key) { char[] val = get(key); return checkBoolean(key, val); } // 真偽で取得
	// 文字列の追加
	void   set(char[] key, char[] val)
	{
		if(!(key in _data))
		{
			_index++;
			_indices ~= key;
		}
		_data[key] = val;
	}

	void   set(char[] key, int    val) { set(key, std.string.toString(val)); } // 整数の追加
	void   set(char[] key, float  val) { set(key, std.string.toString(val)); } // 浮動小数の追加
	void   set(char[] key, bool   val) { set(key, std.string.toString(val)); } // 真偽の追加
	void write(char[] filepath=null)
	{
		if(filepath is null) filepath = _filepath;
		std.file.write(filepath, "");
		foreach(k; _indices)
		{
			append(filepath, format("%s = %s\n", k, _data[k]));
		}
	}
	void dump()
	{
		printf("{\n");
		foreach(k; _data.keys)
		{
			printf("  \"%.*s\" : \"%.*s\",\n", k, _data[k]);
		}
		printf("}\n");
	}
private:
	void checkKey(char[] key)
	{
		if(key in _data) return;
		throw new Error(format("ConfigParser.checkKey(): not exist key ... '%s'", key));
	}
	int checkInt(char[] key, char[] val)
	{
		foreach(c; val)
		{
			if(!isdigit(c)) throw new Error(format("not digit value ... '%s' -> '%s'", key, val));
		}
		return atoi(val);
	}
	float checkFloat(char[] key, char[] val)
	{
		foreach(c; val)
		{
			if(!isdigit(c) && c != '.') throw new Error(format("not digit value ... '%s' -> '%s'", key, val));
		}
		return atof(val);
	}
	bool checkBoolean(char[] key, char[] val)
	{
		switch(val)
		{
		case "1", "yes", "true",  "on":  return true;
		case "0", "no",  "false", "off": return false;
		default: throw new Error(format("not boolean value ... '%s' -> '%s'", key, val));
		}
	}
}

 

使い方はこんな感じ。

void main()
{
	// オブジェクト生成
	ConfigParser cfg = new ConfigParser();
	// 読み込み
	cfg.load("setting.ini");
	// デバッグ出力
	cfg.dump();
	// "TIMER_SHOT"の値の取得
	printf("%.*s\n", cfg.get("TIMER_SHOT"));
	// キー"hoge"、値"piyo"を追加・更新
	cfg.set("hoge", "piyo");
	// INIファイルに出力
	cfg.write();
}

インデックス用の配列を持って、

記述した順番で、出力するようにしてみました。

 

まあ、

  • インデントが崩れる
  • コメントが全部消える

という欠点はありますがー。

 

コメントを残すのは難しいですねー。

 

あと、set()は、値の更新・追加になります。

 

参考

PythonのConfigParser

http://www.python.jp/doc/2.4/lib/RawConfigParser-objects.html

追記

連想配列初期化が間違っていたので修正しました。

	//foreach(k; _data) _data.remove(k);
	_data           = null;

Dだと、nullを入れるのが、連想配列初期化する方法みたい。

o_megao_mega2007/09/20 12:17これでも十分便利だけど、Outputもできると汎用部品になりそう。といってみるテスト。
iniファイルのコメントを残しつつ出力できるとよさげー

wang-zhiwang-zhi2007/09/20 12:31ASCのときは何も考えずにcsv形式のデータをばっこんばっこん読み書きしてたんだけどキー定義のファイルのほうがいいね。

kenmokenmo2007/09/20 18:05>キー定義のファイルのほうがいいね
今回のは、「定数」を外出しするという目的なので、INI形式だと楽なんですよねー。
キャラのデータベースのような表形式のデータの場合は、CSVの方が扱いやすいし、
より複雑なデータを扱うなら、XMLかYAMLでしょうね。