PEAK
You are viewing a potentially older version of this package. View Latest Version
Install with App

Details

Date Uploaded
17 hours ago
Downloads
31
Size
59KB

FateBound 命运绑定 — v0.2.1

玩家之间被命运锁链绑定:

  • 链条会碰地形,不会穿模(物理模拟);
  • 前面的人走会拉住后面的人;后面的人一直不走,前面的人会越来越吃力;
  • 一个人受伤全队一起受伤,一个人饿全队一起饿;
  • 掉队缓慢持续中毒,距离拉回来就停止;
  • 大厅(机场)阶段完全不出链条,少于 2 名真人玩家也不生效;
  • 游戏内可以按快捷键开关(默认 F9,可改绑)。

一、v0.2.1:主机调参数、客机自动同步 —— 做出来了

效果

谁开房,谁调参数,其他玩家什么都不用动。 房主每 5 秒(可调)把自己这份配置广播给全房间。

同步的内容(共 28 项):

段 项目
浮点 MaxDistance、PoisonPerSecond、GraceSeconds、ShareRatio、ChainWidth、ChainThickness、ChainSpacing、ChainSlack、ChainMaxSag、ChainHideRadius、ChainMaxLength、ChainTextureScale、TugStartDistance、MaxPullForce、MaxDragForce、DragSpeedDiff、DragRampSeconds
开关 ShareHunger、ShareInjury、SharePoison、DrawChain、ChainPhysics、EnableTug、ChainLinkTexture、RequireTwoPlayers、DisableInAirport
整数 ChainMaxPoints

不参与同步:ToggleKey(快捷键)和 Enabled(总开关)。 理由是这两个是每个客户端自己的事 —— 要是被房主同步过来, 你在自己机器上按 F9 关掉,5 秒后又被房主掰回去,那就没法用了。

新增两个配置项([FateBound.Sync]):

参数 默认 说明
SyncFromHost true 当房主时是否广播;当客机时是否接受。客机改成 false 就不被房主覆盖
SyncIntervalSeconds 5 广播周期。中途加入的人最多等这么久拿到参数

实现方式,以及为什么绕了一圈

一开始想用 PhotonNetwork.RaiseEvent 做事件广播。你给的 PhotonRealtime.dll 里确实有 RaiseEventOptions / ReceiverGroup / LoadBalancingClient.EventReceived, 但编译时发现游戏自带的这份 Photon 是被裁过的:

PhotonUnityNetworking.dll  —— 整个 DLL 只有 7 个类型、9 个方法
  Photon.Pun.PhotonNetwork 上只剩:InRoom、IsMasterClient、PlayerCount、photonView、RPC
  RaiseEvent、AddCallbackTarget、LocalPlayer、MasterClient 全都**不存在**

而 Photon3Unity3D.dll 提供的是:
  ExitGames.Client.Photon.SendOptions / EventData / Hashtable / OperationResponse

所以我改用游戏自己在用的那套机制:PhotonView.RPC + [PunRPC]。

依据是硬证据,不是猜的:

  • PhotonUnityNetworking.dll 里存在 Photon.Pun.PunRPC 类型(就是 [PunRPC] 这个特性);
  • Assembly-CSharp.dll 里能搜到 PunRPC 的引用;
  • 游戏自己有一百多个 RPCA_ 开头的方法(RPCA_AddForceAtPosition 等),走的就是这条路径。

本组件本来就挂在角色身上,而角色身上本来就有 PhotonView, 所以不需要额外创建网络物体。

参数包怎么编解码

压成一个 byte[]:魔数 "FB[1" | 浮点段长度 + 数据 | 开关段长度 + 数据 | 整数段长度 + 数据。

为什么用 byte[] 而不是 object[]:Photon 对不认识的类型在序列化上有讲究, 而 byte[] 是它一定能正确搬过去的载体 —— 用最保守的形式,跨客户端才不会有意外。

版本不一致会自动拒绝:任何一段的长度对不上,整包丢掉,不会把参数套错位。 也就是说你和我装的版本不同时,不会出现半个参数生效、半个错乱的情况。

必须说清楚的两点

① 这个功能我没法实测。 我进不了你的游戏,更没法开两个客户端联机。 能确认的是:用的是游戏自己的 RPC 机制、编译通过、[PunRPC] 特性确实挂上去了、 包格式有长度校验。但真实的双人往返我没跑过 —— 第一次联机时请留意: 如果房主改了参数客机没跟着变,把 BepInEx\LogOutput.log 发我 (需要先在 BepInEx.cfg 里把 [Logging.Disk] Enabled 改成 true)。

② 链路长度限制带来的一个副作用还在:参数同步过去之后, 房主把 ChainMaxLength 调成 0(不限)时,客机那边也会跟着不限 —— 但链越长链节之间盲区越大这个物理事实不会因为同步而改变。 建议还是保持 30 米左右。

本版核验

编译:       0 warning / 0 error,产物 34,816 字节
IL:         SyncTick 里确实调用了 get_InRoom / get_IsMasterClient / PhotonView::RPC / BuildSyncBlob
             RPCA_FateBoundSync 存在且调用 ApplySyncBlob
             PunRPC 自定义特性确实挂在方法上
             PhysicsChain.Step 的调用链仍是 v0.2.0 的几何求解(未被本版影响)

二、v0.2.0:链条不再是物理模拟,改成纯几何求解 —— 实测穿模归零

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

  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), 所以每个人读得到队友的数值,但只能写自己。

结论:企划里的「增益共享:有人吃到恢复/力量,全队都获得」在 PEAK 这套权限下做不到。 吃东西只回你自己的。想要这个效果只能走 Photon RPC 硬塞,那属于绕过游戏的权限模型, 有同步风险,我没有默认做——你要的话我可以另开一个开关。


十、v0.1.5:可改绑的开关快捷键

游戏里按 F9 就能开关命运锁链(当场生效:锁链立刻出现或消失)。

改键:打开配置文件,把 ToggleKey 改掉即可。

[FateBound]
切换命运锁链开关的快捷键(只影响你自己的客户端)。
改成 None 可以彻底关掉快捷键。默认 F9 —— 原来那个 Chained 模组用的是 F10,
两个都装的话 F9 / F10 各管一个,不会打架。
# Setting type: KeyCode
# Default value: F9
ToggleKey = F9

按键名就写 Unity 的 KeyCode 名字,例如 F8、F10、G、H、Insert、None。 没装 ConfigurationManager 也能改,直接写文本就行。

两点要说清楚:

  • 快捷键只影响你自己的客户端。联机时你关掉只是你这边不生效,队友那边照旧。
  • 每次切换会在 BepInEx 控制台打印一行 FateBound:命运锁链已开启 / 已关闭。 游戏里没有弹窗提示 —— 最直观的反馈就是锁链当场消失或出现。

十一、v0.1.4:链条不再穿模

为什么之前会穿:v0.1.3 及以前,锁链是用 LineRenderer 画的两点直线 (positionCount = 2),从你直接连到队友 —— 一个碰撞检测都没有。 两点之间隔着山、隔着墙,它当然直接穿过去。

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

  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)会削减最大耐力。所以「共享生命池」 落实为 共享受伤池:任何人受伤,全队一起背上同样的伤,全队的最大耐力一起下降。


十六、已实现 / 未实现

机制 状态
快捷键开关(可改绑) ✅
链条物理碰撞(不穿模) ✅
大厅(机场)不生效 ✅
锁链拖拽(拉住 + 拖累负重) ✅
掉队惩罚(缓慢持续中毒) ✅
命运锁链(必须联机) ✅
共享伤害 / 共享生命池 / 共享饱食度 ✅ 只传播恶化,单人疗愈有效
增益共享(吃东西喂全队) ❌ 游戏权限不允许
难度三档、锁链变色(灰 → 黄 → 红) ✅
随机事件:重力/视角/物品/同步动作 ⬜ 下一版(挂点已确认)
命运祭坛 / 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 框架生成,无法改。
Thunderstore development is made possible with ads. Please consider making an exception to your adblock.