OCC 星球漫游 · 开发指南
面向"要动代码"的人:怎么把环境跑起来、改完怎么看、以及哪些地方一碰就出静默事故。
核对基线:2026-10-15。本机环境为 Windows。
0. 环境要求
| 项 | 要求 |
|---|---|
| Node | >= 20(package.json engines) |
| 包管理 | npm;运行时依赖只有 4 个:three / express / ws / cannon-es |
| 打包器 | 没有,也不需要。前端是纯 ES Module + importmap |
| 浏览器 | 任意现代浏览器(WebGL2) |
⚠️ 升级 three 必须同步 public/vendor/ 下的构建产物,且 three.module.min.js 依赖同目录的 three.core.min.js(漏一个就白屏)。
1. 启动服务
1.1 前台跑(调试用)
npm start # = node server/server.js
npm run dev # = node --watch server/server.js
打开 http://127.0.0.1:8790。同一个端口同时提供:作品(/)、文档站(/docs/)、WebSocket(/ws)。
1.2 Admin常驻(推荐日常用)
python scripts/svc.py start # 启动
python scripts/svc.py status # 状态 + 进程链 + HTTP 探活
python scripts/svc.py logs # 生命周期日志 + 服务日志
python scripts/svc.py stop # 停止(杀到进程链根)
python scripts/svc.py restart
原理:Windows 计划任务 occ-planet → deploy/_svc_occgame.vbs(隐藏窗口)→ .bat → node。 这样进程挂在任务计划服务下,脱离终端与工具会话,关窗口也不死。
规则:任何需要"活过本轮对话"的服务一律走 svc.py,不要用前台阻塞或Admin子进程的方式拉起。 前台方式在工具会话结束时会被回收,表现为"我这边显示还在跑、你那边点开连不上"——静默失败,很难察觉。
1.3 改完代码要重启
🔴 服务端改动不重启不生效。这是本项目最高频的自伤:改完 server/*.js 发现"没变化",排查半天最后发现是没重启。
python scripts/svc.py restart
前端(public/)不需要重启,但浏览器要硬刷新(Ctrl+Shift+R),否则 no-cache 协商缓存仍可能给旧文件。
2. 目录导览
| 目录 | 内容 | 改动后要做什么 |
|---|---|---|
public/src/ | 前端 48 个模块 | 硬刷新浏览器 |
public/index.html / styles.css | 页面结构与全站样式 | 硬刷新 |
public/vendor/ | three 构建产物 | 一般不动 |
server/ | 服务端 11 个模块 | 必须重启服务 |
assets/ | 美术资产(943MB) | 见 美术资产管线 |
docs/ | 文档系统(见 §6) | 跑构建链 |
tools/e2e/ | 自动化测试(58 个脚本) | 见 测试指南 |
_logs/ | 运行日志、测试截图、临时诊断脚本 | 不进版本库 |
_data/ | 运行存档 state.json | ⚠️ 删除 = 清空所有账号与建筑 |
3. 调试
3.1 调试句柄 window.__OCC__
main.js 把核心对象挂在 window.__OCC__ 上,浏览器控制台可直接用:
| 类别 | 能拿到什么 |
|---|---|
| UI 模块 | bagUI、resources、lab、craft、loot、mining、buildMode、depotUI、starMap、guide 等 |
| 运行时对象 | input、player、npcs、boss、rig、planes、phys、hud、net 等 |
常见用法:
// 看帧循环是否还在跑(冻屏排查第一招)
// 页面内 rAF 计数:数字不动 = 主循环已死
// 看玩家当前世界与方向
__OCC__.player.position.length()
// 看背包快照
__OCC__.bagUI.bag
// 看某个矿节点
__OCC__.resources.nearest()
3.2 冻屏排查(本项目最常见的事故)
症状:3D 画面与操控彻底不动、键鼠全无反应、只能 F5;但 UI 提示条还能动(DOM 不受影响)。
机制:renderer.setAnimationLoop 的回调抛一次错就不再排下一帧。而且 F5 会清控制台,玩家回头报"没有报错"。
排查步骤:
- 打开控制台,看有没有
[occ] <标签> 失败的 warn(safe()按标签只打一次)。 - 用页面内 rAF 计数确认帧是否停住(停着不动 = 已冻)。
- 看报错栈里最深的那个模块 —— 通常是"跨模块传了普通
{x,y,z}对象却调了.dot()/.crossVectors()"。
两道防线(新增模块时必须照做):
| 防线 | 做法 |
|---|---|
safe() 包裹 | 主循环里每个模块调用包 safe('标签', fn);出错只丢该模块这一帧 |
| Vector3 copy | 跨模块传方向 / 法线 / 点,必须 copy 成 THREE.Vector3,不要透传普通对象 |
3.3 常用诊断脚本
_logs/ 下积累了大量一次性诊断脚本(如 _diag.cjs、_check.cjs),排查同类问题时优先找现成的:
| 问题类型 | 可参考 |
|---|---|
| 车/机甲不动 | tools/e2e/diag.cjs |
| Boss 看不见 | tools/e2e/boss_diag.cjs |
| 帧循环冻死 | _logs/_freeze_probe.cjs(把可疑函数换成必抛版本,看帧号是否静止) |
| 改完想验证 | 对应功能的 _logs/*_check.cjs |
4. 改代码的硬约束(改动前必读)
| 约束 | 后果 |
|---|---|
| 主循环里裸调用模块 | 一抛错整局冻死 |
| 跨模块传普通对象当向量 | xxx.dot is not a function → 冻屏 |
| 服务端改完不重启 | 改了等于没改(最高频自伤) |
| 世界参数只改一侧 | server/world-config.js 与 public/src/config.js 两侧同步问题 |
| 改动影响 flora 生成顺序 | 障碍物 id = 生成顺序,全部错位 ⇒ 必须同时清 _data/state.json,且改前先停服 |
| 给冶炼加科技门槛 | 零到一死锁(铁锭只有冶炼厂能产,冶炼厂要铁锭) |
客户端按 r.by === type 过滤配方 | 与服端口径不一致;只有服务端下发的 bldRecipes 算数 |
给 public/ 静态资源加 immutable | 文件名无内容 hash,发布后老用户永远跑旧代码 |
在 collide 回调里 removeBody | narrowphase 中改数组 → undefined 崩溃;必须延后到帧末 |
| 用世界坐标点乘做障碍物角度比较 | 全量障碍物被放进物理世界 |
用 player.position.length() 当贴地基准 | 飞行时含高度,弹道被抬到高空(要用 player.R) |
| 写自定义坐标换算 | 与共享层不一致 ⇒ "贴着矿却说太远";去 import 现成的 |
改函数定义的补丁锚点不带 export | 锚点写 function foo( 会插进 export function foo( 中间 → 服务起不来 |
5. 加新模块的检查单
新写一个玩法模块并接进主循环时:
- 它是不是"两端共用"的? 是的话放
public/src/并让服务端 import 同一文件,别写两份。 - 主循环里包
safe(),标签用模块名。 - 跨模块传的向量 copy 成
Vector3。 - 权威边界想清楚:这个状态谁说了算?客户端状态一律视为不可信。
- 要落盘吗? 要就挂到
profile上并走markDirty();不落盘的运行时状态要能重启后自愈。 - 要不要进
welcome/bag快照? 客户端需要静态定义清单时,随bag下发(那是唯一入口)。 - 要不要过滤世界? 广播类消息必须按世界过滤(
broadcast与relayState两条通道都要)。 - 加一条 e2e(见 测试指南)。
- 更新文档(至少 玩法系统现状 与 接口与协议)。
6. 文档系统
文档站与作品同端口(/docs/),也可以 file:// 直接双击打开(页面已自包含化)。
| 目录 | 内容 | 维护方式 |
|---|---|---|
docs/*.md | 源文件 | 手写;改这里 |
docs/guide/*.html | 由 md 生成的"出版版" | 不要手改,会被覆盖 |
docs/md.html | 全部 md 的原文直读版 | 自动生成 |
docs/art/*.html | 美术设定图集与设计稿 | 手写静态 HTML |
docs/index.html | 文档站首页 | 手写 |
docs/_assets/menu.js | 左侧菜单定义 | 手写;增删文档要同步 |
docs/_build/ | 构建脚本 | 见下 |
改完 Markdown 后依次跑:
node docs/_build/md2html.mjs # md → guide/*.html
node docs/_build/build-md-viewer.mjs # md → md.html
node docs/_build/inline.mjs # 自包含化(必跑,且必须最后跑)
改 CSS 只需重跑 inline.mjs。新增一docs时,除了跑上面三条,还要在 md2html.mjs 的 DOCS_LIST / LINK_MAP 和 _assets/menu.js 的 MENU 里登记,否则不会生成、菜单里也找不到。
6.1 Markdown 语法限制(自研解析器)
md-lib.mjs 是手写的轻量解析器,只支持:标题、列表、表格、代码块、引用、分隔线、段落、行内 code / bold / [链接]()。
不支持的写法(写了会渲染坏):
| 不支持 | 替代 |
|---|---|
| 嵌套列表 | 拆成多段,或用表格 |
| 表格单元格里出现半角竖线 | 换成 / 或全角竖线 |
| 引用块里嵌表格 / 列表 | 把内容移出引用块 |
** 内部含 * | 避免 |
| 图片语法 | 美术稿用独立 HTML 页 |
7. 前端两条最容易忘的约定
| 约定 | 说明 |
|---|---|
| 坐标系 | +Y = 法线、+Z = 前方;右向量 = heading × normal(写反会镜像) |
| 距离 | 一律球面弧长,不是直线距离 |
细节见 系统架构 第 7 节。