🗺️ 你的任务地图
🗺️ Your quest map
- Own the game — copy it, run it, play it
- How to play your game
- Under the hood: the 3D movie set
- Under the hood: blocks & the world
- Under the hood: the laser (break & build)
- Under the hood: hills & saving
- ⭐ Make it YOURS — your own block images
- Tweak the world — easy experiments
- Grow it with your AI
- 🏰 AI Architect — paste JSON, grow a castle
- 🚀 The supercharged game — every upgrade combined
- Quest tracker
🎒 你需要准备什么
来自教程 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, notfile://. - 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 insideanimate(), or you never calledanimate(). - All blocks black: the lights are missing. No sun = silhouettes.
- Stretched blocks: the camera's
800/500must 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.ylines 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/ 4to/ 2 - Taller peaks: change
* 2to* 4 - Bigger world: change the
-8and8to-12and12(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 agold.pngin 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 行走、空格跳跃,加上简单重力让我能站在方块上。解释你改了什么。"
"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 棵树。"
"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),并且激光不能挖水。"
"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 秒内让天空颜色渐变、太阳变暗,然后白天再回来。夜里加一盏柔和的月光。"
"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 方块(红色,按键 6)。点击 TNT 方块时,不要直接删除,而是让它闪烁 1 秒,然后把 3 格以内的所有方块一起炸掉。"
"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 秒后复活;按空格从屏幕中心的十字准星发射激光,射中的怪物缩小消失。解释你加的每一部分。"
"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."
"这是双人抓人游戏的 server.py 和我的 mine.html【都贴上】。让放置和挖掉的方块通过服务器在玩家之间同步,这样我和朋友就能在同一个世界里一起盖房子。"
"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.