
2026/03/20 7:33
マインクラフトのソースコードは興味深いです。
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
元の要約は高レベルのアイデアを捉えているものの、キー・ポイントリストに含まれる多くの具体的実装詳細が省略されています。より完全なバージョンには次の項目を含めるべきです。
- ポインタカウンタ用の正確なパッキング方式と、そのロックフリー読み取りメカニズム。
- 特定のライト圧縮インデックス(
)と、チャンクあたり約16 KBから約3 KBへのメモリ節約効果。128/129 - モンロー(Z‑order)インデクシング手法、16ビット圧縮レベルフィールド、および均一ブロックがタイルIDをインデックスに直接格納する方法。
を用いた物理ページのアロケーション、チャンクごとの固定8 KB割り当て、および PS3 ハードウェアでヒープ断片化を回避する理由。XPhysicalAlloc- 爆発ロジックのレイマーチング詳細と、オブシディアン壁後ろにあるダメージバグ修正。
- エンティティID用の2048ビットビットフィールドアロケータ、並列配列、および ID 再利用衝突を防ぐ仕組み。
- 「ワーム」洞窟アルゴリズムの詳細(楕円形バブル、
ベルカーブ、「ファス」係数)と隣接バイオームの水色ブレンド。sin(d/16π) - パーリンノイズにおける五次平滑多項式、オクターヴスタッキング、および 16 777 216 ブロックでの座標ラッピングによるシームレスワールド化。
- スレッドごとの一時 AABB プール戦略、XOR‑ツリー SIMD 差分チェック、符号拡張トリック。
これらの詳細を含めることで、要約はキー・ポイントリストと完全に整合しつつ、主旨を明確かつ理解しやすく保つことができます。
本文
2026年3月19日
Minecraft の「Old Console」ソースコードが 2026 年 3 月 1 日に流出しました。多くの人々がポートを始め、モッドや動画を作成しています。ソースをレビューしているうちに、共有したい興味深いトリックとアルゴリズムが数多く見つかりました。
1. 時代の初期
- Mojang は「4J 形状」のブロッキーワールドをアップデートごとに作り上げてきました。
- マイクロソフトは Fire Nation が攻撃した後に到着し、領域は新しい時代へ…(ただの馬鹿みたいなこと)。
- PS3 バージョンも「またひどいですが、我々のスタイルのひどさです」。
元々 Minecraft は Notch によって Java で書かれました。コンソールは Java を実行できないため、誰かが全体を C++ に書き直す必要がありました。4J Studios(スコットランドの小さなゲームスタジオ)が、Java 版 Minecraft をコンソール向けにポートするために雇われました。
ポートは約 3,300 の C++ ソースファイルで構成され、2 つのプロジェクトに分かれています:
Minecraft.Client – レンダリング、UI、プラットフォーム関連。
Minecraft.World – ゲームプレイ、ワールド生成、ネットワーク。
地形生成からクリーパー爆発、レッドストーンまで、すべてを 256 MB の RAM と PS3 の Cell プロセッサ向けに C++ で最初から書き直しました。
2. ポインタとカウンタを一つの数値に詰め込む
SparseLightStorage.cpp では、メモリポインタと光平面数を 64 ビット整数にまとめています:
dataAndCount = 0x007F000000000000L | ((__int64)planeIndices & 0x0000ffffffffffffL);
- x86‑64 の仮想アドレスは下位 48 ビットしか使わず、上位 16 ビットはゼロです。
- 4J はその上位ビットを光平面の割り当て数として利用しています。
読み出すとき:
int count = (sourceDataAndCount >> 48) & 0xffff; unsigned char *ptr = (unsigned char *)(sourceDataAndCount & 0x0000ffffffffffffL);
この値全体を単一の compare‑and‑exchange で原子操作できるため、光データの読み取りはロック不要です。書き込みのみがコピーとスワップのコストを負担しますが、ライト変更はまれなので、書き込みは稀です。
コンパイラはポインタ切り捨てについて警告します;4J は
で抑制しています。#pragma warning ( disable : 4826 )
これにより秒間何百万ものロック取得を削減できます。
3. 約20行のガーベジコレクタ
ポインタは更新時に他スレッドから読み取られる可能性があるため、古いポインタを即座に解放できません。そこで三つの delete キューを回転させます:
void SparseLightStorage::queueForDelete(unsigned char *data) { deleteQueue[deleteQueueIndex].Push(data); } void SparseLightStorage::tick() { int freeIndex = (deleteQueueIndex + 1) % 3; unsigned char *toFree = NULL; do { toFree = deleteQueue[freeIndex].Pop(); if (toFree) free(toFree); } while (toFree); deleteQueueIndex = (deleteQueueIndex + 1) % 3; }
書き込みインデックスは毎ティックで 1 進みますが、解放は常に 2 ステップ先のキューから行われます。これにより、すべてのポインタは少なくとも二回分のゲームティックを経過してから解放され、読者は完了する時間を確保できます。
Linux カーネルのデータ構造に似た epoch‑based メモリ再利用スキームで、約 20 行のコードです。
4. ライト圧縮
Minecraft の照明はブロックあたり 4 ビット(0–15)です。
単純なチャンクは 128 × 16 × 16 × 0.5 = 16 384 バイトのライトデータを保持します。
観察:ほとんどの水平平面は全てゼロ(地下のブロック光)または全て十五(地上の天空光)です。したがってフル平面ではなくインデックスだけで済むようにしました:
static const int ALL_0_INDEX = 128; // この平面は全暗 static const int ALL_15_INDEX = 129; // この平面は全明
各 Y レベルに 1 バイトを割り当てます。128 または 129 の場合、データは割り当てられず値が暗黙的になります。混合照明の平面だけが 128 バイトを使用します。
典型的なチャンクでは約 20–30 平面に実際のデータが必要 → 80 % のメモリ削減。
この圧縮でほとんどのチャンクは 3 KB、元は 16 KB に抑えられ、256 MB の RAM を持つコンソールでも大きな違いを生みます。
5. ビット散乱座標(Z‑order 曲線)
圧縮タイルストレージは各 16×128×16 チャンクを 4×4×4 タイルの 512 ブロックに分割します。
(x,y,z) をブロックとタイルインデックスへ変換する際にビット交差(Morton コード)を使用:
inline void CompressedTileStorage::getBlockAndTile(int *block, int *tile, int x, int y, int z) { *block = ((x & 0x0c) << 5) | ((z & 0x0c) << 3) | (y >> 2); *tile = ((x & 0x03) << 4) | ((z & 0x03) << 2) | (y & 0x03); }
メモリオフセットへ戻す:
inline int CompressedTileStorage::getIndex(int block, int tile) { // index bits: xxxxzzzzyyyyyyy int index = ((block & 0x180) << 6) | ((block & 0x060) << 4) | ((block & 0x01f) << 2); index |= ((tile & 0x30) << 7) | ((tile & 0x0c) << 5) | (tile & 0x03); return index; }
これは手作りの Morton コードです。X、Y、Z 座標のビットを交差させることで、3D 空間で近接したブロックがメモリ上でも近くに配置され、キャッシュヒット率が大幅に向上します。
ブロックの 16‑bit インデックスは圧縮モードをエンコード:
…00 → 1 ビット/タイル(2 種類) …01 → 2 ビット/タイル(4 種類) …10 → 4 ビット/タイル(16 種類) …11 → 8 ビット/タイル(完全、未圧縮) 111 → 0 ビット/タイル(一様ブロック、ID が上位 8 ビットに格納)
4×4×4 ブロック全体が同一タイル(石・空気・水など)であれば、タイル ID はインデックスの上位 8 ビットに直接保存されます。ポインタや間接参照は不要です。
結果:32 KB のチャンクが約 4 KB に圧縮。1 KB を節約することで、より多くのチャンクをロード状態に保てるため、描画距離が延び、世界が大きく感じられます。
6. ヒープをバイパス
圧縮ライト用メモリは
XPhysicalAlloc で確保し、ヒープを回避します:
indicesAndData = (unsigned char *)XPhysicalAlloc(32768 + 4096, MAXULONG_PTR, 4096, PAGE_READWRITE);
コメントでは物理アロケーションによりページ単位でクリーンに解放できると説明。標準ヒープを使うと時間経過とともに断片化が進み、コンソールの限られた RAM で問題になるためです。
チャンクは最大でも 8 KB の圧縮データしか必要としないことが測定されており、予測可能なオーバーヘッドは許容範囲です。
7. Java 標準ライブラリのクローン
PS3 ワールドは Java Edition と完全に一致させる必要があるため、Java の標準ライブラリを多く再実装しました:
(48 ビット状態の LCG)Random
補助ハッシュHashMap- ストリーム・ファイルクラス(
、InputStream
など)OutputStream - タスクスケジューリング用
インタフェースComparable - ポーション効果用
List.hashCode()
例:Java の整数キーに対する補助ハッシュ:
int operator()(const int &k) const { int h = k; h += ~(h << 9); h ^= (unsigned int)(h >> 14); h += (h << 4); h ^= (unsigned int)(h >> 10); return h; }
64 ビットキーの場合:
int operator()(const __int64 &k) const { return hash((int)(k ^ (((__uint64)k) >> 32))); }
8. 爆発:物理ではなくレイキャスティング
爆発は 16×16×16 グリッドの表面セルから約 1,352 本のレイを放ちます:
for (int xx = 0; xx < 16; ++xx) for (int yy = 0; yy < 16; ++yy) for (int zz = 0; zz < 16; ++zz) { if ((xx != 0 && xx != 15) && (yy != 0 && yy != 15) && (zz != 0 && zz != 15)) continue; // 内部はスキップ double xd = xx / 15.0 * 2 - 1; double yd = yy / 15.0 * 2 - 1; double zd = zz / 15.0 * 2 - 1; double d = sqrt(xd*xd + yd*yd + zd*zd); xd /= d; yd /= d; zd /= d; float remainingPower = r * (0.7f + level->random->nextFloat() * 0.6f); float stepSize = 0.3f; while (remainingPower > 0) { int t = level->getTile(xt, yt, zt); if (t > 0) remainingPower -= (Tile::tiles[t]->getExplosionResistance(source)+0.3f)*stepSize; if (remainingPower > 0) toBlow.insert(TilePos(xt, yt, zt)); xp += xd * stepSize; yp += yd * stepSize; zp += zd * stepSize; remainingPower -= stepSize * 0.75f; } }
レイはパワーがゼロになると停止し、各ブロックが爆発抵抗を減算します。これによりオブシディアン壁が TNT を防ぎ、土が阻止される現象を再現できます。
9. エンティティ ID:ビットフィールド割り当て
ネットワーク化されたエンティティはサーバーとクライアントで同一の ID を持つ必要があります。2048 スロットのビットフィールドアロケータを使用:
unsigned int Entity::entityIdUsedFlags[64] = {0}; int Entity::getSmallId() { for (int i=0; i<64; ++i) { unsigned int uiFlags = entityIdUsedFlags[i]; if (uiFlags != 0xffffffff) { unsigned int uiMask = 0x80000000; for (int j=0; j<32; ++j) { if ((uiFlags & uiMask) == 0) { uiFlags |= uiMask; entityIdUsedFlags[i] = uiFlags; return i*32 + j; } uiMask >>= 1; } } } app.DebugPrintf("Out of small entity Ids... possible leak?\n"); __debugbreak(); }
ID 0–2047 は 11 ビットで表現でき、帯域幅を節約します。ローカル専用エンティティは 2048 以上の ID を使用します。
10. 洞窟掘削:ワームアルゴリズム
洞窟は「ワーム」が空間を移動し、方向が徐々に変化しながら各ステップで楕円形バブルを切り取ります:
double r = (Mth::sin(d / 16.0f * PI) * radius + 1) * ss + 1; if (xd*xd + yd*yd + zd*zd < random->nextDouble() * fuss + (1 - fuss)) // ブロックを除去
正弦はベルカーブを作り、洞窟の中央で広がり両端で狭くなる形状にします。
fuss 要因が壁面に粗さを付与します。
11. 水色合成
バイオーム境界では水は隣接する 9 個のバイオームの色を平均化します:
for (int oz = -1; oz <= 1; ++oz) for (int ox = -1; ox <= 1; ++ox) { int waterColor = level->getBiome(x+ox, z+oz)->getWaterColor(); totalRed += (waterColor & 0xff0000) >> 16; totalGreen += (waterColor & 0x00ff00) >> 8; totalBlue += (waterColor & 0x0000ff); } return (((totalRed/9)&0xFF)<<16) | (((totalGreen/9)&0xFF)<<8) | ((totalBlue/9)&0xFF);
12. 木は上から下へ成長
葉は幹より先に配置され、最高 Y 座標から順に下方向へ進みます:
for (int yy = y + treeHeight; yy >= y - grassHeight + treeHeight; --yy) { int yo = yy - (y + treeHeight); int offs = extraWidth + 1 - yo/2; for (int xx = x - offs; xx <= x + offs; ++xx) for (int zz = z - offs; zz <= z + offs; ++zz) { if (abs(xo)==offs && abs(zo)==offs && (random->nextInt(2)==0 || yo==0)) continue; if (!Tile::solid[level->getTile(xx, yy, zz)]) placeBlock(level, xx, yy, zz, Tile::leaves_Id, leafType); } }
ブロックを下から上へ配置すると、各葉で高さマップが再計算されるため非効率です。上から下に更新すれば列ごとに一度だけ高さマップが更新されます。
13. ゾンビの日照確率
ゾンビが火をつけるのは確率的です:
if (br > 0.5f && random->nextFloat() * 30 < (br - 0.4f) * 2 && level->canSeeSky(floor(x), floor(y), floor(z))) { setOnFire(8); }
明るさ 0.5 のときは約 0.67 %/ティック、最大明るさで約 4 %。自然に見える挙動を実現しています。
14. チャージドクリーパー
void Creeper::thunderHit(const LightningBolt *bolt) { Monster::thunderHit(bolt); entityData->set(DATA_IS_POWERED, (byte)1); }
発砲時:
if (isPowered()) level->explode(this, x, y, z, 6, destroyBlocks); // 半径 6 else level->explode(this, x, y, z, 3, destroyBlocks); // 半径 3
1 バイトのフラグで爆発半径が 3→6 に倍増します。爆発力は半径に比例するため、チャージドクリーパーは約四倍破壊力を持ちます。
15. 地形生成(Perlin ノイズ)
勾配計算はビット操作でコンパクト化:
double ImprovedNoise::grad(int hash, double x, double y, double z) { int h = hash & 15; double u = h < 8 ? x : y, v = h < 4 ? y : (h == 12 || h == 14 ? x : z); return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); }
平滑多項式:
double u = x*x*x*(x*(x*6-15)+10); // 6x^5 – 15x^4 + 10x^3
マルチオクターブノイズは周波数を重ねます:
for (int i=0; i<levels; ++i) { value += noiseLevels[i]->getValue(x*pow, y*pow, z*pow)/pow; pow /= 2; }
地形は 16 777 216 ブロックごとに繰り返します(
xb %= 16777216;)。
16. AABB プール
衝突検出には一時的な軸揃った境界箱(AABBs)が必要です。各スレッドで循環バッファを使いヒープ割り当てを回避します:
AABB *AABB::newTemp(double x0, double y0, double z0, double x1, double y1, double z1) { ThreadStorage *tls = (ThreadStorage *)TlsGetValue(tlsIdx); AABB *thisAABB = &tls->pool[tls->poolPointer]; thisAABB->set(x0, y0, z0, x1, y1, z1); tls->poolPointer = (tls->poolPointer + 1) % ThreadStorage::POOL_SIZE; return thisAABB; }
各スレッドに 1024 個の事前割り当て済み AABB があり、new/delete やロック競合は発生しません。
17. XOR 木でメモリ比較
チャンクが変更されたか判断するために 64 バイトブロックを手動 SIMD で比較します:
__int64 d0 = pOld[0] ^ pNew[0]; ... __int64 d7 = pOld[7] ^ pNew[7]; d0 |= d1; d2 |= d3; d4 |= d5; d6 |= d7; d0 |= d2; d4 |= d6; if (d0 | d4) return false;
8 回の XOR、4 回の OR、さらに 2 回の OR、最後に一度だけ判定。ブランチレスで高速です。
18. 座標復元用符号拡張
座標がキャッシュエントリにパックされている場合、復元時に符号拡張します:
xx = (xx << 22) >> 22; // 負の値を符号拡張
1 行で両方の符号(正・負)を扱える単純かつ高速な手法です。
開発者メモ
コードには
// 4J Stu – I have no idea what was going on here や // didn’t went ok といったコメントが散在しています。これは小規模チーム向けに書かれた実運用ビルドの真相を示すサインであり、公開向けではありません。ソースファイルには変更理由や必要なハック、まだ脆弱と感じる箇所への正直なメモが含まれています。
以上 – Minecraft コンソール版の混沌としたながらも高性能な世界を垣間見ることができました。