1
This commit is contained in:
@@ -29,14 +29,20 @@ public class GameManager : MonoSingleton<GameManager>
|
||||
/// </summary>
|
||||
/// <summary>头像更新事件</summary>
|
||||
public event Action<Sprite> OnAvatarChanged;
|
||||
/// <summary>头像框更新事件</summary>
|
||||
public event Action<Sprite> OnAvatarFrameChanged;
|
||||
/// <summary>用户数据更新事件(金币/钻石/等级等发生变化时触发)</summary>
|
||||
public event Action OnUserDataChanged;
|
||||
|
||||
/// <summary>头像字典(name → sprite),从 Resources/Avatar 加载</summary>
|
||||
private Dictionary<string, Sprite> avatarSpriteDict = new Dictionary<string, Sprite>();
|
||||
/// <summary>头像框字典(name → sprite),从 Resources/AvatarK 加载</summary>
|
||||
private Dictionary<string, Sprite> avatarFrameDict = new Dictionary<string, Sprite>();
|
||||
|
||||
/// <summary>默认头像(新账号无头像时使用)</summary>
|
||||
private Sprite defaultAvatar;
|
||||
/// <summary>默认头像框</summary>
|
||||
private Sprite defaultAvatarFrame;
|
||||
|
||||
/// <summary>触发头像更新事件</summary>
|
||||
public void TriggerAvatarChanged(Sprite sprite)
|
||||
@@ -114,6 +120,63 @@ public class GameManager : MonoSingleton<GameManager>
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 头像框 ====================
|
||||
|
||||
/// <summary>触发头像框更新事件</summary>
|
||||
public void TriggerAvatarFrameChanged(Sprite sprite)
|
||||
{
|
||||
OnAvatarFrameChanged?.Invoke(sprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化头像框缓存(从 Resources/AvatarK 加载所有Sprite)
|
||||
/// </summary>
|
||||
public void InitAvatarFrameCache()
|
||||
{
|
||||
avatarFrameDict.Clear();
|
||||
var sprites = Resources.LoadAll<Sprite>("AvatarK");
|
||||
foreach (var s in sprites)
|
||||
avatarFrameDict[s.name] = s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据名称获取头像框 Sprite
|
||||
/// </summary>
|
||||
public Sprite GetAvatarFrameSprite(string avatarFrameName)
|
||||
{
|
||||
if (avatarFrameDict.Count == 0) InitAvatarFrameCache();
|
||||
if (string.IsNullOrEmpty(avatarFrameName) || !avatarFrameDict.TryGetValue(avatarFrameName, out var sprite))
|
||||
sprite = GetDefaultAvatarFrame();
|
||||
return sprite;
|
||||
}
|
||||
|
||||
private Sprite GetDefaultAvatarFrame()
|
||||
{
|
||||
if (defaultAvatarFrame == null && avatarFrameDict.Count > 0)
|
||||
{
|
||||
foreach (var kv in avatarFrameDict)
|
||||
{
|
||||
defaultAvatarFrame = kv.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return defaultAvatarFrame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更换头像框:上传服务端 → 更新本地 → 触发全局刷新
|
||||
/// </summary>
|
||||
public void ApplyAvatarFrame(string avatarFrameName)
|
||||
{
|
||||
APIRequestList.UpdateAvatarFrame(avatarFrameName, (result) =>
|
||||
{
|
||||
user.BaseInfo.avatarFrame = avatarFrameName;
|
||||
|
||||
var sprite = GetAvatarFrameSprite(avatarFrameName);
|
||||
TriggerAvatarFrameChanged(sprite);
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdataUserData(UserData user)
|
||||
{
|
||||
this.user = user;
|
||||
|
||||
@@ -147,6 +147,7 @@ namespace Assets.MyGame.Scripts.Net
|
||||
dic_API[APIType.User_BindWX] = "/api/User/BindWX";
|
||||
dic_API[APIType.User_BindMobile] = "/api/User/BindMobile";
|
||||
dic_API[APIType.User_UpdateAvatar] = "/api/User/UpdateAvatar";
|
||||
dic_API[APIType.User_UpdateAvatarFrame] = "/api/User/UpdateAvatarFrame";
|
||||
dic_API[APIType.User_GetUser] = "/api/User/GetUser";
|
||||
dic_API[APIType.User_GetUserList] = "/api/User/GetUserList";
|
||||
dic_API[APIType.User_SendEmail] = "/api/User/SendEmail";
|
||||
|
||||
@@ -53,6 +53,17 @@ namespace Assets.MyGame.Scripts.Net.APIRequest.Params
|
||||
public string avatarUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新头像框参数(GET)
|
||||
/// Swagger: /api/User/UpdateAvatarFrame?avatarFrame=xxx
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class UpdateAvatarFrameParam : RequestParamBase
|
||||
{
|
||||
/// <summary>头像框地址</summary>
|
||||
public string avatarFrame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公告详情请求参数(GET /api/Notice/get?id=)
|
||||
/// </summary>
|
||||
|
||||
@@ -95,6 +95,18 @@ namespace Assets.MyGame.Scripts.Net
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新头像框(GET)
|
||||
/// Swagger: /api/User/UpdateAvatarFrame?avatarFrame=xxx
|
||||
/// </summary>
|
||||
public static void UpdateAvatarFrame(string avatarFrame, Action<string> callback = null)
|
||||
{
|
||||
APIManager.Response<string>(APIType.User_UpdateAvatarFrame, new UpdateAvatarFrameParam
|
||||
{
|
||||
avatarFrame = avatarFrame
|
||||
}, callback);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rank
|
||||
|
||||
@@ -76,6 +76,7 @@ public class UserBaseInfo
|
||||
public string password { get; set; }
|
||||
public string number { get; set; }
|
||||
public string avatarUrl { get; set; }
|
||||
public string avatarFrame { get; set; }
|
||||
public string mobile { get; set; }
|
||||
public string wxOpenId { get; set; }
|
||||
public string channel { get; set; }
|
||||
|
||||
@@ -289,6 +289,9 @@ namespace Assets.MyGame.Scripts.Net
|
||||
/// <summary>更新头像(GET)</summary>
|
||||
User_UpdateAvatar,
|
||||
|
||||
/// <summary>更新头像框(GET)</summary>
|
||||
User_UpdateAvatarFrame,
|
||||
|
||||
/// <summary>获取用户信息(GET)</summary>
|
||||
User_GetUser,
|
||||
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
# 机器人(人机)后端控制方案 v2.0
|
||||
|
||||
> 重新梳理:哪些归后端控制,哪些前端自己处理
|
||||
|
||||
---
|
||||
|
||||
## 一、边界划分
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 后端控制 (策略层) │
|
||||
│ ├─ 总开关 / 状态 / 爆率因子 │
|
||||
│ ├─ 各技能的概率范围 │
|
||||
│ └─ 各状态下的行为参数范围 │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 前端自理 (执行层) │
|
||||
│ ├─ 具体找哪条鱼、怎么旋转、子弹怎么飞 │
|
||||
│ ├─ 按后端概率随机掷骰子决定技能开关 │
|
||||
│ └─ 技能组合运行时管理、具体物理表现 │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**核心原则**:后端提供"概率范围",前端在范围内"随机掷骰子"。
|
||||
|
||||
---
|
||||
|
||||
## 二、后端控制字段(后端传什么)
|
||||
|
||||
### 2.1 数据模型
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 机器人AI配置 — 后端 GameConfig / RoomConfig 下发
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class BotConfig
|
||||
{
|
||||
// ===== 一、全局控制 =====
|
||||
|
||||
/// <summary>机器人总开关</summary>
|
||||
public bool enabled;
|
||||
|
||||
/// <summary>AI 状态: "active" | "idle" | "aggressive"</summary>
|
||||
public string aiState;
|
||||
|
||||
/// <summary>状态刷新间隔(秒),0=后端主动推送时才变</summary>
|
||||
public float stateRefreshInterval;
|
||||
|
||||
/// <summary>爆率因子: 1.0=正常, >1=容易杀, <1=难杀。服务端 KillFish 判定时使用</summary>
|
||||
public float luckFactor;
|
||||
|
||||
|
||||
// ===== 二、技能配置 (后端提供概率范围,前端随机掷骰子) =====
|
||||
|
||||
public BotSkillConfig skills;
|
||||
|
||||
|
||||
// ===== 三、各状态行为参数 =====
|
||||
|
||||
/// <summary>active 状态参数</summary>
|
||||
public BotBehaviorConfig activeBehavior;
|
||||
|
||||
/// <summary>idle 状态参数</summary>
|
||||
public BotBehaviorConfig idleBehavior;
|
||||
|
||||
/// <summary>aggressive 状态参数</summary>
|
||||
public BotBehaviorConfig aggressiveBehavior;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 技能概率配置 — 每种技能独立,可以随机组合
|
||||
/// 前端每隔 skillRefreshInterval 秒掷一次骰子决定各技能开关
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class BotSkillConfig
|
||||
{
|
||||
/// <summary>技能刷新间隔(秒): 每隔这个时间重新掷骰子决定技能组合</summary>
|
||||
public float skillRefreshInterval;
|
||||
|
||||
/// <summary>
|
||||
/// 各技能的开关概率(0~1)。前端独立掷骰子,可以同时开多种。
|
||||
/// 例如 lockProb=0.3 表示 30% 时间开启锁定
|
||||
/// </summary>
|
||||
|
||||
/// <summary>锁定 — 自动瞄准最近鱼</summary>
|
||||
public float lockProb;
|
||||
public float lockMinTime;
|
||||
public float lockMaxTime;
|
||||
|
||||
/// <summary>狂暴 — 开火速度翻倍</summary>
|
||||
public float frenzyProb;
|
||||
public float frenzyMinTime;
|
||||
public float frenzyMaxTime;
|
||||
|
||||
/// <summary>三发散弹 — 一次打3发</summary>
|
||||
public float tripleProb;
|
||||
public float tripleMinTime;
|
||||
public float tripleMaxTime;
|
||||
|
||||
/// <summary>双倍炮 — 消耗翻倍/伤害翻倍</summary>
|
||||
public float doubleProb;
|
||||
public float doubleMinTime;
|
||||
public float doubleMaxTime;
|
||||
|
||||
/// <summary>冰冻 — 冻住全场鱼</summary>
|
||||
public float freezeProb;
|
||||
public float freezeMinTime;
|
||||
public float freezeMaxTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 行为参数 — 每个 aiState 一组,控制该状态下机器人的表现
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class BotBehaviorConfig
|
||||
{
|
||||
/// <summary>开火间隔范围(秒): 实际值随机取 [min, max]</summary>
|
||||
public float fireIntervalMin;
|
||||
public float fireIntervalMax;
|
||||
|
||||
/// <summary>炮台旋转速度(度/秒)</summary>
|
||||
public float rotateSpeed;
|
||||
|
||||
/// <summary>目标锁定保持时间范围(秒)</summary>
|
||||
public float targetHoldTimeMin;
|
||||
public float targetHoldTimeMax;
|
||||
|
||||
/// <summary>超时后切换目标的概率(0~1)</summary>
|
||||
public float targetSwitchChance;
|
||||
|
||||
/// <summary>优先打高分鱼(0=纯距离优先, 1=纯分数优先)</summary>
|
||||
public float targetPreferHighScore;
|
||||
|
||||
/// <summary>是否允许打Boss</summary>
|
||||
public bool canTargetBoss;
|
||||
|
||||
/// <summary>是否允许打特殊鱼(红包/钻头等)</summary>
|
||||
public bool canTargetSpecial;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 JSON 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"botConfig": {
|
||||
"enabled": true,
|
||||
"aiState": "active",
|
||||
"stateRefreshInterval": 0,
|
||||
"luckFactor": 1.0,
|
||||
|
||||
"skills": {
|
||||
"skillRefreshInterval": 15,
|
||||
"lockProb": 0.3, "lockMinTime": 5, "lockMaxTime": 20,
|
||||
"frenzyProb": 0.15, "frenzyMinTime": 3, "frenzyMaxTime": 8,
|
||||
"tripleProb": 0.2, "tripleMinTime": 4, "tripleMaxTime": 12,
|
||||
"doubleProb": 0.1, "doubleMinTime": 3, "doubleMaxTime": 6,
|
||||
"freezeProb": 0.05, "freezeMinTime": 1, "freezeMaxTime": 3
|
||||
},
|
||||
|
||||
"activeBehavior": {
|
||||
"fireIntervalMin": 0.3,
|
||||
"fireIntervalMax": 1.2,
|
||||
"rotateSpeed": 40,
|
||||
"targetHoldTimeMin": 2,
|
||||
"targetHoldTimeMax": 5,
|
||||
"targetSwitchChance": 0.7,
|
||||
"targetPreferHighScore": 0.2,
|
||||
"canTargetBoss": true,
|
||||
"canTargetSpecial": true
|
||||
},
|
||||
|
||||
"idleBehavior": {
|
||||
"fireIntervalMin": 1.5,
|
||||
"fireIntervalMax": 4.0,
|
||||
"rotateSpeed": 20,
|
||||
"targetHoldTimeMin": 5,
|
||||
"targetHoldTimeMax": 10,
|
||||
"targetSwitchChance": 0.3,
|
||||
"targetPreferHighScore": 0.1,
|
||||
"canTargetBoss": false,
|
||||
"canTargetSpecial": false
|
||||
},
|
||||
|
||||
"aggressiveBehavior": {
|
||||
"fireIntervalMin": 0.1,
|
||||
"fireIntervalMax": 0.4,
|
||||
"rotateSpeed": 80,
|
||||
"targetHoldTimeMin": 1,
|
||||
"targetHoldTimeMax": 3,
|
||||
"targetSwitchChance": 0.9,
|
||||
"targetPreferHighScore": 0.6,
|
||||
"canTargetBoss": true,
|
||||
"canTargetSpecial": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、机器人用户数据配置(每个 bot 实例独有)
|
||||
|
||||
### 3.1 后端在 players 列表中下发的 bot 字段
|
||||
|
||||
每个 bot 以 `RoomPlayerInfo` 的形式出现在 `RoomChangeResponse.players` 中,`isBot = true`。后端需要为每个 bot 提供以下字段:
|
||||
|
||||
```csharp
|
||||
// 以 RoomPlayerInfo 为例,bot 需要的字段:
|
||||
{
|
||||
"userId": "bot_a1b2c3d4", // "bot_" + 8位GUID,后端生成
|
||||
"userName": "小渔夫", // 从 BotProfile 随机选取
|
||||
"avatar": "https://...", // 从 BotProfile 随机选取
|
||||
"isBot": true, // ← 前端据此识别机器人
|
||||
"position": 1, // 座位号 0~3
|
||||
"gold": 50000, // 初始金币(后端控制金额)
|
||||
"diamond": 0, // 钻石(bot 通常为0)
|
||||
"cannonId": "1", // 炮台皮肤ID(后端决定用什么炮)
|
||||
"wingsId": "", // 翅膀皮肤ID
|
||||
"warGodId": "", // 战神皮肤ID
|
||||
"cannonMultiple": 10, // 炮倍(后端决定基础炮倍)
|
||||
"beginCoin": 50000, // 进场金币
|
||||
"beginTime": 1718700000, // 进场时间戳
|
||||
"state": 0 // 0=正常
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 字段职责划分
|
||||
|
||||
| 字段 | 谁决定 | 说明 |
|
||||
|------|--------|------|
|
||||
| `userId` | **后端** | 格式 `bot_` + GUID,后端创建 bot 玩家时生成 |
|
||||
| `userName` | **后端** | 从 MongoDB `bot_avatar_config` 集合随机选 |
|
||||
| `avatar` | **后端** | 同上,从 BotProfile 获取 |
|
||||
| `gold` | **后端** | 初始金币金额。后续由服务端 `BotFireCannon`/`BotKillFish` 自动管理加减 |
|
||||
| `diamond` | **后端** | bot 不需要钻石 |
|
||||
| `cannonId` | **后端** | 决定 bot 用什么炮台。可随机从炮台列表选,也可固定 |
|
||||
| `wingsId` | **后端** | bot 是否带翅膀皮肤 |
|
||||
| `cannonMultiple` | **后端** | bot 的基础炮倍。如后端不提供,前端用 `paoBeiList[0]` 兜底 |
|
||||
| `position` | **后端** | 按空余座位顺序分配 |
|
||||
| `beginCoin` | **后端** | 进场金币,由后端记录 |
|
||||
| `beginTime` | **后端** | 进场时间戳 |
|
||||
|
||||
### 3.3 前端处理
|
||||
|
||||
```csharp
|
||||
// GameMainPanel_FishBase.cs — OnShow() 中遍历 players
|
||||
foreach (var item in GameManager.Instance.gameData.currRoomData.players)
|
||||
{
|
||||
JoinGame(item.position, item.gold, item.diamond, item.cannonMultiple, 0,
|
||||
item.userId, item.cannonId, item.wingsId);
|
||||
// JoinGame 内部已处理: user.StartsWith("bot_") → isBot = true
|
||||
}
|
||||
// 不需要前端自己创建 bot!后端 players 列表已经包含所有 bot 玩家数据
|
||||
```
|
||||
|
||||
### 3.4 后端创建 bot 流程(参考)
|
||||
|
||||
```
|
||||
第一个真人玩家 JoinRoom
|
||||
→ 从 MongoDB bot_avatar_config 随机选 robotCount 个 BotProfile
|
||||
→ 为每个 BotProfile:
|
||||
生成 userId = "bot_" + Guid.NewGuid().ToString("N")[..8]
|
||||
随机选一个空座位 position
|
||||
计算初始 gold = maxMultiple * Random(100, 501) (或其他规则)
|
||||
设置 cannonId / cannonMultiple
|
||||
→ 插入 Redis room.players
|
||||
→ RoomJoinNotice 广播给所有在线玩家
|
||||
```
|
||||
|
||||
### 3.5 炮倍切换
|
||||
|
||||
bot 的炮倍有两种玩法:
|
||||
- **固定炮倍**:后端给定 `cannonMultiple` 后不再变化
|
||||
- **随机切换**:前端在 `BotConfig` 中加一个 `randomCannonMultiple` 参数,bot 每隔一定时间随机换炮倍
|
||||
|
||||
```csharp
|
||||
// BotConfig 中可新增:
|
||||
public bool randomCannonMultiple = false; // 是否随机切换炮倍
|
||||
public float cannonMultipleRefreshInterval = 30f; // 切换间隔(秒)
|
||||
public int[] cannonMultipleOptions = { 1, 5, 10, 20, 50, 100 }; // 可选炮倍列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、状态机 + 技能组合
|
||||
|
||||
### 4.1 状态机
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ active (正常) │
|
||||
│ 按 activeBehavior 参数运行 │
|
||||
└───────┬──────────────────┬────────────┘
|
||||
│ │
|
||||
后端推送 │ │ 后端推送
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────────┐
|
||||
│ idle (待机) │ │ aggressive (激进) │
|
||||
│ 懒散/低活跃度 │ │ 高频/高伤害/短间隔 │
|
||||
└──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 技能随机组合流程
|
||||
|
||||
```
|
||||
每个 bot 独立运行:
|
||||
|
||||
每隔 skillRefreshInterval 秒:
|
||||
├─ 掷骰子: lockProb → rand() < prob ? 开启锁定(持续 lockMin~lockMax 秒)
|
||||
├─ 掷骰子: frenzyProb → rand() < prob ? 开启狂暴(持续 frenzyMin~frenzyMax 秒)
|
||||
├─ 掷骰子: tripleProb → rand() < prob ? 开启三发(持续 tripleMin~tripleMax 秒)
|
||||
├─ 掷骰子: doubleProb → rand() < prob ? 开启双倍(持续 doubleMin~doubleMax 秒)
|
||||
└─ 掷骰子: freezeProb → rand() < prob ? 触发冰冻(持续 freezeMin~freezeMax 秒)
|
||||
```
|
||||
|
||||
**组合效果示例**:
|
||||
- `锁+三发` → 自动锁定目标后发射3发散弹 → 看起来像高手
|
||||
- `狂暴+双倍` → 超高速开火且双倍伤害 → 看起来像土豪
|
||||
- `三发+狂暴` → 高速连发3发散弹 → 看起来很猛
|
||||
- 只开`锁定` → 普通玩家水平
|
||||
- 什么都不开 → 菜鸟玩家
|
||||
|
||||
每种技能独立随机,**6种技能可以有 2^5=32 种组合**(冰冻不和其他技能叠加算),机器人看起来千人千面。
|
||||
|
||||
---
|
||||
|
||||
## 五、前端实现要点
|
||||
|
||||
### 5.1 技能运行时结构
|
||||
|
||||
```csharp
|
||||
// 每个 bot 座位的运行时技能状态
|
||||
private class BotSkillRuntime
|
||||
{
|
||||
public bool lockActive;
|
||||
public float lockEndTime;
|
||||
|
||||
public bool frenzyActive;
|
||||
public float frenzyEndTime;
|
||||
|
||||
public bool tripleActive;
|
||||
public float tripleEndTime;
|
||||
|
||||
public bool doubleActive;
|
||||
public float doubleEndTime;
|
||||
|
||||
public bool freezeActive;
|
||||
public float freezeEndTime;
|
||||
|
||||
public float nextRefreshTime; // 下次掷骰子时间
|
||||
}
|
||||
private Dictionary<int, BotSkillRuntime> botSkills = new Dictionary<int, BotSkillRuntime>();
|
||||
```
|
||||
|
||||
### 5.2 HandleBotAI 改造
|
||||
|
||||
```csharp
|
||||
private void HandleBotAI()
|
||||
{
|
||||
BotConfig cfg = botConfig; // 从后端读取
|
||||
if (cfg == null || !cfg.enabled) return;
|
||||
// ... 房主检查 ...
|
||||
|
||||
BotBehaviorConfig behavior = GetBehaviorForState(cfg);
|
||||
|
||||
foreach (var sit in list_Sits)
|
||||
{
|
||||
if (!sit.isBot) continue;
|
||||
// ... 找目标 (用 behavior.targetHoldTimeMin/Max, targetSwitchChance 等) ...
|
||||
|
||||
// ---- 技能刷新 ----
|
||||
RefreshBotSkills(sit, cfg.skills);
|
||||
|
||||
// ---- 开火 (用行为参数 + 技能修正) ----
|
||||
float interval = Random.Range(behavior.fireIntervalMin, behavior.fireIntervalMax);
|
||||
var skill = botSkills[sit.posID];
|
||||
if (skill.frenzyActive) interval *= 0.5f; // 狂暴加速
|
||||
|
||||
botFireTimer[sit.posID] += Time.deltaTime;
|
||||
if (botFireTimer[sit.posID] >= interval)
|
||||
{
|
||||
botFireTimer[sit.posID] = 0f;
|
||||
// 三发/双倍通过 fireBurstCount 控制
|
||||
int count = skill.tripleActive ? 3 : 1;
|
||||
long gold = skill.doubleActive ? sit.consumeGoldNum * 2 : sit.consumeGoldNum;
|
||||
WebSocketSendList.BotFireCannon(roomId, sit.user, gold, count);
|
||||
SyncFire_ForBot(sit, sit.currentAngle);
|
||||
|
||||
// 冰冻是独立事件,概率触发
|
||||
if (skill.freezeActive)
|
||||
{
|
||||
skill.freezeActive = false;
|
||||
// 触发冰冻... (调用现有的冰冻逻辑)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 技能掷骰子
|
||||
|
||||
```csharp
|
||||
private void RefreshBotSkills(SitPosData sit, BotSkillConfig skillCfg)
|
||||
{
|
||||
if (!botSkills.TryGetValue(sit.posID, out var s))
|
||||
{
|
||||
s = new BotSkillRuntime();
|
||||
botSkills[sit.posID] = s;
|
||||
}
|
||||
|
||||
// 未到刷新时间
|
||||
if (Time.time < s.nextRefreshTime) return;
|
||||
s.nextRefreshTime = Time.time + skillCfg.skillRefreshInterval;
|
||||
|
||||
// 独立掷骰子
|
||||
s.lockActive = s.lockActive && Time.time < s.lockEndTime // 还在持续中就不用重新掷
|
||||
|| Random.value < skillCfg.lockProb;
|
||||
if (s.lockActive && Time.time >= s.lockEndTime)
|
||||
s.lockEndTime = Time.time + Random.Range(skillCfg.lockMinTime, skillCfg.lockMaxTime);
|
||||
|
||||
s.frenzyActive = s.frenzyActive && Time.time < s.frenzyEndTime
|
||||
|| Random.value < skillCfg.frenzyProb;
|
||||
if (s.frenzyActive && Time.time >= s.frenzyEndTime)
|
||||
s.frenzyEndTime = Time.time + Random.Range(skillCfg.frenzyMinTime, skillCfg.frenzyMaxTime);
|
||||
|
||||
s.tripleActive = s.tripleActive && Time.time < s.tripleEndTime
|
||||
|| Random.value < skillCfg.tripleProb;
|
||||
if (s.tripleActive && Time.time >= s.tripleEndTime)
|
||||
s.tripleEndTime = Time.time + Random.Range(skillCfg.tripleMinTime, skillCfg.tripleMaxTime);
|
||||
|
||||
s.doubleActive = s.doubleActive && Time.time < s.doubleEndTime
|
||||
|| Random.value < skillCfg.doubleProb;
|
||||
if (s.doubleActive && Time.time >= s.doubleEndTime)
|
||||
s.doubleEndTime = Time.time + Random.Range(skillCfg.doubleMinTime, skillCfg.doubleMaxTime);
|
||||
|
||||
// 冰冻单独处理(触发型,不是持续型)
|
||||
if (Random.value < skillCfg.freezeProb)
|
||||
{
|
||||
s.freezeActive = true;
|
||||
// 触发冰雪效果...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、需要后端新增的接口
|
||||
|
||||
### 6.1 Hub 事件:`SyncBotConfig`
|
||||
|
||||
服务端 → 所有客户端推送机器人配置变更
|
||||
|
||||
```json
|
||||
// 事件: connection.on("SyncBotConfig", botConfig)
|
||||
{
|
||||
"aiState": "aggressive",
|
||||
"skills": { ... },
|
||||
"aggressiveBehavior": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**触发时机**:
|
||||
- GM 后台修改配置 → 实时推送
|
||||
- Boss 出场 → 切 aggressive
|
||||
- 机器人金币不足 → 切 idle
|
||||
- 定时:`stateRefreshInterval > 0` 时自动切换
|
||||
|
||||
### 6.2 下发位置
|
||||
|
||||
放在 `GameConfig.RoomConfig` 中(按房型配置)或 `RoomChangeResponse` 中(按房间实例配置)。
|
||||
|
||||
---
|
||||
|
||||
## 七、不归后端管的(前端自行处理)
|
||||
|
||||
| 内容 | 原因 |
|
||||
|------|------|
|
||||
| 具体找哪条鱼(最近/最贵) | 纯客户端渲染逻辑 |
|
||||
| 旋转角度计算 | 坐标系相关,后端不感知 |
|
||||
| 子弹飞行/碰撞/特效 | 纯客户端表现 |
|
||||
| 技能效果视觉表现 | UI 层面 |
|
||||
| Random 掷骰子的具体时机 | 服从概率就行,具体时间点前端定 |
|
||||
| 不同 bot 之间的差异化 | 通过统一概率 + 各自独立 random 自然产生 |
|
||||
|
||||
---
|
||||
|
||||
## 八、总结
|
||||
|
||||
```
|
||||
后端做的事: 前端做的事:
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ 给一个概率配置 │ ──下发──► │ 拿配置自己掷骰子 │
|
||||
│ "30%概率锁定5~20秒" │ │ rand < 0.3 → 锁定打开 │
|
||||
│ "15%概率狂暴3~8秒" │ │ rand < 0.15 → 狂暴打开 │
|
||||
│ "三状态下各参数范围" │ │ 三发×狂暴×锁定 随机组合 │
|
||||
│ │ │ 每个bot独立运行 │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
后端只关心 **"机器人群体看起来多强/多活跃"**,前端负责 **"具体怎么做让每个机器人看起来不一样"**。
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2f9d7e5f1b24e84da03d19ec586183e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user