语言 / Language:

⛏️ 在浏览器里做一个
你自己的 MINECRAFT!

⛏️ Build Your Own MINECRAFT
in the Browser!

你先拿到一个完整的游戏 —— 挖方块、盖房子、捏地形、保存世界。 然后真正的任务开始:把它的美术换成你自己的图片,改造它的世界, 再和你的 AI 编程搭档一起,一个功能一个功能地把它养大。

You get the complete game first — break blocks, build, shape hills, save your world. Then the real quest begins: replace its art with your own images, change its world, and grow it feature by feature with your AI coding agent.

🧱 真·3D▶️ 2 分钟就能玩🖼️ 你自己的方块美术🌄 你自己的地形🤖 AI 升级 🧱 real 3D▶️ plays in 2 minutes🖼️ your own block art🌄 your own terrain🤖 AI upgrades

🎒 你需要准备什么

来自教程 1 的技能: 游戏文件夹和本地服务器(python -m http.server 8000)—— 3D 模块必须用服务器;双击文件只会得到白屏。 新原料:three.js,一个帮你做高难度 3D 数学的免费库。 什么都不用安装 —— 文件顶部的 importmap 几行会从网上把它借来。

🎒 What you need

Skills from tutorial 1: a game folder and the local web server (python -m http.server 8000) — 3D modules require the server; double-clicking the file gives a blank page. New ingredient: three.js, a free library that does the hard 3D math. Nothing to install — your file borrows it from the internet with the importmap lines at the top.

1 🎮 拥有这个游戏 —— 复制、运行、开玩

1 🎮 Own the game — copy it, run it, play it

这就是整个游戏。真的 —— 当 three.js 负责干重活时,一个完整的 3D Minecraft 大约只要 100 行。把下面全部复制到游戏文件夹里一个叫 mine.html 的文件中:

Here's the whole game. Yes, really — a complete 3D Minecraft is about 100 lines when three.js does the heavy lifting. Copy ALL of it into a file called mine.html in your game folder:

<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<body style="margin:0">
<script type="importmap">
{ "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

// ===== 摄影棚 —— scene(世界)、camera(眼睛)、renderer(放映机)=====
const scene = new THREE.Scene();
scene.background = new THREE.Color("#87ceeb");                // 天空蓝
const camera = new THREE.PerspectiveCamera(60, 800/500, 0.1, 100);
camera.position.set(14, 12, 14);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(800, 500);
document.body.appendChild(renderer.domElement);
const sun = new THREE.DirectionalLight("#ffffff", 2);         // 阳光 ☀️
sun.position.set(5, 10, 7);
scene.add(sun);
scene.add(new THREE.AmbientLight("#ffffff", 0.6));            // 柔和的天光
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;                                // 顺滑的镜头手感

// ===== 飞行按键 —— WASD / 方向键移动,Q 下降,E 上升 =====
const nav = {};
addEventListener("keydown", e => nav[e.key.toLowerCase()] = true);
addEventListener("keyup",   e => nav[e.key.toLowerCase()] = false);
function flyCam() {
  const fwd = new THREE.Vector3();
  camera.getWorldDirection(fwd);               // 摄像机现在朝哪边?
  fwd.y = 0; fwd.normalize();                  // 只在水平方向飞
  const right = new THREE.Vector3().crossVectors(fwd, new THREE.Vector3(0, 1, 0));
  const move = new THREE.Vector3();
  if (nav.w || nav.arrowup)    move.add(fwd);
  if (nav.s || nav.arrowdown)  move.sub(fwd);
  if (nav.d || nav.arrowright) move.add(right);
  if (nav.a || nav.arrowleft)  move.sub(right);
  if (nav.e) move.y += 1;
  if (nav.q) move.y -= 1;
  if (move.lengthSq() === 0) return;
  move.normalize().multiplyScalar(0.15);
  camera.position.add(move);                   // 摄像机和它环绕的目标点一起移动,
  controls.target.add(move);                   // 所以拖动旋转永远围着你所在的地方转
}

// ===== 方块类型 —— 你的快捷栏。(很快你就会把这些颜色换成你自己的图片!)=====
const TYPES = {
  "1": { name: "grass", color: "#6abe30" },
  "2": { name: "dirt",  color: "#8b5a2b" },
  "3": { name: "stone", color: "#9099a1" },
  "4": { name: "wood",  color: "#a0742f" },
};
const materials = {};                          // 每种类型共用一罐"颜料"
for (const [key, t] of Object.entries(TYPES))
  materials[key] = new THREE.MeshLambertMaterial({ color: t.color });
let currentType = "1";

// ===== 方块 —— 全世界的方块共用同一个立方体形状 =====
const geometry = new THREE.BoxGeometry(1, 1, 1);
const blocks = [];
function addBlock(x, y, z, type) {
  const block = new THREE.Mesh(geometry, materials[type]);
  block.position.set(x, y, z);
  block.userData.type = type;                  // 记住它是哪种方块(存档要用)
  scene.add(block);
  blocks.push(block);
}

// ===== 地形 —— sin/cos 是波浪机器:一秒造出起伏的山丘 =====
function makeHills() {
  for (let x = -8; x < 8; x++)
    for (let z = -8; z < 8; z++) {
      const height = Math.round(Math.sin(x / 4) * 2 + Math.cos(z / 4) * 2);
      for (let y = -2; y <= height; y++)
        addBlock(x, y, z, y === height ? "1" : "2");   // 顶上铺草,下面是泥土
    }
}

// ===== 存档/读档 —— 你的世界只是一张清单(教程 1 的招数!)=====
function saveWorld() {
  const data = blocks.map(b => ({ x: b.position.x, y: b.position.y,
                                  z: b.position.z, t: b.userData.type }));
  localStorage.setItem("myWorld", JSON.stringify(data));
}
function loadWorld() {
  const data = JSON.parse(localStorage.getItem("myWorld"));
  if (!data) return false;
  for (const b of data) addBlock(b.x, b.y, b.z, b.t);
  return true;
}
if (!loadWorld()) makeHills();                 // 有存档读存档,没有就造新山丘

onkeydown = e => {
  if (TYPES[e.key]) currentType = e.key;       // 1,2,3,4 选方块
  if (e.key === "s") saveWorld();              // 随时按 S 保存
};

// ===== 激光 —— 点击挖掉,Shift+点击建造 =====
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let downX = 0, downY = 0;
renderer.domElement.addEventListener("pointerdown", e => { downX = e.clientX; downY = e.clientY; });
renderer.domElement.addEventListener("click", e => {
  if (Math.hypot(e.clientX - downX, e.clientY - downY) > 9) return;  // 拖镜头 ≠ 点击!
  // 激光要用"屏幕数学":中心是 (0,0),边缘是 -1 到 +1
  const box = renderer.domElement.getBoundingClientRect();
  mouse.x =  ((e.clientX - box.left) / box.width)  * 2 - 1;
  mouse.y = -((e.clientY - box.top)  / box.height) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);      // 从摄像机穿过鼠标的方向瞄准
  const hits = raycaster.intersectObjects(blocks);
  if (hits.length === 0) return;
  const hit = hits[0];                         // 激光碰到的第一个方块
  if (e.shiftKey) {                            // SHIFT+点击 = 建造 🏗️
    const spot = hit.object.position.clone().add(hit.face.normal);
    addBlock(spot.x, spot.y, spot.z, currentType);
  } else {                                     // 普通点击 = 挖掉 ⛏️
    scene.remove(hit.object);
    blocks.splice(blocks.indexOf(hit.object), 1);
  }
});

// ===== 心跳 —— 和教程 1 一样的游戏循环 =====
function animate() {
  flyCam();                                    // 飞行按键
  controls.update();                           // 顺滑镜头必需
  renderer.render(scene, camera);              // 给世界拍一张照
  requestAnimationFrame(animate);              // "浏览器,下一帧再运行我一次!"
}
animate();
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<body style="margin:0">
<script type="importmap">
{ "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

// ===== THE MOVIE SET — scene (world), camera (eyes), renderer (projector) =====
const scene = new THREE.Scene();
scene.background = new THREE.Color("#87ceeb");                // sky blue
const camera = new THREE.PerspectiveCamera(60, 800/500, 0.1, 100);
camera.position.set(14, 12, 14);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(800, 500);
document.body.appendChild(renderer.domElement);
const sun = new THREE.DirectionalLight("#ffffff", 2);         // sunshine ☀️
sun.position.set(5, 10, 7);
scene.add(sun);
scene.add(new THREE.AmbientLight("#ffffff", 0.6));            // soft sky glow
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;                                // smooth camera feel

// ===== FLY KEYS — WASD / arrows to move, Q = down, E = up =====
const nav = {};
addEventListener("keydown", e => nav[e.key.toLowerCase()] = true);
addEventListener("keyup",   e => nav[e.key.toLowerCase()] = false);
function flyCam() {
  const fwd = new THREE.Vector3();
  camera.getWorldDirection(fwd);               // which way is the camera facing?
  fwd.y = 0; fwd.normalize();                  // fly level, not into the ground
  const right = new THREE.Vector3().crossVectors(fwd, new THREE.Vector3(0, 1, 0));
  const move = new THREE.Vector3();
  if (nav.w || nav.arrowup)    move.add(fwd);
  if (nav.s || nav.arrowdown)  move.sub(fwd);
  if (nav.d || nav.arrowright) move.add(right);
  if (nav.a || nav.arrowleft)  move.sub(right);
  if (nav.e) move.y += 1;
  if (nav.q) move.y -= 1;
  if (move.lengthSq() === 0) return;
  move.normalize().multiplyScalar(0.15);
  camera.position.add(move);                   // camera and its orbit target move together,
  controls.target.add(move);                   // so drag-to-spin always circles where YOU are
}

// ===== BLOCK TYPES — your hotbar. (You'll swap these colors for YOUR IMAGES soon!) =====
const TYPES = {
  "1": { name: "grass", color: "#6abe30" },
  "2": { name: "dirt",  color: "#8b5a2b" },
  "3": { name: "stone", color: "#9099a1" },
  "4": { name: "wood",  color: "#a0742f" },
};
const materials = {};                          // one shared paint-pot per type
for (const [key, t] of Object.entries(TYPES))
  materials[key] = new THREE.MeshLambertMaterial({ color: t.color });
let currentType = "1";

// ===== BLOCKS — one cube shape shared by every block in the world =====
const geometry = new THREE.BoxGeometry(1, 1, 1);
const blocks = [];
function addBlock(x, y, z, type) {
  const block = new THREE.Mesh(geometry, materials[type]);
  block.position.set(x, y, z);
  block.userData.type = type;                  // remember what KIND it is (for saving)
  scene.add(block);
  blocks.push(block);
}

// ===== TERRAIN — sin/cos are wave machines: instant rolling hills =====
function makeHills() {
  for (let x = -8; x < 8; x++)
    for (let z = -8; z < 8; z++) {
      const height = Math.round(Math.sin(x / 4) * 2 + Math.cos(z / 4) * 2);
      for (let y = -2; y <= height; y++)
        addBlock(x, y, z, y === height ? "1" : "2");   // grass on top, dirt below
    }
}

// ===== SAVE / LOAD — your world is just a list of notes (tutorial 1 trick!) =====
function saveWorld() {
  const data = blocks.map(b => ({ x: b.position.x, y: b.position.y,
                                  z: b.position.z, t: b.userData.type }));
  localStorage.setItem("myWorld", JSON.stringify(data));
}
function loadWorld() {
  const data = JSON.parse(localStorage.getItem("myWorld"));
  if (!data) return false;
  for (const b of data) addBlock(b.x, b.y, b.z, b.t);
  return true;
}
if (!loadWorld()) makeHills();                 // saved world, or fresh hills

onkeydown = e => {
  if (TYPES[e.key]) currentType = e.key;       // 1,2,3,4 picks your block
  if (e.key === "s") saveWorld();              // S saves anytime
};

// ===== THE LASER — click to break, Shift+click to build =====
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let downX = 0, downY = 0;
renderer.domElement.addEventListener("pointerdown", e => { downX = e.clientX; downY = e.clientY; });
renderer.domElement.addEventListener("click", e => {
  if (Math.hypot(e.clientX - downX, e.clientY - downY) > 9) return;  // camera drag ≠ click!
  // the laser needs "screen math": center = (0,0), edges = -1 to +1
  const box = renderer.domElement.getBoundingClientRect();
  mouse.x =  ((e.clientX - box.left) / box.width)  * 2 - 1;
  mouse.y = -((e.clientY - box.top)  / box.height) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);      // aim from the camera through the mouse
  const hits = raycaster.intersectObjects(blocks);
  if (hits.length === 0) return;
  const hit = hits[0];                         // the FIRST block the laser touched
  if (e.shiftKey) {                            // SHIFT+click = build 🏗️
    const spot = hit.object.position.clone().add(hit.face.normal);
    addBlock(spot.x, spot.y, spot.z, currentType);
  } else {                                     // plain click = break ⛏️
    scene.remove(hit.object);
    blocks.splice(blocks.indexOf(hit.object), 1);
  }
});

// ===== THE HEARTBEAT — same game loop as tutorial 1 =====
function animate() {
  flyCam();                                    // fly keys
  controls.update();                           // required for the smooth camera
  renderer.render(scene, camera);              // take a photo of the world
  requestAnimationFrame(animate);              // "browser, run me again next frame!"
}
animate();
</script>
</body>
</html>

在游戏文件夹里启动服务器,打开 http://localhost:8000/mine.html —— 你就站在自己的 3D 世界里了。

Start your server in the game folder, open http://localhost:8000/mine.html — and you're standing in your own 3D world.

🕹️ 现在就玩 —— 这个游戏本尊,一行不多一行不少🕹️ PLAY IT RIGHT HERE — this exact game, nothing extra

拖动旋转 · 滚轮缩放 · 点方块挖掉 · Shift+点击放方块 · 先点一下世界,再用 1–4 选方块、WASD 飞行(Q 降 E 升)。这就是上面那份代码在跑 —— 简单,但已经是真正的 Minecraft 了。页面最底下还有一个吃下全部升级的超级版等着你!Drag to spin · scroll to zoom · click a block to break it · Shift+click to place · click the world once, then keys 1–4 pick your block and WASD flies (Q down, E up). This is exactly the code above, running — simple, but already a real Minecraft. A supercharged version with every upgrade waits at the bottom of the page!

⚠️ 白屏?3D 三大经典翻车

  • 双击打开的?3D 模块需要服务器。地址栏必须是 localhost:8000,不能是 file://。
  • 按 F12 了吗?控制台会指出出错的那一行 —— 重新复制之前先读一读。
  • 没联网?importmap 那几行要从网上借 three.js;断网 = 没有 3D 库。

⚠️ Blank page? The three classic 3D fails

  • Opened with double-click? 3D modules need the server. Address bar must say localhost:8000, not file://.
  • Pressed F12? The console names the exact line that broke — read it before re-copying.
  • No internet? The importmap lines borrow three.js from the web; offline = no 3D library.

2 🕹️ 怎么玩你的游戏

2 🕹️ How to play your game

  • 拖动 —— 让镜头绕着世界转(这就是 OrbitControls)
  • 滚轮 —— 拉近拉远
  • WASD / 方向键 —— 在世界里飞来飞去(Q 下降,E 上升)
  • 点一个方块 —— ⛏️ 挖掉它
  • Shift + 点击 —— 🏗️ 在你点的那个面上放一个新方块
  • 1 / 2 / 3 / 4 —— 选择草 / 土 / 石 / 木
  • S —— 💾 保存世界(刷新后还在!)
  • Drag — spin the camera around your world (that's OrbitControls)
  • Scroll — zoom in and out
  • WASD / arrow keys — fly around the world (Q down, E up)
  • Click a block — ⛏️ break it
  • Shift + click — 🏗️ place a block on the face you clicked
  • 1 / 2 / 3 / 4 — choose grass / dirt / stone / wood
  • S — 💾 save your world (it comes back after refresh!)

去挖一个洞穴。盖一座通天塔。用石头拼出你的名字。 给所有人炫耀完再回来 —— 接下来你会弄懂每一行代码,然后换上你自己的美术。

Go dig a cave. Build a tower to the sky. Spell your name in stone. When you're done showing everyone, come back — because next you'll learn what every line does, and then replace the art with your own.

3 🎬 引擎盖下:3D 摄影棚

3 🎬 Under the hood: the 3D movie set

每个 3D 游戏都是一个摄影棚,里面有三个角色 —— 在你文件的开头找到它们:

Every 3D game is a movie set with three jobs — find them at the top of your file:

  • Scene(场景)—— 世界。每个方块都加进它。
  • Camera(摄像机)—— 眼睛。它看到什么,玩家就看到什么。
  • Renderer(渲染器)—— 放映机。每秒 60 次,renderer.render(scene, camera) 给世界拍一张照片并放映出来。
  • Scene — the world. Every block gets added to it.
  • Camera — the eyes. What it sees is what the player sees.
  • Renderer — the projector. Sixty times a second, renderer.render(scene, camera) takes a photo of the world and shows it.

还有灯光:DirectionalLight 是太阳,AmbientLight 是天空的柔光。 方块顶面亮、侧面暗就是它们的功劳 —— 这正是 3D 看起来是 3D 的原因。

Plus the lights: a DirectionalLight is the sun, AmbientLight is the soft glow of the sky. They're why your blocks have bright tops and shadowy sides — that's what makes 3D look 3D.

🕹️ 最小的场景 —— 一个方块、一个摄像机、一盏灯🕹️ The smallest possible scene — one block, one camera, one light

方块 = 形状 + 颜料:new THREE.Mesh(geometry, material)。它会转,是因为循环每帧给旋转加 0.01。

A block is just a shape + paint: new THREE.Mesh(geometry, material). This one spins because the loop adds 0.01 to its rotation every frame.

⚠️ 动手改之前,先认识 3D 三大经典 bug

  • 黑屏:renderer.render(...) 不在 animate() 里,或者你从没调用 animate()。
  • 方块全黑:灯忘了加。没有太阳 = 剪影。
  • 方块被拉伸:摄像机的 800/500 必须等于渲染器尺寸 —— 宽 ÷ 高。

⚠️ The three classic 3D bugs (worth knowing before you edit)

  • Black screen: renderer.render(...) isn't inside animate(), or you never called animate().
  • All blocks black: the lights are missing. No sun = silhouettes.
  • Stretched blocks: the camera's 800/500 must match the renderer size — width ÷ height.

4 🧱 引擎盖下:方块和世界

4 🧱 Under the hood: blocks & the world

在文件里找到 addBlock。每个方块都是同一个立方体形状 (geometry 只做一次、所有方块共用),穿着四罐共享"颜料" (materials)之一。方块把自己的类型记在 userData.type 里 —— 存档就是靠它知道该写下什么。

Find addBlock in your file. Every block is the SAME cube shape (geometry is made once and shared) wearing one of four shared "paint-pots" (materials). The block remembers its own type in userData.type — that's how saving knows what to write down.

世界诞生在 makeHills() 里:两个循环像棋盘的行和列一样 走过 16×16 的网格,在每个位置上往上堆方块。

The world is born in makeHills(): two loops walk across a 16×16 grid like rows and columns of a checkerboard, and stack blocks at each spot.

💡 为什么是 16×16 而不是 1000×1000?

每个方块都是电脑每帧都要画的独立物体。256 列 = 顺滑。几万个 = 幻灯片。 真正的 Minecraft 用"分块(chunking)"和"贪婪合并(greedy meshing)"这些技巧 把方块拼在一起 —— 以后非常值得问问你的 AI!

💡 Why 16×16 and not 1000×1000?

Every block is a separate object the computer draws, every frame. 256 columns = smooth. Tens of thousands = slideshow. Real Minecraft merges blocks together with tricks called "chunking" and "greedy meshing" — a great thing to ask your AI about later!

🧠 大想法

教程 1 里,角色是网格里的字母 —— 图形就是数据。这里,整个 3D 世界 只是一张 {x, y, z, type} 清单。同一个想法,多了一个维度。

🧠 Big idea

In tutorial 1, a sprite was letters in a grid — graphics were data. Here a whole 3D world is just a list of {x, y, z, type}. Same idea, one dimension bigger.

5 ⛏️ 引擎盖下:激光(挖与建)

5 ⛏️ Under the hood: the laser (break & build)

游戏怎么知道你在 3D 里点的是哪个方块?它从摄像机出发、穿过你的鼠标指针, 向世界射出一道隐形的激光(Raycaster),并报告激光碰到的每个方块。 hits[0] 就是第一个 —— 你正看着的那个。

How does the game know which block you clicked — in 3D? It shoots an invisible laser (a Raycaster) from the camera, through your mouse pointer, into the world, and reports every block it touches. hits[0] is the first one — the block you're looking at.

这个处理函数里有两处聪明的地方,值得搞懂:

Two clever bits in that handler worth understanding:

  • 奇怪的鼠标数学。激光不用像素 —— 它要"屏幕数学":中心是 (0,0),边缘是 -1 到 +1。 mouse.x / mouse.y 那两行就是换算公式。抄一次,用一辈子。
  • 用 face.normal 建造。激光还会报告你打中的是哪个面,每个面都有一条 法线(normal):一支从面上垂直伸出去的小箭头。新方块 = 被点方块的位置 + 这支箭头。 点顶面 → 箭头朝上 → 方块落在顶上。点侧面 → 方块从侧面长出来。 一行向量数学 = 原汁原味的 Minecraft 建造手感。
  • The weird mouse math. The laser doesn't use pixels — it wants "screen math" where the center is (0,0) and the edges are -1 to +1. The two mouse.x / mouse.y lines convert. Copy them once, use them forever.
  • Building with face.normal. The laser also reports which FACE you hit, and every face has a normal: a little arrow pointing straight out of it. New block = clicked block's position + that arrow. Click a top → arrow points up → block lands on top. Click a side → block sticks out the side. One line of vector math = the exact building feel of Minecraft.

⚠️ 点击总打到错误的方块?

鼠标数学必须像你文件里那样用 getBoundingClientRect()。网上那些除以 window.innerWidth 的版本只在画布全屏时正确 —— rect 版本在哪里都对。

⚠️ Clicks hitting the wrong block?

The mouse math must use getBoundingClientRect() like your file does. Internet versions that divide by window.innerWidth only work full-screen — the rect version works everywhere.

6 🌄 引擎盖下:山丘与存档

6 🌄 Under the hood: hills & saving

山丘

The hills

Math.sin() 和 Math.cos() 是波浪机器: 喂给它们 x 和 z,吐回平滑起伏的高度。两个波相加,看起来意外地像真实地形 —— 不需要任何花哨的库。这就是程序化生成(procedural generation): 真正的 Minecraft 世界也是这个思想长大后造出来的。

Math.sin() and Math.cos() are wave machines: feed them x and z, get back smooth rolling heights. Two waves added together look surprisingly like real landscape — no fancy libraries needed. This is procedural generation: the same grown-up idea that builds real Minecraft worlds.

存档系统

The save system

直接来自教程 1:saveWorld() 把每个方块变成一张小纸条 {x, y, z, t},JSON.stringify + localStorage 把纸条永远收好。游戏启动时先问:有存档吗?有就读。没有?造新山丘。 这正是大游戏"继续游戏 / 新游戏"的套路。

Straight from tutorial 1: saveWorld() turns every block into a tiny note {x, y, z, t}, and JSON.stringify + localStorage keep the notes forever. At startup the game asks: is there a save? Load it. No save? Fresh hills. That's the same "CONTINUE or NEW GAME" pattern big games use.

7 🖼️ 变成你的 —— 换上你自己的方块图片

7 🖼️ Make it YOURS — your own block images

纯色只是热身。真正的 Minecraft 方块穿的是贴图(texture) —— 一张包在立方体上的小图片。该把你的美术放进游戏了。

Plain colors were just the warm-up. A real Minecraft block wears a texture — a small picture wrapped around the cube. Time to put YOUR art in the game.

第 A 步 —— 准备四张方块图片

Step A — get four block pictures

在 mine.html 旁边建一个 imgs/ 文件夹, 放四张正方形图片,名字必须是:grass.png、dirt.png、 stone.png、wood.png。图片哪里来?哪里都行!

Make an imgs/ folder next to mine.html and put four square pictures in it, named exactly: grass.png, dirt.png, stone.png, wood.png. Where do pictures come from? Anywhere!

  • 让 AI 画 —— 魔法提示词:「16×16 像素风 Minecraft 草方块贴图,无缝平铺,平面」
  • 自己画 —— 任何画图软件都行;哪怕是乱涂,包到立方体上都好看
  • 用照片!你家猫的脸。你自己的脸。你的乐高。一块饼干。🍪 正方形照片直接能用。
  • Ask an AI image maker — the magic words: "16×16 pixel art Minecraft-style grass block texture, seamless, flat"
  • Draw your own in any paint app — even scribbles look amazing wrapped on a cube
  • Use photos! Your cat's face. Your own face. Your LEGO bricks. A cookie. 🍪 Square photos work instantly.

第 B 步 —— 教游戏穿上它们(一处小改动)

Step B — teach the game to wear them (one small edit)

在文件里找到制作 materials(颜料罐)的那几行,换成这个:

In your file, find where materials are made (the paint-pots) and replace those lines with this:

const loader = new THREE.TextureLoader();      // 取图器
function blockTexture(file) {
  const t = loader.load("imgs/" + file);
  t.magFilter = THREE.NearestFilter;           // 让像素保持锐利、不发糊 ——
  t.colorSpace = THREE.SRGBColorSpace;         //   这才是 Minecraft 的味道!
  return t;
}

const materials = {};
for (const [key, t] of Object.entries(TYPES))
  materials[key] = new THREE.MeshLambertMaterial({
    map: blockTexture(t.name + ".png")        // "grass" → imgs/grass.png,以此类推
  });
const loader = new THREE.TextureLoader();      // the picture-fetcher
function blockTexture(file) {
  const t = loader.load("imgs/" + file);
  t.magFilter = THREE.NearestFilter;           // keep pixels CRISP, not blurry —
  t.colorSpace = THREE.SRGBColorSpace;         //   this is the Minecraft look!
  return t;
}

const materials = {};
for (const [key, t] of Object.entries(TYPES))
  materials[key] = new THREE.MeshLambertMaterial({
    map: blockTexture(t.name + ".png")        // "grass" → imgs/grass.png, etc.
  });

刷新。你的世界穿上了你的美术。🤯 最妙的是:因为存档存的是方块类型 (不是颜色),旧存档完全能读 —— 现在还自动换上了新贴图。

Refresh. Your world is now wearing your art. 🤯 And here's the beautiful part: because saving stores block types (not colors), your saved worlds still load perfectly — now dressed in the new textures.

💡 贴图不显示?

① 名字必须完全一致 —— grass.png,不是 Grass.PNG。 ② 必须在 mine.html 旁边的 imgs/ 文件夹里。 ③ 必须在服务器上(localhost:8000)—— file:// 加载不了贴图。 ④ 按 F12,控制台会说出找不到哪个文件。

💡 Texture not showing?

① Name must match EXACTLY — grass.png, not Grass.PNG. ② It must be in the imgs/ folder next to mine.html. ③ You must be on the server (localhost:8000) — textures won't load from file://. ④ F12 console says exactly which file it couldn't find.

🧠 大想法

你刚刚学会了游戏引擎(代码 —— 一行没动)和素材(美术 —— 现在全是你的) 的区别。不碰引擎、只换素材,正是游戏公司的真实工作方式。10 分钟给整个游戏换一身皮: 糖果世界、岩浆世界、猫猫世界……

🧠 Big idea

You just learned the difference between a game's engine (the code — didn't change) and its assets (the art — all yours now). Swapping assets without touching the engine is exactly how game studios work. Re-skin your whole game in 10 minutes: candy world, lava world, cat world...

第 C 步 —— 炫耀

Step C — show it off

用你自己的方块盖点什么,按 S 保存,把所有人叫过来。 "游戏是我做的,美术也是我做的" —— 能说出这句话的人可不多。😎

Build something with your own blocks, press S to save, and call everyone over. "I made the game AND the art" is a sentence very few people can say. 😎

8 🔧 调整世界 —— 简单小实验

8 🔧 Tweak the world — easy experiments

一行改动,巨大变化。每个都试试,刷新,看看会发生什么:

One-line changes, big effects. Try each, refresh, see what happens:

  • 更尖的山:在 makeHills 里把 / 4 改成 / 2
  • 更高的峰:把 * 2 改成 * 4
  • 更大的世界:把 -8 和 8 改成 -12 和 12(感受一下变慢了多少 —— 现在你知道为什么要"分块"了!)
  • 第五种方块:往 TYPES 里加 "5": { name: "gold", color: "#ffd700" } —— 快捷键自动生效。(用贴图的话:往 imgs/ 丢一张 gold.png。)
  • 日落世界:改天空颜色和太阳颜色 —— 气氛瞬间不同
  • Spikier mountains: in makeHills, change / 4 to / 2
  • Taller peaks: change * 2 to * 4
  • Bigger world: change the -8 and 8 to -12 and 12 (watch how much slower it gets — now you know why chunking exists!)
  • A fifth block: add "5": { name: "gold", color: "#ffd700" } to TYPES — the hotbar key works automatically. (With textures: drop a gold.png in imgs/.)
  • Sunset world: change the sky color and the sun's color — instant mood

9 🤖 和 AI 一起把它养大

9 🤖 Grow it with your AI

引擎是你的,美术是你的 —— 现在加功能。教程 1 的黄金法则依然管用: 一次一个功能、贴上整个文件、说清看到的 vs 想要的、让它解释、保存版本 (mine_v2.html!)。可以交给 AI 的任务:

The engine is yours, the art is yours — now add features. Golden rules from tutorial 1 still rule: one feature at a time, paste your whole file, say what you saw vs. what you wanted, ask it to explain, save versions (mine_v2.html!). Quests to hand your AI:

🚶 第一人称模式(大招)

"这是我的 three.js 方块游戏【贴上 mine.html】。把 OrbitControls 换成第一人称控制:点击锁定鼠标、移动鼠标环视、WASD 行走、空格跳跃,加上简单重力让我能站在方块上。解释你改了什么。"

🚶 First-person mode (the big one)

"Here is my three.js voxel game [paste mine.html]. Replace OrbitControls with first-person controls: click to lock the mouse, look by moving it, walk with WASD, jump with space, simple gravity so I stand on blocks. Explain what you changed."

🌳 树

"加一个 makeTree(x, z) 函数:3 个木头方块做树干,3×3×2 的树叶做树冠,然后在生成新山丘时随机种 5 棵树。"

🌳 Trees

"Add a makeTree(x, z) function that builds a trunk of 3 wood blocks and a 3×3×2 leaf top, then sprinkle 5 trees at random spots when making new hills."

🌊 水

"让高度 0 以下的每个空位变成半透明的蓝色水方块(透明材质,opacity 0.6),并且激光不能挖水。"

🌊 Water

"Make every empty spot below height 0 a see-through blue water block (transparent material, opacity 0.6), and don't let the laser break water."

🌗 昼夜循环

"加一个昼夜循环:60 秒内让天空颜色渐变、太阳变暗,然后白天再回来。夜里加一盏柔和的月光。"

🌗 Day and night

"Add a day/night cycle: over 60 seconds, fade the sky color and dim the sun, then bring the day back. Add a soft moon light at night."

🧨 TNT

"加一种 TNT 方块(红色,按键 6)。点击 TNT 方块时,不要直接删除,而是让它闪烁 1 秒,然后把 3 格以内的所有方块一起炸掉。"

🧨 TNT

"Add a TNT block type (red, key 6). When I click a TNT block, instead of just removing it, make it blink for 1 second and then remove every block within 3 blocks of it."

👾 怪物、红心和激光枪(像本页演示那样)

"这是我的 mine.html【贴上】。加上:每隔几秒在随机方块顶上出现一只紫色小怪物(带红眼睛),慢慢飘向摄像机;屏幕左上角显示 3 颗红心,怪物碰到我就扣 1 颗,扣完显示游戏结束并在 3 秒后复活;按空格从屏幕中心的十字准星发射激光,射中的怪物缩小消失。解释你加的每一部分。"

👾 Monsters, hearts & a blaster (like this page's demo)

"Here is my mine.html [paste]. Add: a small purple monster with red eyes that appears on top of a random block every few seconds and slowly floats toward the camera; 3 hearts in the top-left corner — touching a monster costs 1, at 0 show game over and respawn the hearts after 3 seconds; pressing SPACE fires a laser from the center crosshair, and a hit monster shrinks away. Explain each part you add."

👫 多人模式(和教程 1 合体!)

"这是双人抓人游戏的 server.py 和我的 mine.html【都贴上】。让放置和挖掉的方块通过服务器在玩家之间同步,这样我和朋友就能在同一个世界里一起盖房子。"

👫 Multiplayer (combine with tutorial 1!)

"Here is my server.py from the 2-player tag game and my mine.html [paste both]. Make placed and broken blocks sync between players through the server, so my friend and I build in the same world."

10 🏰 AI 建筑师 —— 让 AI 在你的世界里盖楼

10 🏰 AI Architect — let your AI build in your world

最后一招,也是最神奇的一招。你的 AI 看不见你的世界 —— 但它会写数据。 还记得吗?你的世界就是一张 {x, y, z, t} 清单。所以:让 AI 把一座城堡设计成 JSON, 粘贴进来,看它在你的世界里长出来。哪怕是最普通的小笔记本电脑也能玩 —— 想象力在 AI 那边,施工在浏览器这边。

The final trick — and the most magical one. Your AI can't see your world, but it can write data. Remember? Your world is just a list of {x, y, z, t}. So: ask your AI to design a castle as JSON, paste it in, and watch it grow in your world. Works on the smallest laptop — the imagination runs on the AI's side, the building runs in your browser.

第 1 步 —— 把这个提示词交给你的 AI

Step 1 — hand this prompt to your AI

为我的方块游戏设计一座小城堡。只回复一个 JSON 数组,不要任何其他文字,
也不要代码围栏。每一项长这样:{"x":0,"y":6,"z":0,"t":"3"}
t 的意思是:"1"=草方块 "2"=泥土 "3"=石头 "4"=木头
x 和 z 在 -8 到 8 之间;y 在 5 到 14 之间(y 是向上,地面山顶大约在 y=4)。
用 100 到 300 个方块。要有城墙、四个角塔和一扇木头大门。
在建筑下方加石头地基方块,一直加到 y=2,免得它悬在半空。
Design a small castle for my voxel game. Reply with ONLY a JSON array —
no other text, no code fences. Each item looks like: {"x":0,"y":6,"z":0,"t":"3"}
t means: "1"=grass "2"=dirt "3"=stone "4"=wood
Keep x and z between -8 and 8, and y between 5 and 14 (y points UP;
the hilltops end around y=4). Use 100 to 300 blocks.
Include walls, four corner towers, and a wooden gate.
Add stone foundation blocks under the building down to y=2 so it doesn't float.

第 2 步 —— 把 AI 的回答粘贴进超级版游戏

Step 2 — paste your AI's answer into the supercharged game

页面最底下的🚀 超级版游戏里有一个 AI 建筑师粘贴框 —— 把 AI 给你的 JSON 贴进去,按"开工",看着它在世界里一块一块长出来。

The 🚀 supercharged game at the bottom of the page has an AI Architect paste box — drop your AI's JSON in, press "Build it!", and watch it grow block by block.

💡 AI 给的 JSON 读不懂?

最常见的原因:AI 在 JSON 周围加了解释文字或 ``` 代码围栏。 回复它:"只要 JSON 数组本身,不要任何其他文字。"(我们也会自动帮你剥掉代码围栏。) 建出来的东西很怪?把你想要的样子描述得更具体,再试一次 —— 这就是提示词练习!

💡 AI's JSON won't parse?

The usual cause: the AI wrapped the JSON in explanation text or ``` code fences. Tell it: "ONLY the JSON array itself, no other text." (We also strip code fences for you automatically.) Building looks weird? Describe what you want more precisely and try again — that's prompt practice!

第 3 步 —— 把 AI 建筑师装进你自己的 mine.html

Step 3 — put the AI Architect in YOUR mine.html

在你自己的游戏里,把原来的 onkeydown 换成这个 —— 以后按 B 就能粘贴 AI 的设计:

In your own game, replace your old onkeydown with this — then press B anytime to paste an AI design:

// 🏰 AI 建筑师:把 AI 的 JSON 清单变成真建筑
function buildFromJSON(text) {
  const design = JSON.parse(text.replace(/```json|```/gi, ""));  // 剥掉 AI 的代码围栏
  let i = 0;
  for (const b of design)
    if (TYPES[b.t])                              // 只接受认识的方块类型
      setTimeout(() => addBlock(b.x, b.y, b.z, b.t), i++ * 10);  // 一块一块长出来!
}

onkeydown = e => {
  if (TYPES[e.key]) currentType = e.key;
  if (e.key === "s") saveWorld();
  if (e.key === "b") {                           // B = 召唤 AI 建筑师!
    const text = prompt("把 AI 的 JSON 方块清单粘贴到这里:");
    if (text) buildFromJSON(text);
  }
};
// 🏰 AI Architect: turn your AI's JSON list into a real building
function buildFromJSON(text) {
  const design = JSON.parse(text.replace(/```json|```/gi, ""));  // strip AI code fences
  let i = 0;
  for (const b of design)
    if (TYPES[b.t])                              // only block types we know
      setTimeout(() => addBlock(b.x, b.y, b.z, b.t), i++ * 10);  // grows block by block!
}

onkeydown = e => {
  if (TYPES[e.key]) currentType = e.key;
  if (e.key === "s") saveWorld();
  if (e.key === "b") {                           // B = summon the AI Architect!
    const text = prompt("Paste your AI's JSON blocks here:");
    if (text) buildFromJSON(text);
  }
};

第 4 步 —— 让城堡开局自动出现

Step 4 — make your castle load automatically

特别喜欢某个设计?把它变成世界的一部分,每个新世界都自带它。 把 AI 的 JSON 直接粘贴进一个常量(JSON 本身就是合法的 JavaScript!), 然后让"造新山丘"的那一行顺便盖城堡:

Love one of the designs? Make it part of your world, so every new world is born with it. Paste the AI's JSON straight into a constant (JSON is already valid JavaScript!), then teach the "fresh hills" line to build it too:

// 把 AI 的整个 JSON 数组粘贴到方括号的位置:
const MY_CASTLE = [{"x":0,"y":6,"z":0,"t":"3"}, /* …你的整个设计… */];

// 找到这一行:  if (!loadWorld()) makeHills();   把它换成:
if (!loadWorld()) {                              // 全新世界?
  makeHills();                                   // 先造山丘…
  buildFromJSON(JSON.stringify(MY_CASTLE));      // …再自动盖上你的城堡!
}
// paste your AI's whole JSON array between the brackets:
const MY_CASTLE = [{"x":0,"y":6,"z":0,"t":"3"}, /* …your whole design… */];

// find this line:  if (!loadWorld()) makeHills();   and replace it with:
if (!loadWorld()) {                              // brand-new world?
  makeHills();                                   // hills first…
  buildFromJSON(JSON.stringify(MY_CASTLE));      // …then your castle, automatically!
}

💡 为什么放在 if 里面?

这样只有全新的世界才会自带城堡 —— 你保存过的世界原样读回来, 不会每次刷新都多盖一座。(需要第 3 步的 buildFromJSON 函数已经在你的文件里。)

💡 Why inside the if?

So only brand-new worlds spawn with the castle — your saved worlds load back exactly as you left them, without an extra castle stacking up on every refresh. (Needs the buildFromJSON function from Step 3 already in your file.)

🧠 大想法

你刚刚搭好了一条人类 → AI → 程序的流水线:你用文字描述想要的东西, AI 把它翻译成数据,你的程序把数据变成世界。换掉提示词,同一条流水线能造 宇宙飞船、迷宫、巨型雕像…… 这正是大人们用 AI 工作的方式。

🧠 Big idea

You just built a human → AI → program pipeline: you describe what you want in words, the AI translates it into data, and your program turns the data into a world. Swap the prompt and the same pipeline builds spaceships, mazes, giant statues... This is exactly how grown-ups work with AI.

11 🚀 超级版 —— 把所有升级合体

11 🚀 The SUPERCHARGED game — every upgrade combined

还记得第 1 节那个朴素的小游戏吗?下面是同一个游戏吃下这一页所有升级后的样子 —— 每个功能都是你刚刚读过的一小步:

Remember the plain little game in section 1? Here's that same game after eating every upgrade on this page — each feature is one small step you just read about:

  • 🟩 一层平地开局 + 大小滑块(最大 100×100 —— 拉到底感受为什么需要"分块"!)
  • 🌄 一键生成山丘 + 豪华城堡(程序化生成 + AI 建筑师的设计)
  • 🎨 七种方块、🛩️ WASD 飞行、⛶ 全屏模式
  • 👾 怪物会来抓你 —— ❤️❤️❤️ 三条命,十字准星 + 空格激光枪
  • 🏰 AI 建筑师粘贴框 —— 第 10 步的提示词在这里开工
  • 🟩 Flat-layer start + a size slider (up to 100×100 — max it out and FEEL why chunking exists!)
  • 🌄 One-click hills + the fancy castle (procedural generation + AI Architect designs)
  • 🎨 Seven block types, 🛩️ WASD flight, ⛶ full-screen mode
  • 👾 Monsters come for you — ❤️❤️❤️ three lives, crosshair + SPACE laser blaster
  • 🏰 The AI Architect paste box — step 10's prompts build here

🕹️ 超级版 —— 全部功能,开玩!🕹️ THE SUPERCHARGED GAME — everything, live!

+

拖动旋转 · 滚轮缩放 · 点一下世界后用 WASD/方向键飞行(Q 降 E 升)· 点方块挖掉 · 🏗️ 建造模式放方块 · 👾 怪物会来抓你 —— 用十字准星对准它,按空格开枪! ❤️❤️❤️ 是你的命。存档刷新后还在!Drag to spin · scroll to zoom · click the world once, then fly with WASD/arrows (Q down, E up) · click to break · 🏗️ Build mode to place · 👾 monsters will come for you — aim with the crosshair and press SPACE to shoot! ❤️❤️❤️ are your lives. Save survives a refresh!

🏰 示例城堡可不简单:城垛、四座红顶旗帜角塔、高高的主楼、城门红旗, 还有踩着石柱的木吊桥 —— 用 WASD 飞过吊桥进城!(比你的 mine.html 多三种方块:红 "5"、白 "6"、蓝 "7" —— 第 8 节教你加新 TYPES!)

🏰 The sample castle means business: battlements, four red-roofed flag towers, a tall keep, gate banners, and a drawbridge on stilts — fly over it with WASD! (Three bonus blocks your mine.html doesn't have yet: red "5", white "6", blue "7" — section 8 teaches you to add new TYPES!)

🧠 大想法

从第 1 节的小游戏到这里,没有一次"重写" —— 只有一次次小升级:加一个函数、加一个按钮、 加一种方块。真正的游戏就是这样长大的。轮到你了:把你的 mine.html 一步一步喂成你自己的超级版!

🧠 Big idea

From section 1's little game to this, there was never a "rewrite" — just small upgrades, one at a time: add a function, add a button, add a block type. That's how real games grow. Your turn: feed your mine.html one upgrade at a time until it becomes YOUR supercharged version!

🏆 任务追踪器

🏆 Quest Tracker

每完成一个任务就打个勾。当然是用 localStorage 保存的。😎

Check off each quest as you beat it. Saved with localStorage, of course. 😎

进度会自动保存在这台设备上。

Saved on this device automatically.