PEAK
Install with App

Details

Latest version
0.2.13
Last Updated
First Uploaded
Downloads
31
Likes
1
Size
51KB
Dependants

更新日志

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:链条不再是物理模拟,改成纯几何求解 —— 实测穿模归零

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

  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 次

[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. 下垂量过大。 链条总长被设成 距离 × (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), 高度本身是合理的。所以这个症状不是挂点造成的,是下垂量与地面粘连造成的。


[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 积分推进(受重力、会下垂摆动), 每个链节对场景做碰撞,碰到地形/模型就停在表面。

防穿模的两层保险

  1. 球体扫掠:把链节这一帧的位移做成球体扫掠,撞到东西就停在接触点,绝不穿过去。
  2. 推出兜底:对仍然嵌在碰撞体里的链节(比如被两块地形夹住),用 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 个专属道具。
Thunderstore development is made possible with ads. Please consider making an exception to your adblock.