石橋を叩いて壊すページ

Spigotで経験値オーブが合算されてしまうのを防ぐ

引き続きボスモンスタープラグインを作っていて、気がついたことがあったのでメモ。
バニラでエンダードラゴンを倒すと経験値オーブが大量に発生するが、ボスを倒したときもあんな感じにしたいと考えた。
そうしないと、複数人で協力してボスを倒したとしても、オーブが1個しか出ないのではすべての経験値をたった一人で独占するほかない。それはよくない。

しかしfor文でループさせて経験値オーブを大量に発生させても、実際には経験値オーブが1個しか発生しなかった。なぜだ。

以下は、実際に試したコード。経験値オーブを1度に100個生成する。
ついでに、拾ったオーブの経験値量を調べるコードも書いた。

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

	// スライムボールを持ってクリックしたのか
	if (e.getPlayer().getItemInHand().getType() == Material.SLIME_BALL) {

		// プレイヤーのいる世界の座標(0,5,0)の座標を作る
		Location loc = new Location(e.getPlayer().getWorld(), 0, 5, 0);

		// 100回繰り返す
		for (int i = 0; i < 100; i++) {
			// 経験値オーブを生成
			ExperienceOrb orb = loc.getWorld().spawn(loc, ExperienceOrb.class);
			// オーブに経験値を設定
			orb.setExperience(10);
		}
	}
}

/**
 * プレイヤーが経験値を取得したときに呼ばれる
 * 
 * @param e
 *            経験値取得イベント
 */
@EventHandler
public void onEvent(PlayerExpChangeEvent e) {
	// 経験値を取得したプレイヤーの名前と経験値量を表示する
	Bukkit.broadcastMessage(e.getPlayer().getName() + "は" + e.getAmount()
			+ "の経験値を取得した。");
}


上のコードを実行したところ。一瞬だけ大量のオーブが表示されるが、直後にはオーブはひとつだけになってしまう。
オーブを拾っても、経験値は10にしかならない。

Spigotサーバでは、経験値オーブが合算される

結論を書いてしまうと、Spigotサーバでは経験値オーブがスポーンするとき、近接する場所にあるオーブが(おそらくサーバの負荷軽減のために)ひとつに合算されてしまうようだ。
というか、World#spawn()メソッドで経験値オーブをスポーンさせるとき、おおむね半径5m以内に経験値オーブがあると、新しいオーブをスポーンさせず、既存のオーブのオブジェクトを返してきているように見える。
そして、上記したコードではオーブの経験値をorb.setExperience(10);と決め打ちしているため、既存のオーブの経験値を上書きすることになり、最終的に経験値は10だけになってしまっていた。

実際、上記コードを以下のように書き換え、経験値を決め打ちするのをやめて既存の経験値に新たに加算する方式にすると、オーブ100個分の経験値が合算されているのがわかる。

【変更前】
// オーブに経験値を設定
orb.setExperience(10);
【変更後】
// オーブに経験値を設定
orb.setExperience(orb.getExperience() + 10);


経験値オーブを拾ったところ。オーブ1個に経験値が1000入っている。 経験値10×オーブ100個だから、経験値が正しく合算されているとわかる。


参考までに、Spigotサーバでエンダードラゴンを倒してみたところ。
ものの見事に経験値オーブが一個しかでない。やはりバニラとは違う処理になっていると考えられる。
一応、経験値量は正しく合算されて12000取得できたが、見た目的には虚しいことこの上ない。

経験値オーブを大量に発生させるには?

さて、話を戻すと、自分がやりたいことはオーブの大量発生だ。
上記したとおり、Spigotは経験値オーブをスポーンさせるためのコードに手を入れていると思われる。ならばそれを回避すればいい。
以下のコードでは、オーブをいったん別の場所にスポーンさせたあと、目的地へテレポートさせる方法で、「近くにある既存のオーブのオブジェクトを返す」という動作を回避している。

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

	// スライムボールを持ってクリックしたのか
	if (e.getPlayer().getItemInHand().getType() == Material.SLIME_BALL) {

		// プレイヤーのいる世界の座標(0,5,0)の座標を作る
		Location loc = new Location(e.getPlayer().getWorld(), 0, 5, 0);

		// 上記座標をコピーして、Y=300に設定する
		Location loc2 = loc.clone();
		loc2.setY(300);

		// 100回繰り返す
		for (int i = 0; i < 100; i++) {
			// 経験値オーブをY=300に生成
			ExperienceOrb orb = loc2.getWorld().spawn(loc2, ExperienceOrb.class);
			// オーブに経験値を設定
			orb.setExperience(orb.getExperience() + 10);
			// 経験値オーブを目的地へテレポート
			orb.teleport(loc);
		}
	}
}


上記コードを実行したところ。オーブを大量にスポーンさせることができた。
経験値もすべて10で、合算が起きていないことがわかる。

これで目的は達成できた。
一応この方法の難点を書いておくと、オーブをテレポートさせても画面への反映には時間差があり、オーブ100個がすべて表示されるまで1秒程度を要してしまう。
また、サーバが用意した負荷軽減策を棒に振る以上、その妥当性と影響は考慮する必要がある。

この記事を評価

この記事にコメント

  1. ...

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

Menu