ETWML-FateBound icon

FateBound

命运绑定:所有玩家被一条命运锁链连在一起,祸福同担。链条有物理模拟、会碰撞地形不穿模;前面的人走会拉住后面的人。共享受伤与饥饿,掉队缓慢中毒、距离回正即停。大厅不生效,至少 2 人联机才能玩。配置项与游戏内提示全中文,链条颜色可自选。

By ETWML
Last updated 9 hours ago
Total downloads 25
Total rating 1 
Categories Client Side All Clients Gameplay
Dependency string ETWML-FateBound-0.2.5
Dependants 0 other packages depend on this package

This mod requires the following mods to function

BepInEx-BepInExPack_PEAK-5.4.75301 icon
BepInEx-BepInExPack_PEAK

BepInEx pack for PEAK. Preconfigured and ready to use.

Preferred version: 5.4.75301

README

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)。按一下会:

  1. 在游戏内通知栏逐行列出每个人的受伤 / 饥饿 / 中毒数值,以及和你的距离;
  2. 同时追加写入 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:链条不再是物理模拟,改成纯几何求解 —— 实测穿模归零

你要的两件事,这一版都做了

  1. 链条有模型(看起来是一根真链条):新增链环贴图,一节一节的金属环、中间镂空、首尾相接。
  2. 绕开障碍物、不穿模:求解方式整个换掉,改成每帧纯几何投影。

实测数据(独立物理测试台,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. 链条长度被算成「两点直线距离 × 1.01」——它几何上只能是一根直棍, 一点多余长度都没有,想绕开石头也没有可绕的余地。
  2. 链节数量有硬上限(旧默认 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. 下垂量过大。链条总长被设成 距离 × (1 + 0.12),多出 12% 长度。对跨度 d、多余长度 ΔL, 下垂量约 √(3·d·ΔL/8) —— 跨 8 米就下垂 1.70 米,而角色躯干离地只有 0.8 米左右,必然落地。
  2. 接触锚点把链条粘死在地面。链节一贴地就记下接触点、之后每次迭代都被拽回去。 一旦落地,它就不再是「连着两个人」,而是「连着地面」。

修法:

  • 下垂量: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),从你直接连到队友 —— 一个碰撞检测都没有。 两点之间隔着山、隔着墙,它当然直接穿过去。

现在的做法:换成物理模拟的链条。

  1. 在两个玩家之间铺一串链节(质点),Verlet 积分推进 —— 链节受重力,会自然下垂、摆动;
  2. 每个链节对场景做碰撞,两层保险:
    • 球体扫掠:把链节这一帧的位移做成球体扫掠,撞到东西就停在接触点,绝不穿过去;
    • 推出兜底:对仍然嵌在碰撞体里的链节(比如被两块地形夹住),用 Physics.ComputePenetration 沿最短方向推出来;
  3. 接触锚点(静摩擦):链节贴上地形时记住那个位置并反复被拽回去,所以链条能"钩"在山壁、 石头棱上,而不是顺着表面滑走。

把 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)。


十七、进游戏后的验证清单

  1. 加载:控制台出现 FateBound v0.1.7 已加载,难度=Normal,切换键=F9,无红色报错。
  2. 快捷键(本次重点):进游戏后按 F9,锁链应当当场消失,再按一次当场出现; 控制台能看到对应的开 / 关日志。改 ToggleKey 后再进游戏,新按键应当生效。
  3. 不再穿模:让两个人中间隔着一块大石头 / 山脊 / 墙, 链条应该沿着地形绕过去,而不是直接穿过物体。
  4. 链条在两人之间(本次重点):两人相隔 8~15 米站好,链条应该悬在两人之间、 从你的躯干连到队友躯干,而不是趴在地上。轻轻下垂是正常的,贴地就是没修好。 还贴地就把 ChainMaxSag 调到 0.2。
  5. 还穿就调 ChainThickness 到 0.1 —— 这是最直接有效的参数。
  6. 大厅不出链条:多人待在机场时不应看到任何连线。
  7. 拉拽手感:链条绷紧后,站着不动的那个人应该被拽着动起来; 走的那个人应该感到越走越吃力(约 4 秒到最重),一停就消失。
  8. 掉队中毒速度:拉开超过 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 框架生成,无法改。