This commit is contained in:
2026-07-05 02:10:44 +08:00
parent f1720ab889
commit e4fae4dbc8
115 changed files with 9453 additions and 390 deletions
+1
View File
@@ -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; }
+3
View File
@@ -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: