石橋を叩いて壊すページ

シリアライズ用のクラスを作ってデータを保存する

前回の記事では、FileConfigurationを使ってデータを保存・読み込みする方法を書いた。
FileConfigurationは文字列・数値・リスト・マップなど多種多様なアイテムのデータを保存・読み込みすることができるが、しかし万能ではなく、保存・読み込みできないデータも存在する。
この記事では、通常では保存できないデータを保存・読み込みする方法について記載する。

シリアライズ用のクラスを作る

FileConfigurationは、元々取り扱い可能な文字列や数値などのほかに、ConfigurationSerializableインターフェースを実装したクラスのオブジェクトを保存・読み込みすることができる。
ConfigurationSerializableインターフェースを実装したクラスに、オブジェクトをシリアライズ・デシリアライズするメソッドを書いておく。
するとFileConfigurationは、そのメソッドを使ってデータをシリアライズして保存し、読み込んでデシリアライズしてくれるようになる。

実際にやってみよう。
今回は練習用のお題として「ゾンビ・クリーパー・スケルトンを倒した数をカウントする機能」を実装してみる。
まず、モンスターの討伐数を格納するクラスを作る。

package jp.jias.bukkit;

import java.util.HashMap;
import java.util.Map;

import org.bukkit.Bukkit;
import org.bukkit.configuration.serialization.ConfigurationSerializable;

/**
 * ゾンビ・クリーパー・スケルトンが倒された数をカウントするオブジェクト
 */
public class SubjugationInfo implements ConfigurationSerializable{

	/** ゾンビが倒された数を保存するキー */
	static final private String ZOMBIE="zombie";

	/** クリーパーが倒された数を保存するキー */
	static final private String CREEPER="creeper";

	/** スケルトンが倒された数を保存するキー */
	static final private String SKELETON="skeleton";

	/** ゾンビが倒された数 */
	public int zombie=0;

	/** クリーパーが倒された数 */
	public int creeper=0;

	/** スケルトンが倒された数 */
	public int skeleton=0;

	/** コンストラクタ */
	public SubjugationInfo(){
	}

	/** ゾンビが倒された数を設定する */
	public void setZombie(int zombie){
		this.zombie=zombie;
	}

	/** ゾンビが倒された数を取得する */
	public int getZombie(){
		return zombie;
	}

	/** クリーパーが倒された数を設定する */
	public void setCreeper(int creeper){
		this.creeper=creeper;
	}

	/** クリーパーが倒された数を取得する */
	public int getCreeper(){
		return creeper;
	}

	/** スケルトンが倒された数を設定する */
	public void setSkeleton(int skeleton){
		this.skeleton=skeleton;
	}

	/** スケルトンが倒された数を取得する */
	public int getSkeleton(){
		return skeleton;
	}

	/**
	 * 情報をシリアライズする
	 * 
	 * @return シリアライズされた情報
	 */
	public Map<String,Object> serialize(){

		// マップを作る
		Map<String,Object> map=new HashMap<String,Object>();

		// 情報を「キー」と「値」のペアにして書き出す
		map.put(ZOMBIE,zombie);
		map.put(CREEPER,creeper);
		map.put(SKELETON,skeleton);
		
		Bukkit.broadcastMessage("test: "+map);

		// データを保存したマップを返す(このマップの中身がconfig.ymlなどに書かれる)
		return map;
	}

	/**
	 * 情報をデシリアライズする
	 * 
	 * @param map
	 *            シリアライズされた情報
	 * @return デシリアライズされた情報
	 */
	public static SubjugationInfo deserialize(Map<String,Object> map){

		// config.ymlから読み込まれたマップが引数に渡されるので、このデータを元に情報を作成(復元)する

		//オブジェクトを作る
		SubjugationInfo subjugation=new SubjugationInfo();

		//「キー」で「値」を取り出してオブジェクトにセット
		subjugation.setZombie(Integer.parseInt(map.get(ZOMBIE).toString()));
		subjugation.setCreeper(Integer.parseInt(map.get(CREEPER).toString()));
		subjugation.setSkeleton(Integer.parseInt(map.get(SKELETON).toString()));

		//オブジェクトを返す
		return subjugation;
	}
}

このクラスの前半は、定数や変数の宣言、変数の値を取得・設定するメソッドなので、大したことはしていない。注目すべきは後半だ。
serialize()メソッドには、このオブジェクトをシリアライズする処理を、
deserialize(…)メソッドには、シリアライズされた情報をデシリアライズする処理を書いてある。

より具体的に言うと、serialize()メソッドはこのオブジェクト自身が持っている情報を、FileConfigurationが元々取り扱えるデータ型に「変換」して返している。
前回の記事に書いたとおり、FileConfigurationはもともと文字列や数値・マップなどをconfig.ymlに保存する機能を持っているから、 結果的にFileConfigurationは「変換」されたSubjugationInfoのデータをconfig.ymlに保存することができる。

デシリアライズ時には逆に、FileConfigurationはconfig.ymlから読み込んだ文字列や数値・マップなどのデータをdeserialize(…)メソッドに送ってくる。
このためプログラマは、deserialize(…)メソッドに送られたデータをSubjugationInfoクラスのオブジェクトの形に戻す「逆変換」の処理を書いておけばいい。

このように、ConfigurationSerializableインターフェースを実装したオブジェクトの保存・読み込みは、
つまるところプログラマの書いたシリアライズ・デシリアライズ処理と、それをファイルに保存・読み込みするFileConfigurationの処理の二人三脚で実現するわけである。

さて、上にかいたコードにはBukkitプラグインとしての機能(onEnable()とか)は入っていないので、そっちの処理も書かなければならない。

package jp.jias.bukkit;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.plugin.java.JavaPlugin;

public class TestPlugin extends JavaPlugin implements Listener{

	/** モンスターの討伐数を格納するオブジェクト */
	private SubjugationInfo subjugation=null;

	/**
	 * プラグインが有効化されるとき呼び出される
	 */
	public void onEnable(){

		// SubjugationInfoオブジェクトをシリアライズ・デシリアライズするのに使うクラスをBukkitに登録する
		ConfigurationSerialization.registerClass(SubjugationInfo.class);

		// 設定されている値を取得してデシリアライズする
		subjugation=(SubjugationInfo) getConfig().get("subjugation");
		if(subjugation == null){
			// データを読み取れなかった場合はオブジェクトを新規作成
			subjugation=new SubjugationInfo();
		}

		// イベントリスナーの登録
		getServer().getPluginManager().registerEvents(this,this);
	}

	/**
	 * プラグインが無効化されるとき呼び出される
	 */
	public void onDisable(){
		// データを設定ファイルに保存する
		saveConfig();
	}

	/**
	 * エンティティが死亡するときに呼ばれる
	 * 
	 * @param e
	 */
	@EventHandler
	public void onEntityDeathEvent(EntityDeathEvent e){

		// ゾンビが倒された場合はゾンビの討伐数を+1する
		if(e.getEntityType() == EntityType.ZOMBIE){
			subjugation.setZombie(subjugation.getZombie() + 1);

			// 討伐数を表示
			Bukkit.broadcastMessage("いままでに倒したゾンビの数:" + ChatColor.YELLOW + subjugation.getZombie());
		}
		// クリーパーが倒された場合はクリーパーの討伐数を+1する
		else if(e.getEntityType() == EntityType.CREEPER){
			subjugation.setCreeper(subjugation.getCreeper() + 1);

			// 討伐数を表示
			Bukkit.broadcastMessage("いままでに倒したクリーパーの数:" + ChatColor.YELLOW + subjugation.getCreeper());
		}
		// スケルトンが倒された場合はスケルトンの討伐数を+1する
		else if(e.getEntityType() == EntityType.SKELETON){
			subjugation.setSkeleton(subjugation.getSkeleton() + 1);

			// 討伐数を表示
			Bukkit.broadcastMessage("いままでに倒したスケルトンの数:" + ChatColor.YELLOW + subjugation.getSkeleton());
		}

		// データを保存
		getConfig().set("subjugation",subjugation);
	}
}

こちらで特筆することは3点ある。
そのうち2点は、既に前回の記事でも紹介した、保存データを読み込むgetConfig()メソッドの実行と、データを保存するsaveConfig()メソッドの実行である。
それぞれプラグイン起動時に呼ばれるonEnable()と、プラグイン終了時に呼ばれるonDisable()に書いておいた。
こうすれば、プラグイン起動時にデータが読み込まれ、プラグイン終了時にデータが保存される。とても単純だ。

残る特筆すべき1点は、onEnable()の最初の処理、registerClass(…)メソッドの実行である。
このメソッドは、FileConfigurationに対して「独自のシリアライズ・デシリアライズ処理を書いたクラス」を登録するものである。
ここでSubjugationInfoクラスを登録しておけば、SubjugationInfoオブジェクトのデータの保存・読み込み時に、 SubjugationInfoクラスに書いたserialize()とdeserialize(…)が呼ばれるようになる。

これで、あとはビルドして実行してゾンビでも倒せば、画面にログが表示されるはずである。


いままでに倒したモンスターの数がカウントされている。
/reloadコマンドでプラグインを再起動しても、データが保存されているのでカウントは継続する。

既存のクラスのシリアライズには、シリアライズ用のラッパークラスを作る

上記で書いたモンスターの討伐数をカウントするクラスのように、もともと自分で書いたクラスであれば、
データをシリアライズ・デシリアライズするserialize()とdeserialize(…)メソッドを自分で書けばデータを保存・読み込みできるようになる。
では、BukkitのLocationクラスのように、他人が書いたクラスを保存・読み込みしたい場合はどうすればいいだろうか。

まあ単純に考えて、serialize()とdeserialize(…)を自分で書いてしまうのが一番手っ取り早い。
しかし既存のクラスの定義を変えようとすると話が大きくなるので、今回はLocationオブジェクトをシリアライズ・デシリアライズするだけのラッパークラスのようなものを自分で書いてみる。

package jp.jias.bukkit;

import java.util.HashMap;
import java.util.Map;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.serialization.ConfigurationSerializable;

/**
 * Location(座標)をシリアライズ・デシリアライズするためのラッパークラス
 */
public class SerializableLocation implements ConfigurationSerializable{

	/** ワールド名を保存するキー名 */
	static final private String WORLD="world";

	/** X座標の値を保存するキー名 */
	static final private String X="x";

	/** Y座標の値を保存するキー名 */
	static final private String Y="y";

	/** Z座標の値を保存するキー名 */
	static final private String Z="z";

	/** ヨーの値(向いている方角。東西南北どちらを向いているかの角度)を保存するキー名 */
	static final private String YAW="yaw";

	/** ピッチの値(向いている仰俯角。上下どちらを向いているかの角度)を保存するキー名 */
	static final private String PITCH="pitch";

	/** 座標 */
	private Location loc=null;

	/**
	 * コンストラクタ
	 * 
	 * @param loc
	 *            座標
	 */
	public SerializableLocation(Location loc){
		this.loc=loc;
	}

	/**
	 * 座標を返す
	 * 
	 * @return 座標
	 */
	public Location getLocation(){
		return loc;
	}

	/**
	 * 座標をシリアライズする
	 * 
	 * @return シリアライズされた座標
	 */
	public Map<String,Object> serialize(){

		// マップを作る
		Map<String,Object> map=new HashMap<String,Object>();

		// この座標のワールド名をマップに保存
		map.put(WORLD,loc.getWorld().getName());

		// この座標のX,Y,Z軸の値をマップに保存
		map.put(X,loc.getX());
		map.put(Y,loc.getY());
		map.put(Z,loc.getZ());

		// この座標の角度の値をマップに保存
		map.put(YAW,loc.getYaw());
		map.put(PITCH,loc.getPitch());

		// データを保存したマップを返す(ここで返したマップの中身がconfig.ymlなどに保存される)
		return map;
	}

	/**
	 * 座標をデシリアライズする
	 * 
	 * @param map
	 *            シリアライズされた座標
	 * @return デシリアライズされた座標
	 */
	public static SerializableLocation deserialize(Map<String,Object> map){

		// config.ymlなどから読み込まれたマップが引数に渡されるので、このマップを元に座標を作成(復元)する

		// 保存されているワールド名を元にワールドを取得
		World world=Bukkit.getWorld(map.get(WORLD).toString());

		// 保存されているX,Y,Z軸の値を少数として読み込む
		double x=Double.parseDouble(map.get(X).toString());
		double y=Double.parseDouble(map.get(Y).toString());
		double z=Double.parseDouble(map.get(Z).toString());

		// 保存されている角度の値を少数として読み込む
		float yaw=Float.parseFloat(map.get(YAW).toString());
		float pitch=Float.parseFloat(map.get(PITCH).toString());

		// Locationクラスのコンストラクタに読み込んだ6つの引数を渡してオブジェクトを作成
		Location loc=new Location(world,x,y,z,yaw,pitch);

		// 作成したLocationオブジェクトをSerializableLocationでラップして返す
		return new SerializableLocation(loc);
	}
}

このクラスの仕事はとても単純だ。座標(Locationオブジェクト)を受け取り、serialize()が呼ばれたら座標をマップに変換して返す。
deserialize(…)が呼ばれたら、マップを座標に変換して返すだけである。

続いて、上記のクラスを使うプラグインクラスを書く。
今回のプラグインでは、プレイヤーがクリックしたときにその座標を覚え、/reload後にその座標を読み込んで表示する。

package jp.jias.bukkit;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.plugin.java.JavaPlugin;

public class TestPlugin extends JavaPlugin implements Listener{

	/**
	 * プラグインが有効化されるとき呼び出される
	 */
	public void onEnable(){

		// SerializableLocationオブジェクトをシリアライズ・デシリアライズするのに使うクラスをBukkitに登録する
		ConfigurationSerialization.registerClass(SerializableLocation.class);

		// 設定ファイルを読み込む
		FileConfiguration conf=getConfig();

		// SerializableLocationが保存されているか調べる
		if(conf.contains("loc")){
			// SerializableLocationクラスを読み込む
			SerializableLocation sloc=(SerializableLocation) getConfig().get("loc");

			// SerializableLocationクラスにラップされている(包まれている)Locationを取り出す
			Location loc=sloc.getLocation();

			// 読み込まれたLocationを表示する
			Bukkit.broadcastMessage("保存されていた座標:" + ChatColor.YELLOW + loc);
		}
		else{
			// 保存されていない旨を表示
			Bukkit.broadcastMessage("座標は保存されていません");
		}

		// イベントリスナーの登録
		getServer().getPluginManager().registerEvents(this,this);
	}

	/**
	 * プラグインが無効化されるとき呼び出される
	 */
	public void onDisable(){
		// データを設定ファイルに保存する
		saveConfig();
	}

	/**
	 * プレイヤーがクリックしたときに呼ばれる
	 * 
	 * @param e
	 */
	@EventHandler
	public void onPlayerInteractEvent(PlayerInteractEvent e){

		// プレイヤーの座標を取得
		Location loc=e.getPlayer().getLocation();

		// 保存する前の座標を画面に表示する
		Bukkit.broadcastMessage("座標を保存しました:" + ChatColor.YELLOW + loc);

		// Locationクラスのままでは保存できないので、SerializableLocationクラスでLocationをラップする(包む)
		SerializableLocation sloc=new SerializableLocation(loc);

		// SerializableLocationクラスを保存する
		getConfig().set("loc",sloc);
	}
}

ここで特筆すべきはSerializableLocationクラスの使い方である。
Locationクラスはそのままではconfig.ymlに書き出すことができないが、SerializableLocationクラスにLocationオブジェクトを包む(ラップする)ことによって書き出しが可能になる。
逆に/reloadが実行された際は、SerializableLocationクラスに包まれていた(ラップされていた)Locationオブジェクトを取り出して使用している。


適当にクリックし、/reloadコマンドを実行したところ。
保存する前のLocationと読み込んだ後のLocationを比較すると、一字一句まで正しく再現されていることが分かる。

ちなみに、registerClass(…)メソッドで登録したクラスはそのオブジェクトがネスト状態にあっても正しく作用する。
例えばリストやマップのなかにSerializableLocationオブジェクトがあったとしても、 FileConfigurationはリストはリスト、マップはマップ、SerializableLocationはSerializableLocationとして正しく読み込んでくれる。


List<SerializableLocation>型のデータを保存して読み込んだところ。
SerializableLocationの定義は何も変更していないが、リストにネストされたSerializableLocationを正しく読み込んでいることがわかる。ソース

※補足
上記ではLocationオブジェクトはconfig.ymlに出力できないと書いたが、実はラップしなくてもそのまま出力できる。
ただdeserialize(…)が用意されていないのか、出力したきり読み込むことはできなかった。意味がない。

この記事を評価

この記事にコメント

  1. ...

【この記事にコメント】
お名前:
コメント:

Menu