Files
5.24BuYu/Assets/MyGame/Scripts/Manager/GameManager.cs
T
2026-10-20 23:36:47 +08:00

668 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Assets.MyGame.Scripts.Net;
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass;
using CameraShake;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class GameManager : MonoSingleton<GameManager>
{
public Camera cam;
[Header("人机")]
public bool enableBot = true; // 人机总开关
public BotConfig botConfig; // 人机AI详细配置(Inspector可调,本地测试用)
[Tooltip("是否使用后端下发的 BotConfig。false=用 Inspector 本地配置(默认,方便测试);true=用后端 GameConfig 返回的 botConfig 覆盖本地")]
public bool useRemoteBotConfig = false;
[Header("日志文件")]
[Tooltip("运行时开关:是否实际生成日志文件。需要同时启用 ENABLE_LOG_FILE 预编译指令才生效。\ntrue=生成日志文件;false=不生成(运行时关闭)")]
public bool enableLogFile = true;
[Header("爆奖模式")]
[Tooltip("其他玩家爆奖处理方式。\ntrue=新模式(not-self 分流:座位显示 img_BaoJiangZhong,等待帧同步 baoJiangEnd 后再显示最终 BJEndUI 加分);\nfalse=旧模式(所有玩家走相同流程,直接克隆 effect_DieAward 播完整特效)")]
public bool useNotSelfBaoJiangMode = false;
public GameConfig gameConfig;
public ActivityConfig activityConfig;
public Dictionary<string, FishTypeConfig> dic_fishTypeConfigs = new Dictionary<string, FishTypeConfig>();
public UserData user;
public GameData gameData = new GameData();
public Scene scene;
/// <summary>
/// 头像更新事件(换了头像后触发,所有头像控件统一刷新)
/// </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)
{
OnAvatarChanged?.Invoke(sprite);
}
public CameraShaker cameraShake;
public Camera ZhuCam;
// // 指定此方法在加载时立即调用,在显示启动动画前之前执行
// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
// private static void BeforeSplashScreen()
// {
// //平台适配
//#if UNITY_WEBGL
// // 如果在 WebGL 平台上运行,立即停止启动动画
// SplashScreen.Stop(SplashScreen.StopBehavior.StopImmediate);
//#else
// // 如果不是在 WebGL 平台上,启动一个异步任务来停止启动动画
// System.Threading.Tasks.Task.Run(() =>
// {
// SplashScreen.Stop(SplashScreen.StopBehavior.StopImmediate);
// });
//#endif
// }
/// <summary>
/// 初始化头像缓存(从 Resources/Avatar 加载所有Sprite)
/// </summary>
public void InitAvatarCache()
{
avatarSpriteDict.Clear();
var sprites = Resources.LoadAll<Sprite>("Avatar");
foreach (var s in sprites)
avatarSpriteDict[s.name] = s;
}
/// <summary>
/// 根据名称获取头像 Sprite
/// </summary>
public Sprite GetAvatarSprite(string avatarName)
{
if (avatarSpriteDict.Count == 0) InitAvatarCache();
if (string.IsNullOrEmpty(avatarName) || !avatarSpriteDict.TryGetValue(avatarName, out var sprite))
sprite = GetDefaultAvatar();
return sprite;
}
private Sprite GetDefaultAvatar()
{
if (defaultAvatar == null && avatarSpriteDict.Count > 0)
{
// 取缓存中第一个头像作为默认
foreach (var kv in avatarSpriteDict)
{
defaultAvatar = kv.Value;
break;
}
}
return defaultAvatar;
}
/// <summary>
/// 更换头像:上传服务端 → 更新本地 → 触发全局刷新
/// </summary>
public void ApplyAvatar(string avatarUrl)
{
APIRequestList.UpdateAvatar(avatarUrl, (result) =>
{
user.BaseInfo.avatarUrl = avatarUrl;
user.BaseInfo.avatarSprite = null;
var sprite = GetAvatarSprite(avatarUrl);
TriggerAvatarChanged(sprite);
});
}
// ==================== 头像框 ====================
/// <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;
OnUserDataChanged?.Invoke();
}
protected override void Awake()
{
// ⭐ botConfig 默认初始化,避免 null 导致技能系统不跑
if (botConfig == null) botConfig = new BotConfig();
// ⭐ 游戏启动时加载已保存的画质和帧率设置
int quality = PlayerPrefs.GetInt("GraphicsQuality", 1);
// applyExpensiveChanges=false:URP 下切换画质会重建渲染管线,立即应用(true)易触发 GPU 重置闪退
QualitySettings.SetQualityLevel(quality, false);
int fps = PlayerPrefs.GetInt("TargetFps", 1);
// 中高档统一为 60 帧。移动端强制垂直同步,45 不可用(60Hz 整数倍才合法),故中档按 60 处理。
Application.targetFrameRate = fps switch
{
0 => 30,
1 => 60,
2 => 60,
_ => 60
};
// ⭐ 防止手机屏幕自动黑屏休眠
Screen.sleepTimeout = SleepTimeout.NeverSleep;
#if ENABLE_LOG_FILE
// ⭐ 启动日志文件自动保存:把所有 Debug.Log/Warning/Error 写入 persistentDataPath/Logs/
// 需要在 Player Settings → Scripting Define Symbols 中添加 ENABLE_LOG_FILE 才会编译
// 运行时可通过 Inspector 的 enableLogFile 勾选框关闭
if (enableLogFile) InitLogFileWriter();
#endif
AudioResourcesData audioResourcesData = new AudioResourcesData();
InItLoad();
//TurretResourcesData turretResourcesData = new TurretResourcesData();
Application.runInBackground = true;
base.Awake();
cameraShake = Camera.main.GetComponent<CameraShaker>();
gameData.currUser = "123";
//string json = Resources.Load<TextAsset>("Config/GameConfig").text;
//gameConfig = JsonConvert.DeserializeObject<APIResponse<GameConfig>>(json).data;
APIManager.Response<GameConfig>(APIType.GameConfig_Get,
res =>
{
gameConfig = res;
var v = CreateFishManager.Instance.CreateDic;
foreach (var item in gameConfig.fishTypeConfigs)
{
dic_fishTypeConfigs[item.fishNumber] = item;
}
// ⭐ 后端 BotConfig 覆盖本地(开关开启且后端返回了 botConfig)
if (useRemoteBotConfig && gameConfig.botConfig != null)
{
botConfig = gameConfig.botConfig;
Debug.Log("[BotConfig] 已使用后端下发的配置覆盖本地");
}
});
APIRequestList.GetActivityConfig(res => { activityConfig = res; });
// ⭐ 注入相机获取委托给框架层 AddSubCamera(框架层不再反向依赖 GameManager)
AddSubCamera.GetMainCamera = () => ZhuCam;
AddSubCamera.GetBulletCamera = () => cam;
}
#if ENABLE_LOG_FILE
// ============================ 日志文件系统 ============================
// 启用方法: Player Settings → Other Settings → Scripting Define Symbols 添加 ENABLE_LOG_FILE
// 关闭方法: 删除该符号,或 Inspector 中 enableLogFile 取消勾选
// 说明: LogGoldFlow 用 [Conditional] 标记,未定义 ENABLE_LOG_FILE 时所有调用点会被编译器自动移除,零开销
private StreamWriter _logFileWriter;
private StreamWriter _goldFlowWriter;
private StreamWriter _fishFlowWriter;
private void InitLogFileWriter()
{
try
{
string logDir = Path.Combine(Application.persistentDataPath, "Logs");
if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir);
string logPath = Path.Combine(logDir, DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".log");
_logFileWriter = new StreamWriter(logPath, true) { AutoFlush = true };
Application.logMessageReceived += OnLogMessageReceived;
Debug.Log($"[GameManager] 日志文件已创建: {logPath}");
InitGoldFlowWriter(logDir);
InitFishFlowWriter(logDir);
}
catch (Exception e)
{
Debug.LogError($"[GameManager] 初始化日志文件失败: {e.Message}");
}
}
private void InitFishFlowWriter(string logDir)
{
try
{
string fishLogPath = Path.Combine(logDir, "鱼生命周期_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".log");
_fishFlowWriter = new StreamWriter(fishLogPath, true) { AutoFlush = true };
_fishFlowWriter.WriteLine($"# 鱼生命周期日志 - 启动于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_fishFlowWriter.WriteLine($"# 当前玩家: {user?.BaseInfo?.userName ?? "未登录"}({user?.id ?? "未登录"})");
_fishFlowWriter.WriteLine($"# 说明: 记录鱼的创建/注册、回收/销毁、guid冲突、找不到鱼等事件,用于排查鱼阵鱼无法销毁的问题");
_fishFlowWriter.WriteLine($"# 分类: CREATE=创建注册 | REMOVE=成功回收 | NOT_FOUND=找不到鱼(异常) | KILL_FX=带特效击杀 | COLLISION=guid冲突 | POOL_GET=从池复用 | POOL_NEW=新建实例 | POOL_RETURN=回收到池");
_fishFlowWriter.WriteLine($"# 格式: 时间|service=client|INFO|FishFlow|[分类] guid=xxx instId=xxx 描述 dictCount=xxx");
_fishFlowWriter.WriteLine(new string('-', 80));
Debug.Log($"[GameManager] 鱼生命周期日志文件已创建: {fishLogPath}");
}
catch (Exception e)
{
Debug.LogError($"[GameManager] 初始化鱼生命周期日志失败: {e.Message}");
}
}
private void InitGoldFlowWriter(string logDir)
{
try
{
string goldLogPath = Path.Combine(logDir, "金币流水_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".log");
_goldFlowWriter = new StreamWriter(goldLogPath, true) { AutoFlush = true };
// ===== 文件头:只记录当前玩家自己(进游戏前可能 user 还没拉到,做 null 保护) =====
string userName = user?.BaseInfo?.userName ?? "未登录";
string userId = user?.id ?? "未登录";
_goldFlowWriter.WriteLine($"# 金币流水日志 - 启动于 {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_goldFlowWriter.WriteLine($"# 当前玩家: {userName}({userId})");
_goldFlowWriter.WriteLine($"# 说明: 只记录当前玩家自己的金币变动,不记录其他玩家");
_goldFlowWriter.WriteLine($"# 格式: 时间|service=client|INFO|GoldFlow|[分类] 玩家=名称(ID) 事件=target 描述");
_goldFlowWriter.WriteLine($"# 分类: 收到加钱/已加钱/收到扣钱/已扣钱/金币流水(通用)");
_goldFlowWriter.WriteLine(new string('-', 80));
Debug.Log($"[GameManager] 金币流水日志文件已创建: {goldLogPath}");
}
catch (Exception e)
{
Debug.LogError($"[GameManager] 初始化金币流水日志失败: {e.Message}");
}
}
private void OnLogMessageReceived(string condition, string stackTrace, LogType type)
{
if (_logFileWriter == null) return;
try
{
string prefix = type switch
{
LogType.Error => "[ERROR]",
LogType.Assert => "[ASSERT]",
LogType.Warning => "[WARN]",
LogType.Log => "[LOG]",
LogType.Exception => "[EXCEPTION]",
_ => "[LOG]"
};
_logFileWriter.WriteLine($"{DateTime.Now:HH:mm:ss.fff} {prefix} {condition}");
if (type == LogType.Error || type == LogType.Exception)
{
_logFileWriter.WriteLine($" Stack: {stackTrace}");
}
}
catch { }
}
private void OnDestroy()
{
if (_logFileWriter != null)
{
Application.logMessageReceived -= OnLogMessageReceived;
try { _logFileWriter.Flush(); _logFileWriter.Close(); } catch { }
_logFileWriter = null;
}
if (_goldFlowWriter != null)
{
try { _goldFlowWriter.Flush(); _goldFlowWriter.Close(); } catch { }
_goldFlowWriter = null;
}
if (_fishFlowWriter != null)
{
try { _fishFlowWriter.Flush(); _fishFlowWriter.Close(); } catch { }
_fishFlowWriter = null;
}
}
#endif
// ============================ 金币流水日志(与后端逐条对比) ============================
// 拆成4类,方便和后端日志逐行对比:
// - 收到扣钱信息(推送): 后端发来扣钱通知,客户端尚未执行
// - 已扣钱(本地执行): 客户端本地已扣完金币
// - 收到加钱信息(推送): 后端发来加钱通知,客户端尚未执行
// - 已加钱(本地执行): 客户端本地已加完金币
//
// 控制方式:
// 1. 编译时: 未定义 ENABLE_LOG_FILE 符号时,所有调用被编译器自动移除([Conditional]特性),零开销
// 2. 运行时: 即使编译了,Inspector 中 enableLogFile=false 也不会写入文件
//
// 通用记录(无关扣/加,如退出房间)继续用 LogGoldFlow
/// <summary>记录"收到扣钱信息"(后端推送,尚未执行)。如:后端推送 SyncPlayerGold 差值为负</summary>
[System.Diagnostics.Conditional("ENABLE_LOG_FILE")]
public static void LogGoldFlowReceiveCost(string eventName, string description)
{
#if ENABLE_LOG_FILE
WriteGoldFlow("收到扣钱", eventName, description);
#endif
}
/// <summary>记录"已扣钱"(本地已执行扣金币)。如:开火 ConsumeGold 扣完</summary>
[System.Diagnostics.Conditional("ENABLE_LOG_FILE")]
public static void LogGoldFlowAppliedCost(string eventName, string description)
{
#if ENABLE_LOG_FILE
WriteGoldFlow("已扣钱", eventName, description);
#endif
}
/// <summary>记录"收到加钱信息"(后端推送,尚未执行)。如:RoomJoinNotice/RoomInfo 推送金币</summary>
[System.Diagnostics.Conditional("ENABLE_LOG_FILE")]
public static void LogGoldFlowReceiveGain(string eventName, string description)
{
#if ENABLE_LOG_FILE
WriteGoldFlow("收到加钱", eventName, description);
#endif
}
/// <summary>记录"已加钱"(本地已执行加金币)。如:杀鱼 KillFish 加完、进房 JoinGame 设置初始金币</summary>
[System.Diagnostics.Conditional("ENABLE_LOG_FILE")]
public static void LogGoldFlowAppliedGain(string eventName, string description)
{
#if ENABLE_LOG_FILE
WriteGoldFlow("已加钱", eventName, description);
#endif
}
/// <summary>通用金币流水记录(无关扣/加,如退出房间)。需要明确分类时请用上面4个方法</summary>
[System.Diagnostics.Conditional("ENABLE_LOG_FILE")]
public static void LogGoldFlow(string eventName, string description)
{
#if ENABLE_LOG_FILE
WriteGoldFlow("金币流水", eventName, description);
#endif
}
#if ENABLE_LOG_FILE
/// <summary>金币流水核心写入(私有)。所有公开 LogGoldFlow* 方法最终都走这里</summary>
private static void WriteGoldFlow(string category, string eventName, string description)
{
var inst = Instance;
if (inst == null || !inst.enableLogFile || inst._goldFlowWriter == null) return;
try
{
string userName = inst.user?.BaseInfo?.userName ?? "未知";
string userId = inst.user?.id ?? "未知";
string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
inst._goldFlowWriter.WriteLine($"{timeStr}|service=client|INFO|GoldFlow|[{category}] 玩家={userName}({userId}) 事件={eventName} {description}");
}
catch { }
}
#endif
// ============================ 鱼生命周期日志(排查鱼阵鱼无法销毁问题) ============================
// 分类:
// CREATE 鱼创建并注册到字典(RegisterFishInstance)
// REMOVE 鱼从字典成功移除(Id_DieFish_NotFX 成功路径)
// NOT_FOUND 找不到鱼(Id_DieFish_NotFX 失败路径 —— 这就是bug)
// KILL_FX 带特效击杀(Id_DieFish_FX)
// COLLISION guid 冲突(新鱼覆盖旧鱼,旧鱼变野鱼)
// POOL_GET 从池复用鱼对象
// POOL_NEW 新建鱼实例(池中没有)
// POOL_RETURN 回收鱼对象到池
//
// 控制方式同金币流水: ENABLE_LOG_FILE 编译时 + enableLogFile 运行时
/// <summary>
/// 记录鱼生命周期事件。instId 传 0 表示不需要实例ID。
/// </summary>
[System.Diagnostics.Conditional("ENABLE_LOG_FILE")]
public static void LogFishFlow(string category, string guid, string description, int instId = 0)
{
#if ENABLE_LOG_FILE
WriteFishFlow(category, guid, description, instId);
#endif
}
#if ENABLE_LOG_FILE
/// <summary>鱼生命周期核心写入(私有)</summary>
private static void WriteFishFlow(string category, string guid, string description, int instId)
{
var inst = Instance;
if (inst == null || !inst.enableLogFile || inst._fishFlowWriter == null) return;
try
{
string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
string instStr = instId > 0 ? $" instId={instId}" : "";
// dictCount 从 CreateFishManager 取,这里只负责写文件
inst._fishFlowWriter.WriteLine($"{timeStr}|service=client|INFO|FishFlow|[{category}] guid={guid}{instStr} {description}");
}
catch { }
}
#endif
public async Task InItLoad()
{
await TurretResourcesData.LoadTurretAssetsData();
}
public void StartGame(Action action)
{
switch (gameData.currGameType)
{
case GameType.JQQD:
break;
case GameType.defaultPanel:
UIManager.Instance.ShowAsync(UIPanel.MainGamePanelDefault);
break;
case GameType.FLZY:
//UIManager.Instance.ShowAsync(UIPanel.MainGamePanelFLZY);
ConnectGameSocket(action);
break;
case GameType.YYSC:
ConnectGameSocket(action);
break;
case GameType.ZGXQ:
//UIManager.Instance.ShowAsync(UIPanel.MainGamePanelZGXQ);
//这里设置触发棋盘的初始化
break;
case GameType.XJMH:
ConnectGameSocket(action);
break;
case GameType.HHWK:
//UIManager.Instance.ShowAsync(UIPanel.MainGamePanelDefault);
ConnectGameSocket(action);
break;
default:
break;
}
}
/// <summary>
/// 通过 /api/Room/Assign 获取房间专属 socket 地址后连接 game socket
/// </summary>
private void ConnectGameSocket(Action action)
{
string roomId = gameData.currRoomData?.roomId;
if (string.IsNullOrEmpty(roomId))
{
#if UNITY_EDITOR
Debug.LogWarning("[GameManager] currRoomData.roomId 为空,使用默认 game socket 地址");
#endif
WebSocketManager.Instance.Connect(SocketType.game, action);
return;
}
APIRequestList.RoomAssign(roomId, url =>
{
if (!string.IsNullOrEmpty(url))
WebSocketManager.Instance.Connect(SocketType.game, url, action);
else
WebSocketManager.Instance.Connect(SocketType.game, action);
});
}
/// <summary>
/// 快速加入:用 /api/Room/QuickJoin 返回的 url 直接连 game socket(无需再调 Assign)
/// </summary>
/// <param name="url">QuickJoin 返回的房间专属 socket 地址</param>
/// <param name="action">连接成功后的回调(一般是 JoinRoom(roomId, userId, -1))</param>
public void StartGameQuickJoin(string url, Action action)
{
switch (gameData.currGameType)
{
case GameType.FLZY:
case GameType.YYSC:
case GameType.XJMH:
case GameType.HHWK:
if (!string.IsNullOrEmpty(url))
WebSocketManager.Instance.Connect(SocketType.game, url, action);
else
WebSocketManager.Instance.Connect(SocketType.game, action);
break;
default:
WebSocketManager.Instance.Connect(SocketType.game, action);
break;
}
}
// Start is called before the first frame update
public Button btn_Skip;
IEnumerator yanchi(ILoadingPanel loadingPanel)
{
yield return new WaitForSeconds(5);
if (btn_Skip != null) btn_Skip.gameObject.SetActive(true);
yield return new WaitForSeconds(23f);
// ⭐ 防御:loadingPanel 可能已被 UIManager.CloseAll 销毁(如 Logout 时)
if (loadingPanel != null)
loadingPanel.LoadBar(100);
}
void Start()
{
btn_Skip.gameObject.SetActive(false);
// 过场视频由 LoginPanel 根节点的 VideoPlayer 投影到共享 RT,加载面板只是显示它
var v = UIManager.Instance.ShowLoadPanel();
StartCoroutine(yanchi(v));
btn_Skip.onClick.AddListener(() =>
{
UIManager.Instance.StopShowPanel();
v.LoadBar(100);
});
}
// Update is called once per frame
void Update()
{
#if UNITY_EDITOR
if (Keyboard.current.qKey.wasPressedThisFrame)
{
UIManager.Instance.ShowBoardGongGao("太有实力了dadadadwawafdsdrgdfghdsufhkjghjksdfdgdffghgjhgykhjkhjghjfg");
}
#endif
//// FPS 统计
//fpsFrameCount++;
//fpsTimer += Time.unscaledDeltaTime;
//if (fpsTimer >= 0.5f)
//{
// fps = Mathf.RoundToInt(fpsFrameCount / fpsTimer);
// fpsFrameCount = 0;
// fpsTimer = 0f;
//}
}
//// ===== FPS 显示 =====
//private int fpsFrameCount;
//private float fpsTimer;
//private int fps;
//void OnGUI()
//{
// // 左上角显示帧数
// GUIStyle style = new GUIStyle(GUI.skin.label)
// {
// fontSize = 28,
// fontStyle = FontStyle.Bold
// };
// style.normal.textColor = fps >= 50 ? Color.green : (fps >= 30 ? Color.yellow : Color.red);
// GUI.Label(new Rect(10, 10, 200, 40), "FPS: " + fps, style);
//}
public void Logout(Action onDone = null)
{
// ⭐ 标记登出中:阻止 WebSocket 关闭后触发自动重连(避免访问已 null 的 user)
WebSocketManager.Instance.isLoggingOut = true;
// 关闭所有 WebSocket 连接(内部会先 StopAllReconnects 停止所有重连协程)
WebSocketManager.Instance.CloseAll();
// 清空用户数据
user = null;
// 关闭所有 UI
UIManager.Instance.CloseAll();
// ⭐ 登出流程完成,立即恢复重连开关(不依赖 ShowAsync 回调,避免 UI 异常导致永不重置)
WebSocketManager.Instance.isLoggingOut = false;
// 切换到登录界面(⭐ 过场视频由 UILoginPanel.Show 里从头重播,避免面板出来前只有声音没画面)
UIManager.Instance.ShowAsync(UIPanel.LoginPanel, null, (panel) =>
{
onDone?.Invoke();
});
}
}