
FateBound
命运绑定:玩家被命运锁链连在一起。链条做物理模拟、会碰撞地形不穿模;前面的人走会拉住后面的人,后面的人一直不走,前面的人越来越重。共享受伤与饥饿(吃东西能正常回饥饿值),掉队缓慢中毒,大厅不生效,至少 2 人才能玩。FateBound 命运绑定 — v0.2.1
玩家之间被命运锁链绑定:
- 链条会碰地形,不会穿模(物理模拟);
- 前面的人走会拉住后面的人;后面的人一直不走,前面的人会越来越吃力;
- 一个人受伤全队一起受伤,一个人饿全队一起饿;
- 掉队缓慢持续中毒,距离拉回来就停止;
- 大厅(机场)阶段完全不出链条,少于 2 名真人玩家也不生效;
- 游戏内可以按快捷键开关(默认
F9,可改绑)。
一、v0.2.1:主机调参数、客机自动同步 —— 做出来了
效果
谁开房,谁调参数,其他玩家什么都不用动。 房主每 5 秒(可调)把自己这份配置广播给全房间。
同步的内容(共 28 项):
| 段 | 项目 |
|---|---|
| 浮点 | MaxDistance、PoisonPerSecond、GraceSeconds、ShareRatio、ChainWidth、ChainThickness、ChainSpacing、ChainSlack、ChainMaxSag、ChainHideRadius、ChainMaxLength、ChainTextureScale、TugStartDistance、MaxPullForce、MaxDragForce、DragSpeedDiff、DragRampSeconds |
| 开关 | ShareHunger、ShareInjury、SharePoison、DrawChain、ChainPhysics、EnableTug、ChainLinkTexture、RequireTwoPlayers、DisableInAirport |
| 整数 | ChainMaxPoints |
不参与同步:ToggleKey(快捷键)和 Enabled(总开关)。
理由是这两个是每个客户端自己的事 —— 要是被房主同步过来,
你在自己机器上按 F9 关掉,5 秒后又被房主掰回去,那就没法用了。
新增两个配置项([FateBound.Sync]):
| 参数 | 默认 | 说明 |
|---|---|---|
SyncFromHost |
true | 当房主时是否广播;当客机时是否接受。客机改成 false 就不被房主覆盖 |
SyncIntervalSeconds |
5 | 广播周期。中途加入的人最多等这么久拿到参数 |
实现方式,以及为什么绕了一圈
一开始想用 PhotonNetwork.RaiseEvent 做事件广播。你给的 PhotonRealtime.dll 里确实有
RaiseEventOptions / ReceiverGroup / LoadBalancingClient.EventReceived,
但编译时发现游戏自带的这份 Photon 是被裁过的:
PhotonUnityNetworking.dll —— 整个 DLL 只有 7 个类型、9 个方法
Photon.Pun.PhotonNetwork 上只剩:InRoom、IsMasterClient、PlayerCount、photonView、RPC
RaiseEvent、AddCallbackTarget、LocalPlayer、MasterClient 全都**不存在**
而 Photon3Unity3D.dll 提供的是:
ExitGames.Client.Photon.SendOptions / EventData / Hashtable / OperationResponse
所以我改用游戏自己在用的那套机制:PhotonView.RPC + [PunRPC]。
依据是硬证据,不是猜的:
PhotonUnityNetworking.dll里存在Photon.Pun.PunRPC类型(就是[PunRPC]这个特性);Assembly-CSharp.dll里能搜到PunRPC的引用;- 游戏自己有一百多个
RPCA_开头的方法(RPCA_AddForceAtPosition等),走的就是这条路径。
本组件本来就挂在角色身上,而角色身上本来就有 PhotonView,
所以不需要额外创建网络物体。
参数包怎么编解码
压成一个 byte[]:魔数 "FB[1" | 浮点段长度 + 数据 | 开关段长度 + 数据 | 整数段长度 + 数据。
为什么用 byte[] 而不是 object[]:Photon 对不认识的类型在序列化上有讲究,
而 byte[] 是它一定能正确搬过去的载体 —— 用最保守的形式,跨客户端才不会有意外。
版本不一致会自动拒绝:任何一段的长度对不上,整包丢掉,不会把参数套错位。 也就是说你和我装的版本不同时,不会出现半个参数生效、半个错乱的情况。
必须说清楚的两点
① 这个功能我没法实测。 我进不了你的游戏,更没法开两个客户端联机。
能确认的是:用的是游戏自己的 RPC 机制、编译通过、[PunRPC] 特性确实挂上去了、
包格式有长度校验。但真实的双人往返我没跑过 —— 第一次联机时请留意:
如果房主改了参数客机没跟着变,把 BepInEx\LogOutput.log 发我
(需要先在 BepInEx.cfg 里把 [Logging.Disk] Enabled 改成 true)。
② 链路长度限制带来的一个副作用还在:参数同步过去之后,
房主把 ChainMaxLength 调成 0(不限)时,客机那边也会跟着不限 ——
但链越长链节之间盲区越大这个物理事实不会因为同步而改变。
建议还是保持 30 米左右。
本版核验
编译: 0 warning / 0 error,产物 34,816 字节
IL: SyncTick 里确实调用了 get_InRoom / get_IsMasterClient / PhotonView::RPC / BuildSyncBlob
RPCA_FateBoundSync 存在且调用 ApplySyncBlob
PunRPC 自定义特性确实挂在方法上
PhysicsChain.Step 的调用链仍是 v0.2.0 的几何求解(未被本版影响)
二、v0.2.0:链条不再是物理模拟,改成纯几何求解 —— 实测穿模归零
你要的两件事,这一版都做了
- 链条有模型(看起来是一根真链条):新增链环贴图,一节一节的金属环、中间镂空、首尾相接。
- 绕开障碍物、不穿模:求解方式整个换掉,改成每帧纯几何投影。
实测数据(独立物理测试台,60 帧/秒 × 6 秒)
场景:两名玩家之间横着一块 2.0×2.5×2.0 米的岩石,链节最深钻入岩石多少米。
| 场景 | v0.1.12 物理模拟 | v0.1.13 改良物理 | v0.2.0 纯几何 |
|---|---|---|---|
| ChainThickness 拉满,相距 12 米 | 0.963 米 | 0.400 米 | 0.000 米 |
| 半径 0.15,相距 12 米 | 0.795 米 | 0.400 米 | 0.000 米 |
| 半径 0.15,相距 30 米 | 0.983 米 | 0.400 米 | 0.000 米 |
| 松弛量给到 0.25 | 0.822 米 | 0.400 米 | 0.000 米 |
「最深入地」(钻到地面以下多深)在所有场景下也全部是 0.000 米。
为什么这次是"构造上不可能穿",而不是"调参调到位"
旧版是物理模拟:铺一串质点 → Verlet 积分 → 每个链节对场景做碰撞推出。 它有一个无解的死结:链条长度被算成"两点直线距离 + 1%",几乎是刚好拉直, 两人之间横着一块岩石时它没有多余长度可以绕,几何上只能从岩石中间穿过去 (实测把松弛量提到 25% 也毫无改善);而且链节一旦钻进非凸 MeshCollider —— PEAK 的岩石、树木、木箱全是非凸 MeshCollider —— 就没救了: Unity 默认不命中背面,从网格内部朝外打射线什么都碰不到,它就永远卡在里面。 这就是你看到的"必须手动按 F9 关掉再打开才能回来"。
v0.2.0 换成纯几何求解,每帧从零开始解,不保留任何速度状态:
1) 在两端连线上铺一串采样点
2) 松弛若干轮:长度约束(带松弛量,所以它有余量弓起来绕)+ 平滑(变成柔顺曲线)+ 重力下垂
3) 投影出几何体(跑在最后,重复三轮)—— 把每个点推出所有障碍物,并保证不低于地表
第 3 步跑在最后是全部的关键:渲染出去的每个点都刚刚被推出来过。
推出之前会先把 Physics.queriesHitBackfaces 打开(这个属性游戏自己在用,
Assembly-CSharp 里就有它的 get/set 引用),于是从物体内部也能打到墙,
"我在里面还是外面"直接用偶数-奇数射线问出来:从点向上打一条长射线,
数穿过了几个面,奇数就在里面,就朝最近的出口穿出去。
每帧从直线重新解还有个额外好处:没有累积误差、不会卡死、不会抖 —— 玩家瞬移、重生、网络抖动都不会把链条留在坏状态里,也就再不需要按 F9 了。
链条现在长什么样
新增 ChainLinkTexture(默认开):贴图是插件自己用代码画出来的,
不依赖任何外部图片文件 —— 所以这个包拖进 plugins 就能用,不用再下别的东西,
也不会因为贴图路径不对而加载失败变成一坨粉色。
贴图本身是灰白金属色,最终颜色还会被 LineRenderer 的 startColor/endColor 乘上去, 所以「灰 → 黄 → 红」那套距离提示依然有效:离得近是灰白铁链,超出 MaxDistance 变红。
新增两个参数(都在 [FateBound.Chain]):
| 参数 | 默认 | 说明 |
|---|---|---|
ChainLinkTexture |
true | 关掉就是一条纯色带子(旧版的样子) |
ChainTextureScale |
0.5 | 链环大小。越小环越大,越大环越密 |
ChainWidth |
0.13 | 改了默认值:0.08 细得像条白线,0.13 才有链条的实体感 |
(这两个值在链条被隐藏又重新显示之后生效 —— 贴图是建线时读一次的。 改完想立刻看到效果,按一下 F9 关掉再打开即可。)
本版核验
编译: 0 warning / 0 error,产物 31,744 字节
Step() 调用链:IsFinite → HasInvalidPoint → Reset(自愈)
ResolveCollisions 那一套已删除
ProjectOut(点, 内部判定) → PushOut → ClampSag → ClampAboveGround → MeasureTension
ProjectOut 里 IsInsideGeometry 确实被调用;Step 里确实读写了 queriesHitBackfaces
构造函数: Mathf.Clamp(radius, 0.02, MaxColliderRadius)
Mathf.Clamp(maxSag, 0, MaxSagCeiling)
独立测试台: 替身 UnityEngine 已实现 queriesHitBackfaces(开启后射线从盒内命中出口面),
真实 PhysicsChain.cs 原文件跑 60 帧/秒 × 6 秒,全部场景归零
一个必须说清楚的边界
测试台里的岩石是解析方盒,真实 PEAK 里是非凸网格。两者不可完全等同。 但这次用的方法不依赖具体形状:开背面命中 → 偶数-奇数判定内外 → 朝最近出口穿出, 这套逻辑对任意闭合网格都成立,方盒只是用来量化验证"每帧结束时点确实在外面"。
你还差一步的:主机调参、客机自动同步
你给的 PhotonRealtime.dll 里确实有 RaiseEventOptions / ReceiverGroup /
LoadBalancingClient.EventReceived,但 SendOptions 和事件数据 EventData
在另一个程序集 Photon3Unity3D.dll 里(编译报错明确指出:
The type 'EventData' is defined in an assembly that is not referenced. You must add a reference to assembly 'Photon3Unity3D')。
再给我这一个文件,下一版就能做:
D:\Steam\steamapps\common\PEAK\PEAK_Data\Managed\Photon3Unity3D.dll
拿到之后会做成:主机(开房间的人)把自己这份参数周期性广播出去,客机收到直接生效, 自己那边的 cfg 不用动。快捷键和总开关不参与同步 —— 那两个是每个客户端自己的事, 不该被房主覆盖。
三、v0.1.13:这一版是实测过的,不是"我以为修好了"
我怎么测的
我进不了你的游戏(PEAK 装在你的电脑上,要走 Steam 和你的账号),
但我把 PhysicsChain.cs 原文件拎出来,配一个我自己写的解析几何世界 ——
地面 + 一块 2.0×2.5×2.0 米的岩石(非凸 MeshCollider,和 PEAK 的岩石一致)——
按 60 帧/秒真跑 6 秒,量两个数:
- 最深入地:链节最深钻到地面以下多少米
- 最深陷进岩石:链节最深钻到岩石内部多少米
同一个场景跑两遍:一遍是 v0.1.12 的老代码,一遍是新代码。
实测数据
| 场景 | 老代码 陷进岩石 | 新代码 陷进岩石 |
|---|---|---|
ChainThickness 拉满(半径 1.0),相距 12 米 |
0.963 米 | 0.400 米 |
| 半径 1.0,相距 30 米 | 0.884 米 | 0.400 米 |
| 半径 0.15,相距 12 米 | 0.795 米 | 0.400 米 |
| 半径 0.15,相距 30 米 | 0.983 米 | 0.400 米 |
| 松弛量加到 0.25(多给 25% 长度) | 0.822 米 | 0.400 米 |
最深入地:所有场景 0.000 米 —— 链条从不钻到地面以下。
为什么"一碰到模型就直接往地下"
ResolvePenetration 是六向推出:哪边在半径之内碰到东西,就朝反方向推开。
方向轮询到 up 时,含义是"链节上方有东西",于是它会把链节往下推。
而位移量 = 半径 − 命中距离 —— 你把 ChainThickness 拉满,推力就跟着一起拉满,
一碰模型整条扎下去。
修法:这条分支直接跳过。对悬空的绳链来说,被上方物体顶下去没有任何好处。
为什么"必须手动按 F9 才能回来"
这是链条钻进了非凸 MeshCollider 之后再也出不来。PEAK 的岩石、树木、木箱都是非凸 MeshCollider:
PushOut用的ComputePenetration不支持非凸 MeshCollider → 直接跳过;- 从链节朝外打射线也没用:Unity 默认不命中背面,链节已经在网格内部, 射线从里面朝外打什么都碰不到 → 永远卡住。
修法:新增 ResolveEmbedded,改成由外向内打 —— 从链节外面朝链节打,
先撞到的一定是这个网格的正面,那个交点就是离它最近的出口,把链节挪过去就出来了。
六个方向都试,取出口最近的(命中距离最长的那一个)。
它必须每帧跑,不能降频。 第一版挂在"每 3 帧"的降频上,实测几乎没用: 长度约束跑在碰撞之前,每帧都在把链节往回拉,被捞出来的下一帧立刻又被拽回去, 等于 2/3 的时间都嵌在石头里。改成每帧跑之后,嵌入深度从 0.963 掉到 0.400 且稳定不抖。
另外两处硬性钳制
| 项 | 处理 |
|---|---|
ChainThickness |
碰撞球半径硬性钳在 0.4 米以内,你填多大都按 0.4 算 |
ChainMaxSag |
填 0(不限)或填很大,实际按 2.0 米算 |
下垂为什么要钳:链条一路垂到地形以下之后,向下的探针起点已经在地表以下, 同样因为不命中背面而找不到地面,就再也捞不回来。 为此还加了第二段探针:常规探针(上方 0.6 米)没找到地面时, 从上方 3 米重新打一次,把已经陷进去的链节捞回来。
还剩 0.400 米,为什么
这不是没修,是几何上绕不过去:链条长度 = 两点直线距离 + 1%,几乎是刚好拉直的, 两人之间横着一块 2 米厚的岩石时它没有多余长度可以绕。 实测把松弛量提到 0.25(多给 25% 长度)也毫无改善。
结论:想让长链完全不穿岩石,只能让链短。
ChainMaxLength 建议保持默认 30 米。你要的"玩家相连"是 NearestWeb 模式保证的 ——
30 米内的玩家都会互相连上,正常一起爬图完全够。真觉得短可以调到 50,
但不要继续往上:链越长,链节之间那几米就是越大的盲区。
你问的"主机调参数、客机自动同步"
这一版没做,原因很具体:那要调用 PEAK 联网用的 Photon 接口,
而 RaiseEventOptions / SendOptions / EventData / ReceiverGroup 这些类型
在 PhotonRealtime.dll 里,我这边没有这个文件,编译不过去。
请从游戏目录里取这个文件发我,下一版就能做:
D:\Steam\steamapps\common\PEAK\PEAK_Data\Managed\PhotonRealtime.dll
拿到之后会做成:主机进图后把自己的链条 / 拖拽参数广播出去,客机收到直接生效, 自己那边的 cfg 不用动。
本版核验
编译: 0 warning / 0 error,产物 30,208 字节
Step() 调用链: IsFinite → HasInvalidPoint → Reset(自愈)
ResolveCollisions → ResolvePenetration → ResolveEmbedded
→ ClampSag → ClampAboveGround → UpdateContactAnchors → MeasureTension
构造函数: Mathf.Clamp(radius, 0.02, MaxColliderRadius)
Mathf.Clamp(maxSag, 0, MaxSagCeiling)
独立测试台: /tmp/harness(替身 UnityEngine + 真实 PhysicsChain.cs,测完即弃)
四、v0.1.12:修「老是穿模」的真正根因 + 不用再手动按 F9
⚠ 这一版必须删掉旧 cfg 才生效
BepInEx 只会把新配置项追加到 cfg 末尾,不会覆盖已存在的旧值。你之前跑过, cfg 里已经存着间距 0.45、碰撞球 0.05、24 节这些旧值,它们会一直压着新默认值。
安装前删除这个文件(进游戏会自动重新生成,其它设置不受影响):
D:\Steam\steamapps\common\PEAK\BepInEx\config\com.quaree.fatebound.cfg
问题一:链条老是穿模 —— 这次找到了机制上的根因
两件事叠在一起:
- 链条长度被算成「两点直线距离 × 1.01」——它几何上只能是一根直棍, 一点多余长度都没有,想绕开石头也没有可绕的余地。
- 链节数量有硬上限(旧默认
ChainMaxPoints = 24),链一长, 相邻链节之间就被拉开到好几米。
关键在后面:碰撞检测是逐链节做的 —— 链节自己会被地形顶开, 但链节与链节之间那一段,只是一条画出来的直线,从来不做碰撞检测。
于是:一条 100 米的链,24 节 → 相邻链节隔 4 米多 → 中间这几米根本不检测 → 必然从岩石正中间穿过去。 你截图里那根笔直穿过石头柱子的木条,就是这么来的。
修法(几处一起):
| 项 | 旧 | 新 | 作用 |
|---|---|---|---|
ChainMaxLength |
无 | 30 米 | 新增。两人距离超过就不画这条链,杜绝超长链 |
ChainMaxPoints |
24 | 56 | 链节更多,长链也不会被拉散 |
ChainSpacing |
0.45 | 0.55 | 56 × 0.55 = 30.8 米,正好覆盖 30 米上限 |
ChainThickness |
0.05 | 0.15 | 碰撞球半径 ×3,链节更早被地形挡住,不容易钻进去 |
ChainMaxSag |
0.35 | 0.6 | 放宽一点,允许它被地形顶高,不再被硬压成直线 |
前三项配合起来的效果:链节间距全程贴着 0.55 米, 每一个链节都在认真做地形检测,链条才会真的贴着实景走。
问题二:每次都要手动按 F9 才能回来
你这一句是决定性线索 —— 说明链条会卡死在一个坏状态里, 只有整条重置才能恢复,而 F9 关掉再打开正好等于重置一次。
根因:只要任何一个链节的坐标变成 NaN / 无穷,整条链就再也画不出来, 而且永远不会自己恢复 —— 因为后续每一步运算都会把 NaN 原样传下去。 角色传送、重生、网络同步抖动都可能把 NaN 带进来。
修法:Step() 最前面加了自愈。
发现链节坐标坏掉 → 当场 Reset 重来
端点自己是 NaN → 先不动,等下一帧拿到有效坐标(角色正在传送 / 重生)
以后不需要再按 F9 了。
另外:默认拓扑改成 NearestWeb
Ring 是「全队连成一个闭环」,环的收尾那条边可能把地图两端的人连起来。
NearestWeb 是「每个人都连到离自己最近的那个人」,链永远最短、人人都被连上,
最符合你要的「自动连到该连的人」。两种都受 30 米上限约束,不会再出现横跨地图的链。
本版核验
Step(): IL_3b49 IsFinite(start) → brfalse → ret
IL_3b51 IsFinite(end) → ret
IL_3b63 HasInvalidPoint → brfalse → Reset → ret
BuildPairs(): IL_3142 call PairExists
IL_3164 call WithinChainLength (NearestWeb 分支)
IL_3201 call WithinChainLength (Ring 分支)
WithinChainLength():ldsfld Plugin.ChainMaxLength → bgt.un → ret
callvirt Character::get_Center ×2 → Vector3::Distance
配置注册: ChainMaxLength / ChainMaxPoints / ChainSpacing /
ChainThickness / ChainMaxSag / ChainTopology 全部在列
编译: 0 warning / 0 error
五、v0.1.11:修掉「链条连到哪去了」和「没有拖拽效果」
这两个都是上一版引入/遗留的真问题,已定位到具体代码行。
问题一:链条连到莫名其妙的地方
根因:v0.1.10 排环链时用的是 Character.AllCharacters 的顺序来配对 ——
那是玩家加入房间的顺序,跟站在哪里毫无关系。于是地图两端、相隔几百米的两个人
会被直接连起来。
修法:配对一律看位置。
Ring(默认):从固定起点出发,每次跳到最近的未访问玩家,排成一个邻近闭环 —— 每条链都是挨得近的两个人。起点取实例 ID 最小者(不是「我」),保证各客户端算出同一条环。- 新增
NearestWeb:每个人都连到离自己最近的那个人,画出全部这些链。 链永远最短、人人都被连上,也不可能出现横跨半张地图的怪链。想要更自然的效果就用这个。
问题二:完全没有拖拽效果
这里其实是两个 bug 叠在一起。
bug 1:触发距离高得根本够不着。 旧代码写的是:
float tautDist = maxDist * tautRatio; // 30 × 0.75 = 22.5 米
if (d <= tautDist ...) { dragAccum = 0f; return; }
要两人拉开 22.5 米以上拖拽才开始工作。正常一起爬图的队伍根本到不了这个距离, 所以它一次都没触发过 —— 表现就是「完全没有拖拽效果」。
改用独立参数 TugStartDistance,默认 6 米;并且把绷紧强度从纯线性改成开方,
让 6~15 米这段就已经明显有感觉(纯线性的话 10 米处只有 0.17,几乎拽不动)。
bug 2:拖人的那个方向是反的。
force = dragForce; // 旧:朝向他
ch.RPCA_AddForceAtPosition(dir * force, ...);
dir 是「从你指向队友」的方向。朝向他施加,只有在我正好远离他时才算阻力;
如果我正朝他走,这个力反而推我一把,越走越快。现在改成逆着我自己的移动方向
(-selfDir)施加,无论往哪走都是在拖后腿 —— 这才是「负重感」。
顺带把 MaxDragForce 默认值从 100 提到 160,拖起来更有分量。
配置变更
| 项 | 变化 |
|---|---|
TugStartDistance |
新增,默认 6(米)。拉开多少米后开始拽人。调小更容易被拽 |
ChainTopology |
取值改为 Ring / NearestWeb / AllPairs(原来的 Nearest 已移除) |
MaxDragForce |
默认 100 → 160 |
ChainTautRatio |
已废弃,留着不影响行为(旧版就是它算出 22.5 米的) |
本版核验
BuildPairs(): 转调 PairExists + BuildRingOrder,并现算 Vector3::Distance
BuildRingOrder(): Object::GetInstanceID 定起点 + 最近邻循环
FixedUpdate(): 读到 Plugin.TugStartDistance / Mathf::Sqrt /
Vector3::op_UnaryNegation(逆着自身移动方向的阻力)/ RPCA_AddForceAtPosition
SampleSpeeds(): Vector3::Lerp + get_normalized(平滑后的移动方向)
版本字符串: 0.1.11 出现 6 次,旧值 0.1.10 / 0.1.7 均为 0 次
六、v0.1.10:为什么只有一条链 —— 改成多链条 + 拓扑可选
现象
你截图里排行榜有 4 个人(逢坂大河 / AtroSenet / moonlight / 南栀晚安),但场上只有一条链, 而且只连向离你最近的那个人。
根因:不是连不上,是只有本地玩家那一条被画出来
旧代码第一行就写着:
if (nearest == null || !ch.IsLocal) { Hide(); return; } // 只有本地玩家那条在画
每个客户端只画自己那一条,而且只连最近的队友。所以你永远只能看到 「自己 → 最近的队友」,队友和队友之间的链在屏幕上根本不存在 —— 看起来就像锁链没生效。 这跟离得远不远没关系,是渲染方式的问题。
修法:每个客户端画出全部链条
本地玩家的控制器负责画当前拓扑下的所有链;远处玩家身上的控制器依旧什么都不画, 避免 n 份重复模拟。
新增配置:ChainTopology(怎么连)
| 值 | 行为 | 4 人时的链数 |
|---|---|---|
Ring(默认,新) |
全队首尾连成一个闭环 A-B-C-D-A。每个人都有一条链,人人都被连上 | 4 条 |
AllPairs |
两两互连,最「万物互联」,也最费性能 | 6 条 |
Nearest |
只连最近的队友、且只画自己那条(旧行为,留作对比) | 1 条 |
Ring 下每个玩家恰好一条链、方向顺着排队一圈 —— 这就是「命运锁链把全队串在一起」。
顺便修掉的两个问题
1. 近摄像机裁剪现在两端都裁。 环链下同时有两条链挂在你身上:一条以你为起点, 另一条以你为终点。只裁起点的话,另一条会从你身体朝外再长出一个同样巨大的白楔子。
2. 六向推出改成每 3 帧一次。 多链条模式下这层开销会乘上链数 (4 条链 × 每条最多 24 节 × 6 个方向),而它是纯矫正,晚两帧没有任何感知。初始化时仍然立刻执行。
另外发现并修掉的一个隐藏 bug
PluginVersion 这个常量从 v0.1.7 之后就一直没跟着升,也就是说 v0.1.8 / v0.1.9 在
BepInEx 日志和插件列表里报的都是 0.1.7 —— 你之前想确认自己装的是哪一版时,
日志给的版本号是错的。现在已改为跟随实际版本。
重要:这一步只改显示,不改玩法
掉队中毒、状态共享、锁链拖拽这三套机制仍然按最近的队友判定,本版完全没有动。
所以换成 Ring 之后,你看到的链变多了,但掉血/中毒/拖拽的手感跟之前一致。
如果你希望「离任何一个队友太远都要中毒」,那是另一套改动,说一声我再做。
本版核验
Draw(): Hide -> BuildPairs -> DrawOne -> HideLink
BuildPairs(): 各拓扑分支的 Add 序列(Ring / AllPairs / Nearest 三条路径)
ResolvePenetration(force): 读到了 ldc.i4.3 -> rem -> brfalse -> ret 的降频门
版本字符串: 0.1.10 出现 5 次,旧值 0.1.7 / 0.1.9 均为 0 次
七、v0.1.9:贴脸白楔子 + 方块树木穿模(含 0.1.8 内容)
本版是唯一需要安装的版本,把 0.1.8 和 0.1.9 的内容合并交付。
1. 你截图里那个白色大楔子,就是链条本身
链条起点是 Character.Center(你自己的躯干),而第一人称摄像机的距离只有 0.2 米左右。
按 0.08 米链宽、60° 视场反推,这一节在屏幕上占 近 300 像素宽 —— 于是渲染成一个朝下伸的
巨大白色楔子,看着就像"下面没有人却连着一条链子"。截图上方那几条细白线才是链条的正常部分。
颜色也能对上:ApplyChainColor 近距离给的正是 (0.55, 0.6, 0.65) 浅灰蓝。
修法:近摄像机段裁剪。落在摄像机 ChainHideRadius(默认 0.85 米)以内的起始链节被丢弃,
链条从稍远处开始显示 —— 第一人称绳索都是这么做的,你看不到自己身上的挂点。
2. 跟方块、树木、岩石穿模
ComputePenetration 不支持非凸 MeshCollider,而 PEAK 的岩石、树木、木箱全是这种,
旧版对它们完全没有推出能力。
修法:新增 ResolvePenetration() —— 六向射线推出。每个链节朝上下左右前后各打一条
radius 长的射线,哪边碰到东西就往反方向推开。射线不受碰撞体类型限制。
它救的是"浅层嵌进"(陷进去不到链节半径);嵌得很深的极端情况由球体扫掠在进入前拦住。
3. 补上 v0.1.7 漏掉的调用
ClampSag() 在 v0.1.7 里是永不执行的死代码(替换静默失配)。本版接上,并新增
ClampAboveGround():向下短射线找脚下地形,绕过 SphereCast 与 ComputePenetration 的两个盲区。
4. 新增配置
| 项 | 默认 | 说明 |
|---|---|---|
ChainHideRadius |
0.85 |
近摄像机裁剪半径。还看到贴脸白块就调到 1.0;想看原状设 0 |
5. 这次的核验
把编出来的 DLL 反编译回来看方法体(上一版就是只查"成员存在"、没查"是否被调用"才失手的):
Step(): Array.Clear -> ResolveCollisions -> ResolvePenetration
-> ClampSag -> ClampAboveGround -> UpdateContactAnchors -> MeasureTension
Reset(): ResolveCollisions -> ResolvePenetration -> ClampSag -> ClampAboveGround -> UpdateContactAnchors
Draw(): 相机分支确认存在 Camera.main 与 NearCameraTrim
八、v0.1.7:修掉「链条不连人、跟地链的」
症状:链条不像连着两个玩家,而是低低地贴着地面走、像被钉在地上。
根因(两条叠加):
- 下垂量过大。链条总长被设成
距离 × (1 + 0.12),多出 12% 长度。对跨度 d、多余长度 ΔL, 下垂量约√(3·d·ΔL/8)—— 跨 8 米就下垂 1.70 米,而角色躯干离地只有 0.8 米左右,必然落地。 - 接触锚点把链条粘死在地面。链节一贴地就记下接触点、之后每次迭代都被拽回去。 一旦落地,它就不再是「连着两个人」,而是「连着地面」。
修法:
- 下垂量:
ChainSlack默认0.12→0.01,并加 0.25 米绝对上限。 - 最大下垂硬上限(新增
ChainMaxSag,默认 0.35 米):链节最多只能低于「两人中较低的那个」 0.35 米,超出就抬回去。这条是必须的兜底 —— 实测只靠「重力 + 长度约束」压不住下垂: 位置约束求解始终有残余拉伸,链节越多、跨度越大累计下垂越大,12 米以上整条链会趴到地上。 水平方向不受限制,所以它依然能被地形顶开、绕开障碍物。 - 接触锚点改为张力门控:只有链条被拽直(张力 > 0)时才启用静摩擦。松弛下垂时完全无摩擦, 链条自由摆动,不会粘在地面。
- 约束迭代次数随链节数自适应(
max(8, 链节数 × 2),上限 64)。原来固定 8 次, 而位置约束是逐链节传播的 —— 19 个链节迭代 8 次根本拉不直。
验证(独立 2D 物理仿真,复刻同一套 Verlet 积分 / 距离约束 / 地面碰撞,挂点离地 0.8 米):
| 跨度 | 修复前最低点 | 修复后最低点 |
|---|---|---|
| 3 m | 0.64 m 悬空 | 0.58 m 悬空 |
| 8 m | 0.05 m 贴地 | 0.45 m 悬空 |
| 12 m | 0.05 m 贴地 | 0.45 m 悬空 |
| 25 m | 0.05 m 贴地 | 0.45 m 悬空 |
| 30 m | 0.05 m 贴地 | 0.45 m 悬空 |
顺带说明挂点:Character.Center 取的是 BodypartType.Torso(枚举 Hip=0, Mid=1, Torso=2, Neck=3, Head=4),高度本身合理。所以这个症状不是挂点造成的。
还有一点要讲清楚:链条是「每个人连到最近的那个队友」,不是所有人两两互连 —— 4 人局两两互连要 6 条链,开销不划算。所以 3 人以上时,你只会看到自己这条链。
九、v0.1.6:修掉「吃东西不回饥饿值」
这是 v0.1.1 我写的共享逻辑的缺陷,不是游戏的问题。
旧实现是「每帧把每个人拉到全队最高值」。它等于一个单向棘轮——你吃一口东西, 饥饿值降下来,下一帧立刻又被拉回队友那个高值,所以吃东西完全没用。 同理,治疗、解毒这类单人疗愈动作也全都失效。
根因:currentStatuses 的索引就是 STATUSTYPE,而 Hunger 是一项"恶劣状态"
(游戏自己有 AddHunger() / ClearHunger()),吃东西是降低这个值。
旧逻辑只肯把值往上拉,所以任何下降都会被瞬间撤销。
修法:改成只传播「恶化」的增量,不再对齐绝对值。
- 有人变差、把全队最高值顶高了 → 这份增量补给低于最高值的人(一个人饿,全队一起饿);
- 有人变好(吃东西 / 治疗)→ 什么都不做,他自己的值就留在低位。
仿真对照(同样输入喂给两套逻辑):
| 吃东西前 | 吃东西后 | 结果 | |
|---|---|---|---|
| 旧逻辑 | A=0.30 | A=0.60(被拉回) | 吃东西=白吃 ← 你遇到的问题 |
| 新逻辑 | A=0.30 | A=0.00(降下来了) | 吃东西有效;队友仍为 0.30 |
新逻辑下,吃完之后队友继续恶化,那份增量照样会传给自己,共享没有被削弱。
由此查清的一条硬限制
AddStatus 和 SetStatus 内部都有 photonView.IsMine 门禁——对别人的角色调用会直接 return。
而状态数组本身是全网同步的(RPC_ApplyStatusesFromFloatArray / PushStatuses),
所以每个人读得到队友的数值,但只能写自己。
结论:企划里的「增益共享:有人吃到恢复/力量,全队都获得」在 PEAK 这套权限下做不到。 吃东西只回你自己的。想要这个效果只能走 Photon RPC 硬塞,那属于绕过游戏的权限模型, 有同步风险,我没有默认做——你要的话我可以另开一个开关。
十、v0.1.5:可改绑的开关快捷键
游戏里按 F9 就能开关命运锁链(当场生效:锁链立刻出现或消失)。
改键:打开配置文件,把 ToggleKey 改掉即可。
[FateBound]
切换命运锁链开关的快捷键(只影响你自己的客户端)。
改成 None 可以彻底关掉快捷键。默认 F9 —— 原来那个 Chained 模组用的是 F10,
两个都装的话 F9 / F10 各管一个,不会打架。
# Setting type: KeyCode
# Default value: F9
ToggleKey = F9
按键名就写 Unity 的 KeyCode 名字,例如 F8、F10、G、H、Insert、None。
没装 ConfigurationManager 也能改,直接写文本就行。
两点要说清楚:
- 快捷键只影响你自己的客户端。联机时你关掉只是你这边不生效,队友那边照旧。
- 每次切换会在 BepInEx 控制台打印一行
FateBound:命运锁链已开启 / 已关闭。 游戏里没有弹窗提示 —— 最直观的反馈就是锁链当场消失或出现。
十一、v0.1.4:链条不再穿模
为什么之前会穿:v0.1.3 及以前,锁链是用 LineRenderer 画的两点直线
(positionCount = 2),从你直接连到队友 —— 一个碰撞检测都没有。
两点之间隔着山、隔着墙,它当然直接穿过去。
现在的做法:换成物理模拟的链条。
- 在两个玩家之间铺一串链节(质点),Verlet 积分推进 —— 链节受重力,会自然下垂、摆动;
- 每个链节对场景做碰撞,两层保险:
- 球体扫掠:把链节这一帧的位移做成球体扫掠,撞到东西就停在接触点,绝不穿过去;
- 推出兜底:对仍然嵌在碰撞体里的链节(比如被两块地形夹住),用
Physics.ComputePenetration沿最短方向推出来;
- 接触锚点(静摩擦):链节贴上地形时记住那个位置并反复被拽回去,所以链条能"钩"在山壁、 石头棱上,而不是顺着表面滑走。
把 ChainPhysics 改成 false 可以退回两点直线,用来对比确认效果。
顺手修掉的一个隐患
移植时发现原来的球体扫掠用的是 Physics.SphereCast,它只返回第一个命中。
如果第一个命中的是玩家自己的身体,代码会把它丢弃 —— 但它背后的地形就再也不会被检测,
那条链节照样穿过去。现在改成取出全部命中、挑第一个「非玩家」的,
并改用 SphereCastNonAlloc / OverlapSphereNonAlloc 预分配缓冲,不再每帧产生 GC。
性能
- 只有本地玩家的控制器模拟链条。否则每个客户端会为场上每个角色各模拟一条链, 4 人局就是 4 条,开销翻 4 倍。
- 链节数量有上限(
ChainMaxPoints,默认 24)。
十二、v0.1.3:锁链拖拽
链条绷紧(超过最大距离的 75%)之后,机制启动。判定看的是双方速度差,不依赖"谁在物理上的前面":
| 局面 | 结果 |
|---|---|
| 我在走,队友基本不动 | 我拖着他 —— 我承受一个朝他的阻力,越拖越重(4 秒内从 0 涨到满) |
| 队友在走,我基本不动 | 我被他拽 —— 我承受一个朝他方向、把我拉过去的力 |
| 都在走 / 都没走 | 不施力 |
他一开始走、或者距离拉回 75% 以内,拖累立刻归零,负重感马上消失。
「负重」为什么不是游戏原生的负重状态:PEAK 确实有 STATUSTYPE.Weight,但
CharacterAfflictions.UpdateWeight() 每帧会根据背包里物品的 CarryWeight 重算它 ——
外部写进去的值会被覆盖。所以这里改用等效的物理阻力来做「负重感」。
十三、v0.1.2:大厅不出链条
PEAK 的「大厅」就是开局那个机场集合区。判定用游戏自带的 public 属性 Character.inAirport:
只要还在机场,不画连线、不共享状态、不掉队惩罚、不施力 —— 跟没装这个 mod 一样。 真正进入跑图之后,锁链才开始绑上。
想在大厅也生效,把 DisableInAirport 改成 false。
十四、v0.1.1:掉队惩罚为什么之前那么狠
v0.1.0 往「受伤(Injury)」上每秒累积 超出米数 × 0.35。但 currentStatuses 是
0~1 归一化的数组(索引即 STATUSTYPE:Injury=0、Hunger=1、Cold=2、Poison=3、Weight=7)。
拉开 10 米,每秒就往满格里怼 3.5 —— 一秒顶穿 3 倍,这就是「根本没法玩」的原因。
现在改用游戏原生接口 AddStatus(STATUSTYPE.Poison, amount, ...),
默认每秒只加 3.5%,约 29 秒满一格(比原来慢 100 倍),并且:
- 有宽限:掉队满 5 秒后才开始累积;
- 距离拉回范围内立即停止(已中的毒交给游戏自身的衰减机制慢慢消退);
- 默认不共享给全队:只由掉队者本人承担(可用
SharePoison打开)。
十五、一处设计改动(重要)
企划写的是「共享生命池:全队共用一个总血量条」。但 PEAK 没有独立血条 —— 它的核心资源是耐力,而受伤(Injury)会削减最大耐力。所以「共享生命池」 落实为 共享受伤池:任何人受伤,全队一起背上同样的伤,全队的最大耐力一起下降。
十六、已实现 / 未实现
| 机制 | 状态 |
|---|---|
| 快捷键开关(可改绑) | ✅ |
| 链条物理碰撞(不穿模) | ✅ |
| 大厅(机场)不生效 | ✅ |
| 锁链拖拽(拉住 + 拖累负重) | ✅ |
| 掉队惩罚(缓慢持续中毒) | ✅ |
| 命运锁链(必须联机) | ✅ |
| 共享伤害 / 共享生命池 / 共享饱食度 | ✅ 只传播恶化,单人疗愈有效 |
| 增益共享(吃东西喂全队) | ❌ 游戏权限不允许 |
| 难度三档、锁链变色(灰 → 黄 → 红) | ✅ |
| 随机事件:重力/视角/物品/同步动作 | ⬜ 下一版(挂点已确认) |
| 命运祭坛 / BOSS / PVP / 专属道具 | ⬜ 后续 |
十七、安装与调参
把 plugins/com.quaree.fatebound.dll 复制到 PEAK/BepInEx/plugins/,所有联机玩家都要装。
调参数改这一个文件(改数值不用重新编译):
你的PEAK安装目录\BepInEx\config\com.quaree.fatebound.cfg
文件不存在说明装好后还没启动过游戏 —— cfg 由 mod 首次加载时自动生成,先启动一次。
关于文件里那些英文:每个设置项下面的 # Setting type: 和 # Default value: 两行是
BepInEx 自己写的(框架固定格式),不是这个 mod 的文本,改不掉。mod 自己写的说明文字
(## 开头那些)全都是中文。
[FateBound] — 主开关与基础
| 配置项 | 默认 | 说明 |
|---|---|---|
Enabled |
true |
总开关 |
ToggleKey |
F9 |
游戏内切换快捷键,可改成任意按键或 None |
Difficulty |
Normal |
改动会覆盖 MaxDistance / PoisonPerSecond / ShareRatio |
MaxDistance |
30 |
锁链最大距离(米) |
GraceSeconds |
5 |
掉队宽限秒数 |
PoisonPerSecond |
0.035 |
掉队每秒累积的中毒量。太快调 0.015,太慢调 0.06 |
DisableInAirport |
true |
大厅阶段是否不生效 |
ShareRatio |
1.0 |
共享强度 0~1 |
ShareHunger / ShareInjury |
true |
共享饱食度 / 共享受伤 |
SharePoison |
false |
是否把掉队中毒也共享给全队 |
DrawChain |
true |
是否画锁链 |
ChainWidth |
0.08 |
链条渲染宽度 |
RequireTwoPlayers |
true |
少于 2 人不生效 |
[FateBound.Chain] — 链条物理
| 配置项 | 默认 | 说明 |
|---|---|---|
ChainPhysics |
true |
物理链总开关。改 false 退回两点直线(对比排查用) |
ChainSpacing |
0.45 |
链节间距(米)。越小越贴合地形,开销越大 |
ChainThickness |
0.05 |
链节碰撞球半径(米)。还是偶尔穿就调大到 0.1 |
ChainSlack |
0.01 |
松弛比例。越大越松、下垂越明显,0.12 会让链条垂到地上 |
ChainMaxSag |
0.35 |
最大下垂(米),防止链条趴到地上的兜底;0 = 不限制 |
ChainMaxPoints |
24 |
链节数上限(性能保护) |
[FateBound.Tug] — 拖拽
| 配置项 | 默认 | 说明 |
|---|---|---|
EnableTug |
true |
拖拽总开关 |
ChainTautRatio |
0.75 |
最大距离的百分之多少开始绷紧 |
MaxPullForce |
160 |
被拽的力。拽不动调大(320),太猛调小(80) |
MaxDragForce |
100 |
拖累阻力上限(负重感强度) |
DragSpeedDiff |
0.5 |
速度差超过多少米/秒才算「一个走一个没走」 |
DragRampSeconds |
4 |
阻力从 0 涨到满要几秒 |
难度预设:
| 难度 | MaxDistance | PoisonPerSecond | ShareRatio |
|---|---|---|---|
| Easy | 50 米 | 0.02 | 0.6 |
| Normal | 30 米 | 0.035 | 1.0 |
| Hell | 15 米 | 0.07 | 1.0 |
自己编译:./build.sh "/path/to/PEAK"(Windows 用 build.bat)。
十七、进游戏后的验证清单
- 加载:控制台出现
FateBound v0.1.7 已加载,难度=Normal,切换键=F9,无红色报错。 - 快捷键(本次重点):进游戏后按
F9,锁链应当当场消失,再按一次当场出现; 控制台能看到对应的开 / 关日志。改ToggleKey后再进游戏,新按键应当生效。 - 不再穿模:让两个人中间隔着一块大石头 / 山脊 / 墙, 链条应该沿着地形绕过去,而不是直接穿过物体。
- 链条在两人之间(本次重点):两人相隔 8~15 米站好,链条应该悬在两人之间、
从你的躯干连到队友躯干,而不是趴在地上。轻轻下垂是正常的,贴地就是没修好。
还贴地就把
ChainMaxSag调到0.2。 - 还穿就调
ChainThickness到0.1—— 这是最直接有效的参数。 - 大厅不出链条:多人待在机场时不应看到任何连线。
- 拉拽手感:链条绷紧后,站着不动的那个人应该被拽着动起来; 走的那个人应该感到越走越吃力(约 4 秒到最重),一停就消失。
- 掉队中毒速度:拉开超过 30 米、保持 5 秒后才开始中毒,状态条应缓慢爬升; 距离拉回 30 米内立刻停止。
十七、已知限制
- 未实机验证:编译环境离线搭建(真实
Assembly-CSharp.dll+ 真实 BepInEx 5.4 + 真实 UnityEngine), 编译 0 warning / 0 error,产物引用的类型与成员逐项核对通过,但没有在游戏里跑过。 - 快捷键只影响本机:联机时关掉只是你这边不生效,不会同步给队友。
- 穿模不是零风险:链节是离散的(默认间距 0.45 米)。如果地形上有一道比链节间距还窄的
薄缝、正好落在两个链节之间,那一段仍可能看起来穿过去。
ChainSpacing调小能改善,但开销上升。 - 性能:链条物理只在本地玩家那条链上跑;4 人局每客户端 1 条。觉得卡就把
ChainMaxPoints调小(例如16)。 - 只模拟自己这条链:你看到的是"你到最近队友"那条。队友的客户端会各自画他们的。
- 力度量纲是估的:
MaxPullForce/MaxDragForce没有 PEAK 实机标定,需要按第九节第 7 条微调。 - cfg 里有两行英文:
# Setting type:/# Default value:由 BepInEx 框架生成,无法改。