我想在网站的 Games 页面放一段真正可玩的内容,但目标不是制作一份缩小版《守夜人》。更合理的做法,是从游戏本体中抽取一条最小且完整的战斗闭环:角色可以移动和跳跃,可以攻击与闪避;敌人有韧性,破防后可被处决;一段约一分钟的交互足以让人理解游戏的基础节奏。
这段 Demo 最终可以在浏览器里直接运行。访客不需要安装 Godot,也不需要下载游戏客户端。但在它达到“像本体”的程度之前,我们经历了几轮很典型的失败:角色倒着走、攻击滑步、怪物瞬移、平台只能挡路却站不上去、贴身攻击没有伤害、连续输入偶尔丢失,以及加载完成后还要再点一次游戏画面才能操作。
这些问题值得记录,因为它们都指向同一个结论:移植战斗手感时,能运行只是最低标准;真正的标准是行为是否仍然遵守本体的契约。
这份战斗标本已嵌入游戏页面,可直接在浏览器中体验移动、攻击、闪避、破防与处决。前往游戏页体验 Demo ↗
目标:移植闭环,而不是搬运整套游戏
网页 Demo 的范围一开始就被限制得很窄:固定等级、固定攻击力、一个简单敌人、一处高低平台,以及移动、跳跃、攻击、闪避、破防、处决六个必要动作。它不读取存档,不接任务、装备、地图或成长系统,也不与游戏本体共享运行时脚本。
这样做有两个目的。第一,网页包必须足够小,静态托管即可分发;第二,Demo 必须可以单独验证,不能因为本体某个系统更新就悄悄失效。
这里的“隔离”不等于重新发明。Demo 有自己的最小场景和控制脚本,但动作素材、帧数据、音效、字体以及关键战斗规则仍以游戏本体为准。它是本体的一张切片,不是第二条游戏分支。
素材边界:白名单同步,而不是复制目录
直接复制项目目录最快,也最容易失控。无关资源、编辑器缓存和本体脚本会把体积与依赖一起带进网页包,而且很难回答“这个 Demo 到底用了什么”。
最终采用的是显式白名单:同步脚本只从本体复制 31 项素材,包括主角的待机、跑步、跳跃、闪避、攻击、受击与死亡动作,小型敌人的动作与 manifest,五层森林背景,像素字体,以及挥剑、受击、破防、翻滚、死亡、咆哮和处决等音效。名单之外的内容一律不进入 Demo。
这一步看似只是整理文件,实际建立了最重要的工程边界:每一项依赖都可说明、可检查,也可在本体素材更新后重新同步。
第一次实现:能动,但不像本体
第一版很快就跑了起来,也几乎立刻暴露出问题。最明显的是角色方向相反;随后是移动带有不属于本体的惯性,攻击时会滑步,跳到平台后停在一张蹲姿帧上。敌人会在接触角色时突然位移,贴得太近反而打不中;玩家被击中一次就长时间硬直,也没有本体已有的音效、伤害数字和像素字体。
这些并不是零散的小 bug,而是“用常见平台动作游戏的近似实现,代替本体实际规则”的结果。
| 表面现象 | 根因 | 修正方向 |
|---|---|---|
| 角色倒着走 | 精灵朝向约定与通用实现相反 | 读取并遵守本体的朝向约定 |
| 攻击时滑步 | 攻击状态没有接管水平位移 | 按本体状态冻结或限制移动 |
| 怪物瞬移 | 给敌人加入了本体不存在的阻挡碰撞 | 移除实体推挤,只保留命中与受击关系 |
| 平台站不上去 / 落地蹲住 | 单向平台、落地检测与动画状态混在一起 | 拆分落地判定和动作状态切换 |
| 贴身攻击无伤害 | 命中逻辑过度依赖朝向与中心距离 | 使用攻击可达范围,允许零距离重叠 |
| 一击即长硬直 | 省略了玩家韧性契约 | 恢复韧性条,缩短普通受击硬直 |
如果原型“能玩”却不像本体,先停止继续润色。回到本体检查朝向、状态时序、碰撞层、攻击距离、韧性和位移规则;不要用更多补丁掩盖错误的基础假设。
回到本体:逐项提取战斗契约
修正工作不再从 Demo 的表现倒推,而是直接对照游戏本体。角色动画使用原有帧表和脚底锚点;攻击期间的移动、受击停顿与后滑重新按本体逻辑安排;敌人取消阻挡式身体碰撞,双方可以接近和穿过,真正的接触只发生在攻击命中时。
韧性也重新进入闭环。普通命中削减韧性,但不会让目标每次都长时间停住;韧性归零后进入破防状态,才开放处决。处决判定范围比普通攻击更大,按下 F 后角色会自动靠近到演出位置,再开始处决,而不是要求玩家精确贴在某一个像素点上。
连续攻击则加入了约 0.35 秒的输入缓冲:当玩家在当前攻击尚未结束时再次按下攻击键,输入会暂存并在允许衔接的窗口触发。它解决的不是“让连击更快”,而是避免快速操作因帧时序而被吞掉。
这一轮还接回了本体已有的音效、伤害数字和像素字体。此前缺少这些内容,并不是网页平台做不到,而是移植时错误地把它们当成“以后再加的装饰”。实际上,挥剑声、命中停顿、数字弹出和字形共同参与反馈;省掉其中任何一项,手感都会变薄。
Web 包装:让引擎退到档案之后
隔离场景完成后,Godot 导出的是浏览器可执行的 Web 构建。当前发布包共 7 个文件,约 10.6 MiB;其中 WebAssembly 主体经 gzip 后以静态文件分发,由浏览器的 DecompressionStream 在本地解压。访客只需要支持 WebAssembly 的现代浏览器,不需要安装 Godot。
网页没有在进入 Games 页面时立刻下载这 10.6 MiB。Demo 先显示一张档案式启动面板,只有用户主动进入战斗标本后,才创建 iframe 并开始加载。这样不试玩的人不会承担游戏资源成本,普通页面的首屏也不被引擎拖慢。
Godot 默认加载页随后被隐藏,替换成与网站一致的加载桥。外层页面监听真实下载进度,显示当前档案包与百分比;内层引擎完成初始化后,通过 postMessage 向页面发送 ready 信号,再切换到游戏画布。加载失败时则保留错误与重试状态,而不是停在一张无说明的黑屏上。
最后一个问题发生在“已经加载成功”之后:桌面端读条结束,键盘仍然没有响应,必须再用鼠标点击游戏框。原因是键盘焦点还停留在外层页面。现在 ready 信号到达后,外层会把焦点交给 iframe,内层再显式聚焦 Godot canvas;用户按下启动按钮的那次操作因此可以自然延续到游戏控制。
验证:自动检查守边界,实机试玩判断手感
这一类工作不能只靠一种测试。构建与自动检查负责确认资源路径、静态输出、页面状态和消息桥没有断裂;桌面与手机预览负责检查尺寸、全屏、滚动、加载错误和键盘焦点;真正的移动、攻击、贴身命中、平台落地、破防与处决,则必须通过连续实机操作判断。
这次最有价值的反馈,恰恰来自那些自动测试很难定义的句子:“走路有惯性感”“攻击会滑步”“怪物像瞬移”“处决范围不对”。它们不是模糊意见,而是本体手感与移植结果之间的差异报告。把这些差异逐项翻译成状态、碰撞和时序问题,才完成了真正的移植。
结论:Demo 是战斗契约的标本
- 缩小范围,不改规则:只保留一条闭环,但闭环里的行为必须来自本体。
- 素材按白名单进入:让体积、来源与更新路径始终可追踪。
- 表现问题回到契约解决:朝向、碰撞、韧性、输入缓冲和处决位移不能靠观感近似。
- 引擎服从网页体验:按需加载、真实进度、错误恢复与焦点交接都属于 Demo 本身。
- 自动化守住工程边界,人工试玩决定手感是否成立。
这段网页 Demo 的价值不只在于“网站里可以玩”。它把《守夜人》最小的一组战斗契约从完整工程中分离出来,迫使每一条隐含规则变得可说明、可测试、可部署。以后本体继续变化时,这个标本也可以成为一份很直接的比较对象:不是展示所有内容,而是确认那条最基本的战斗节奏仍然成立。