This commit is contained in:
2026-07-11 17:07:32 +08:00
parent 9e0d1f04ba
commit d2c8d91748
9 changed files with 733 additions and 71 deletions
+179
View File
@@ -0,0 +1,179 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 子弹专用对象池 — 按 prefab 实例ID 分桶,与鱼的 Pool 完全隔离
/// 用法:
/// var go = BulletPool.Instance.Get(prefab, pos, rot, parent);
/// BulletPool.Instance.Return(go); // 代替 Destroy
/// </summary>
public class BulletPool : Singleton<BulletPool>
{
// prefab.GetInstanceID() → 该 prefab 的池
private Dictionary<int, BulletPoolData> dic = new Dictionary<int, BulletPoolData>();
// 每个池子的最大缓存数(超出直接 Destroy,防止内存膨胀)
private const int MaxCachePerPrefab = 200;
// 池子根节点(隐藏在场景里)
private GameObject poolRoot;
/// <summary>从池中取一个子弹(池空则 Instantiate)</summary>
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent)
{
if (prefab == null) return null;
int id = prefab.GetInstanceID();
GameObject obj;
if (dic.TryGetValue(id, out var data) && data.poolList.Count > 0)
{
obj = data.GetObj();
obj.transform.position = position;
obj.transform.rotation = rotation;
obj.transform.SetParent(parent, false);
obj.SetActive(true);
}
else
{
obj = GameObject.Instantiate(prefab, position, rotation, parent);
obj.SetActive(true);
}
// 确保挂上 tracker 记录 prefabID(回收时凭此找到对应池)
var tracker = obj.GetComponent<BulletPoolTracker>();
if (tracker == null)
tracker = obj.AddComponent<BulletPoolTracker>();
tracker.prefabID = id;
return obj;
}
/// <summary>无 parent 重载</summary>
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
{
return Get(prefab, position, rotation, null);
}
/// <summary>回收子弹到池(超容量则 Destroy)</summary>
public void Return(GameObject obj)
{
if (obj == null) return;
var tracker = obj.GetComponent<BulletPoolTracker>();
int id = tracker != null ? tracker.prefabID : 0;
// 取不到 id → 兜底 Destroy(非池内对象)
if (id == 0)
{
GameObject.Destroy(obj);
return;
}
if (!dic.TryGetValue(id, out var data))
dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id);
if (data.poolList.Count >= MaxCachePerPrefab)
{
GameObject.Destroy(obj);
return;
}
// 重置子弹状态(若有 BullitMove 组件)
var bm = obj.GetComponent<BullitMove>();
if (bm != null) bm.ResetState();
data.PushObj(obj);
}
/// <summary>预加载 N 个某 prefab 到池</summary>
public void Preload(GameObject prefab, int count)
{
if (prefab == null || count <= 0) return;
int id = prefab.GetInstanceID();
if (!dic.TryGetValue(id, out var data))
dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id);
for (int i = 0; i < count; i++)
{
var obj = GameObject.Instantiate(prefab, EnsurePoolRoot().transform);
var tracker = obj.GetComponent<BulletPoolTracker>();
if (tracker == null)
tracker = obj.AddComponent<BulletPoolTracker>();
tracker.prefabID = id;
data.PushObj(obj);
}
}
/// <summary>清理所有子弹池(切场景时调用)</summary>
public void Clear()
{
foreach (var kv in dic.Values)
kv.Clear();
dic.Clear();
if (poolRoot != null)
{
GameObject.Destroy(poolRoot);
poolRoot = null;
}
}
private GameObject EnsurePoolRoot()
{
if (poolRoot == null)
{
poolRoot = new GameObject("[BulletPool]");
poolRoot.SetActive(false); // 池根节点隐藏,池内对象跟随隐藏
}
return poolRoot;
}
}
/// <summary>挂在池内对象上,记录所属 prefabID(回收时凭此找到对应池)</summary>
public class BulletPoolTracker : MonoBehaviour
{
public int prefabID;
}
/// <summary>单个 prefab 的子弹池数据</summary>
public class BulletPoolData
{
public Transform fatherObj;
public List<GameObject> poolList;
public BulletPoolData(Transform root, int id)
{
fatherObj = new GameObject("bulletPool_" + id).transform;
fatherObj.SetParent(root, false);
poolList = new List<GameObject>();
}
public void PushObj(GameObject obj)
{
if (obj == null) return;
poolList.Add(obj);
obj.transform.SetParent(fatherObj, false);
obj.SetActive(false);
}
public GameObject GetObj()
{
GameObject obj = poolList[poolList.Count - 1];
poolList.RemoveAt(poolList.Count - 1);
return obj;
}
public void Clear()
{
foreach (var obj in poolList)
{
if (obj != null) GameObject.Destroy(obj);
}
poolList.Clear();
if (fatherObj != null)
GameObject.Destroy(fatherObj.gameObject);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b450d462c4ecd3349b396f1ff2014f40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+4 -3
View File
@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
@@ -17,11 +17,12 @@ public class PoolData
public void PushObj(GameObject obj,int id)
{
obj.SetActive(false);
// 先实例化,再隐藏(原代码对 prefab 调 SetActive(false) 无效且语义混乱)
obj = GameObject.Instantiate(obj);
obj.name = id.ToString();
poolList.Add(obj);
obj.transform.parent = fatherObj.transform;
obj.SetActive(false);
poolList.Add(obj);
}
public void PushObj(GameObject obj)
{
@@ -62,13 +62,30 @@ public class BullitMove : MonoBehaviour
transform.position = vector3_startPosition;
}
/// <summary>对象池回收前调用:重置所有运行时状态,避免复用时残留旧数据</summary>
public void ResetState()
{
userid = null;
playerID = null;
bulletID = null;
currShowTime = 0f;
isTriggerFirst = true;
target = null;
targetTra = null;
// 解除锁定 + 恢复碰撞器
NotLock();
// 重置起始位置(z=-400 是原 Awake→Init 的默认值)
vector3_startPosition = transform.position;
vector3_startPosition.z = -400;
}
void Update()
{
// 子弹生命周期:超时自动销毁(默认10秒)
currShowTime += Time.deltaTime;
if (currShowTime >= desTime)
{
Destroy(gameObject);
BulletPool.Instance.Return(gameObject);
return;
}
@@ -136,7 +153,7 @@ public class BullitMove : MonoBehaviour
monster.monsterData.charactersHealth.OnHurt(10, CharacterHurtType.none);
Destroy(monster.gameObject);
}
Destroy(gameObject);
BulletPool.Instance.Return(gameObject);
}
}
}
@@ -228,6 +245,6 @@ public class BullitMove : MonoBehaviour
}
monster.OnHurt();
Destroy(gameObject);
BulletPool.Instance.Return(gameObject);
}
}
@@ -30,6 +30,146 @@ public class BotConfig
[Header("aggressive 状态参数")]
public BotBehaviorConfig aggressiveBehavior = new BotBehaviorConfig();
[Header("倍数策略")]
public BotMultiplierConfig multiplier = new BotMultiplierConfig();
[Header("Boss策略")]
public BotBossStrategyConfig bossStrategy = new BotBossStrategyConfig();
[Header("退出条件")]
public BotExitConfig exitCondition = new BotExitConfig();
}
/// <summary>机器人倍数(炮台档位)策略配置</summary>
[Serializable]
public class BotMultiplierConfig
{
[Tooltip("进场初始炮台档位索引(对应 paoBeiList 的下标, 0=最低档)")]
public int initialLevelIndex = 0;
[Tooltip("低于此倍数(当前金币/进场金币) → 降档")]
public float decreaseThreshold = 0.5f;
[Tooltip("高于此倍数(当前金币/进场金币) → 升档")]
public float increaseThreshold = 1.5f;
[Tooltip("每次调整的档位数")]
public int step = 1;
[Tooltip("倍数检查间隔(秒)")]
public float checkInterval = 3f;
[Tooltip("最低保持档位索引(不会降到低于此值)")]
public int minLevelIndex = 0;
[Tooltip("最高保持档位索引(不会升到高于此值), -1=不限制")]
public int maxLevelIndex = -1;
}
/// <summary>机器人Boss策略配置</summary>
[Serializable]
public class BotBossStrategyConfig
{
[Tooltip("视为Boss的鱼类型 (按 FishTypeEnum 的int值)")]
public int[] bossFishTypes = { 2, 5 }; // Boss鱼=2, 特殊Boss=5
[Tooltip("无Boss时启用散射乱射模式")]
public bool scatterWhenNoBoss = true;
[Tooltip("无Boss时定点三发概率(0~1), 否则单射")]
[Range(0f, 1f)] public float tripleShotProb = 0.5f;
[Tooltip("有Boss时优先锁定最大倍率(赔率)的Boss")]
public bool preferMaxMultiplierBoss = true;
[Tooltip("Boss数量超过此值 → 开启散射模式")]
public int scatterModeThreshold = 3;
[Tooltip("Boss数量<=此值时随机狂暴技能概率(0~1)")]
[Range(0f, 1f)] public float frenzyProb = 0.6f;
[Tooltip("狂暴持续时间下限(秒)")]
public float frenzyMinTime = 3f;
[Tooltip("狂暴持续时间上限(秒)")]
public float frenzyMaxTime = 8f;
}
/// <summary>机器人退出条件配置</summary>
[Serializable]
public class BotExitConfig
{
[Tooltip("金币超过此值时退出(盈利离场), 0=不检测")]
public long exitWhenGoldAbove = 0;
[Tooltip("金币低于此值时退出(输完离场)")]
public long exitWhenGoldBelow = 100;
[Tooltip("退出检查间隔(秒)")]
public float checkInterval = 5f;
[Tooltip("退出时是否发送离房网络消息")]
public bool sendLeaveRoom = true;
[Tooltip("退出后座位是否显示等待状态(等待下一个机器人)")]
public bool showWaitingAfterExit = true;
}
/// <summary>机器人当前行为模式</summary>
public enum BotBehaviorMode
{
Idle, // 待机(未开始)
ScatterRandom, // 散射乱射(无Boss): 随机定点三发或单射
LockBoss, // 锁定Boss
ScatterMode, // 散射模式(Boss数量>3)
RandomFrenzy, // 随机狂暴(Boss数量<=3)
Exited // 已退出
}
/// <summary>每个 bot 座位的运行时状态(进场金币/倍数/模式/退出)</summary>
public class BotRuntimeState
{
// ===== 进场 =====
[Tooltip("进场时的金币(基准)")]
public long entryGold;
[Tooltip("进场时间")]
public float entryTime;
// ===== 倍数 =====
[Tooltip("当前炮台档位索引")]
public int currentLevelIndex;
[Tooltip("下次倍数检查时间")]
public float nextMultiplierCheckTime;
// ===== 行为模式 =====
[Tooltip("当前行为模式")]
public BotBehaviorMode currentMode = BotBehaviorMode.Idle;
[Tooltip("上次Boss检测时间")]
public float lastBossCheckTime;
// ===== 狂暴(Boss<=3时随机) =====
[Tooltip("狂暴是否激活")]
public bool frenzyActive;
[Tooltip("狂暴结束时间")]
public float frenzyEndTime;
// ===== 退出 =====
[Tooltip("下次退出检查时间")]
public float nextExitCheckTime;
[Tooltip("是否已退出")]
public bool hasExited;
/// <summary>当前金币相对进场金币的倍数</summary>
public float GetGoldRatio(long currentGold)
{
return entryGold > 0 ? (float)currentGold / entryGold : 0f;
}
}
/// <summary>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@@ -76,13 +76,63 @@ namespace Assets.MyGame.Scripts.Net.WebResponses.ChineseChess
public string createUserId { get; set; }
/// <summary>
/// 状态 1 未开始 2 进行中 3 已结束
/// 状态 0 空房间 1 未开始 2 进行中 3 已结束
/// </summary>
public int state { get; set; } = 1;
/// <summary>
/// 金币数量
/// </summary>
public long gold { get; set; }
/// <summary>
/// 机器人数量(房间内 AI 玩家数,计入总人数)
/// </summary>
public int robotCount { get; set; }
/// <summary>
/// 结束游戏请求记录(前端不用,仅占位反序列化)
/// </summary>
public object endGameRequests { get; set; }
/// <summary>
/// 最后活跃时间(Unix 秒)
/// </summary>
public long lastActiveTime { get; set; }
/// <summary>
/// 超时秒数
/// </summary>
public int timeoutSeconds { get; set; } = 60;
/// <summary>
/// 超时输方用户ID
/// </summary>
public string timeoutLoserUserId { get; set; }
/// <summary>
/// 待定赢方用户ID(可能为 null)
/// </summary>
public string pendingWinUserId { get; set; }
/// <summary>
/// 游戏是否已结束
/// </summary>
public bool isGameEnded { get; set; }
/// <summary>
/// 房间唯一 GUID(与 roomId 不同,roomId 是业务编号)
/// </summary>
public string id { get; set; }
/// <summary>
/// 是否已删除
/// </summary>
public bool isDelete { get; set; }
/// <summary>
/// 创建信息(前端不用,仅占位反序列化)
/// </summary>
public object createInfo { get; set; }
}
[Serializable]
/// <summary>
@@ -73,13 +73,14 @@ public class UIChineseChessDistrict : SubUIBase
{
switch (ChineseChessMainPanel.dic_RoomData[item.roomId].state)
{
case 1:
case 0: // 空房间
case 1: // 未开始
int temp = ChineseChessMainPanel.dic_RoomData[item.roomId].users.Count;
if (temp == 2)
if (temp >= 2)
{
UIManager.Instance.ShowBoardError("玩家满员");
}
else if (temp == 1)
else
{
uIManager.ShowAsync(UIPanel.ZGXQJoinGame, item);
}
@@ -88,7 +89,6 @@ public class UIChineseChessDistrict : SubUIBase
UIManager.Instance.ShowBoardError("房间已经开始");
break;
case 3:
case 0:
uIManager.ShowAsync(UIPanel.ZGXQCreateRoom, item);
break;
default:
@@ -100,7 +100,8 @@ public class UIChineseChessDistrict : SubUIBase
ChineseChessMainPanel.dic_RoomData[item.roomId] = item;
var slider = tran.transform.GetChild(0).GetComponent<Slider>();
int allPlayerCount = item.users.Count;
// 人数 = 真实玩家 + 机器人(users.Count 只含真实玩家,robotCount 是 AI)
int allPlayerCount = item.users.Count + item.robotCount;
slider.value = (float)allPlayerCount / 2;
if (allPlayerCount == 2)
slider.fillRect.GetChild(0).gameObject.SetActive(true);
@@ -193,13 +194,15 @@ public class UIChineseChessDistrict : SubUIBase
ChineseChessMainPanel.dic_RoomData[roomID] = item;
var tran = itemListDic[roomID];
var slider = tran.transform.GetChild(1).GetComponent<Slider>();
int allPlayerCount = item.users.Count;
// 子节点索引与 Show 保持一致:0=Slider, 1=人数文本, 2=房间号
var slider = tran.transform.GetChild(0).GetComponent<Slider>();
// 人数 = 真实玩家 + 机器人
int allPlayerCount = item.users.Count + item.robotCount;
slider.value = (float)allPlayerCount / 2;
if (allPlayerCount == 2)
slider.fillRect.GetChild(0).gameObject.SetActive(true);
else
slider.fillRect.GetChild(0).gameObject.SetActive(false);
tran.transform.GetChild(2).GetComponent<TextMeshProUGUI>().text = allPlayerCount + "/" + 2;
tran.transform.GetChild(1).GetComponent<TextMeshProUGUI>().text = allPlayerCount + "/" + 2;
}
}
@@ -163,6 +163,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
private Dictionary<int, float> botIdleTimer = new Dictionary<int, float>();
// bot 最近的 FrameData_FireBullet(给其他客户端还原技能状态)
private Dictionary<string, FrameData_FireBullet> botLastFireData = new Dictionary<string, FrameData_FireBullet>();
// bot 运行时状态(进场金币/倍数档位/行为模式/退出)
private Dictionary<int, BotRuntimeState> botRuntimeStates = new Dictionary<int, BotRuntimeState>();
public GameObject bullet_Test;
public GameObject bulletParent;
float changeTurretTime = 0f;
@@ -1510,8 +1512,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (!sit.ConsumeGold(isDouble ? 2 : 1))
return null;
var bullet = Instantiate(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform)
.GetComponent<BullitMove>();
// ⭐ 从对象池取子弹(池空时内部会 Instantiate)
var bulletGo = BulletPool.Instance.Get(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform);
if (bulletGo == null) return null;
var bullet = bulletGo.GetComponent<BullitMove>();
bullet.userid = sit.user;
bullet.playerID = sit.user;
@@ -1896,6 +1900,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (!string.IsNullOrEmpty(user) && user.StartsWith("bot_"))
sit.isBot = true;
// bot 进场:记录运行时状态(进场金币 / 初始档位)
if (sit.isBot)
InitBotRuntimeState(sit, goldNum);
sit.tMP_GoldNum.text = goldNum.ToString();
sit.tMP_DiamondNum.text = diamondNum.ToString();
sit.tMP_TurretConsumeNum.text = consumeGold.ToString();
@@ -1945,20 +1953,35 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (!sit.isBot || sit.gameObject.activeSelf == false) continue;
if (sit.tra_CurrTurret == null) continue;
// ====== 运行时状态 ======
BotRuntimeState state = GetBotRuntimeState(sit);
if (state == null || state.hasExited) continue;
// ====== 退出检查 ======
if (CheckBotExit(sit, state, cfg)) continue;
Vector3 turretPos = sit.tra_CurrTurret.position;
// ====== 技能刷新 ======
// ====== 技能刷新(底层随机技能) ======
if (cfg != null && cfg.skills != null)
RefreshBotSkills(sit, cfg.skills);
var skill = botSkills.ContainsKey(sit.posID) ? botSkills[sit.posID] : null;
// ====== 目标选择(记住当前目标,死了/飞走了才换) ======
MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill);
// ====== Boss 检测 → 决策行为模式 ======
List<MonsterContral> bosses = FindBossesInField(sit, fishList, turretPos, cfg);
BotBehaviorMode mode = DecideBotMode(sit, state, bosses, cfg, skill);
state.currentMode = mode;
// ====== 倍数调整(第一次:基于进场金币比率) ======
AdjustBotMultiplier(sit, state, cfg);
// ====== 目标选择 ======
MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill, mode, bosses, cfg);
if (target == null) continue;
// ====== 锁定技能:走真实锁定系统(显示锁定图标) ======
if (skill != null && skill.lockActive)
// ====== 锁定技能:走真实锁定系统 ======
bool wantLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
if (wantLock)
{
if (sit.monCotl_Lock != target)
{
@@ -1979,10 +2002,13 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
}
}
// ====== 旋转(始终运行,Lerp自然减速) ======
// ====== 旋转 ======
RotateBotToTarget(sit, target.transform.position, rotateSpeed);
// ====== 持续射击 ======
// ====== 倍数再调整(第二次:策略执行后再校验) ======
AdjustBotMultiplier(sit, state, cfg);
// ====== 射击 ======
if (!botFireTimer.ContainsKey(sit.posID))
botFireTimer[sit.posID] = 0f;
botFireTimer[sit.posID] += Time.deltaTime;
@@ -1996,48 +2022,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (botFireTimer[sit.posID] >= fireInterval)
{
botFireTimer[sit.posID] = 0f;
// 攻击动画
sit.ChangeState(TurretState.Attack);
if (!botIdleTimer.ContainsKey(sit.posID))
botIdleTimer[sit.posID] = 0f;
botIdleTimer[sit.posID] = 0f;
float angle = sit.currentAngle;
botLastAngle[sit.user] = angle;
// 技能参数(模仿真人 SyncFire 的行为)
bool isSanShe = skill != null && skill.tripleActive;
bool isDouble = skill != null && skill.doubleActive;
bool isLock = skill != null && skill.lockActive;
int count = isSanShe ? 3 : 1;
long gold = isDouble ? sit.consumeGoldNum * 2 : sit.consumeGoldNum;
// 三发+锁定 → 找 N 个目标鱼的 GUID
List<string> monsterIDs = null;
if (isSanShe && isLock && target != null)
{
monsterIDs = new List<string> { target.guid };
// 额外找 2 个目标(按优先级)
var extraTargets = FindBotLockTargets(sit, target, 2);
foreach (var m in extraTargets) monsterIDs.Add(m.guid);
}
// 存储完整 fireData 供其他客户端还原
botLastFireData[sit.user] = new FrameData_FireBullet
{
angle = angle,
gold = gold,
isSanShe = isSanShe,
twoRTruuect = isDouble,
isSuoDing = isLock,
monsterIDs = monsterIDs
};
WebSocketSendList.BotFireCannon(
GameManager.Instance.gameData.currRoomData.roomId,
sit.user, gold, count);
SyncFire_ForBot(sit, angle, gold, isSanShe, isDouble, isLock, monsterIDs);
BotFire(sit, target, skill, mode, cfg, behavior);
}
// 空闲超时切回 Idle 动画
@@ -2052,12 +2037,245 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
}
}
/// <summary>bot 进场时初始化运行时状态</summary>
private void InitBotRuntimeState(SitPosData sit, long entryGold)
{
var cfg = GameManager.Instance.botConfig;
int initIdx = cfg != null && cfg.multiplier != null ? cfg.multiplier.initialLevelIndex : 0;
var s = new BotRuntimeState
{
entryGold = entryGold,
entryTime = Time.time,
currentLevelIndex = initIdx,
nextMultiplierCheckTime = Time.time + 2f,
nextExitCheckTime = Time.time + 5f,
hasExited = false
};
botRuntimeStates[sit.posID] = s;
// 把初始档位应用到炮台
ApplyBotLevelIndex(sit, initIdx);
}
/// <summary>获取 bot 运行时状态</summary>
private BotRuntimeState GetBotRuntimeState(SitPosData sit)
{
if (botRuntimeStates.TryGetValue(sit.posID, out var s)) return s;
// 兜底:未初始化时按当前金币建一份
InitBotRuntimeState(sit, sit.goldNum);
return botRuntimeStates.TryGetValue(sit.posID, out s) ? s : null;
}
/// <summary>查找场内炮台前方的 Boss 列表</summary>
private List<MonsterContral> FindBossesInField(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotConfig cfg)
{
var result = new List<MonsterContral>();
if (cfg == null || cfg.bossStrategy == null) return result;
var bossTypes = cfg.bossStrategy.bossFishTypes;
if (bossTypes == null || bossTypes.Length == 0) return result;
foreach (var kv in fishList.fishDatas_hashCodekey)
{
var m = kv.Value;
if (m == null || !m.gameObject.activeInHierarchy) continue;
int ft = (int)m.fishenum;
bool isBoss = false;
foreach (var bt in bossTypes) { if (bt == ft) { isBoss = true; break; } }
if (!isBoss) continue;
Vector3 fishPos = m.transform.position;
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
if (!sit.IsFlipped && fishPos.y <= turretPos.y) continue;
result.Add(m);
}
return result;
}
/// <summary>根据 Boss 存在情况决策行为模式,并覆盖技能开关</summary>
private BotBehaviorMode DecideBotMode(SitPosData sit, BotRuntimeState state, List<MonsterContral> bosses, BotConfig cfg, BotSkillRuntime skill)
{
if (cfg == null || cfg.bossStrategy == null) return BotBehaviorMode.ScatterRandom;
var bs = cfg.bossStrategy;
// 狂暴到期清理
if (state.frenzyActive && Time.time >= state.frenzyEndTime)
state.frenzyActive = false;
if (bosses == null || bosses.Count == 0)
{
// ===== 无Boss → 散射乱射 =====
// 随机选择定点三发或单射:覆盖 skill.tripleActive
if (skill != null)
skill.tripleActive = Random.value < bs.tripleShotProb;
// 无Boss时关闭锁定Boss模式
if (skill != null) skill.lockActive = false;
return bs.scatterWhenNoBoss ? BotBehaviorMode.ScatterRandom : BotBehaviorMode.Idle;
}
// ===== 有Boss =====
if (bosses.Count > bs.scatterModeThreshold)
{
// Boss > 阈值 → 散射模式
if (skill != null) skill.tripleActive = true;
return BotBehaviorMode.ScatterMode;
}
// Boss <= 阈值 → 随机狂暴
if (!state.frenzyActive && Random.value < bs.frenzyProb)
{
state.frenzyActive = true;
state.frenzyEndTime = Time.time + Random.Range(bs.frenzyMinTime, bs.frenzyMaxTime);
}
if (state.frenzyActive && skill != null)
skill.frenzyActive = true;
// 锁定最大倍数Boss
return BotBehaviorMode.LockBoss;
}
/// <summary>倍数升降:当前金币/进场金币 < 阈值降档, > 阈值升档</summary>
private void AdjustBotMultiplier(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
if (cfg == null || cfg.multiplier == null) return;
var mc = cfg.multiplier;
if (Time.time < state.nextMultiplierCheckTime) return;
state.nextMultiplierCheckTime = Time.time + mc.checkInterval;
float ratio = state.GetGoldRatio(sit.goldNum);
int maxIdx = mc.maxLevelIndex < 0 ? GetPaoBeiCount() - 1 : mc.maxLevelIndex;
int minIdx = Mathf.Max(0, mc.minLevelIndex);
if (ratio < mc.decreaseThreshold && state.currentLevelIndex > minIdx)
{
state.currentLevelIndex = Mathf.Max(minIdx, state.currentLevelIndex - mc.step);
ApplyBotLevelIndex(sit, state.currentLevelIndex);
}
else if (ratio > mc.increaseThreshold && state.currentLevelIndex < maxIdx)
{
state.currentLevelIndex = Mathf.Min(maxIdx, state.currentLevelIndex + mc.step);
ApplyBotLevelIndex(sit, state.currentLevelIndex);
}
}
/// <summary>当前炮倍档位总数(paoBeiList 长度)</summary>
private int GetPaoBeiCount()
{
if (paoBeiList != null && paoBeiList.Count > 0) return paoBeiList.Count;
return 1;
}
/// <summary>按档位索引设置炮台消耗(直接对齐 consumeGoldNum,避免触发音效/SyncCannonMultiple)</summary>
private void ApplyBotLevelIndex(SitPosData sit, int targetIndex)
{
int count = GetPaoBeiCount();
if (count <= 0 || paoBeiList == null || paoBeiList.Count == 0) return;
targetIndex = Mathf.Clamp(targetIndex, 0, count - 1);
sit.consumeGoldNum = paoBeiList[targetIndex];
if (sit.tMP_TurretConsumeNum != null)
sit.tMP_TurretConsumeNum.text = sit.consumeGoldNum.ToString();
}
/// <summary>退出条件检查:金币>设定 或 金币<设定</summary>
private bool CheckBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
if (cfg == null || cfg.exitCondition == null) return false;
var ec = cfg.exitCondition;
if (Time.time < state.nextExitCheckTime) return false;
state.nextExitCheckTime = Time.time + ec.checkInterval;
bool shouldExit = false;
if (ec.exitWhenGoldAbove > 0 && sit.goldNum >= ec.exitWhenGoldAbove) shouldExit = true;
else if (sit.goldNum <= ec.exitWhenGoldBelow) shouldExit = true;
if (!shouldExit) return false;
HandleBotExit(sit, state, cfg);
return true;
}
/// <summary>处理 bot 退出:发离房消息、座位清理</summary>
private void HandleBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
{
state.hasExited = true;
state.currentMode = BotBehaviorMode.Exited;
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.sendLeaveRoom)
{
string roomId = GameManager.Instance.gameData.currRoomData.roomId;
WebSocketSendList.LeaveRoom(roomId, sit.user);
}
// 清理锁定
sit.isLock = false;
sit.monCotl_Lock = null;
if (sit.lockFX != null) sit.lockFX.SetActive(false);
// 清理记忆
botTargetGuid.Remove(sit.posID);
botFireTimer.Remove(sit.posID);
botIdleTimer.Remove(sit.posID);
botSkills.Remove(sit.posID);
botLastAngle.Remove(sit.user);
botLastFireData.Remove(sit.user);
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
sit.ShowWaiting();
else
sit.gameObject.SetActive(false);
}
/// <summary>bot 开火执行(从 HandleBotAI 抽出)</summary>
private void BotFire(SitPosData sit, MonsterContral target, BotSkillRuntime skill, BotBehaviorMode mode, BotConfig cfg, BotBehaviorConfig behavior)
{
sit.ChangeState(TurretState.Attack);
if (!botIdleTimer.ContainsKey(sit.posID))
botIdleTimer[sit.posID] = 0f;
botIdleTimer[sit.posID] = 0f;
float angle = sit.currentAngle;
botLastAngle[sit.user] = angle;
bool isSanShe = (skill != null && skill.tripleActive) || mode == BotBehaviorMode.ScatterMode || mode == BotBehaviorMode.ScatterRandom;
bool isDouble = skill != null && skill.doubleActive;
bool isLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
int count = isSanShe ? 3 : 1;
long gold = isDouble ? sit.consumeGoldNum * 2 : sit.consumeGoldNum;
// 三发+锁定 → 找 N 个目标鱼的 GUID
List<string> monsterIDs = null;
if (isSanShe && isLock && target != null)
{
monsterIDs = new List<string> { target.guid };
var extraTargets = FindBotLockTargets(sit, target, 2);
foreach (var m in extraTargets) monsterIDs.Add(m.guid);
}
botLastFireData[sit.user] = new FrameData_FireBullet
{
angle = angle,
gold = gold,
isSanShe = isSanShe,
twoRTruuect = isDouble,
isSuoDing = isLock,
monsterIDs = monsterIDs
};
WebSocketSendList.BotFireCannon(
GameManager.Instance.gameData.currRoomData.roomId,
sit.user, gold, count);
SyncFire_ForBot(sit, angle, gold, isSanShe, isDouble, isLock, monsterIDs);
}
/// <summary>
/// 获取或寻找目标:优先用记忆的目标(还在前方就沿用),死了/不在前方了再重新找
/// </summary>
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill)
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill,
BotBehaviorMode mode, List<MonsterContral> bosses, BotConfig cfg)
{
bool isLocked = skill != null && skill.lockActive;
bool isLocked = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
// 1. 检查记忆目标是否还有效
if (botTargetGuid.TryGetValue(sit.posID, out string guid))
@@ -2076,13 +2294,50 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
botTargetGuid.Remove(sit.posID);
}
// 2. 重新找目标(Boss > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近)
// 2. 锁定Boss模式 → 优先最大倍率Boss
if (mode == BotBehaviorMode.LockBoss && bosses != null && bosses.Count > 0
&& cfg != null && cfg.bossStrategy != null && cfg.bossStrategy.preferMaxMultiplierBoss)
{
MonsterContral bossTarget = SelectMaxMultiplierBoss(bosses);
if (bossTarget != null)
{
botTargetGuid[sit.posID] = bossTarget.guid;
return bossTarget;
}
}
// 3. 散射模式 → 在 Boss 中随机选一个(覆盖面广)
if (mode == BotBehaviorMode.ScatterMode && bosses != null && bosses.Count > 0)
{
MonsterContral bossTarget = bosses[Random.Range(0, bosses.Count)];
botTargetGuid[sit.posID] = bossTarget.guid;
return bossTarget;
}
// 4. 重新找目标(Boss > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近)
MonsterContral target = SelectBestTarget(sit, fishList, turretPos);
if (target != null)
botTargetGuid[sit.posID] = target.guid;
return target;
}
/// <summary>从 Boss 列表中选倍率(beilv)最大的</summary>
private MonsterContral SelectMaxMultiplierBoss(List<MonsterContral> bosses)
{
MonsterContral best = null;
int maxBeiLv = int.MinValue;
foreach (var b in bosses)
{
if (b == null) continue;
if (b.beilv > maxBeiLv)
{
maxBeiLv = b.beilv;
best = b;
}
}
return best;
}
/// <summary>
/// 按优先级选目标:特殊Boss(5) > Boss(2) > 黄金鱼(3) > 特殊鱼(4) > 普通鱼(1)
/// 同一优先级内选最近的,只选炮台前方的鱼
+7 -1
View File
@@ -14640,13 +14640,16 @@ MonoBehaviour:
m_EditorClassIdentifier:
img_MianFei: {fileID: 4217081698748454709}
img_MianFeiDuiGou: {fileID: 4330799887900817023}
img_MianFeiBackForword: {fileID: 0}
tmp_MianFeiPropCount: {fileID: 2852674146106605074}
img_JingYing: {fileID: 7888319788910343562}
img_JingYingDuiGou: {fileID: 8171562548857813552}
img_JingYingBackForword: {fileID: 0}
img_JingYingLocal: {fileID: 2255445385472211906}
tmp_JingYingPropCount: {fileID: 9179038560444378523}
img_ZhiZun: {fileID: 5489928632928310148}
img_ZhiZunDuiGou: {fileID: 7680212169677354408}
img_ZhiZunBackForword: {fileID: 0}
img_ZhiZunLocal: {fileID: 4908228247286929228}
tmp_ZhiZunPropCount: {fileID: 5372547274870681584}
tmp_currLevel: {fileID: 1548048096998792596}
@@ -21780,13 +21783,16 @@ MonoBehaviour:
m_EditorClassIdentifier:
img_MianFei: {fileID: 4680046685106973707}
img_MianFeiDuiGou: {fileID: 7485202241167141595}
img_MianFeiBackForword: {fileID: 0}
tmp_MianFeiPropCount: {fileID: 2402272884510985401}
img_JingYing: {fileID: 1845906190580021354}
img_JingYingDuiGou: {fileID: 5578691452543731326}
img_JingYingBackForword: {fileID: 0}
img_JingYingLocal: {fileID: 1603058651523562701}
tmp_JingYingPropCount: {fileID: 962158706182398437}
img_ZhiZun: {fileID: 6578186223567257880}
img_ZhiZunDuiGou: {fileID: 1456507539317513943}
img_ZhiZunBackForword: {fileID: 0}
img_ZhiZunLocal: {fileID: 3480563155218242293}
tmp_ZhiZunPropCount: {fileID: 4221848766627442506}
tmp_currLevel: {fileID: 2337044026528177313}
@@ -25849,7 +25855,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 117.685, y: -25}
m_SizeDelta: {x: 235.37, y: 50}
m_SizeDelta: {x: 0, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4614401808319948171
CanvasRenderer: