はじめに
マイクラエンジニアの @yo16 です。嘘です。マイクラ大好きエンジニアでした。
今日は、世界中のマイクラユーザーが知っているSlime Finderというサイト(ウェブアプリ)の一部の、スライムチャンクを可視化する部分の実装してみました。
なぜ、車輪の再開発どころかコピーをしているかというと、このサイト、おそらくものすごいPV数を得ていると思います。得られる情報がマイクラユーザーにとっては重要で、UIも使いやすいため。そして技術も高い。
ただ日本人にとって唯一の使いづらさが英語であるということです。いや、英語と言っても全然いらないんですけど、でもこのサイトの使い方を説明しているブログ記事はものすごい多いんです。
ということで、まぁ正直に言うと、二匹目のドジョウというか・・・いや!技術検証です!
このサイトの中の人は開発元でない(と思う)んですが、こんな情報どうやって得ているの??と疑問に思ったことが本当に本当のきっかけです。
今のところ本家の足元にも及びませんが、私が作ったのはこちら。我ながら、開くたびにダサッて思いますが、技術検証だから。そのうち綺麗にして、PVを。。
ソース
確認したい技術
シード値とプレイヤーがいる座標から、スライムチャンクかどうかをどう判定しているか、です。
そもそもスライムチャンクとは
非マイクラユーザーのためにさらっと説明すると、マイクラ世界の中には座標値があります。天地の空方向がY+、北方向がZ-、東方向がX+です。空の上から見て、ディスプレイのように左上が原点で考えます。
その座標を、16x16に区分分けしてチャンクと呼び、チャンク単位で挙動が変わる設定があります。そのうちの1つが、スライムチャンクで、スライムが発生するという挙動です。それ以外は発生しない。全チャンク中、スライムチャンクである確率は1/10。
スライムからしか得られないアイテムがあり、重要。そしてスライムチャンクの中ですら発生確率が低い。
自動でスライムを倒す仕組みを作ってしばらく放置することになるんだけど、その仕組みを作るのがかなり手間です。超掘らないといけない。なので、仕組みを作って得られるまで、場所を間違っていないかすごく心配になります。
したがって今回は、isSlimeChunk(chunkX, chunkZ)
が、どう実装されているのか?という検証です。
実装
仕様(という名のコード)
いきなり答えですみませんが、実は、fandomのマイクラのページに、Javaでの実装コードが書かれています。
import java.util.Random;
public class checkSlimechunk{
public static void main(String args[])
{
// the seed from /seed as a 64bit long literal
long seed = 12345L;
int xPosition = 123;
int zPosition = 456;
Random rnd = new Random(
seed +
(int) (xPosition * xPosition * 0x4c1906) +
(int) (xPosition * 0x5ac0db) +
(int) (zPosition * zPosition) * 0x4307a7L +
(int) (zPosition * 0x5f24f) ^ 0x3ad8025fL
);
System.out.println(rnd.nextInt(10) == 0);
}
}
ここでseed
は、マイクラの1つのワールドで一意のランダムシードです。
またxPosition
はチャンクの座標です。プレイヤー座標の0~16までが0、17~32までが1、です。
ロジックは、下記です。
-
seed
と(x,z)座標をコネコネして、チャンク用のランダムシードを作る -
java.util.Random
のnextInt(10)
で0~9の整数を得る - 0の場合がスライムチャンク
(このロジックがなぜ流出しているのかは謎)
課題は、java.util.Random
で生成されるランダム値を、JavaScriptでも再現できるかという点。
チャラい課題から、急に言語の奥の方に入ってきました。
JavaScriptでjava.util.Randomを実装
JavaのRandomの実装はオープンになっているので、今回必要な個所である、コンストラクタとnextInt()
を確認します。
それを元に、JavaScript(TypeScript)で実装したのがこちら。かなりChatGPTさん頼みです。
// Javaのjava.util.Randomクラスの実装
export class JavaRandom {
private seed: bigint;
constructor(seed: bigint) {
const multiplier = BigInt("0x5DEECE66D");
const mask = (BigInt(1) << BigInt(48)) - BigInt(1);
this.seed = (seed ^ multiplier) & mask;
}
next(bits: number): number {
const multiplier = BigInt("0x5DEECE66D");
const addend = BigInt(0xB);
const mask = (BigInt(1) << BigInt(48)) - BigInt(1);
this.seed = (this.seed * multiplier + addend) & mask;
return Number(this.seed >> (BigInt(48) - BigInt(bits)));
}
nextInt(bound: number): number {
if (bound <= 0) {
throw new Error("bound must be positive");
}
if ((bound & (bound - 1)) === 0) { // bound is a power of 2
return (bound * this.next(31)) >>> 31;
}
let bits, val;
do {
bits = this.next(31);
val = bits % bound;
} while (bits - val + (bound - 1) < 0);
return val;
}
}
理屈は全然わからないけど、必要な部分だけであれば、追うことはぎりぎりできそうです。(値をハードコーディングはしてはいけないと習ったけど、このレベルだとそんなことだらけなのかもしれない。値を流用することもないもんな。)
ここだけでなくReact周辺もそうですが、seed値がnumberのままだと上限を超えるので、いちいちbigintにしないといけないところめんどくさいです。でも、TypeScriptでなくJavaScriptではぬるっと作れてしまう部分を、TypeScriptではしっかり叱られるという意味で、大変ありがたいです。
そうしてJavaのrandom関数をゲットできたので、ようやくそれらしい関数に移ります。
isSlimeChunkの実装
import { JavaRandom } from "./JavaRandom";
export function isSlimeChunk(seed: bigint, x: number, z: number): boolean {
const randomSeed = BigInt(seed) +
BigInt(x * x * 0x4c1906) +
BigInt(x * 0x5ac0db) +
BigInt(z * z * 0x4307a7) +
BigInt(z * 0x5f24f) ^
BigInt("0x3ad8025f");
const rnd = new JavaRandom(randomSeed);
return rnd.nextInt(10) === 0;
}
ここはほぼ、fandomに書かれていた通りです。
ちなみに、コーディング途中でJavaRandomの動作確認中にこんなコード(下記)を書いて、「nextInto(10)
からはゼロが返ってきているのに、この関数の戻り値はfalse
になる!?」という、アホな不具合に遭遇しました。
自分の恥を晒して、他の人のミスを防止するスタイル。
const rnd = new JavaRandom(randomSeed);
console.log(rnd.nextInt(10)); // どんな値が返ってきているか確認
return rnd.nextInt(10) === 0;
rnd.nextInt(10)
は呼ぶたびに値が変わるからですね。お気を付けください・・・(本当に恥)
React周辺は割愛
そんなこんなで、そこさえクリアできてしまえば、Reactはもう大したことないです。チャンクごとにisSlimeChunk()
って聞いていけばいいだけなので、大きく割愛。
詳細は、ソースをご覧ください。
結果
と判断できましたというお話でした。・・・あ、違った。
「スライムチャンクに従って、正しい場所に仕組みを作ったこと」でなく、「スライムチャンクファインダーが、正しくスライムチャンクを指し示すこと」を証明するんだった。いつの間にか、エンジニア視点からゲームユーザー視点になってた。
それは、本家のSlime Finderでも同じ値だし、なによりこの後本当にスライムが湧いたので、正しいことがわかりました。
おわりに
いやー数年ぶりにJavaのコーディングに触れました。今回環境を作ったからJAVA_HOMEとか、javacの存在も忘れてました。久しぶりすぎる。
本家のSlime Finderの技術の話は、あまり書きませんでしたが、チャンクのマスの部分をマウス操作でドラッグ&拡大縮小ができてすごいです。中身はcanvasで実装されていました。
canvasは、私が今ハマっているReactと相性が良くないと思っているのですが、どうでしょう。
ではReact+SVGで描くとすると、マイクラの世界は座標の範囲が無限なので、無限スクロールのようなものを実装しないといけない。ドラッグで、任意方向の要素を生成したり消滅させたりするのかと思うと、それはそれで大変そう。
いいアイデアがあったら教えてください。
また、こんなツールあったらいいのに、というのもあったら教えてください。
ではよきマイクラライフ&Reactライフを。