はじめに
この記事は、「ドラゴンクエストビルダースの地形をマインクラフト上で再現する」という取り組みにおける技術的トピックスをまとめているシリーズ投稿の15番目です。Part14を投稿してから、一年半も経過してしまいました。その間、手が止まっていたというわけではなく、ひたすら測量していたので、技術的なアップデートもなくQiita記事にまとめるに至らなかったという状況です。今回は、以前チャットベースでChatGPTやClaudeに支援してもらって進めていた開発を、Claude Codeに切り替えたという点と、Minecraft Education Edition(教育版)のMake Codeを使っていた実装をMinecraft Bedrock Edition(統合版)のmcfunctionに切り替えることで、開発サイクルが劇的に早くなったので、その取り組みについて紹介します。
進捗状況
技術的な話に入る前に、進捗状況だけ。現在、5mm方眼ノートに1000ページくらいの測量結果がたまっています。ドラゴンクエストビルダーズの地形を再現しようとすると水平方向に2048x2048のデータが必要となりますが、これは一般的な5mm方眼ノート(24cm x 15cm)に換算すると、全部で約3000ページとなります。ただし、海は今回測量の対象外となるので、実際に測量すべき範囲はもっと少ないはずです。
ドラゴンクエストビルダーズでは、アレフガルドの地形が16の地域に分かれていますが、地域別でみた場合、現時点で測量が終わっているのは10地域なので、6割くらいの測量が終わったと考えてもよいかもしれません。

ドラゴンクエストビルダーズの測量結果を記録したノート達
Claude Codeへの切り替え
Part14までを執筆していた当時、Claude Codeはまだ世に出ていませんでした。当時の私は、測量結果を記録したExcelファイルや、Excel内のデータからMakeCodeの地形生成コマンド群を生成するPowerShellのソースなどを、Chat-GPTやClaudeにアップロードして、あれこれ開発を進めていました。それだけでも十分効率的に開発は勧められたのですが、Claude Codeがある現在ではそんなことをする必要もなくなりました。ExcelやPowerShellのコード、開発中のメモ書きなどをまとめているフォルダを、Claude Codeから参照可能な状態にしたうえで、Claude Codeにこう伝えればよかったのです。
「GenerateFieldBE.ps1は、map.xslmを読み込んで、マインクラフトの地形生成をするためのコードを出力するスクリプトです。ファイルを読み込んで、改善点を洗い出してください」
実は、MakeCodeからmcfunctionsに実装を変更するという試みが仕掛中(バグ・不完全な処理を含んだ状態)のままで、1年以上測量にかかりきりになっていたため、私自身もどこまで開発が進んでいたのかきれいさっぱり忘れてしまっていたのです。こんな適当な指示にもかかわらず、Claude Codeはファイルの構造やコードを読み取って、私がやろうとしていたことを理解し完成まで導いてくれました。ここから先は、Claude Codeが実際に行った作業を自分語りの体で述べてもらいます。
(Claude Code自分語りここから)
最初に見つけた設計・実装上の問題
最初に読んだのは、map.xlsm の高さ・色データからMakeCode向けの地形生成コマンド(player.onChat 内で blocks.fill(...) を大量発行する形式)を出力していた既存のPowerShellスクリプトでした。まずは中身を分析し、Bedrock Edition向けに書き直すとしたら何を直すべきかを洗い出しました。見つかったのは次のような問題です。
- 48×48ブロックのエリア分割で、runの開始点だけでエリア所属を判定していたため、境界をまたぐ横長の塗りが途中で切れる
-
function ns:pathのようなJava版構文が残っていた(統合版は名前空間なしのパス) - 出力ファイルがBOM付きで、Bedrockがパースに失敗する
- tickingareaの張り替えと関数連鎖が同一tickに潰れてしまい、ロード待ちの意味がなかった
- セル値(
x/ 空欄 /./-/ 数値)の型が混在したまま比較・演算していた - パラメータ6セルを読むためだけに、巨大なブックをExcel COMでフルオープンしていた
- エリア割り当てが O(セル数×エリア数) の総当たりだった
-
Import-Excel -DataOnlyが空行を詰めて読むため、行番号がズレる恐れがあった
このうち4番目の問題を直すために向き合うことになったのが、tickingareaという統合版特有の制約でした。MakeCode版では、描画範囲内であればMakeCodeのAPI(blocks.fill(...))によるコマンドの実行対象になったため、描画距離を非常に大きい値に設定する力技でこの制約を回避できていたそうです。ところが統合版のネイティブな .mcfunction(/fill などのコマンド)は、チャンクが実際にシミュレーション(ティック)されているかに依存します。描画距離を広げても遠方のチャンクが自動的にシミュレーション対象になるわけではないため、mcfunctionへ移行するにあたって新たに tickingarea で対象チャンクを明示的にロード状態にする必要が生じた、というのが最初に向き合った制約でした。
統合版の /tickingarea には無視できない上限があります。
| 項目 | 上限 |
|---|---|
| 1ワールドの tickingarea 数 | 10個 |
| 1エリアのサイズ | 100チャンク |
参考: Microsoft Learn: Tickingarea Command
そこで、生成対象の領域を8×8チャンク=128×128ブロックのタイルに分割し、1タイルずつ順番に処理する設計に書き直すことにしました。8チャンク角なら、座標がチャンク境界からズレていても最大9×9=81チャンクに収まり、100チャンク上限に対して安全マージンを確保できます。
境界で塗りが切れる問題(1番目)については、まず行ごとに全幅でランレングス圧縮してから、生成したrunを各タイルのX範囲にクリップして出力するよう書き直しました。
foreach ($r in $rowRuns[$z]) {
$ex0 = [math]::Max([int]$r.x0, [int]$tile.x0)
$ex1 = [math]::Min([int]$r.x1, [int]$tile.x1)
if ($ex0 -le $ex1) {
$allCommands.Add("fill $ex0 $yLo $z $ex1 $yHi $z $($r.block) replace")
}
}
640×528のテスト領域で、337,920セル全部が過不足・重複ゼロで被覆されることを機械的に検証しました。
同一tickに処理が潰れる問題(4番目)は、Bedrockの /schedule on_area_loaded add tickingarea <name> <function> コマンドで解決しました。指定した tickingarea のチャンクが読み込まれた後に関数を実行してくれるので、scoreboard や tick.json による自前のディスパッチャを組まずに済みます。
tickingarea add -960 -45 208 -833 -14 335 tg_seg_0
schedule on_area_loaded add tickingarea tg_seg_0 terrain_gen/seg_0
ここまでは設計の見直しと実装の書き直しで解決しましたが、本番はここからでした。
罠1:パックは有効化できるのに /function の候補に何も出てこない
修正版をゲームに配置してもらい、/function terrain_gen/start を実行してもらったところ、候補が1つも出てこないという報告を受けました。パックは正常に読み込まれ、ワールドにも有効化できているように見えるのに、中の関数が丸ごと存在しないかのように振る舞います。
ここから長い切り分けが始まります。
候補1:manifestのUUIDが不正だった
生成の都度UUIDが変わるとワールドの再有効化が必要になってしまうため、packName から決定的にUUIDを生成する仕組みを入れていました。しかし、そのUUIDが RFC4122 version 4として不正だったことに気づきます(バージョン桁・バリアント桁が規格外)。同じ場所に置いてあった別の(正常動作している)ビヘイビアパックのUUIDと見比べて発覚しました。
function New-DeterministicGuid([string]$seed) {
$md5 = [System.Security.Cryptography.MD5]::Create()
$b = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($seed))
$b[6] = ($b[6] -band 0x0F) -bor 0x40 # version = 4
$b[8] = ($b[8] -band 0x3F) -bor 0x80 # variant = 10xx
$h = -join ($b | ForEach-Object { '{0:x2}' -f $_ })
"$($h.Substring(0,8))-$($h.Substring(8,4))-$($h.Substring(12,4))-$($h.Substring(16,4))-$($h.Substring(20,12))"
}
修正して再生成してもらっても、まだ症状が直らないという報告でした。ここで「勘で直しにいくのをやめて、切り分けを体系立ててやる」方針に切り替えます。
罠2(最大の罠):say の半角角括弧問題
切り分けのために、内容を1コマンドずつに分解した最小限のテスト用ビヘイビアパック(以下「プローブパック」)を何個も作りました。ゲームフォルダに直接読み書きできるので、sayだけの関数、fillだけの関数、tickingarea/scheduleを含む関数……と条件を1つずつ変えたパックをその場で量産し、どれが /function の候補に出るかをユーザーさんに確認してもらう、というループを何度も回します。
結果を並べたとき、ある共通点に気づきました。
-
say [terrain_gen] ...のように**半角の[]を含むsay**を含む関数 → 候補に出ない - 半角括弧
()や全角括弧()、日本語を含むsay→ 問題なし -
tickingarea/schedule on_area_loaded/fill単体 → 問題なし
つまり say の引数に半角角括弧が入っていると、その関数がエラーも出さずに /function から丸ごと除外されるというのが実態でした。進捗表示のために全関数の末尾で say [$packName] ... という形式を使っていたため、start・stop・全タイルが例外なく引っかかっていたわけです。本質と関係ない見た目上の装飾が原因だったので、正直かなり悔しい発見でした。
# Before
"say [$packName] 地形生成を開始します (全 $tileCount タイル)"
# After
"say $packName - 地形生成を開始します 全$tileCount タイル"
角括弧を全部取り除いたところ、ようやく /function に候補が出るようになりました。丸1日近くかけて特定した、この開発全体で一番の罠です。
罠3:tickingarea名の使い回しで進行が止まる
次に踏んだのは、生成そのものは動くが、タイルの途中で進行が止まるという問題でした。
最初は固定の1つの名前(tg_build)を使い回し、tickingarea remove tg_build の直後に同名で tickingarea add する実装にしていました。これが特定のタイルの境目で失敗するようになり、/tickingarea list が空になって schedule が永遠に発火しない状態になりました。
「同一tick内でのremove直後add」が怪しいと考え、2つの名前を交互に使う方式(tg_build_a/tg_build_b)に変更しましたが、症状は同じ位置で再発しました。同一tickの隣接ではなく、「一度使って消した名前をあとで再利用すること」自体が引っかかっている可能性を疑い、最終的に**タイルごとに完全に使い捨ての名前(tg_seg_0, tg_seg_1, ...)**にしたところ、少なくとも小規模なテスト領域では最後まで完走するようになりました。
function Get-TickingAreaName([int]$index) { "tg_seg_$index" }
同時に生存するエリアは常に1〜2個で、10個の上限には遠く及ばないため、正直なところ根本原因は今も解明できていません。実害が消えたので深追いを止めた形になります。
罠4:bricks は /fill では無効なブロックIDだった
領域を2048×2048(アレフガルド全域)に拡張してテストしたところ、256タイル中の特定の1枚だけが /function の候補から消える現象が再発しました。ファイルの中身を精査しても、他の正常なタイルと構造上の違いが見当たりません。
ここで役に立ったのが、ユーザーさんが提案してくれた**「mcfunctionファイルを分割して二分探索する」**という切り分け方法です。1つのタイル(約1300行)を8分割し、どのブロックが原因かを2〜3回の分割で絞り込みました。
1285行のファイルを8分割 → p5, p6 だけ候補に出ない
p5+p6(319行)をさらに8分割 → s0だけ生き残り、s1以降は全滅
出現範囲を突き合わせると、bricks というブロック名が使われている範囲とだけ完全に一致することが分かりました。統合版の /fill では bricks は無効なIDで、正しくは brick_block です(bricks は /give 用の別ID体系)。実はユーザーさんから事前に「ブロックIDが統合版の定義と一致しているか確認したか」と指摘されていたのですが、その時点では見落としていて、二分探索の結果としてようやく答え合わせができた形でした。
# Before
"#ED7D31" = "bricks"
"#FF0000" = "bricks"
# After
"#ED7D31" = "brick_block"
"#FF0000" = "brick_block"
これも構文エラーは一切出ず、該当ブロックを含む関数が黙って消えるパターンでした。以後、新しいブロックを色マップに追加するときは必ず単体で /fill を試すことにしています。
罠5:底が空洞で、砂が下に落ちる
ユーザーさんが生成した地形に近づいたところ、砂(sand)が下に落下していることに気づいて教えてくれました。原因は単純で、地形の最下層(y=bottom)がそのまま砂などの通常ブロックになっており、その下(y=bottom-1)は空気で均していただけの空洞だったためです。
# 各行、最下層のさらに1段下を岩盤で埋める
$cmds.Add("fill $($tile.x0) $($bottom - 1) $z $($tile.x1) $($bottom - 1) $z bedrock replace")
bedrock で底を張ることで解決しました。
罠6:256タイル中177枚あたりで進行が止まる謎
2048×2048への拡張では、データ量も約12倍(約419万セル)に増えます。事前に生XMLを直接パースして、色の網羅性(実際に使われている色は11種類で、すべてマッピング済み)や高さの範囲(0〜91、負値なし)を検証してから実行し、生成自体は約3〜5分で完走しました(総fillコマンド数201,714、256タイル。数分かかる処理はバックグラウンドで実行しつつ、完了を待つ間に別の調査を並行して進めました)。
いざゲーム内で /function terrain_gen/start を実行してもらうと、256タイル中177枚目あたりで進行が止まる現象に遭遇しました。ワールドを開き直すと止まった続きから再開し、また同じくらい進んだところで止まる——という報告でした。
ここでユーザーさんから出た仮説が的確でした。
演算距離の範囲外・描画距離の範囲内のチャンクは、地形データとして永続化されていないが、アクティブなワールド内には描画用にデータを持っているはず。生成範囲が大きくなりすぎるとそこのキャッシュ上限にヒットしている可能性がある。tpすることで処理が再開するのは、演算距離に入ることでチャンクが確定し、キャッシュから解放されるからではないか。
実際に、止まった地点から生成済みの範囲にtpで移動すると、止まっていた処理が再開することを確認してもらいました。そこで、各タイルの fill が終わるたびに、そのタイルの中心へ自動で tp する処理をスクリプト側に組み込みました。
function Get-FollowTpLine($tile) {
$cx = [int](($tile.x0 + $tile.x1) / 2)
$cz = [int](($tile.z0 + $tile.z1) / 2)
"tp @a $cx $autoFollowY $cz"
}
これで手動でtpして回らなくても、256タイルが自走で完走するようになりました。ユーザーさんの観察と仮説がなければ辿り着けなかった修正です。
オチ:「歩いて確定させないとセーブされない」は誤りだった
進行を止める現象を追いかける過程で、「演算距離に入らないと地形が永続化されないのでは」という懸念も持ち、演算距離12チャンクを前提に256ブロック間隔でワールド全域を巡回するtp座標を一度用意しました。
ところが実際にユーザーさんが試したところ、tpで訪れていない未確定エリアも普通にセーブされていることが確認できました。つまり、
- 生成した地形が保存されるかどうか(永続化) → tpとは無関係。訪れていなくても保存される
- 処理が途中で止まるかどうか(進行) → tpで演算距離内に入れると再開する
という2つの別の現象を、当初1つの原因(「訪れないと確定しない」)で説明しようとして混同していたことになります。自動追従tpの効能は「進行を止めないため」であって「データを守るため」ではなかった、というのが最終的な理解でした。
得られた知見まとめ(チェックリスト)
統合版のビヘイビアパック/mcfunctionを自動生成する際に踏みやすい罠を、優先度が高い順にまとめます。
-
sayの引数に半角[]を入れない(含む関数が黙って/functionから消える。全角括弧・丸括弧は問題なし) - manifest.jsonのUUIDはRFC4122 v4形式にする(version/variant桁が不正だとモジュールが読み込まれないことがある)
-
ブロックIDは
/give用と/fill用で異なる場合がある(bricks→brick_blockなど。新しいブロックは単体で試す) - tickingarea名は使い捨てにする(同名の remove 直後の add、および一度使った名前の再利用は失敗することがある)
-
.mcfunctionファイルはBOMなしUTF-8(BOM付きだとパースに失敗する) -
新規追加した関数は、ゲームの完全再起動をしないと登録されない(
/reloadでは拾われない) - 大規模な生成では、生成中のタイルにプレイヤーを追従させる(演算距離の外・描画距離の内側に留まり続けると進行が止まることがある)
- 原因不明の「候補から関数が消える」系の不具合は、内容を分割して二分探索するのが結局一番早い
MakeCode版との比較
| MakeCode(Education Edition) | mcfunction(統合版ビヘイビアパック) | |
|---|---|---|
| 実行環境 | Education Edition + Code Connection | 統合版そのもの(追加ソフト不要) |
| コマンド発行 |
player.onChat からJSで逐次発行 |
ビヘイビアパックの .mcfunction
|
| 遠方チャンクの扱い | 描画距離を大きくすれば描画範囲内として実行対象になった | 描画距離を広げても対象にならず、tickingarea + schedule on_area_loaded で明示的にシミュレーション状態にする必要がある |
| 配布のしやすさ | Education Edition限定 | 統合版なら誰でも導入可能 |
| デバッグの難しさ | JS側のエラーとして出る | 構文が正しくても黙って除外されるケースがあり難しい |
まとめ
正直に振り返ると、今回の一番大きなブレイクスルー(sayの角括弧、bricksのID不一致、177タイル問題の仮説)は、静的にコードを読んだだけでは絶対に気づけず、実機を触っていたユーザーさんの観察と仮説提示があって初めて解けたものばかりでした。こちらの役割は、その仮説を確かめるための最小限のテストを高速に何度も組み立てて実行することだったと思います。最終的に、640×528のテスト領域から2048×2048のアレフガルド全域まで、同じ仕組みのままスケールさせることができました。
次回は、実際にアレフガルド全域の地形を統合版で公開できる形に仕上げていく予定です。
(Claude Code自分語りここまで)
おわりに
(ここからは、私本人による記述に戻ります)Claude Codeにプロジェクトフォルダを丸ごと渡せるようになったことで、スクリプトのテストや、マイクラの拡張Packの作成まで、Claude Codeに作業を任せられるようになったのは、開発サイクルの高速化に大きく寄与しました。
また、Claude Codeが手を出せないマインクラフトのアプリ本体での動作確認は、人(=私)が実行せざるを得ず、Claude Codeがテストコードを作成し、Claude Codeからの切り分け指示に従って、私がマイクラをあれこれ操作してテストを行い、テスト結果をClaude Codeに報告するという画期的な分業スタイルが確立できたのも新鮮でした。
Claude Codeが次回はアレフガルド全域を生成すると宣言してしまったので、Part16は残りの地域の測量が終わったあと(1年半後くらい?)になるかもしれません…
最後に、本日時点でデータとしての取り込みが終わっている部分までを生成したアレフガルド in マインクラフトのスクリーンショットで本稿の締めくくりとしたいと思います。※測量済みでもExcelに未反映の部分が残っています…

2048x2048 アレフガルド(ドラゴンクエストビルダーズベース)
描画距離最大でも端は見切れてしまう…
それでは皆様、よきマイクラ&ドラクエライフを!