石橋を叩いて壊すブログ

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

前回の記事では、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(…)が用意されていないのか、出力したきり読み込むことはできなかった。意味がない。

この記事を評価

17 4

コメント

  1. 【ゲスト】 はじめまして
    質問なのですが、指定したプラグインを無効化してpluginsから削除できるようにするにはどうすればいいですか?
    関係するページがなかったのでプラグイン関係で最後に更新されたここで質問させて頂きました。 [2015/3/21 23:45]
  2. 【石橋】 はじめまして。Bukkitのコマンドリスト見てみたけど無効化コマンドなさそうだし、サーバーが動作したままプラグインを削除するのは無理じゃないかな?
    サーバーを停止→プラグインを削除→サーバーを起動するしかないと思う。 [2015/3/22 04:09]
  3. 【ゲスト】 spigot1.8で、一定時間毎にチャット欄に"*プレイヤー名*さんようこそ"のようなのを見るのですが、どうしているのでしょうか? [2015/5/6 10:03]
  4. 【石橋】 spigotはbukkitの派生だとMinecraftJapanWikiに書いてあるから、やり方は同じでいいんじゃないかな。Bukkitの場合のやり方の記事→http://www.jias.jp/blog/?41 [2015/5/7 20:57]
  5. 【ゲスト】 >>4
    返信ありがとうございます。
    プレイヤーを一定時間おきに取得というのはどうやるのですか?
    Bukkit.getPlayer(name);を試してみましたができませんでした。 [2015/5/10 20:48]
  6. 【石橋】 興味あったんでspigot入れてみた。まだ5分くらいしか動かしてないけどチャンクの生成早いしいいねこれ。
    で、取り急ぎBukkit V1.7でビルドしてSpigot V1.8で動かしてる手抜きでやった限りは、Bukkit.getPlayer(name);は動いたよ。


    public class TestPlugin extends JavaPlugin implements Listener{

        public void onEnable(){
            // 200ticks(10秒)おきにrun()の処理をする
            new TestRunnable().runTaskTimer(this,200,200);
        }

        class TestRunnable extends BukkitRunnable{

            public void run(){

                // ログインしている全員の一覧を取得する
                Player[] players=Bukkit.getServer().getOnlinePlayers();

                // ログイン人数を表示する
                Bukkit.broadcastMessage("現在のログイン人数は" + players.length + "です。");

                // 全員に個別にメッセージを送る
                for(Player player:players){
                    player.sendMessage("welcome test server! A");
                }

                // 名前で指定して個別にメッセージを送る(※i_shi_ba_shiがログインしてないとぬるぽになる)
                Bukkit.getPlayer("i_shi_ba_shi").sendMessage("welcome test server! B");
            }
        }
    }

    とりあえず、一定時間おきに「*プレイヤー名*さんようこそ」というのがちょっとよくわからない。
    XXさんがログインしたときに「XXさんようこそ」ならわかるけど、なぜ一定時間おきにそんなメッセージを…? [2015/5/12 22:14]
  7. 【ゲスト】 遅れてすみません。上の者です。JPMCにあるようなゲーム内で出る統計確認の案内のように、一定時間おきにプレイヤー名を出したいです。 [2015/6/4 21:40]
  8. 【石橋】 特定のサーバのゲーム内機能の話してる?それログインしたことある人でないと答えられないよ。 [2015/6/6 07:15]
  9. 【ゲスト】 一定時間おきにminecraftjp/IDのように出るんです。それは、 "minecraftjp/"+player だと思うんです。それで、一定時間おきにプレイヤー名を個別に取得していると思いました。 [2015/6/6 11:09]
  10. 【石橋】 ログイン中の全プレイヤーを取得するなら上に書いたとおりgetOnlinePlayers()でできるので、そこからランダムに一人選べばいいんじゃないかな?一定時間おきの処理については以前の記事参照 http://www.jias.jp/blog/?41 [2015/6/6 15:22]

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

295,349

記事カテゴリー

最近の記事

  1. FF14を翻訳したら国によって結構特色が違った[12/11]
  2. ゼロから始めるFactorioの回路構築(実践例編)[7/24]
  3. ゼロから始めるFactorioの回路構築(条件回路・論理演算編)[7/24]
  4. ゼロから始めるFactorioの回路構築(定数回路・算術回路編)[7/24]
  5. ゼロから始めるFactorioの回路構築(入力・出力・混合編)[7/24]

RSS1.0 RSS2.0

最近のコメント

  1. おー、お役に立てたようでなによりです。再起…[9/11]
  2. この記事が非常の役立ちました。おかげさまで…[9/9]
  3. 1.14.2のAPIで試しましたが、MapInitializeEv…[9/9]
  4. 天晴れ!押したらポーションのモヤモヤでまし…[5/26]
  5. 習うより慣れろって感じで。この記事は記事冒…[4/7]

記事を検索


管理人

石橋

絵画センスは皆無