第 12 课 · Découverte

浏览器里的多人 ARPG

搭好的世界、会记住的数据库、会应答的服务器、会推送的 websocket——还有同一个标签页里的两个玩家。

~11 min7 个关卡
▶ 视频🧩 练习📄 备忘单

42 秒 · 含中文字幕 · 8.5 Mo

你会学会的 7 件事

第 12 课:浏览器里的多人 ARPG

Regarde la leçon (42 s) : un monde bâti, une base qui se souvient, un serveur qui répond, un websocket qui pousse — quatre étapes, et ton ARPG tourne dans un onglet.

一格一格搭世界

Écris `cellule(x, y, sol)` : une case du monde — un Hash avec `"x"`, `"y"` et `"sol"` (ex: `"herbe"`). Le monde 8-bit commence par UNE case.

会记住的数据库

Écris `charge(rows, id)` : lire la base — `rows` est un tableau de lignes Hash (avec `"id"`), renvoie LA ligne dont l'id vaut `id`, sinon `nil`. Sans ce lire, la partie recommence à zéro.

服务器应答路由

Écris `route(chemin)` : le petit routeur — `"/joueurs"` → `:joueurs` · `"/monde"` → `:monde` · `"/parties"` → `:parties` · tout autre chemin → `nil`. Le serveur ne fait QUE ça : un chemin, un symbole.

websocket 推送动作

Écris `evenement(msg)` : le serveur reçoit `msg` (un Hash avec `"type"`) — `"tire"` → `:inflige` · `"parle"` → `:diffuse` · tout autre type → `:ignore`. Le websocket ne fait que ça : type → action.

送到【对的】对象手里

Écris `remets?(element, acteur)` : un élément venu de la base porte `"pour"` (un id) et `"role"` (`"pnj"` ou `"joueur"`) ; l'acteur porte `"id"` et `"role"`. true SEULEMENT si l'id correspond ET le rôle correspond — au bon joueur, au bon PNJ.

第 12 课:OK

Sans aide : `transfert(rows, id, acteur)` — 1) cherche la ligne `id` dans la base `rows` (absente → `false`) · 2) livre seulement si `"pour"` == `"id"` de l'acteur ET `"role"` == `"role"` de l'acteur. Le reste, c'est `false`.

分享这一课

← 全部课程

📄 备忘单

打印出来贴显示器上,就当是你自己的主意

📄 备忘单 — 浏览器里的多人 ARPG(世界 · 数据库 · 流 · websocket)

浏览器里的 8-bit 多人 ARPG 就四步:搭世界、记进数据库、在路由上应答、用 websocket 推送——然后把每个元素送到【对的】对象手里。

旅程的四步

格子 → 数据库 → 路由 → websocket        (然后:投递给对的对象)
cellule()  charge()  route()  evenement()

1. 世界:格子,不是引擎

def cellule(x, y, sol)
  { "x" => x, "y" => y, "sol" => sol }
end
cellule(2, 5, "herbe")   # => {"x"=>2, "y"=>5, "sol"=>"herbe"}

整个世界 = 这些 hash 的数组(第 6 + 8 课,复习)。贴图在后面——数据先行。

2. 记忆:会记住的数据库

def charge(rows, id)
  rows.find { |r| r["id"] == id }   # 那一行……或者 nil
end

两次会话之间,只有数据库能活过标签页。加载、玩、存——三次调用,世界就有了记忆。

3. 流:一个路径,一个符号

case chemin
when "/joueurs"  then :joueurs
when "/monde"    then :monde
when "/parties"  then :parties
else nil                 # 不认识:服务器说不
end

一次请求不过如此:客户端喊一个路径,服务器回一个符号 (或者 nil——一个诚实的 404)。

4. websocket:类型 → 动作,推送出去

case msg["type"]
when "tire"  then :inflige     # 服务器执行……
when "parle" then :diffuse     # ……并且【广播】给其他人
else :ignore                   # 不是玩法:扔掉
end

和路由的区别:服务器先开口。 连接一直开着,动作实时推给所有人。

5. 投递:给对的对象,绝不「差不多」

def remets?(element, acteur)
  element["pour"] == acteur["id"] && element["role"] == acteur["role"]
end

来自数据库的元素自带目的地(pour)和受众(role:"pnj" 或 "joueur")。 两个条件——只看 id 会送错 NPC,只看角色会送给任何人。这就是多人分流的 &&。

完整的一趟(服务器每条消息在做的)

def transfert(rows, id, acteur)
  el = rows.find { |r| r["id"] == id }   # 1. 数据库
  return false unless el                # 2. 没找到 → 什么都不发
  el["pour"] == acteur["id"] && el["role"] == acteur["role"]   # 3. 对的收件人
end

数据库 → 查找 → 双重条件 → 送达。五行 Ruby 顶一个 MMO 后端。 (真的还要加持久化、隐藏 id、延迟——但形状就是这样。)

以后别再说

  • 「做多人得有游戏引擎」 → 一个数据库、一条路由、一个 socket
  • 「服务器决定一切」 → 它决定【什么为真】:谁有什么,数据库说了算
  • 「websocket = 实时魔法」 → 对消息的 case/when,在循环里推送

SlopCourse Microduck — 从 slop 到干净的代码。