1
This commit is contained in:
@@ -258,6 +258,12 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
|
||||
[Header("攻击参数")]
|
||||
public float attackTime = 0.5f;
|
||||
[Tooltip("远端座位攻击状态看门狗(秒):最后收到某座位的攻击状态/开火消息后,超过这个时间还没有新消息," +
|
||||
"就判定这个座位已经不在攻击(典型:攻击中直接退出/掉线,永远不会再发待机包)→ 本地收回待机。\n" +
|
||||
"实际等待时间 = max(本值, attackTime×3),避免开火间隔比看门狗还长的配置被误判")]
|
||||
public float remoteAttackIdleTimeout = 1.5f;
|
||||
/// <summary>远端座位最近一次"还在攻击"的心跳时间:收到该座位的攻击状态广播或开火消息时刷新</summary>
|
||||
private readonly Dictionary<int, float> remoteAttackHeartbeat = new Dictionary<int, float>();
|
||||
[Header("人机参数")]
|
||||
public float botRotateSpeed = 80f; // 兜底旋转速度(BotConfig 为空时用)
|
||||
float clickTime = 0;
|
||||
@@ -1197,6 +1203,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
UpdateLockFX(); // 锁定图标跟随目标
|
||||
RefreshTripleLockIcons();
|
||||
|
||||
// ⭐ 远端攻击状态看门狗:对方攻击中退出/掉线时不会再发待机包,本地按"还有没有攻击心跳"收回待机
|
||||
UpdateRemoteTurretAttackTimeout();
|
||||
|
||||
LoopLaser();
|
||||
SettingClose();
|
||||
DaoJiShi();
|
||||
@@ -3029,6 +3038,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
|
||||
CrossPlatformInput input = GetInput();
|
||||
|
||||
// ⭐ 记录"开火键是否按住":手动开火是"按下进 Attack、松手回 Idle",
|
||||
// 自动开火回收状态时要靠它区分"自动开火没开火"和"玩家正按住手动开火"(见 EnsureTurretIdle)
|
||||
_firePointerDown = input.isPressed;
|
||||
|
||||
if (!input.isPressed && !input.wasPressedThisFrame && !input.wasReleasedThisFrame)
|
||||
return;
|
||||
if (input.wasPressedThisFrame)
|
||||
@@ -3518,6 +3531,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
if (sit == null || bullitIDs == null || bullitIDs.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[FireBullet] sit 或 bullitIDs 为空,跳过本次开火");
|
||||
// ⭐ 服务端这一发没回子弹(拒绝/异常)→ 本地不能再停在 Attack 空转:
|
||||
// 攻击状态是开火请求发出前就切好的,不回滚就会一直循环攻击动画但一颗子弹都没有
|
||||
if (sit == currSitData) EnsureTurretIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3785,6 +3801,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
bool needAutoFire = currSitData.IsAutoAttackOn || (toggle_Lock.isOn && currSitData.isLock && currSitData.lockFX.gameObject.activeSelf);
|
||||
if (!needAutoFire)
|
||||
{
|
||||
// ⭐ 自动开火条件不再满足(关自动攻击/松开锁定目标):把还卡在 Attack 的炮台收回 Idle,
|
||||
// 否则攻击动画一直循环但一颗子弹都不发射
|
||||
EnsureTurretIdle();
|
||||
autoFireTimer = 0f;
|
||||
return;
|
||||
}
|
||||
@@ -3795,11 +3814,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
// ⭐ 本地金币校验:没钱直接返回,不切换攻击状态、不发送开炮请求
|
||||
// autoFireTimer 保持满,金币恢复后立即开火
|
||||
if (!HasEnoughGoldToFire(toggle_Three.isOn, toggle_Shuang != null && toggle_Shuang.isOn))
|
||||
{
|
||||
EnsureTurretIdle(); // 打不出去就别停在攻击状态
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ 子弹上限校验(先于状态切换和扣费)
|
||||
if (IsBulletLimitReached(currSitData.user))
|
||||
{
|
||||
EnsureTurretIdle(); // 子弹已满打不出去,回收攻击状态(金币恢复/上限解除后下一轮会重新进 Attack)
|
||||
autoFireTimer = 0f; // 重置计时,下次再试
|
||||
return;
|
||||
}
|
||||
@@ -3819,6 +3842,85 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
SyncFire(angle, isSanShe, isDouble, isLock);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ⭐ 打不出去时把炮台从 Attack 收回 Idle(只处理 Attack,不碰 LaserStart/LaserLoop)。
|
||||
/// 背景:攻击状态是在"按下/自动开火"那一刻就切好的(也要先把状态广播出去给其他客户端),
|
||||
/// 但真正出弹要等服务端 RoomFireNotice 回来。中间任何一环被跳过——自动开火条件不再满足、
|
||||
/// 金币不足、子弹上限、服务端没回子弹——以前都只 return 不收回状态,炮台就停在 Attack:
|
||||
/// 攻击动画一直循环、一颗子弹都不发射(远端视角同样跟着卡住,因为状态是同步过去的)。
|
||||
/// 🔁 只在"当前不是 Idle"时切一次:ChangeState 同状态早退 + 这里的判断,保证不会每帧刷广播。
|
||||
/// </summary>
|
||||
private void EnsureTurretIdle()
|
||||
{
|
||||
if (currSitData == null) return;
|
||||
|
||||
// ⚠ 玩家正按住手动开火时不能抢:手动开火的状态是"按下进 Attack、松手才回 Idle",
|
||||
// 此时自动开火这一帧不开火(needAutoFire=false)不代表炮台不该是攻击状态。
|
||||
// 少了这道判断会在按下同一帧把手动 Attack 顶回 Idle → 表现为"根本切不到攻击状态"。
|
||||
if (IsManualFireActive) return;
|
||||
|
||||
// 状态由 OnStateChange 指向的那个 TurretData 接收:战神装备中指向战神(见 SitPosData.LoadZhanShen)
|
||||
var turret = currSitData.currentZhanShenData != null ? currSitData.currentZhanShenData : currSitData.currentTurret;
|
||||
if (turret == null || turret.CurrState != TurretState.Attack) return;
|
||||
|
||||
currSitData.ChangeState(TurretState.Idle);
|
||||
SyncTurretState(TurretState.Idle);
|
||||
}
|
||||
|
||||
/// <summary>开火键是否按住(HandleInput 每帧刷新)</summary>
|
||||
private bool _firePointerDown;
|
||||
|
||||
/// <summary>
|
||||
/// ⭐ 远端攻击状态看门狗:别的座位停在 Attack 上时,靠"这个座位还在不在发攻击消息"判断它是不是真的在攻击。
|
||||
/// 为什么会卡:攻击状态是同步过来的,而"收回待机"也要等对方发待机包(changeTurretState=Idle)。
|
||||
/// 对方攻击中直接退出/掉线/被踢 → 既不会再攻击也不会再发待机包 → 本端炮台永远停在攻击动画。
|
||||
/// 这里按身份判定:开着战神时状态由战神接收(同 EnsureTurretIdle)。
|
||||
/// 🔁 只改本地表现,不再广播回去(否则多端会互相刷 Idle 包)。
|
||||
/// </summary>
|
||||
private void UpdateRemoteTurretAttackTimeout()
|
||||
{
|
||||
if (list_Sits == null || list_Sits.Count == 0) return;
|
||||
|
||||
float now = Time.time;
|
||||
// 等待时间至少是开火间隔的 3 倍:配置把攻击间隔调大时看门狗不能比它短,否则会误收回正在攻击的座位
|
||||
float timeout = Mathf.Max(remoteAttackIdleTimeout, attackTime * 3f);
|
||||
|
||||
foreach (var sit in list_Sits)
|
||||
{
|
||||
if (sit == null || sit == currSitData) continue; // 本地座位由本地开火/回收逻辑管
|
||||
|
||||
var turret = sit.currentZhanShenData != null ? sit.currentZhanShenData : sit.currentTurret;
|
||||
if (turret == null || turret.CurrState != TurretState.Attack) continue;
|
||||
|
||||
float last;
|
||||
if (!remoteAttackHeartbeat.TryGetValue(sit.posID, out last))
|
||||
{
|
||||
// 没收到过心跳(进房间前的残留状态等)→ 给一个完整等待窗口,不立刻收回
|
||||
remoteAttackHeartbeat[sit.posID] = now;
|
||||
continue;
|
||||
}
|
||||
if (now - last <= timeout) continue;
|
||||
|
||||
// 超过等待时间没有任何"还在攻击"的消息 → 这个座位已经不在攻击了,收回待机
|
||||
sit.ChangeState(TurretState.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动开火是否正在生效:手动开火只在"自动攻击关 + 锁定关"时才工作
|
||||
/// (TryFire 开头被这两个开关提前 return 拦掉,锁定模式下手动按住只旋转不开火),
|
||||
/// 此时 Attack 状态完全由按下/按住驱动,回收逻辑必须让位。
|
||||
/// </summary>
|
||||
private bool IsManualFireActive
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_firePointerDown) return false;
|
||||
if (currSitData != null && currSitData.IsAutoAttackOn) return false;
|
||||
return toggle_Lock == null || !toggle_Lock.isOn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5095,6 +5197,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
SitPosData sit = list_Sits.Find(s => s.user == sync.userId);
|
||||
if (sit == null) return;
|
||||
|
||||
// ⭐ 攻击心跳:这个座位刚发了开火消息(bot 的状态广播是节流的,只有开火消息每发都来)
|
||||
remoteAttackHeartbeat[sit.posID] = Time.time;
|
||||
|
||||
// ⭐ 座位镜像:botLastAngle/botLastFireData 缓存的是"源角度"(下方座位系);
|
||||
// 真人兜底用 sit.currentAngle(本端世界角度,无需转换)
|
||||
bool angleFromBotMemory = botLastAngle.TryGetValue(sync.userId, out float a);
|
||||
@@ -5135,6 +5240,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
}
|
||||
var targetSit = list_Sits[dispPos];
|
||||
|
||||
// ⭐ 攻击心跳:这个座位刚发了开火消息(真人每发都走这里,等于"还在攻击"的心跳)
|
||||
remoteAttackHeartbeat[targetSit.posID] = Time.time;
|
||||
|
||||
// ⭐⭐ 战神散射(三发+锁定):发送端每发都带 isSanShe/isSuoDing 和3个激光目标GUID(monsterIDs)
|
||||
// → 其他玩家的座位跟着补齐3个锁位 + 刷新散射标记:3条激光 + 2个散射起点球
|
||||
// (JGFXLeftParent/JGFXRightParent) 一起显示。
|
||||
@@ -5276,6 +5384,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
var data = FrameDataParser.Parse<FrameData_TurretState>(sync.frameData);
|
||||
var sit = list_Sits[dispPos];
|
||||
|
||||
// ⭐ 攻击心跳:真人每次开火都会广播一次 Attack 状态(SyncTurretState(Attack) 在开火循环里),
|
||||
// 等于"这个座位还在攻击"的心跳。刷新它,看门狗就不会误收回正在攻击的座位。
|
||||
if (data.state == TurretState.Attack)
|
||||
remoteAttackHeartbeat[sit.posID] = Time.time;
|
||||
|
||||
sit.ChangeState(data.state);
|
||||
}
|
||||
|
||||
@@ -6115,6 +6228,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
botSkills.Remove(sit.posID);
|
||||
botLastAngle.Remove(sit.user);
|
||||
botLastFireData.Remove(sit.user);
|
||||
remoteAttackHeartbeat.Remove(sit.posID); // 攻击心跳(看门狗用)
|
||||
|
||||
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致房主显示异常
|
||||
// 1) 清除 bot 的房主引用(如果它是房主)
|
||||
|
||||
Reference in New Issue
Block a user