FateBound
命运绑定:所有玩家被一条命运锁链连在一起,祸福同担。链条有物理模拟、会碰撞地形不穿模;前面的人走会拉住后面的人。共享受伤与饥饿,掉队缓慢中毒、距离回正即停。大厅不生效,至少 2 人联机才能玩。配置项与游戏内提示全中文,链条颜色可自选。
By ETWML
| Date uploaded | 16 hours ago |
| Version | 0.2.5 |
| Download link | ETWML-FateBound-0.2.5.zip |
| Downloads | 22 |
| Dependency string | ETWML-FateBound-0.2.5 |
This mod requires the following mods to function
BepInEx-BepInExPack_PEAK
BepInEx pack for PEAK. Preconfigured and ready to use.
Preferred version: 5.4.75301README
FateBound 命运绑定 — v0.2.5
最新版(v0.2.5):配置项全部汉化
现在打开配置文件长这样
[命运绑定]
## 总开关。false 时这个 mod 完全不干预游戏。
启用 = true
## 最大锁链距离(米)。
最大锁链距离 = 30
[命运绑定.链条]
## 链条底色。十六进制色号,可以带 # 也可以不带。
链条颜色 = "#C08A4A"
分组名和键名全是中文,# 开头的注释本来就说中文。
旧配置不会丢,不用删配置文件
这一点特意做了处理。BepInEx 只在"键不存在"时才写默认值 —— 光改键名的后果是: 你以前调过的数字留在旧英文键里变成孤儿,新中文键拿到默认值, 表现就是"我明明调过,怎么又变回去了"。
所以启动时会自动做一次迁移:把旧英文键里的值搬到对应的中文键下, 然后把旧键从文件里删掉,不会留下两份、也不会留下孤儿项。
判断依据是值本身 —— 新键上的值如果和旧键一致就不动, 所以重复启动是无害的空操作。迁移过几项会写进日志。
键名对照
| 旧键名 | 新键名 |
|---|---|
Enabled |
启用 |
ToggleKey |
启停快捷键 |
Difficulty |
难度 |
MaxDistance |
最大锁链距离 |
GraceSeconds |
超距宽限秒数 |
PoisonPerSecond |
中毒速度 |
ShareRatio |
共享伤害比例 |
ShareHunger |
共享饥饿 |
ShareInjury |
共享受伤 |
SharePoison |
共享中毒 |
DrawChain |
绘制链条 |
ChainWidth |
链条粗细 |
RequireTwoPlayers |
需两人以上 |
DisableInAirport |
大厅内不生效 |
NotifyShared |
共享时弹提示 |
DebugKey |
调试读数键 |
ChainPhysics |
物理链条 |
ChainSpacing |
链环间距 |
ChainThickness |
链条厚度 |
ChainSlack |
链条松弛量 |
ChainMaxSag |
最大下垂 |
ChainHideRadius |
贴脸隐藏半径 |
ChainMaxPoints |
链节上限 |
ChainMaxLength |
链条最大长度 |
ChainTopology |
链条拓扑 |
ChainLinkTexture |
链环贴图 |
ChainTextureScale |
贴图密度 |
ChainTautFeedback |
绷紧反馈 |
ChainColor |
链条颜色 |
ChainColorCycleKey |
换色快捷键 |
SyncFromHost |
跟随房主参数 |
SyncIntervalSeconds |
同步间隔秒数 |
EnableTug |
启用拖拽 |
TugStartDistance |
拖拽起始距离 |
ChainTautRatio |
绷紧比例(已废弃) |
MaxPullForce |
最大拉力 |
MaxDragForce |
最大拖拽力 |
DragSpeedDiff |
拖拽速度差 |
DragRampSeconds |
拖拽渐满秒数 |
分组名对照:FateBound → 命运绑定,FateBound.Chain → 命运绑定.链条,
FateBound.Sync → 命运绑定.同步,FateBound.Tug → 命运绑定.拖拽。
顺手修掉一个重复绑定
ChainLinkTexture 和 ChainTextureScale 之前被绑定了两次(上一次编辑留下的重复块),
现在只保留一份。
汉化的效果范围
- 配置文件的分组名、键名、注释:中文
- 游戏内通知栏提示:中文("命运锁链绷紧了:有人在硬拽"、"链条颜色:古铜")
- 日志前缀:
命运绑定 v0.2.5 已加载,难度=Normal,切换键=F9 难度的值仍填Easy/Normal/Hell—— 这是枚举值,改了会让老配置失效, 所以保持英文,旁边有中文说明。- 插件标识
com.quaree.fatebound保持英文 —— 这是模组身份,改了 BepInEx 就认不出来。
最新版(v0.2.4):链条颜色可以选
为什么改
之前松弛状态的链条颜色是写死的冷灰白 (0.72, 0.76, 0.80) —— 看着确实发怪,
而且铁链本来就不该是白色。现在底色由你定。
怎么选
方式一:改配置(BepInEx\config\com.quaree.fatebound.cfg)
[FateBound.Chain]
ChainColor = "#C08A4A"
十六进制色号,带不带 # 都行。默认 #C08A4A 是古铜色。
方式二:在游戏里按 F8 循环切换预设色板,立刻生效、自动写回配置,并在通知栏报出颜色名:
| 色号 | 名称 | 色号 | 名称 |
|---|---|---|---|
#C08A4A |
古铜(默认) | #3CB371 |
翠绿 |
#9AA3AD |
钢银 | #5B8BD4 |
湖蓝 |
#E0C36B |
暗金 | #8A6FD1 |
紫晶 |
#B22222 |
深红 | #2E2E2E |
玄铁黑 |
底色和绷紧变色怎么共存
底色只是松弛时的颜色。绷紧时仍然会从你的底色往炽橙红过渡,0.85 绷紧度就到顶。 所以颜色有两个作用:一是你自己喜欢的样子,二是绷紧时依然一眼看出"不对劲"。
一个提醒:别把底色选成很接近红色的(比如深红 #B22222),那样绷紧变色就看不出来了。
纯属审美取舍,你想要全红也行。
写错了不会崩
色号解析失败(长度不对、有非法字符)会退回古铜色,链条不会变黑、不会消失。 解析结果带缓存,不会每帧做字符串处理。
新增配置
| 配置项 | 默认 | 作用 |
|---|---|---|
ChainColor |
#C08A4A |
链条底色(十六进制) |
ChainColorCycleKey |
F8 | 游戏内循环切换预设色。填 None 关闭 |
两项都不参与主机同步。
最新版(v0.2.3):让拉力看得见
问题在哪
链条本来就有颜色,按 距离 / MaxDistance 上色 —— 那是一条 0 到 30 米的曲线。
但拖拽在 6 米就已经开始了。
于是你实际看到的是:一条 15 米的链只有半程色,看着还很"温",可你早就被拽住了。 颜色和拉力各讲各的故事,玩家自然得不出「我正被拉」的结论。
改法
颜色和拉力现在共用同一条曲线(6 米起、30 米满,开方):
| 绷紧度 | 链条颜色 | 含义 |
|---|---|---|
| 0(≤6 米) | 冷灰白 | 松弛,不施力 |
| 0.35(约 10 米) | 暖黄 | 开始有阻力 |
| 0.5(约 12 米) | 橙 | 明显在被拽,弹一次「命运锁链绷紧了:有人在硬拽」 |
| 1.0(30 米) | 亮红 | 满力 |
提示带迟滞(0.5 以上触发、掉到 0.3 以下才复位)加 3 秒节流,不会抖着刷屏。 而且只在你身上的那条链才提示 —— 队友之间互相拉,不会在你屏幕上报警。
顺手收掉一个隐患
FixedUpdate 里的拉力公式原本是手写的一份,和上色用的公式各写各的。
两处只要有一处被改动就会悄悄对不上 —— 上面那个 bug 就是这么来的。
现在统一走 Tautness(),只有一个源头。
新增配置
| 配置项 | 默认 | 作用 |
|---|---|---|
ChainTautFeedback |
true | 绷紧时变色 + 提示。关掉就只剩拉力本身 |
同样不参与主机同步。按 F10 的读数里也多了「绷紧度」,0.00 松、1.00 满、≥0.50 会弹提示。
最新版(v0.2.2):让共享看得见,顺便修一个真 bug
一、共享以前是「隐形」的,这是设计失误
v0.2.1 之前,共享发生时传给游戏 AddStatus 的是 playEffects=false, notify=false
—— 也就是「不刷特效、不刷提示」。
结果是:队友摔了一跤,你这边真的被分摊了受伤,但你的屏幕上没有任何东西告诉你, 只能靠自己最大耐力条悄悄变短去猜。玩家的合理反应就是「共享好像没实现」。
现在共享生效时会主动弹一条游戏内提示:
| 触发 | 提示内容 |
|---|---|
| 队友受伤被分摊 | 命运锁链:队友受伤,你被分摊 -12% |
| 队友饥饿被分摊 | 命运锁链:队友饿了,你的饥饿也上来了 +8% |
| 队友中毒被分摊 | 命运锁链:队友中毒,毒素传给你了 +5% |
三类各自节流,1.2 秒内不重复,不会刷屏。想回到以前那种安静模式,把 NotifyShared 改成 false。
二、按 F10 打印一份状态读数
新增调试键(默认 F10)。按一下会:
- 在游戏内通知栏逐行列出每个人的受伤 / 饥饿 / 中毒数值,以及和你的距离;
- 同时追加写入
BepInEx\fatebound-debug.txt(两行之间留空行,方便事后翻)。
用途很直接:让你自己一眼确认「共享到底有没有生效」,不用再靠感觉。 读数是这么写的:
===== FateBound 调试读数 2026-09-27 11:42:03 =====
本机生效=True 场上玩家=2 最近队友距离=8.3m
★我 受伤 0.000 饥饿 0.120 中毒 0.000 距离 0.0m
队友 受伤 0.250 饥饿 0.120 中毒 0.000 距离 8.3m
上面这份的意思就是:队友受伤 0.25,我也被补到了 0.25 —— 共享生效了。 如果两行数字一直不跟着动,那才是真没生效,可以带着这份读数来找我。
调试键同样在大厅里也能按,方便你进游戏先确认按键绑上没有。
注意它放在 Enabled 判断之前 —— 就算总开关关着,也能按出来排查问题。
三、修掉一个真 bug:中途打开共享开关会一次性灌一大坨
共享的实现方式是「记住全队最高值的基线,只传播恶化的增量」。
但基线只在首帧记一次 —— 如果你中途把 ShareInjury 从关改成开,
基线还停在关掉那一刻的旧值上,下一次全队有人受伤,会一次性把攒了很久的一大坨全灌给你。
现在开关一发生变化就立刻重新记基线,把积压的增量吞掉。
四、新增两个配置项
| 配置项 | 默认 | 作用 |
|---|---|---|
NotifyShared |
true | 共享生效时弹游戏内提示。关掉回到安静模式 |
DebugKey |
F10 | 调试读数按键。填 None 可关闭 |
这两项故意不参与主机同步 —— 它们是每台机器自己的显示偏好。 也正因为它们不进同步包,v0.2.2 和 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),
所以每个人读得到队友的数值,但只能写自己。
结论(v0.2.2 修正):企划里的「增益共享:有人吃到恢复/力量,全队都获得」——
客机之间确实做不到(AddStatus / SetStatus 的 IsMine 门禁),
但房主可以做到:RPC_ApplyStatusesFromFloatArray 上有 Sender.IsMasterClient 门禁,
房主发来的包会被接受,而接收方是在自己的客户端上把它写进自己的角色,
IsMine 那一关自然就过了。
所以上一版这里写「做不到」是过度悲观了。正确说法是「只有房主能做」—— 而房主本来就是定规则的人,这个设计反而说得通。后续版本会按「房主广播」的方式补上。 吃东西只回你自己的。想要这个效果只能走 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)会削减最大耐力。所以「共享生命池」 落实为 共享受伤池:任何人受伤,全队一起背上同样的伤,全队的最大耐力一起下降。
十六、已实现 / 未实现
| 机制 | 状态 |
|---|---|
| 快捷键开关(可改绑) | ✅ |
| 链条物理碰撞(不穿模) | ✅ |
| 大厅(机场)不生效 | ✅ |
| 锁链拖拽(拉住 + 拖累负重) | ✅ |
| 掉队惩罚(缓慢持续中毒) | ✅ |
| 命运锁链(必须联机) | ✅ |
| 共享伤害 / 共享生命池 / 共享饱食度 | ✅ 只传播恶化,单人疗愈有效 |
| 增益共享(吃东西喂全队) | ⚠️ 客机做不到,房主可以(v0.2.2 修正);待实现 |
| 难度三档、锁链变色(灰 → 黄 → 红) | ✅ |
| 随机事件:重力/视角/物品/同步动作 | ⬜ 下一版(挂点已确认) |
| 命运祭坛 / 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 框架生成,无法改。
CHANGELOG
更新日志
v0.2.13:拉力新增「竖直拉力比例」
- 拉力是按三维方向算的(从我指向队友),所以头上有队友时,力里天生带一个向上的分量 —— 全量施加会把人往上吊,这就是「上面的人把我拽得飘起来」的来源。
- 新增
竖直拉力比例(默认 0.35):1.0按原始方向全量拉,0.35竖直压到三成,0.0拉力完全落在水平面上。本地手感设置,不参与主机同步 —— 它只影响你自己角色的受力。 - 顺便说明当前规则:多个队友同时拉你时,每一条链各拉一份、矢量相加,不是只挑最近的一个。
v0.2.12:配置热重载,改完不用重启游戏
- 改完设置约 1 秒自动生效,不用重上游戏。模组每秒检查一次,改完会弹提示。
- 用记事本直接改
.cfg→ 比对文件内容发现,然后重新读盘; - 用配置管理器类模组改 → 它改的是内存里的值,靠比对「难度」档位发现。
- 用记事本直接改
- 除难度外的配置项本来就每帧读,改完当场就是新的;只有难度是"一键套三个值",必须重算 —— 现在会重算并立刻同步给客机。
- 两个坑都绕开了:
- 重算难度会把值写回文件、文件时间戳一变就会自己触发自己 → 所以比对的是文件内容而不是时间戳;
- 客机在房间里时参数的权威在房主那边,本地
Reload会把同步来的值冲掉并来回震荡 → 这种情况下跳过文件热重载,一切听房主的。
v0.2.11:难度新增「自定义」档
- 原来只有 简单 / 普通 / 地狱 三档,而每次启动都会按档位覆盖 「最大锁链距离 / 中毒速度 / 共享伤害比例」—— 手动调过的数值下次进游戏就被改回去, 而且没有任何办法关掉这个覆盖。
- 新增
Custom(自定义)档:选它,这三项完全按你自己填的值来,模组不碰。
v0.2.10:多人支持的三个缺口
- 修掉「需两人以上」开关从未生效:这一项原来只 Bind、没有任何代码读它, 改成 false 毫无作用。现在真的读了 —— 关掉后单人也能绑锁链。
- 链条条数上限 8 → 16:6 人用「两两互连」是 15 条链,原来被砍到 8 条, 表现是「有些人身上没链子」。
- 拉力从「只看最近一个人」改成「看每一条链」:3 人以上时一个人可能同时挂在两条链上, 原来只对最近的施力,另一条链等于不存在。现在遍历本帧所有链, 凡是有我参与的就逐条算拉力再叠加。
- 速度采样由「只记最近队友一个速度」改为每个玩家一个速度向量(
velBuf,按下标对齐)。 - 位置突变(瞬移/重生/切场景)的那一帧按静止处理,避免算出天文数字的施力。
v0.2.9:重写拖拽拉力,不再「乱拉」
- 触发条件改为沿锁链轴向判定:只看速度向量在锁链轴上的投影,不再用速度大小之差。 朝队友跑、侧向平移、两人同向跑这些情况现在都不会再触发拉力。
- 阻力方向改回沿锁链:旧版用的是「我上一帧移动方向的反向」(
-selfDir), 跟锁链轴向毫无关系 —— 这是「乱拉」最主要的一条。现在阻力沿锁链、朝队友。 - 最近队友加 2 米黏性:避免两个队友距离接近时每帧换人、施力方向来回翻。 换人时清掉速度向量重新采样。
- 速度采样由「标量大小 + 自身移动方向」改为「速度向量」(
selfSpeed/otherSpeed/selfDir→selfVel/otherVel)。
更早的版本(点击展开)
v0.2.8:锁链启停由房主统一控制
- 新增「启停由房主统一控制」(默认开):联机时房主按 F9 → 全队一起开/关; 客机按 F9 无效,会弹提示。目的是堵住「有人为了跑快点把锁链一关就溜了」。 单机和房主本人不受限制。
- 房主按完立刻广播,不用等同步周期(新增 ForceSyncNow)。
- 客机收到开关变化会弹提示:「命运锁链被房主开启 / 关闭了」。
- 同步字段新增
Enabled、ToggleByHost,包长度变化。接收端有长度校验, 混版本只会「同步不生效」,不会参数错乱 —— 但仍建议全房间一起升到 0.2.8。
v0.2.7:按键看得见,版本看得见
- 修掉「按 F10 屏幕上什么都没出现」:
DumpDebug()原来只把玩家逐行弹到通知栏, 而大厅里players是空的,于是一行都不弹。现在按 F10 一定先弹总览行, 没读到玩家时会明确提示「大厅内不生效,请进关卡再按」。 - 进关卡第一次生效时报版本号:通知栏弹「命运绑定 v0.2.7 已生效 F9 开关 F10 读数」。 用来确认拖进去的新 DLL 到底有没有覆盖成功。
- 读数标题行也带上了版本号。
v0.2.6:链子不会再断
- 修掉「有人跑远链子就消失」:
BuildPairs()里原有一个"距离超过 ChainMaxLength 就不画这条链"的判断,现在删掉了。链子多远都一直连着。 - 不靠断链来防穿模了:链节上限由 56 提到硬上限 200 节(≈110 米)。
PhysicsChain.CountFor()本来就支持按距离自适应加节,只是被 maxPoints 卡住了。 加节之后相邻链节始终维持在「链环间距」附近,每个链节都在做地形检测,所以既不穿模也不断。 - 删除配置项
ChainMaxLength(就是它导致断链)。 ChainMaxPoints改名为链节基础数量,语义从"上限"变成"下限"。- 同步包格式因此变化,同房间所有人需一起更新到 0.2.6。
v0.2.5:配置项全部汉化
- 4 个分组名和 39 个键名全部改成中文(
最大锁链距离、链条颜色、共享饥饿……), 注释本来就是中文。 - 自动迁移旧配置:启动时把老英文键里的值搬到对应中文键下,再把旧键删掉。 所以不用删配置文件,之前调过的参数也不会丢。
- 修掉
ChainLinkTexture/ChainTextureScale被重复绑定两次的问题。 - 描述文字里残留的英文键名(
配合下面的 ChainTextureScale 一起调之类)一并改成中文。 - 游戏内日志前缀由
FateBound改为命运绑定。 - 实测确认 BepInEx 能正确读写中文分组名/键名(含带点的中文分组名),
且
Remove后旧键会从文件里消失、不会留下孤儿。
v0.2.4:链条颜色可以选
- 底色从写死的冷灰白
(0.72,0.76,0.80)改为可配:ChainColor(十六进制色号), 默认#C08A4A古铜色。 - 新增
ChainColorCycleKey(默认 F8):游戏内按预设色板循环切换,立刻生效并写回配置, 同时在通知栏报出颜色名。 - 预设 8 色:古铜 / 钢银 / 暗金 / 深红 / 翠绿 / 湖蓝 / 紫晶 / 玄铁黑。
- 色号解析带缓存(每帧每条链都会调用,不能重复做字符串处理); 解析失败退回古铜色,不会变黑或消失。
- 绷紧变色保留:从你的底色往炽橙红过渡,0.85 到顶。
- 两项新配置均不参与主机同步。
v0.2.3:让拉力看得见
- 链条颜色改走和拖拽完全相同的曲线(原先按
Distance/MaxDistance,30 米才变红, 而拖拽 6 米就开始了 —— 这就是「看不到拉力」的根因)。 现在:冷灰白 → 暖黄(约10m) → 橙(约12m) → 亮红(30m)。 - 绷紧过 0.5 弹一次「命运锁链绷紧了:有人在硬拽」,迟滞 0.5/0.3 + 3 秒节流, 且只对挂在自己身上的链生效。
- 收掉隐患:
FixedUpdate手写的拉力公式改为调用Tautness(),颜色/提示/受力三者同源。 - 新配置
ChainTautFeedback(默认 true,不参与同步)。 - F10 读数新增「绷紧度」与拖拽开关状态。
v0.2.2:共享看得见 + 调试读数 + 修一个真 bug
1. 共享不再是隐形的
以前共享时传的是 playEffects=false, notify=false,发生了但屏幕上毫无动静,
玩家只能猜,表现就是「共享好像没实现」。
现在共享生效会弹游戏内提示:命运锁链:队友受伤,你被分摊 -12%。
三类各自节流 1.2 秒。新配置 NotifyShared(默认 true)可关。
2. 新增调试读数键(默认 F10)
按一下在游戏内逐行列出每个人的 受伤 / 饥饿 / 中毒 与距离,
并追加写入 BepInEx\fatebound-debug.txt。
目的:让玩家自己一眼验证共享有没有生效。新配置 DebugKey。
调试键在大厅里也能用,且放在 Enabled 判断之前。
3. 修 bug:中途打开共享开关会一次性灌一大坨
基线只在首帧记一次,开关从关改成开时基线是旧的, 下一次有人受伤会把积压的增量整份补给自己。 现在开关一变化就立即重新记基线。
4. 兼容性
NotifyShared / DebugKey 不参与主机同步,同步包格式未变 ——
v0.2.2 与 v0.2.1 的客户端仍可互相同步。
5. README 修正
上一版把「增益共享」判成「游戏权限不允许」,属于过度悲观。
正确结论:客机做不到,但房主可以通过 RPC_ApplyStatusesFromFloatArray
(有 Sender.IsMasterClient 门禁)把状态推给全队。后续实现。
一、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 次
[0.1.9] — 修掉「贴脸的大白楔子」与「方块树木穿模」,并补上 0.1.7 漏掉的调用
本版把 0.1.8 与 0.1.9 的内容合并交付,是唯一需要安装的版本。
问题一:链条贴在你脸前,看起来像"下面没有人却连着一条链子"
你截图里那个朝下伸的白色大楔子,就是链条本身。
链条起点取的是 Character.Center(你自己的躯干),而第一人称下摄像机就在你脑袋上 ——
两者相距只有 0.2 米左右。按 0.08 米的链宽、60° 视场反推,这一节在屏幕上会占掉
近 300 像素宽,于是被渲染成一个巨大的白色楔子;从你的视角看它就是"朝下伸进虚空,
下面没人"。截图上方那几条细白线,才是链条正常的那部分。
颜色也对得上:ApplyChainColor 在近距离给的正是 (0.55, 0.6, 0.65) 浅灰蓝。
修法:近摄像机段裁剪。落在摄像机 ChainHideRadius(默认 0.85 米) 以内的起始链节
会被丢掉,画面上的链条从稍远处开始。这是第一人称绳索的常规做法 —— 你看不到自己身上的挂点,
只看到绳子从身前伸出去。直线调试模式同样处理(起点沿指向队友的方向推出去)。
问题二:跟方块、树木、岩石穿模
ComputePenetration 不支持非凸 MeshCollider,而 PEAK 的岩石、树木、木箱全是这种 ——
所以旧版对这些物体根本没有推出能力,链节钻进去就出不来。
修法:新增 ResolvePenetration() —— 六向射线推出。对每个链节朝上下左右前后各打一条
radius 长的射线,哪边在半径内碰到东西,就朝反方向推开对应距离。射线不受碰撞体类型限制,
任何碰撞体都能被打中。
局限说明:它只能救"浅层嵌进"(陷进去不到链节半径)。嵌得很深时六个方向都朝外、射线打不到 近处表面 —— 这种深层情况由球体扫掠在进入前拦住,不让它陷深。
问题三:补上 v0.1.7 漏掉的调用
v0.1.7 声称"修掉了链条趴地",但 ClampSag() 一行都没有执行:方法写进了文件,
Step() 里插入调用语句的那次替换静默失配(我用的定位锚点是 ResolveCollisions();,
而成品代码该行前面还有一行 Array.Clear(collided, 0, count);)。
本版补上,并新增 ClampAboveGround() —— 用向下短射线找脚下地形表面,
绕过"SphereCast 在球体已重叠时不返回命中"和"ComputePenetration 不支持非凸网格"这两个盲区。
探针刻意只往上 0.6 米(避免误命中头顶岩架),并要求命中面法线朝上(避免被墙面误触发)。
这次的核验方式
上一版失手的根因是"只查成员存在、不查是否被调用"。本版改成把编出来的 DLL 反编译回来看 方法体,实际读到的调用链是:
PhysicsChain.Step(): Array.Clear -> ResolveCollisions -> ResolvePenetration
-> ClampSag -> ClampAboveGround -> UpdateContactAnchors -> MeasureTension
PhysicsChain.Reset(): ResolveCollisions -> ResolvePenetration -> ClampSag -> ClampAboveGround -> UpdateContactAnchors
FateBoundController.Draw(): 相机分支里确认存在 Camera.main 与 NearCameraTrim
新增配置项
| 项 | 默认 | 说明 |
|---|---|---|
ChainHideRadius |
0.85 |
近摄像机裁剪半径(米)。贴脸还是有白块就调到 1.0;想看原状设 0 |
装好后 cfg 会自动多出这一项,不用删配置文件。
[0.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),
高度本身是合理的。所以这个症状不是挂点造成的,是下垂量与地面粘连造成的。
[0.1.6] — 修掉「吃东西不回饥饿值」
问题
装上 mod 后吃东西不回饥饿值。这不是游戏的问题,是 v0.1.1 我写的共享逻辑的缺陷。
旧实现是「每帧把每个人拉到全队最高值」。它等于一个单向棘轮:你吃一口东西、 饥饿值降下来,下一帧立刻又被拉回队友那个高值 —— 表现就是吃东西完全没用。 同一个缺陷也让「治疗 / 解毒」这些单人疗愈动作全部失效。
根因(反编译真实 Assembly-CSharp 确认)
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 硬塞,那属于绕过游戏权限模型,有风险,我没有默认做。
另一个细节
AddStatus 内部对 Injury(0) 会把剂量 ×8,所以共享受伤时先按 1/8 折算回去,
否则一份摔伤传给全队会变成 8 倍。
[0.1.5] — 可改绑的开关快捷键
新增
- 游戏内切换开关的快捷键,默认
F9,可在配置里改成任意按键(ToggleKey)。 设成None可以彻底关掉快捷键。默认用 F9 是因为原来那个 Chained 模组占的是 F10, 两个都装的话 F9 / F10 各管一个,不会打架。 - 快捷键只影响你自己的客户端;每次切换会在 BepInEx 控制台打印一行
FateBound:命运锁链已开启 / 已关闭。画面上最直接的反馈就是锁链当场出现或消失。
技术说明
- 快捷键用旧版
UnityEngine.Input.GetKeyDown读取。这是安全的:反编译Assembly-CSharp后确认游戏自己在调用Input.GetKey / GetKeyDown / GetKeyUp, 并且引用了UnityEngine.InputLegacyModule—— 说明输入处理并没有被切成纯新版 InputSystem。 - 快捷键的轮询放在插件自身的
Update()里(每帧一次),而不是放在每个角色的FateBoundController上(那样 4 人局会重复检测 4 次)。
[0.1.4] — 链条不再穿模
修复
- 链条穿模。v0.1.3 及以前,锁链是用
LineRenderer画的两点直线(positionCount = 2), 从玩家直接连到玩家 —— 没有任何碰撞检测,所以必然穿过墙和地形。 - 改为物理模拟的链条:在两个端点之间铺一串链节,Verlet 积分推进(受重力、会下垂摆动), 每个链节对场景做碰撞,碰到地形/模型就停在表面。
防穿模的两层保险
- 球体扫掠:把链节这一帧的位移做成球体扫掠,撞到东西就停在接触点,绝不穿过去。
- 推出兜底:对仍然嵌在碰撞体里的链节(比如被两块地形夹住),用
Physics.ComputePenetration沿最短方向推出来。
顺手修掉的一个隐患
原来的球体扫掠用的是 Physics.SphereCast,它只返回第一个命中。如果第一个命中的是
玩家自己的身体,代码会把它丢弃 —— 但它背后的地形就再也不会被检测,那条链节仍会穿过去。
现在改成取出全部命中、挑第一个「非玩家」的,并改用 SphereCastNonAlloc / OverlapSphereNonAlloc
预分配缓冲,不再每帧产生 GC。
性能
- 只有本地玩家的控制器模拟链条(
Character.IsLocal)。否则每个客户端会为场上每个角色 各模拟一条链,4 人局就是 4 条,开销翻 4 倍。 - 链节数量有上限(
ChainMaxPoints,默认 24)。
新增配置
- 配置组
FateBound.Chain:ChainPhysics、ChainSpacing、ChainThickness、ChainSlack、ChainMaxPoints。 把ChainPhysics改成false可以退回原来的两点直线,用于对比排查。
[0.1.3] — 锁链拖拽:拉住、拖累
新增
- 拉住:链条绷紧(超过最大距离的 75%)时,落后不动的那个人会被前面的人拽着走。
- 拖累负重:前面的人拖着不走的人时,阻力在 4 秒内从 0 涨到满,体现「他一直不走,我越来越重」。
- 配置组
FateBound.Tug。
技术说明
- 施力用游戏自带的 public 入口
Character.RPCA_AddForceAtPosition(force, point, radius)。 - 没有用游戏原生的
Weight状态做负重:CharacterAfflictions.UpdateWeight()每帧会根据 背包里物品的CarryWeight重算它,外部写进去的值会被覆盖。所以改用等效的物理阻力。
[0.1.2] — 大厅阶段不生效
- 机场(游戏大厅)阶段完全不生效:不画连线、不共享状态、不掉队惩罚、不施力。
判定用游戏自带的 public 属性
Character.inAirport。配置项DisableInAirport。
[0.1.1] — 掉队惩罚改为缓慢中毒
- v0.1.0 往
Injury上按「超出米数 × 0.35 每秒」累积,但currentStatuses是 0~1 归一化数组, 一秒就顶穿 3 倍多。改用AddStatus(STATUSTYPE.Poison, amount, ...),默认每秒只加 3.5%, 约 29 秒满一格 —— 比原来慢 100 倍。 - 新增
GraceSeconds(5 秒宽限)、SharePoison(默认 false)。
量纲说明(反编译真实程序集确认)
currentStatuses是float[]、0~1 归一化,索引即STATUSTYPE:Injury=0、Hunger=1、Cold=2、Poison=3、Weight=7。- 游戏的
AddPoison()就是AddStatus(Poison, 0.2f, false, true, true, false)。
[0.1.0] — 命运绑定核心
新增
- 命运锁链(少于 2 名真人玩家不生效)、共享受伤、共享饱食度、掉队惩罚、难度三档、锁链可视化。
- 企划里的「共享生命池」落实为「共享受伤池」——PEAK 没有血条,核心资源是耐力, 受伤会削减最大耐力。
尚未实现(后续版本)
- 随机事件:重力互换、视角互换、物品转移、强制同步动作。
- 命运祭坛 / 献祭通关、共生幻兽 BOSS、多队 PVP、4 个专属道具。