语言 / Language:

🎮 用 HTML + JavaScript + AI
做出真正的电子游戏

🎮 Make REAL Video Games
with HTML + JavaScript + AI

你已经会做游戏了。现在给它们加上超能力:自己的图形、音效、保存的最高分、 在你电脑上运行的真正网络服务器 —— 还有双人模式,让朋友通过 WiFi 加入你的游戏!

You already make games. Now let's give them superpowers: your own graphics, sound effects, saved high scores, a real web server on your computer — and a 2-player mode so a friend can join your game over WiFi!

🎨 图形🔊 音效💾 存档🌐 本地服务器👫 双人🤖 AI 帮手 🎨 Graphics🔊 Sounds💾 Saving🌐 Local server👫 2 players🤖 AI helper

🎬 先看电影版!

🎬 Watch the movie first!

一部 8 分钟的 AI 电影(中文旁白),讲的就是这个教程:提示词(Prompt)、HTML 和 JavaScript 是怎样配合起来做出游戏的。

A 7-minute AI-made movie of this tutorial: how prompts, HTML, and JavaScript work together to make real games.

1 📁 建好你的游戏文件夹

1 📁 Build your game folder

每个游戏制作人都有自己的工作室。你的工作室就是电脑上的一个文件夹。把它做成这样:

Every game maker has a workshop. Yours is one folder on your computer. Make it look like this:

my-game/
  game.html      ← 你的游戏(你已经在做的那个!)
  imgs/          ← 图片:hero.png、enemy.png、coin.png
  sounds/        ← 声音:jump.mp3、boom.mp3(可选!)
  server.py      ← 你的双人服务器(下面我们会一起做)
my-game/
  game.html      ← your game (the one you already make!)
  imgs/          ← pictures: hero.png, enemy.png, coin.png
  sounds/        ← sounds: jump.mp3, boom.mp3 (optional!)
  server.py      ← your 2-player server (we'll build it below)

💡 文件夹为什么重要

当你的游戏写 imgs/hero.png 时,浏览器会在你的 HTML 文件旁边找一个叫 imgs 的文件夹。图片不在那里,就会出现可怕的"碎图"图标。把所有东西都放进 一个游戏文件夹,就永远不会出错 —— 而且你可以把整个文件夹打包发给朋友。

💡 Why folders matter

When your game says imgs/hero.png, the browser looks for a folder called imgs next to your HTML file. If the picture isn't there, you get the dreaded broken-image icon. Keeping everything in one game folder means it always works — and you can zip the whole folder and send your game to a friend.

你的第一个 game.html —— 点一下就能下载!

Your first game.html — one click to download!

还没有 game.html?这就给你一个最小能跑的:一条龙站在画布上等你。 复制下面的代码 —— 或者直接点 ⬇️ 下载,把文件放进 my-game 文件夹, 再用第 2 步的服务器打开它。

No game.html yet? Here's the smallest one that works: a dragon standing on a canvas, waiting for you. Copy the code — or just press ⬇️ Download, drop the file into your my-game folder, and open it with the step-2 server.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">   <!-- 没有这行,emoji 和中文会变乱码! -->
  <title>我的第一个游戏</title>
</head>
<body>

<h1>🎮 我的第一个游戏</h1>
<canvas id="game" width="600" height="400"></canvas>

<script>
const stage = document.getElementById("game");   // 画布 —— 我们的舞台 🎭
const ctx = stage.getContext("2d");              // ctx = 魔法画笔 🖊️

ctx.fillStyle = "#0b1d3a";                       // 1. 涂上夜空(一个大矩形)
ctx.fillRect(0, 0, 600, 400);

ctx.fillStyle = "white";                         // 2. 撒 40 颗星星(小矩形)
for (let i = 0; i < 40; i++)
  ctx.fillRect((i * 97) % 600, (i * 53) % 260, 2, 2);

ctx.fillStyle = "#f5e9b8";                       // 3. 一轮满月(一个圆)
ctx.beginPath();
ctx.arc(500, 70, 30, 0, Math.PI * 2);
ctx.fill();

ctx.fillStyle = "#2e7d32";                       // 4. 绿草地(又一个矩形)
ctx.fillRect(0, 300, 600, 100);

ctx.font = "60px serif";                         // 5. 你的主角(emoji!)
ctx.fillText("🐉", 270, 295);

ctx.fillStyle = "white";                         // 6. 一行字(文本)
ctx.font = "20px sans-serif";
ctx.fillText("成功了!现在让你的 AI 教这条龙动起来吧!", 130, 360);
</script>

</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">   <!-- without this, emoji turn to gibberish! -->
  <title>My First Game</title>
</head>
<body>

<h1>🎮 My First Game</h1>
<canvas id="game" width="600" height="400"></canvas>

<script>
const stage = document.getElementById("game");   // the canvas — our stage 🎭
const ctx = stage.getContext("2d");              // ctx = the magic drawing pen 🖊️

ctx.fillStyle = "#0b1d3a";                       // 1. paint the night sky (a big rectangle)
ctx.fillRect(0, 0, 600, 400);

ctx.fillStyle = "white";                         // 2. sprinkle 40 stars (tiny rectangles)
for (let i = 0; i < 40; i++)
  ctx.fillRect((i * 97) % 600, (i * 53) % 260, 2, 2);

ctx.fillStyle = "#f5e9b8";                       // 3. a full moon (a circle)
ctx.beginPath();
ctx.arc(500, 70, 30, 0, Math.PI * 2);
ctx.fill();

ctx.fillStyle = "#2e7d32";                       // 4. grassy ground (another rectangle)
ctx.fillRect(0, 300, 600, 100);

ctx.font = "60px serif";                         // 5. your hero (an emoji!)
ctx.fillText("🐉", 270, 295);

ctx.fillStyle = "white";                         // 6. a message (text)
ctx.font = "20px sans-serif";
ctx.fillText("It works! Now ask your AI to make the dragon move!", 90, 360);
</script>

</body>
</html>

👀 它画出来长这样 —— 现场直播👀 This is what it draws — live

每一笔都来自上面的一行代码:大矩形 = 夜空和草地,小矩形 = 星星, 圆 = 月亮,emoji = 主角,文本 = 字。这就是画布画画的全部词汇 —— 拿这六招就能画出任何游戏画面!

Every stroke comes from one line above: big rectangles = sky and grass, tiny rectangles = stars, a circle = the moon, an emoji = the hero, text = the message. That's the whole drawing vocabulary — these six moves can paint any game scene!

2 🌐 在真正的网络服务器上运行游戏

2 🌐 Run your game on a REAL web server

现在你可能是双击 HTML 文件来打开它。这样也行,但地址栏显示的是 file://... —— 那是"文件模式",文件模式很弱: 有些游戏功能(比如加载数据、双人模式)根本不工作。

Right now you probably double-click your HTML file to open it. That works, but the address bar says file://... — that's "file mode", and file mode is weak: some game features (like loading data or 2-player) just refuse to work.

真正的游戏运行在网络服务器上。好消息:你的电脑用一条命令就能变成网络服务器。 只要装了 Python,在你的游戏文件夹里打开终端,输入:

Real games run on a web server. Good news: your computer can BE a web server with one command. If you have Python installed, open a terminal in your game folder and type:

python -m http.server 8000

现在打开浏览器,访问:

Now open your browser and go to:

http://localhost:8000/game.html

🎉 那是你的游戏,由你的服务器,在你的电脑上提供。 localhost 的意思是"这台电脑",8000 是门牌号(叫做端口)。

🎉 That's YOUR game, served by YOUR server, on YOUR computer. localhost means "this computer", and 8000 is the door number (called a port).

🛜 加分项:让朋友通过 WiFi 一起玩

🛜 Bonus: let a friend play over WiFi

连着同一个 WiFi 的人都能打开你的游戏!他们只需要你电脑的地址。这样查:

Anyone on the same WiFi can open your game too! They just need your computer's address. Find it like this:

# Windows —— 找 "IPv4 地址"
ipconfig

# Mac —— 通常是 en0
ipconfig getifaddr en0
# Windows — look for "IPv4 Address"
ipconfig

# Mac — usually en0
ipconfig getifaddr en0

你会看到类似 192.168.1.42 的地址。朋友在他们的设备上输入:

You'll see something like 192.168.1.42. Your friend types this on THEIR device:

http://192.168.1.42:8000/game.html

⚠️ 别关终端窗口

服务器只在终端窗口开着的时候运行。关掉窗口 = 服务器停止。 想主动停止它,点一下终端,按 Ctrl + C。

⚠️ Keep the terminal open

The server only runs while the terminal window is open. Close it = server off. To stop it on purpose, click the terminal and press Ctrl + C.

🧠 大想法

互联网就是许许多多台电脑在做你刚才做的事 —— 运行服务器程序,浏览器来要文件就发给它。 你现在知道这个秘密了。🤫

🧠 Big idea

The internet is just LOTS of computers doing exactly what you just did — running server programs and handing out files when browsers ask. You now know the secret. 🤫

3 🎨 图形:让主角登场的 3 种方法

3 🎨 Graphics: 3 ways to get a hero on screen

先搞清楚:这些代码写在哪里?📍

First: WHERE does this code go? 📍

这一节所有画图代码都运行在你游戏的 HTML 文件里的 <script> 标签中 —— 它画在 <canvas>(画布)上, 画布就是你游戏的屏幕。下面是每个画布游戏的起步骨架。 把它存成 game.html 放进你的游戏文件夹:

All the drawing code in this section runs inside your game's HTML file, in a <script> tag — and it draws onto a <canvas>, which is your game's screen. Here's the skeleton every canvas game starts from. Save it as game.html in your game folder:

<!DOCTYPE html>
<html>
<body>

<canvas id="game" width="600" height="400"></canvas>   <!-- 游戏屏幕 -->

<script>
const stage = document.getElementById("game");  // 抓住画布 —— 我们的舞台 🎭
const ctx = stage.getContext("2d");              // ctx = 你的魔法画笔 🖊️

// 👇 这个页面上所有用到 "ctx" 的代码都写在这里 👇

</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>

<canvas id="game" width="600" height="400"></canvas>   <!-- the game screen -->

<script>
const stage = document.getElementById("game");  // grab the canvas — our stage 🎭
const ctx = stage.getContext("2d");              // ctx = your magic drawing pen 🖊️

// 👇 EVERY code snippet on this page that uses "ctx" goes HERE 👇

</script>
</body>
</html>

为什么变量叫 stage(舞台)而不叫 canvas? 叫 canvas 也可以 —— 但那样变量和 <canvas> 标签就重名了,很快就会搞混。 变量想叫什么都行,所以挑让你头脑清楚的名字。我们的屏幕是主角表演的地方:舞台。🎭

Why call the variable stage and not canvas? You could — but then the variable and the <canvas> tag would have the same name, and it gets confusing fast. Variables can be named anything, so pick names that keep your head clear. Our screen is where the heroes perform: a stage. 🎭

⚠️ 新手第一大错误

如果控制台(F12)说 ctx is not defined,说明你贴画图代码时没带那两行 const —— 或者贴到了它们上面。顺序很重要:先造画笔,再用它画。 而且 <script> 必须写在 <canvas> 后面, 不然脚本找不到一个还不存在的屏幕。

⚠️ The #1 beginner error

If the console (F12) says ctx is not defined, you pasted drawing code without the two const lines — or above them. Order matters: make the pen first, then draw with it. And the <script> must come after the <canvas>, or the script can't find a screen that doesn't exist yet.

骨架搭好了?很好 —— 现在用 3 种方法让主角登上屏幕:

Got your skeleton? Great — now let's put a hero on that screen, 3 ways:

方法 1 —— Emoji(秒用,零文件)

Way 1 — Emoji (instant, zero files)

Emoji 每台电脑都自带,是超棒的即用角色:

Emoji are built into every computer. They make great instant sprites:

ctx.font = "40px serif";
ctx.fillText("🐉", dragonX, dragonY);   // 嘭,一条龙
ctx.font = "40px serif";
ctx.fillText("🐉", dragonX, dragonY);   // boom, a dragon

方法 2 —— 图片文件(让 AI 来画!)

Way 2 — Image files (use AI to make the art!)

让 AI 绘图工具给你画游戏素材。提示词里的魔法词: "像素风游戏角色,纯白背景"(pixel art game sprite, plain white background)。 把图片存成 imgs/hero.png,然后画进游戏:

Ask an AI image maker for game art. Magic words to use in your prompt: "pixel art game sprite, plain white background". Save the picture as imgs/hero.png, then draw it in your game:

// 在脚本开头,把图片加载一次
const heroImg = new Image();
heroImg.src = "imgs/hero.png";

// 在游戏循环里画它
ctx.drawImage(heroImg, heroX, heroY, 64, 64);   // 64×64 像素大
// load the picture once, at the top of your script
const heroImg = new Image();
heroImg.src = "imgs/hero.png";

// draw it inside your game loop
ctx.drawImage(heroImg, heroX, heroY, 64, 64);   // 64×64 pixels big

💡 图片不显示?

检查三件事:① 文件真的叫 hero.png(而不是 hero.png.png)吗? ② 它在 imgs 文件夹里吗?③ 第 2 步的网络服务器开着吗? 在浏览器按 F12 —— 控制台(Console)会告诉你它到底找不到哪个文件。

💡 Image not showing?

Check three things: ① is the file really named hero.png (not hero.png.png)? ② is it inside the imgs folder? ③ are you running the web server from Step 2? Press F12 in your browser — the Console tab tells you exactly which file it couldn't find.

方法 3 —— 纯代码做的像素画 🤯

Way 3 — Pixel art made of PURE CODE 🤯

告诉你一个游戏制作人的秘密:角色(精灵图)其实就是一格一格的彩色方块。 你可以用文字描述它 —— 每个字母代表一种颜色!试试在线演示:

Here's a game-maker secret: a sprite is just a grid of colored squares. You can describe one with text — each letter is a color! Try the live demo:

🕹️ 在线演示 —— 一只纯代码做的史莱姆,你可以移动它!🕹️ LIVE DEMO — a code-built slime you can move!

先点一下游戏区域,再用方向键 —— 或者点按钮。

Click the game area first, then use your arrow keys — or tap the buttons.

整个魔法就是这几行字符串:

And here's the whole trick — the slime is just these strings:

const SLIME = [
  "..GGGG..",
  ".GGGGGG.",
  "GGWGGWGG",    // W = 白色(眼睛!)
  "GGBGGBGG",    // B = 黑色(瞳孔)
  "GGGGGGGG",
  "G.GGGG.G",
];
const COLORS = { G: "#4ade80", W: "#ffffff", B: "#000000" };

function drawSprite(grid, x, y, size) {
  for (let row = 0; row < grid.length; row++) {
    for (let col = 0; col < grid[row].length; col++) {
      const letter = grid[row][col];
      if (letter === ".") continue;        // "." = 跳过 = 透明
      ctx.fillStyle = COLORS[letter];
      ctx.fillRect(x + col*size, y + row*size, size, size);
    }
  }
}
drawSprite(SLIME, 100, 100, 8);   // 每个字母变成 8×8 的方块
const SLIME = [
  "..GGGG..",
  ".GGGGGG.",
  "GGWGGWGG",    // W = white (the eyes!)
  "GGBGGBGG",    // B = black (the pupils)
  "GGGGGGGG",
  "G.GGGG.G",
];
const COLORS = { G: "#4ade80", W: "#ffffff", B: "#000000" };

function drawSprite(grid, x, y, size) {
  for (let row = 0; row < grid.length; row++) {
    for (let col = 0; col < grid[row].length; col++) {
      const letter = grid[row][col];
      if (letter === ".") continue;        // "." = skip = transparent
      ctx.fillStyle = COLORS[letter];
      ctx.fillRect(x + col*size, y + row*size, size, size);
    }
  }
}
drawSprite(SLIME, 100, 100, 8);   // each letter becomes an 8×8 square

🧠 大想法

改字母 → 换怪物。加个 R 代表红色,设计一只火焰史莱姆。 画一个 16 行的网格当 Boss。图形就是数据 —— 你甚至可以问 AI:"给我的 drawSprite 函数设计一条 12×12 的像素小龙, 用字母 G、R、W、B、Y 表示颜色。"

🧠 Big idea

Change the letters → change the creature. Add an R for red and design a fire slime. Make a 16-row grid for a boss. Graphics are just data — and you can even ask AI: "design a 12×12 pixel-art dragon for my drawSprite function, letters G, R, W, B, Y".

4 🔊 不用任何音频文件的音效

4 🔊 Sound effects with ZERO sound files

你的浏览器里有一台内置合成器!纯代码就能做出复古游戏音效 —— 不用下载,不用文件。来试试:

Your browser has a synthesizer built in! You can make retro game sounds from pure code — no downloads, no files. Try them:

🕹️ 在线演示 —— 按按钮!🕹️ LIVE DEMO — press the buttons!

经典街机音效,全是数学做出来的。把音量打开!

Classic arcade sounds, made from math. Turn your volume on!

这是跳跃音效 —— 直接抄进你的游戏:

Here's the jump sound — copy it straight into your game:

let audio;   // 浏览器的声音机器

function sfxJump() {
  if (!audio) audio = new AudioContext();
  const beep = audio.createOscillator();   // 一个音调发生器
  const vol  = audio.createGain();         // 一个音量旋钮
  beep.type = "square";                    // "square"(方波)= 复古 8-bit 声音
  beep.frequency.setValueAtTime(150, audio.currentTime);            // 从低音开始…
  beep.frequency.exponentialRampToValueAtTime(500, audio.currentTime + 0.15); // …往上滑!
  vol.gain.setValueAtTime(0.25, audio.currentTime);
  vol.gain.exponentialRampToValueAtTime(0.01, audio.currentTime + 0.18);     // 淡出
  beep.connect(vol); vol.connect(audio.destination);
  beep.start(); beep.stop(audio.currentTime + 0.2);
}
let audio;   // the browser's sound machine

function sfxJump() {
  if (!audio) audio = new AudioContext();
  const beep = audio.createOscillator();   // a tone generator
  const vol  = audio.createGain();         // a volume knob
  beep.type = "square";                    // "square" = retro 8-bit sound
  beep.frequency.setValueAtTime(150, audio.currentTime);            // start low...
  beep.frequency.exponentialRampToValueAtTime(500, audio.currentTime + 0.15); // ...slide up!
  vol.gain.setValueAtTime(0.25, audio.currentTime);
  vol.gain.exponentialRampToValueAtTime(0.01, audio.currentTime + 0.18);     // fade out
  beep.connect(vol); vol.connect(audio.destination);
  beep.start(); beep.stop(audio.currentTime + 0.2);
}

然后角色每次跳起来就调用 sfxJump()。音调往上滑听起来像跳跃; 往下滑听起来像下落或失败。这可是真正的游戏设计心理学!

Then call sfxJump() whenever your character jumps. A pitch that slides up feels like jumping; sliding down feels like falling or losing. That's real game-design psychology!

💡 想要真正的音乐或语音?

长一点的声音就放个 MP3 到 sounds/ 文件夹, 用 new Audio("sounds/boom.mp3").play() 播放。 网络世界的一条规矩:浏览器只在玩家点过或按过什么之后才允许出声 —— 所以用一个"▶ 开始游戏"按钮来启动你的音乐。

💡 Want real music or voices?

For longer sounds, put an MP3 in your sounds/ folder and play it with new Audio("sounds/boom.mp3").play(). One rule of the web: browsers only allow sound after the player clicks or presses something — so start your music from a "▶ START GAME" button.

5 💾 保存最高分 —— 关掉浏览器也不丢

5 💾 Save high scores — even after the browser closes

平时一刷新页面,什么都没了。但浏览器有个内置的小储物箱,叫 localStorage。放进去的东西会一直保存 —— 关掉浏览器、重启电脑,它还在。

Normally, refreshing the page erases everything. But browsers have a tiny built-in storage box called localStorage. Whatever you put in it stays saved — close the browser, restart the computer, it's still there.

🕹️ 在线演示 —— 史莱姆连点!5 秒内你能点多少下?🕹️ LIVE DEMO — Slime Clicker! How fast can you click in 5 seconds?

按"开始"!Press START!

你的最高分用 localStorage 保存 —— 刷新这个页面它还在。试试!

Your best score is saved with localStorage — refresh this page and it will STILL be there. Try it!

整个保存系统就三行:

The whole saving system is three lines:

// 保存 —— 像写在一张浏览器永远保留的便利贴上
localStorage.setItem("highScore", score);

// 读取 —— 看看便利贴(|| 0 的意思是"还没有便利贴就用 0")
const best = Number(localStorage.getItem("highScore")) || 0;

// 删除 —— 撕掉便利贴
localStorage.removeItem("highScore");
// SAVE — like writing on a sticky note the browser keeps forever
localStorage.setItem("highScore", score);

// LOAD — read the sticky note (|| 0 means "use 0 if there's no note yet")
const best = Number(localStorage.getItem("highScore")) || 0;

// ERASE — rip up the note
localStorage.removeItem("highScore");

保存整场冒险,而不只是一个数字

Save a WHOLE game, not just a number

localStorage 只能存文字 —— 但 JSON.stringify 能把任何对象变成文字,JSON.parse 再把它变回来。 所以你可以保存玩家的整场冒险:

localStorage only stores text — but JSON.stringify turns any object into text, and JSON.parse turns it back. So you can save your player's entire adventure:

const player = { name: "Knight", hp: 80, gold: 250, level: 7 };

// 保存整个玩家
localStorage.setItem("saveGame", JSON.stringify(player));

// 下次再读回来
const loaded = JSON.parse(localStorage.getItem("saveGame"));
console.log(loaded.gold);   // 250 —— 欢迎回来!
const player = { name: "Knight", hp: 80, gold: 250, level: 7 };

// save the whole player
localStorage.setItem("saveGame", JSON.stringify(player));

// load it back next time
const loaded = JSON.parse(localStorage.getItem("saveGame"));
console.log(loaded.gold);   // 250 — welcome back!

🧠 大想法

大游戏标题画面上的"继续游戏"就是这么做的。你的游戏一启动就检查有没有存档: 有,就显示继续;没有,就显示新游戏。

🧠 Big idea

This is exactly how big games do "Continue" on the title screen. When your game starts, check if a save exists: if yes, show CONTINUE; if no, show NEW GAME.

6 🔁 游戏循环 —— 每个游戏的心跳

6 🔁 The game loop — the heartbeat of every game

史上每一个游戏 —— Minecraft、马里奥、还有你的 —— 都在做同样的三步,每秒 60 次:

Every game ever made — Minecraft, Mario, yours — runs the same three steps, 60 times every second:

  • 1. 查输入 —— 哪些键正被按着?
  • 2. 更新世界 —— 移动玩家、敌人、子弹;检查碰撞
  • 3. 画所有东西 —— 清空屏幕,按新位置重画一遍
  • 1. Check input — which keys are pressed?
  • 2. Update the world — move players, enemies, bullets; check for collisions
  • 3. Draw everything — clear the screen and redraw the new positions

用 JavaScript 写出来是这样:

In JavaScript, the loop looks like this:

const keys = {};
onkeydown = e => keys[e.key] = true;    // 记住按下去的键…
onkeyup   = e => keys[e.key] = false;   // …和松开的键

function gameLoop() {
  // 1. 输入 —— 按住键,每一帧都平滑移动
  if (keys["ArrowRight"]) heroX += 4;
  if (keys["ArrowLeft"])  heroX -= 4;

  // 2. 更新 —— 你的游戏规则写这里(重力、敌人、计分…)

  // 3. 绘制 —— 擦掉屏幕,重画整个世界
  ctx.clearRect(0, 0, stage.width, stage.height);
  drawSprite(SLIME, heroX, heroY, 8);

  requestAnimationFrame(gameLoop);   // "浏览器,下一帧再运行我一次!"
}
gameLoop();   // 启动心跳 💓
const keys = {};
onkeydown = e => keys[e.key] = true;    // remember keys going down...
onkeyup   = e => keys[e.key] = false;   // ...and coming back up

function gameLoop() {
  // 1. INPUT — holding a key moves you smoothly every frame
  if (keys["ArrowRight"]) heroX += 4;
  if (keys["ArrowLeft"])  heroX -= 4;

  // 2. UPDATE — your game rules go here (gravity, enemies, scoring...)

  // 3. DRAW — wipe the screen, redraw the world
  ctx.clearRect(0, 0, stage.width, stage.height);
  drawSprite(SLIME, heroX, heroY, 8);

  requestAnimationFrame(gameLoop);   // "browser, run me again next frame!"
}
gameLoop();   // start the heartbeat 💓

💡 为什么不直接在 keydown 里移动?

如果把移动直接写进 onkeydown,动起来会一卡一卡的(试试就知道!)。 高手的诀窍就是上面那样:keydown/keyup 只负责记住哪些键被按着, 移动交给游戏循环来做 —— 丝般顺滑。

💡 Why not just move on keydown?

If you move the hero directly inside onkeydown, movement feels stuttery (try it!). The pro trick is what you see above: keydown/keyup only remember what's held, and the loop does the moving — silky smooth.

7 👫 WiFi 双人模式 —— 重头戏

7 👫 2-player mode over WiFi — the BIG one

准备好最酷的升级了吗?两个浏览器、两个玩家、同一个游戏。 这是所有网络游戏的秘密:

Ready for the coolest upgrade of all? Two browsers, two players, one shared game. Here's the secret of every online game:

🧠 多人游戏的真正原理

有一台服务器记着每个人的位置。每个玩家的浏览器反复做两件事: "服务器,这是我的位置"和"服务器,大家都在哪?" 然后把服务器说的画出来。就这么简单。堡垒之夜也是这么干的。我们也来。

🧠 How multiplayer really works

There is one server that remembers where everyone is. Every player's browser does two things, over and over: "Server, here's MY position" and "Server, where is EVERYONE?" Then it draws what the server said. That's it. Fortnite does this. We're going to do it too.

部分 A —— 服务器(存成 server.py)

Part A — the server (save as server.py)

它是第 2 步那个服务器的升级版:照样发文件,但还会记住每个玩家的位置:

This upgrades the Step-2 server: it still hands out your files, but it ALSO remembers every player's position:

import json
from http.server import HTTPServer, SimpleHTTPRequestHandler

players = {}   # 服务器的记忆:名字 -> 位置 + 颜色

class GameServer(SimpleHTTPRequestHandler):

    def do_GET(self):                      # 浏览器来"要"东西
        if self.path == "/players":        # "大家都在哪?"
            body = json.dumps(players).encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            super().do_GET()               # 普通文件(tag.html 等)

    def do_POST(self):                     # 浏览器来"送"东西
        if self.path == "/move":           # "这是我的位置"
            size = int(self.headers["Content-Length"])
            data = json.loads(self.rfile.read(size))
            players[data["name"]] = data   # 记住这个玩家
            self.send_response(200)
            self.end_headers()

    def log_message(self, *args):
        pass                           # 让终端保持安静

print("Game server ON! Open http://localhost:8000/tag.html")
HTTPServer(("0.0.0.0", 8000), GameServer).serve_forever()
import json
from http.server import HTTPServer, SimpleHTTPRequestHandler

players = {}   # the server's memory: name -> position + color

class GameServer(SimpleHTTPRequestHandler):

    def do_GET(self):                      # browser ASKS for something
        if self.path == "/players":        # "where is everyone?"
            body = json.dumps(players).encode()
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            super().do_GET()               # normal files (tag.html etc.)

    def do_POST(self):                     # browser SENDS something
        if self.path == "/move":           # "here's MY position"
            size = int(self.headers["Content-Length"])
            data = json.loads(self.rfile.read(size))
            players[data["name"]] = data   # remember this player
            self.send_response(200)
            self.end_headers()

    def log_message(self, *args):
        pass                           # keep the terminal quiet

print("Game server ON! Open http://localhost:8000/tag.html")
HTTPServer(("0.0.0.0", 8000), GameServer).serve_forever()

部分 B —— 游戏(存成 tag.html,放同一个文件夹)

Part B — the game (save as tag.html, same folder)

<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<body style="background:#0f172a">
<canvas id="game" width="600" height="400"
        style="background:#1e293b;display:block;margin:20px auto;border-radius:12px"></canvas>
<script>
const name  = prompt("你的玩家名字?") || "Player" + Math.floor(Math.random()*99);
const color = "hsl(" + Math.floor(Math.random()*360) + ", 90%, 60%)";  // 随机颜色!
const me = { name, color, x: 100 + Math.random()*400, y: 100 + Math.random()*200 };
let everyone = {};

// 记住按住的键(游戏循环那一节的高手诀窍)
const keys = {};
onkeydown = e => keys[e.key] = true;
onkeyup   = e => keys[e.key] = false;

// 每 0.1 秒:告诉服务器我在哪,再问问大家都在哪
async function talkToServer() {
  await fetch("/move", { method: "POST", body: JSON.stringify(me) });
  everyone = await (await fetch("/players")).json();
}
setInterval(talkToServer, 100);

const ctx = document.getElementById("game").getContext("2d");
function gameLoop() {
  if (keys["ArrowLeft"])  me.x -= 4;
  if (keys["ArrowRight"]) me.x += 4;
  if (keys["ArrowUp"])    me.y -= 4;
  if (keys["ArrowDown"])  me.y += 4;

  ctx.clearRect(0, 0, 600, 400);
  for (const p of Object.values(everyone)) {   // 画出每一个玩家
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x, p.y, 30, 30);
    ctx.fillStyle = "#fff";
    ctx.font = "13px sans-serif";
    ctx.fillText(p.name, p.x, p.y - 6);
  }
  requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<body style="background:#0f172a">
<canvas id="game" width="600" height="400"
        style="background:#1e293b;display:block;margin:20px auto;border-radius:12px"></canvas>
<script>
const name  = prompt("Your player name?") || "Player" + Math.floor(Math.random()*99);
const color = "hsl(" + Math.floor(Math.random()*360) + ", 90%, 60%)";  // random color!
const me = { name, color, x: 100 + Math.random()*400, y: 100 + Math.random()*200 };
let everyone = {};

// remember held keys (the pro trick from the game loop section)
const keys = {};
onkeydown = e => keys[e.key] = true;
onkeyup   = e => keys[e.key] = false;

// every 0.1 seconds: tell the server where I am, ask where everyone is
async function talkToServer() {
  await fetch("/move", { method: "POST", body: JSON.stringify(me) });
  everyone = await (await fetch("/players")).json();
}
setInterval(talkToServer, 100);

const ctx = document.getElementById("game").getContext("2d");
function gameLoop() {
  if (keys["ArrowLeft"])  me.x -= 4;
  if (keys["ArrowRight"]) me.x += 4;
  if (keys["ArrowUp"])    me.y -= 4;
  if (keys["ArrowDown"])  me.y += 4;

  ctx.clearRect(0, 0, 600, 400);
  for (const p of Object.values(everyone)) {   // draw EVERY player
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x, p.y, 30, 30);
    ctx.fillStyle = "#fff";
    ctx.font = "13px sans-serif";
    ctx.fillText(p.name, p.x, p.y - 6);
  }
  requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>

部分 C —— 开玩!🎉

Part C — PLAY! 🎉

  • 在你的游戏文件夹里运行:python server.py
  • 你打开:http://localhost:8000/tag.html
  • 朋友(同一个 WiFi)打开:http://你的IP:8000/tag.html(IP 用第 2 步的方法查)
  • 旁边没朋友?开两个浏览器窗口并排放,自己当两个玩家!
  • In your game folder, run: python server.py
  • You open: http://localhost:8000/tag.html
  • Your friend (same WiFi) opens: http://YOUR-IP:8000/tag.html (your IP from Step 2)
  • No friend around? Open it in two browser windows side by side and be both players!

你们每人都会看到一个写着名字的彩色方块 —— 而且能实时看到对方移动。 你刚刚做出了一个网络游戏。是真的。

You'll each see a colored square with your name — and you'll see each other move in real time. You just built an online game. For real.

🚀 升级玩法(很适合让 AI 帮忙)

  • 变成真正的抓人游戏:两个方块碰到时,被抓的变红、变成"鬼"
  • 用第 3 步的像素史莱姆代替方块
  • 加上随机出现的金币 —— 谁先碰到谁得分
  • 玩家相撞时播放第 4 步的跳跃音效

🚀 Level it up (great things to ask AI for)

  • Make it actual tag: if two squares touch, the tagged one turns red and becomes "it"
  • Draw your pixel-art slime from Step 3 instead of squares
  • Add coins that appear at random spots — first player to touch one scores
  • Play your jump sound from Step 4 when players collide

⚠️ 朋友连不上怎么办

① 确认你们在同一个 WiFi。② 再查一遍 IP 地址 —— 它有时会变。 ③ 电脑的防火墙可能会问"允许 Python 接收连接吗?" —— 选允许。 学校和咖啡馆的 WiFi 有时不让设备互相通信;家里的 WiFi 一般没问题。

⚠️ If your friend can't connect

① Make sure you're on the same WiFi. ② Check the IP address — it changes sometimes. ③ Your computer's firewall may ask "allow Python to accept connections?" — say yes. School and café WiFi sometimes blocks devices from talking to each other; home WiFi usually works.

8 🤖 像高手一样向 AI 提问

8 🤖 How to ask AI for help like a pro

AI 是和你一起做游戏的搭档 —— 但高手知道怎么问。五条黄金法则:

AI is your co-builder — but pros know HOW to ask. Five golden rules:

  • 一次只要一个功能。"加二段跳"效果很好。"把我的游戏变得超棒"只会变成一团糟。
  • 把整个文件贴给它。AI 看不到的代码,它就修不了。
  • 说清你看到了什么 vs 你想要什么。"我按了跳跃键,主角瞬移到了角落 —— 他应该跳起来再落回地面。"
  • 让它解释。加一句"像给 10 岁的孩子那样解释你改了什么" —— 这才是真正让自己升级的方法,而不是光复制粘贴。
  • 保存版本!大改之前,把 game.html 复制成 game_v2.html。AI 改坏了,你随时能退回去。
  • One feature at a time. "Add double-jump" works great. "Make my game amazing" makes a mess.
  • Paste your WHOLE file. AI can't fix code it can't see.
  • Say what you SAW vs. what you WANTED. "I pressed jump and the hero teleported to the corner — he should rise and fall back down."
  • Ask it to explain. Add "explain what you changed like I'm 10" — that's how you actually level UP instead of just copy-pasting.
  • Save versions! Before big changes, copy game.html → game_v2.html. If AI breaks something, you can always go back.

可以直接照抄的提示词 📋

Prompt recipes to steal 📋

🦘 更好的跳跃

"这是我的游戏【贴代码】。加上重力和空格键跳跃:主角跳起、减速、再落回地面。解释你改了什么。"

🦘 Better jumping

"Here is my game [paste code]. Add gravity and jumping with the space bar: the hero rises, slows, and falls back to the ground. Explain what you changed."

👾 敌人

"加 3 个来回巡逻的敌人。敌人碰到主角就从 3 颗心里扣 1 颗,屏幕闪红。0 颗心时显示 GAME OVER。"

👾 Enemies

"Add 3 enemies that move back and forth. If one touches the hero, take away 1 heart out of 3 and flash the screen red. Show GAME OVER at 0 hearts."

🎨 角色设计

"为我的 drawSprite 函数设计一个 12×12 的像素巫师。用字母表示颜色:P=紫色长袍,Y=黄色星星,S=皮肤,W=白胡子,'.'=透明。"

🎨 Sprite design

"Design a 12×12 pixel-art wizard for my drawSprite function. Use letters for colors: P=purple robe, Y=yellow stars, S=skin, W=white beard, '.'=transparent."

🐛 抓虫(Debug)

"我的游戏分数一到 10 就卡死。这是整个文件【贴代码】。找到 bug,修好它,并解释为什么会这样。"

🐛 Bug hunting

"My game freezes when the score reaches 10. Here is the whole file [paste code]. Find the bug, fix it, and explain why it happened."

👫 多人升级

"这是我的 server.py 和 tag.html【都贴上】。让玩家碰到别人时,对方变红 2 秒,抓人的得 1 分。代码保持简单。"

👫 Multiplayer upgrade

"Here are my server.py and tag.html [paste both]. Make touching another player turn them red for 2 seconds, and give the tagger 1 point. Keep the code simple."

🏆 任务追踪器

🏆 Quest Tracker

每完成一个任务就打个勾。你的进度用 —— 你猜对了 —— localStorage 保存。😎

Check off each quest as you beat it. Your progress is saved with — you guessed it — localStorage. 😎

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

Saved on this device automatically.