マイクラのスライムチャンクファインダー（正式版）を作った

# はじめに
少し前に似た記事を書いたのですが、そこでの主要な観点は、ランダムっぽいあのスライムチャンクを決定しているロジックの再現でした。

https://qiita.com/yo16/items/b19d139e13688d9740ec

そしてその時に作ったのが[こちら（α版）](https://small-pieces-sandbox.onrender.com/contents/minecraft_slimechunk_finder_a/index.html)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/4fe7a9fe-f887-b8d0-8ec3-cd4a82d883ef.png)

我ながら何度見てもしょぼいですが、機能はこれで確認できました。

今回はその続きで、技術的にはほぼ最終版のつもりです。
今回作ったのが[こちら（正式版）](https://small-pieces-sandbox.onrender.com/contents/minecraft_slimechunk_finder/index.html)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/4ad4e606-ea3c-6790-9a47-f8b767bc1050.png)

え、大して変わらない？うーん、そうかも？
今回の主要な観点は、動かす部分です。まさにフロントエンド。そして↓こんな感じになりました。

![mv20240608_2_out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/b73f4628-8fbd-69a0-6101-e6ee20337005.gif)

ソースはこちら

https://github.com/yo16/minecraft_slimechunk_finder



# 技術概要
Reactでcanvasを使っています。
マウス操作でcanvasを動かすことは、こちらで試行した内容を書きました。主にドラッグについて。

https://qiita.com/yo16/items/0362f263fee618ad2cee

今回は、そのとき試行したものから表示部を発展させていますが、根本的にはあまり変わらないので割愛します。

あれ、そしたらこの記事では何を説明するの？というあなた、お待ちください。

この１マス１マスについて、`isSlimeChunk()`と聞きまくっていくと、全部のマスを聞いたらパフォーマンス的にヤバイことにならないか？と思いますよね。なるんです。その対応について、ちょっと書こうと思います。

## パフォーマンスチューニング
`isSlilmeChunk()`は、こんな関数です。
```TypeScript: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;
}
```
重いのか軽いのかよくわからないですが、castを多用し、大きめの数値（少し大きめなメモリ領域）を使って計算してます。`JavaRandom()`の中はそうでもないですが。

そうして、一番縮小した場合は、こうなります。大体、1,000 x 1,000なので、チャンクサイズの16で割ると、62x62=3,844マス くらい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/68a29597-d6d0-78ff-21b4-1c1e01ca9171.png)
3,844回回るループなんて大したことないと思うかもしれませんが、これをドラッグすると、ドラッグで動くたびに再描画が走ります。

下記はmousemoveイベントのハンドラ関数です。
```TypeScript:handleMouseMove()
function handleMouseMove(event: MouseEvent){
    //console.log("mouseMove");
    //if (!isDragging) return;
    if (!canvas) return;

    // マウスの位置を取得（ブラウザの左上が原点）
    const mousePos: Point = {
        x: event.clientX,
        y: event.clientY
    };

    // 今のoriginBを取得
    const movingOriginB = getOriginBDragging(mousePos); 
    //if (!movingOriginB) return;

    // マウスの位置を、canvasの座標系に変換
    const rect = canvas.getBoundingClientRect();
    const canvasMousePos: Point = {
        x: mousePos.x - rect.left,
        y: mousePos.y - rect.top
    };

    // 描画
    draw(movingOriginB, canvasMousePos);
}
```
今のマウス位置やドラッグ操作に必要な簡単な計算をしたのちに、`draw()`するという処理です。`draw()`は言うまでもなく、マスの判断を含め、線１本１本、すべてを描画する処理です。

`draw()`の中で表示処理の前に、１マスごとに計算します。
```TypeScript
// チャンクごとのisSlimeChunkの計算
for(let chunkX=chunkXMin; chunkX<=chunkXMax; chunkX++) {
    for(let chunkZ=chunkZMin; chunkZ<=chunkZMax; chunkZ++) {
        setIsSlimeChunkIfNotExists(chunkX, chunkZ);
    }
}
```
`setIsSlimeChunkIfNotExists()`の部分で、無邪気に`result = isSlimeChunk(chunkX, chunkZ)`みたいに計算すると重い、という話です。

実際、カクカクしました。どういう操作をしていたか忘れるほどに。。ということで、毎回 `isSlimeChunk()`を呼び出していられないので、一度計算したら再利用する方法を検討しました。

１度計算した`isSlimeChunk`値を保存する変数の型はこちら。
x座標とz座標でアクセスしたかったので、`Map`を使いました。マイナス方向にもプラス方向にもあり、大きな座標値へジャンプもできるので、単純な配列は難しいです。
```TypeScript
// x,zの順に指定するマップ
type IsSlimeChunkMapZ = Map<number, boolean>;
type IsSlimeChunkMap = Map<number, IsSlimeChunkMapZ>;
```
`IsSlimeChunkMapZ`は、チャンクのz座標をキーに、スライムチャンクか違うかのbooleanを持つMap。そのMapを、チャンクのx座標をキーに持つMap。Mapの入れ子です。

値をセットする`setIsSlimeChunkIfNotExists()`では下記のようにしています。
```TypeScript
// isSlimeChunkMapRefの操作
function ensureZMap(x: number): IsSlimeChunkMapZ {
    let zMap = isSlimeChunkMapRef.current.get(x);
    if (!zMap) {
        zMap = new Map();
        isSlimeChunkMapRef.current.set(x, zMap);
    }
    return zMap;
}
function setIsSlimeChunkIfNotExists(x: number, z: number) {
    const zMap = ensureZMap(x);
    if (!zMap.has(z)) {
        if (version === 0) {
            zMap.set(z, isSlimeChunk(seed, x, z));
        } else {
            zMap.set(z, isSlimeChunkBedrock(x, z));
        }
    }
}
```
`setIsSlimeChunkIfNotExists()`は、チャンクのx座標とz座標を渡し、その結果を`isSlimeChunkMapRef.current`にセットします。

まずx座標に対応するz座標群を入れるMapを、あれば取得、なければ作ります。
z座標に対する値がなければ、`isSlimeChunk()`の結果をMapへ設定します。

`isSlimeChunk()`がMinecraftのJava版、`isSlimeChunkBedrock()`はBedrock版というもので計算方法が違うので２パターンあります。そこは大事な話ではないので割愛。

つまり言いたかったことは、このMapの入れ子を使って、計算済みなら値を取得し、まだなら計算するようにしてあります。
これによって、特に問題なく動くようになりました。

# おわりに
急に終わり？という感じですが、パフォーマンスチューニング以外は、雑用的なものでこまごましているので書くほどじゃないです。

スライムチャンクファインダーというシステムとしては、技術的な構築はほぼ終了だと思っていますが、例えば、Java EditionとBedrock Edtion、どっちを選んだらいいの？とか、Seedって何？といった、使い方の部分の説明を加えようと思っています。

ではよいマイクラライフを。