本文含有大量代码与机制分析,有相关洁癖/对机制不感兴趣的玩家可以绕道。
最近本来是抱着提高自己游戏理解的目的,在游戏代码的屎山里狂游,游着游着就从手段变成目的了,单纯的为了看代码而看代码。虽然花了很多无聊的时间,倒也确实对游戏理解深入了不少。
本文是对鱼雷刷新GCD和卡雷机制的原理说明,这两者看起来表现不一样,但经过研究,其底层逻辑是共享的,只不过当触发条件不同就会有不同的效果。
由于这个现象出现的机制比较复杂,这里直接省流给出对应的触发方式:
当发射完鱼雷,处于GCD期间有舰船死亡时:
如果当前不处于自律模式,则:
若死亡的舰船的鱼雷底座 ≥ 2,则GCD立刻取消,可以立马发射;
若死亡的舰船的鱼雷底座 = 1,则GCD卡在死亡瞬间的位置,持续卡住,除非有带有鱼雷底座的舰船再次死亡;
若死亡的舰船的鱼雷底座 = 0,则不会产生任何影响。
如果当前处于自律模式,则:
若死亡的舰船的鱼雷底座 ≥ 1,则GCD立刻取消,可以立马发射(这里需要特殊说明:虽然看起来都是GCD表现,但实际的UI表现不一样);
若死亡的舰船的鱼雷底座 = 0,则不会产生任何影响。
* 本文均为个人理解,由于工作量大,机制复杂,可能存在错误。如有错误,请您指出。
因为该BUG/特性涉及的底层机制较多也十分深入,因此有必要做一些相关的介绍。可以参照5B5的其他研究合集 - 碧蓝航线WIKI_BWIKI_哔哩哔哩进行阅读,但该文中对武器队列具体机制的介绍也较少,只是简单带过,下面也顺便进行一些深入介绍。对这些具体机制不感兴趣的,可以直接跳到第二章省流。
顾名思义,武器队列用于管理舰船的武器,例如武器的CD,武器的公共CD(GCD),是否有武器正在开火等。
每艘舰船都有自己独立的武器队列机制,因此,不同舰船间的同种武器队列不会互相影响。
武器队列有许多种,在同种武器队列内部的武器不能同时开火,一般来讲,武器能否开火会受到武器自身CD和GCD两方面的约束。
多个底座=复制多个同样的武器加入到武器队列中。这一点是造成本现象的根本原因,请留意。
手动武器队列用于管理舰船的手动开火武器,例如:后排的跨射主炮队列、空袭队列,前排的手动鱼雷队列。
手动武器队列不是武器队列的子类,而是比较像一种“补充队列”。其逻辑与普通武器队列有相当的区别。
普通武器(各种自动开火武器、技能武器等)只会加入到武器队列并被制约,而手动武器会同时加入到武器队列和手动武器队列。
与普通武器队列一样,手动武器队列也是每艘舰船独立的。但有一点不同:普通武器队列的GCD由队列本身管理,但手动武器的GCD是由舰队全局的管理机制来管理的,具体详见后文。
(*这里就能顺便解释一个现象:当按住鱼雷发射按钮时,对应舰船的主炮不会开火。这是因为,大部分手动鱼雷和前排主炮属于同一个武器队列,即武器队列1(取决于武器的queue参数,这里5b5专栏中的说法"手动武器队列有3种,且有顺序..."有歧义,可能会让人误以为手动鱼雷的武器队列的queue为2,事实上这个2对应的是index)。因此,当鱼雷发射按钮被按住时,该手动鱼雷武器处于“即将发射状态(Precast),此时,武器队列便会阻止其他武器开火。但注意:由于不同舰船的武器队列不会互相干扰,因此按住鱼雷发射按钮时,只有对应的那个舰船的主炮会停止,其他舰船不会受影响。
那么聪明的小伙伴可能会思考:反过来如何呢?按照如上机制,当前排主炮开火处于GCD的时候,难道会影响手动鱼雷的发射吗?事实上不会,这就是手动武器队列机制存在的意义了。手动鱼雷在发射时,是通过UnleashTorpedo直接走Fire函数,而不依赖武器队列的Update函数进行自动发射,因此该机制属于一种单向的干扰。)
回到正题。手动武器会维护两个队列:
overheatQueue:过热队列。表示的是发射后暂时还没有进入冷却的武器。
cooldownList:冷却队列。表示的是正在冷却的武器。
举一个实际例子来解释:假设岛风开局直接打出两轮鱼雷,则第一轮鱼雷在冷却队列中,第二轮鱼雷在过热队列中,因为岛风同时只能装填一个武器。
实际的机制相对复杂一点,但也比较简单。武器在开火后,会立刻加入过热队列的队尾,然后检查能否CD,此时会检查对应的冷却队列是否已满,如果未满,就将过热队列的队首加入到冷却队列的队尾;如果已满,则会持续阻塞等待冷却队列空出。
实际上也能看出,冷却队列的最大长度决定了同时能装填几个武器(底座,因为底座本质即武器)。该最大长度由舰船自身的parallel_max参数决定,例如云仙,其鱼雷位置的该参数值即为2,表示她的冷却列表最大长为2,所以最多能同时装填2个武器。
全局武器管理实际上十分常见,我们可见的右下角的各种跨射按钮、鱼雷按钮、空袭按钮都是全局武器管理机制的结果。该机制会统筹全队所有舰船的同类手动武器,对于每类手动武器,都管理一个全局手动武器队列。
下文都以全局手动鱼雷队列为例。
全局手动鱼雷队列与手动队列类似,但会维护三个列表:
overheatQueue:过热队列。表示的是发射后暂时还没有进入冷却的武器。
chargingList:装填队列。表示的是目前正在装填的武器。
readyList:就绪队列。表示的已经准备就绪可以开火的武器。
其逻辑类似手动武器队列。
在冷却时,走的是每个手动武器对应手动武器队列的冷却逻辑,即由每艘舰船自己的手动武器队列来管理自己手动武器的过热队列和冷却队列。
全局武器队列中,通过current和max两个参数来决定当前的进度条显示
current:从该武器装填开始,当前走的时间
max:该武器完成装填需要的时间
这里的"该武器",指的是最快能完成的武器。是通过分别计算每个武器的装填需求(基于每个武器的reload_max属性和武器宿主的装填值来计算的。稍微扯一句,我个人认为这个公式会方便很多,不需要武器在100装填下的基准CD,也能方便处理在变动CD情况下的实际CD计算)来获得对应完成时的时间戳,从中选出距离最近的,按钮UI上就会按照线性百分比来计算需要的显示比例。 这两个参数对于此现象的成因也十分重要,请留意。
舰船死亡的时候,会:
清理自己所有的武器队列;
清理自己所有的手动武器队列;
在全局武器队列中,从对应的队列清除对应的武器。
关键:当从全局武器队列移除武器的时候,会走RemoveWeapon这个函数,该函数是此现象产生的罪魁祸首。之后会详细说明。
该函数的逻辑稍微有点复杂,但我们不用关心全部逻辑,只用关心其中的两个函数:
DispatchOverLoadChange。该函数用于派发一个Event,通知对应按钮的可按性的改变。
refreshCD。该函数用于移除武器后,重新计算进度条UI和武器可用计数UI。
这两个函数是按顺序调用的。
首先分别来看这两个函数。DispatchOverLoadChange会通知对应的weaponbutton类调用回调函数OnOverloadChange,调用isOverLoad函数检查按钮能不能按了,该函数的逻辑为:return current < max or count < 1。也即:
current < max(进度条不满),或者count < 1时,按钮都不能按;
这里count是readyList的长度,即可用武器数。
换句话说,当current >= max,且count >= 1时(进度条满,且有能用的武器),才能按下武器按钮。
refreshCD函数的逻辑稍显复杂,但也只需要看一种情况:readyList的长度不为0时,此时会立刻更新current = max = 1。
其目的是:沉船时,若有舰船的手动武器可用,则刷新UI后,按钮的进度条还是满的。
手动武器的VO通过Update函数来更新,每帧检查一次。其逻辑我们不用细究,只需要知道两点:
此函数开头就会检查是否current < max,只有满足这个条件(表明进度条还在转)的时候才会执行详细逻辑,否则什么都不做。
在if条件内部,也会执行DispatchOverLoadChange来每帧检查一次按钮能不能按了。
通过weaponbutton的Update函数来更新,其逻辑为:当total > 0,且current < max时,才会通过UpdateProgressBar更新进度条UI的效果(每帧重新计算一次)
鱼雷的发射是通过对应武器按钮来发射的。碧蓝航线判定的逻辑是直接在UI层来实现的:只要按钮能按就能发射。
对应的逻辑是,当按钮接收到DispatchOverLoadChange发出的Event时,触发对应的回调函数OnOverloadChange函数。
该函数会:如果上述的isOverLoad返回False,表示按钮可按,则将按钮上面的Block SetActive(false),即可以真正在View层按下按钮了。
按下按钮时,对应的回调函数可以分为以下三个函数:
CastTorpedo:表示按下了按钮,但还没释放;
UnleashTorpedo:表示按下了按钮,并且释放了;
CancelTorpedo:表示按下了按钮,但是移开了。
其中UnleashTorpedo里面就会真正走武器的Fire函数,从而完成武器的真正开火。
而其中对状态的检查只涉及两次:
CastTorpedo:检查该武器的currentState是否为Ready,这点在逻辑上可以对应,因为readyList不为空,下一个武器就是准备好的。
UnleashTorpedo:检查该武器的currentState是否为Precast,这是自然的,因为CastTorpedo的时候,会自动将currentState设置为Precast。
全部通过后,就能到逻辑层实际走鱼雷发射的流程了。例如,后续的冷却就是靠对应舰船的手动武器队列来管理,武器自身计算冷却。数据逻辑层和UI显示层的实际关联之一靠的就是之前提到的获取装填时间戳这个函数。
以上说明的是手动释放鱼雷的流程。而自律释放鱼雷的流程是不一样的。这一点导致了之后说明的现象的不同,请留意。
简单来说,自律模式下相关逻辑被托管给ManualWeaponAutoBot,其在对应的每AI帧(在对应的Facade模式中有相关设置)调用的Update逻辑中,检查对应的全局手动武器队列是否overload(与上文同函数,对应的是手动释放鱼雷时,按钮能不能按了),overload为False时(表示能使用武器),会调用对应的Quick函数。
对于鱼雷,这个函数为QuickCastTorpedo。其逻辑非常简单:只要发现当前武器可用,立马调用Fire函数。
对比手动释放鱼雷,首先需要按钮能按(对应overload),当按下并释放的时候,分别调用CastTorpedo、UnleashTorpedo,再调用Fire函数,后面的逻辑则一致。
下面我们讨论在沉船者:
不处于自律模式:
鱼雷底座 = 0
鱼雷底座 = 1
鱼雷底座 >= 2
处于自律模式:
鱼雷底座 = 0
鱼雷底座 >=1
的情况下对应流程。考虑手动鱼雷GCD=0.5s,假定现在刚好转到GCD的一半=0.25s,如果在这个瞬间死亡:
若沉船瞬间不处于自律模式:
鱼雷底座 = 0:
由于会对每个武器调用RemoveWeapon,因此这里不会(在全局手动鱼雷队列)调用RemoveWeapon。
因此不会产生任何影响。
鱼雷底座 = 1:
由于会对每个武器调用RemoveWeapon,因此这里会调用1次RemoveWeapon。
第一次调用:
DispatchOverLoadChange:检查能否按,因为current=0.25 < max = GCD = 0.5s,所以不能按,按钮是不可用的。
refreshCD:此时会让current = max = 1
此时产生了一个严重的问题:那就是因为current = max,从而执行Update函数时,永远不会进入循环。这就导致了:发射鱼雷永远不能再被按了。
如果按钮不能被按,按照上面的逻辑,那就永远无法触发UnleashTorpedo,从而永远无法使用鱼雷武器。
另一个现象是,进度条会卡着不动。这是因为UI更新也是要检查current < max的,所以这里的情况是:永远无法UpdateProgressBar,所以看上去进度条也卡在死亡瞬间的位置了。
鱼雷底座 >= 2:
由于会对每个武器调用RemoveWeapon,因此这里会调用2次及以上RemoveWeapon。
第一次调用:
DispatchOverLoadChange:检查能否按,因为current=0.25 < max = GCD = 0.5s,所以不能按,按钮是不可用的。
refreshCD:此时会让current = max = 1
第二次调用:
DispatchOverLoadChange:检查能否按,因为current=max=1,所以能按,按钮能用了。
refreshCD:此时会让current = max = 1
只要按钮能用,那就可以恢复到正常逻辑了。但由于refreshCD会让current=max=1,相当于GCD被直接跳过了,直接就能按下按钮进行下一次鱼雷发射。
这也是为什么,在卡雷的情况发生时,再死亡一艘船就能恢复正常,和这里的逻辑是一样的。
后续调用(如有,但现在好像没有3鱼雷底座的船?)
和第二次的逻辑完全一致,无区别。
若沉船瞬间处于自律模式:
鱼雷底座 = 0:
由于会对每个武器调用RemoveWeapon,因此这里不会(在全局手动鱼雷队列)调用RemoveWeapon。
因此不会产生任何影响。
鱼雷底座 >= 1:
由于会对每个武器调用RemoveWeapon,因此这里会调用1次及以上RemoveWeapon。
第一次调用:
DispatchOverLoadChange:检查能否按,因为current=0.25 < max = GCD = 0.5s,所以不能按,按钮是不可用的。
refreshCD:此时会让current = max = 1
此时产生了一个严重的问题:那就是因为current = max,从而执行Update函数时,永远不会进入循环。这就导致了:发射鱼雷永远不能再被按了。
如果按钮不能被按,按照上面的逻辑,那就永远无法触发UnleashTorpedo,从而永远无法使用鱼雷武器。
另一个现象是,进度条会卡着不动。这是因为UI更新也是要检查current < max的,所以这里的情况是:永远无法UpdateProgressBar,所以看上去进度条也卡在死亡瞬间的位置了。
关键不同点来了:现在处于自律模式,而核心在于:ManualWeaponAutoBot是每AI帧即0.1s更新一次的,在里面会跑对应的检查逻辑,而这里的检查逻辑与进度条完全无关,而是从逻辑层面检查是否有武器可用。
因此,因为这个时候已经把current=max=1,所以isOverLoad=false, 且实际上确实有武器可用,从而会触发Fire,使得武器实际上会发射。
这里本质也是因为:进度条被设置为满,从而在这里跳过了GCD。
但实际的UI不会改变,因为这里只是检查的overLoad的情况,没有Dispatch,对应的button view自然不会去执行对应的回调函数,从而UI还是卡死。
为什么说这个时候UI实际上和手动一样是卡死的?可以验证:如果出现卡雷现象,你只需要切换到自律模式,神奇的事情就发生了:虽然手动发射不了鱼雷,但自律可以!
也可以通过观察UI:这种情况下,并不像手动且鱼雷底座>=2时刷新GCD的表现,让UI的进度条瞬间填满,而是在进度条没走完的时候,就能释放鱼雷。
武器发射后,会反馈到对应的VO以及UI上,使得UI也变回正常情况。
后续调用
同样可以发射鱼雷。
理论来讲,UI表现并不一样,此时的UI表现基于手动且鱼雷底座>=2时的分析,自律且鱼雷底座>=2时的情况应该是完全一致的。
但实际从测试来看,并没有明显的按钮填满的现象出现。或许是AI更新间隔带来的某种影响。
最后顺便说一下,如果触发GCD后沉船后,没有任何准备好的武器是什么情况:此时在refreshCD时就不会设置current = max = 1了,而是会走另外的逻辑,具体来讲:
计算最近的武器还需要装填多久,记为t
如果计时器走的时间current比已经比GCD多了
那么max=t,即进度条表示的是到下一个最近的武器需要装填多久
否则,如果计时器走的时间还没有GCD多
max=GCD-current,max-current,t三者中的最大值
最后,将current设为0,表示从头开始转进度条。
上述current < GCD时表示的情况分别是:转完GCD还需要多久、转完之前的最近的武器(下称武器1)还需要多久、转完最新的最近的这个武器(下称武器2)还需要多久(计算过程已经蕴含了减去当前时间,只是逻辑看起来有区别)
是否有可能出现:武器1属于死亡的舰船的情况?确实存在,但是这种情况下,对应的值肯定是小于武器2的,因为在舰船没有死亡前,最近的那个就是武器1,因此武器2对应要装填好的时间会更晚。此时对应取的就是武器2的那个值;
如果武器1不属于死亡的舰船,那这个时候按上述分析,取的是武器1的那个值;
最后,要转动的时间不能少于GCD-current,这作为保底,表示继续走之前的GCD,对应的沉船,目前没有准备好的武器,但马上就有要装填好的武器,且剩余的时间少于一个GCD的情况。
吐槽一下:这里怎么就知道要强制对齐到GCD了...
综上,触发GCD后沉船后,没有任何准备好的武器时,完全按正常逻辑走。
由于以上逻辑实际上并不限定于手动鱼雷武器,因此理论上:
如果有某个舰船有武器在跨射队列,则在GCD期间死亡的表现与上述一致;
如果有某个舰船有武器在空袭队列,则在GCD期间死亡的表现与上述一致;
第一种情况通过前排金狮+双预装填战列已经验证会卡死跨射队列;第二种情况通过前排定安(带水上机)+双预装填航母已经验证会卡死空袭队列。
下面,我们从12种不同情况的测试来验证我们的结论。
测试视频链接:GCD测试留档1_哔哩哔哩bilibili_碧蓝航线
具体分析懒得写了,看视频即可。
该BUG的成因在以下几个方面:
舰船死亡移除武器时,认为readyList长不为0,即只要有可用武器,就肯定不会在CD,所以进度条就应该是满的,直接设置current = max,忽略了进入GCD的情况(这里额外说一句,自动武器的GCD和手动武器的GCD处理方式不一样。自动武器GCD是走的自己武器队列的GCDTimer;但对于手动武器,其GCD似乎就只在对应的VO里处理了)。
移除武器时,DispatchOverLoadChange和refreshCD的顺序有问题。事实上,
就我个人意见,如果要修复卡雷BUG,只需要简单换一下两者顺序即可。此时有>=1个鱼雷底座就能刷新GCD,不再卡雷了。
如果要进一步修复刷新GCD的问题,就需要对RemoveWeapon的判断逻辑进行一定程度的修正,不能简单在#readyList ~= 0时就令current = max = 1。
而对于自律的情况,其实本质一样,current和max到了一个不正确的状态,只不过自律时的Update不会被条件卡死,能多检查一次当前的overLoad状态,从而提前变成了鱼雷底座>=2的情况(但UI表现在某些地方不一样)
其他则大概属于设计架构问题,MVC原则没有处理好,VO过多参与了逻辑,导致UI和逻辑出现了一定程度的状态不同步。
包括VO层和M层的耦合度也似乎过于紧密,调试时需要人工考虑两侧的状态一致性。
(有一说一黄鸡/勇仕该给我发工资,我一个人干了全部的BUG复现+测试+问题定位+文档工作。不过这游戏这么一个开服就有的BUG,8年多了也不改,也就这样了)
此外,关于GCD还有一个BUG,目前原因未知:“前排定安后排突击者,在突击者触发快速起飞gcd状态下沉掉定安立即刷新gcd,1/2-gcd→1/2→0/2瞬间完成”。但在对应的视频【碧蓝航线】「空相交汇点」Ex 7783分_碧蓝航线中,观察能发现:
定安是在突击者四飞完毕后才死亡的,而GCD刷新出现在第三飞和第四飞之间。
再者,就算定安死于第三飞和第四飞之间,第一:定安没有默认水上机(第一个槽位是默认驱逐炮),不带水上机的情况下在空袭队列的武器数为0,按照以上逻辑不会造成任何影响;
第二:UI按钮的表现并不是类似鱼雷GCD刷新一样的“立刻完全填满”,而是“在按钮还灰着,进度条没跑完的时候就能按下按钮发动空袭了”,这与鱼雷刷新GCD的UI视觉表现不一样。
总之,这个现象出现的原因暂时未知,目前的想法是,或许与突击者的"快速起飞"的逻辑有关,该技能的本质是让过热队列的对应装备直接变为可用。待之后深入研究。
Github Copilot的Claude Sonnet 4.5和GPT-5-Codex,帮我辛勤地看机制/找BUG