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; using UnityEngine.Video; public class GameManager : MonoSingleton { 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 dic_fishTypeConfigs = new Dictionary(); public UserData user; public GameData gameData = new GameData(); public Scene scene; /// /// 头像更新事件(换了头像后触发,所有头像控件统一刷新) /// /// 头像更新事件 public event Action OnAvatarChanged; /// 头像框更新事件 public event Action OnAvatarFrameChanged; /// 用户数据更新事件(金币/钻石/等级等发生变化时触发) public event Action OnUserDataChanged; /// 头像字典(name → sprite),从 Resources/Avatar 加载 private Dictionary avatarSpriteDict = new Dictionary(); /// 头像框字典(name → sprite),从 Resources/AvatarK 加载 private Dictionary avatarFrameDict = new Dictionary(); /// 默认头像(新账号无头像时使用) private Sprite defaultAvatar; /// 默认头像框 private Sprite defaultAvatarFrame; /// 触发头像更新事件 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 // } /// /// 初始化头像缓存(从 Resources/Avatar 加载所有Sprite) /// public void InitAvatarCache() { avatarSpriteDict.Clear(); var sprites = Resources.LoadAll("Avatar"); foreach (var s in sprites) avatarSpriteDict[s.name] = s; } /// /// 根据名称获取头像 Sprite /// 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; } /// /// 更换头像:上传服务端 → 更新本地 → 触发全局刷新 /// public void ApplyAvatar(string avatarUrl) { APIRequestList.UpdateAvatar(avatarUrl, (result) => { user.BaseInfo.avatarUrl = avatarUrl; user.BaseInfo.avatarSprite = null; var sprite = GetAvatarSprite(avatarUrl); TriggerAvatarChanged(sprite); }); } // ==================== 头像框 ==================== /// 触发头像框更新事件 public void TriggerAvatarFrameChanged(Sprite sprite) { OnAvatarFrameChanged?.Invoke(sprite); } /// /// 初始化头像框缓存(从 Resources/AvatarK 加载所有Sprite) /// public void InitAvatarFrameCache() { avatarFrameDict.Clear(); var sprites = Resources.LoadAll("AvatarK"); foreach (var s in sprites) avatarFrameDict[s.name] = s; } /// /// 根据名称获取头像框 Sprite /// 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; } /// /// 更换头像框:上传服务端 → 更新本地 → 触发全局刷新 /// 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(); gameData.currUser = "123"; //string json = Resources.Load("Config/GameConfig").text; //gameConfig = JsonConvert.DeserializeObject>(json).data; APIManager.Response(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; }); } #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 /// 记录"收到扣钱信息"(后端推送,尚未执行)。如:后端推送 SyncPlayerGold 差值为负 [System.Diagnostics.Conditional("ENABLE_LOG_FILE")] public static void LogGoldFlowReceiveCost(string eventName, string description) { #if ENABLE_LOG_FILE WriteGoldFlow("收到扣钱", eventName, description); #endif } /// 记录"已扣钱"(本地已执行扣金币)。如:开火 ConsumeGold 扣完 [System.Diagnostics.Conditional("ENABLE_LOG_FILE")] public static void LogGoldFlowAppliedCost(string eventName, string description) { #if ENABLE_LOG_FILE WriteGoldFlow("已扣钱", eventName, description); #endif } /// 记录"收到加钱信息"(后端推送,尚未执行)。如:RoomJoinNotice/RoomInfo 推送金币 [System.Diagnostics.Conditional("ENABLE_LOG_FILE")] public static void LogGoldFlowReceiveGain(string eventName, string description) { #if ENABLE_LOG_FILE WriteGoldFlow("收到加钱", eventName, description); #endif } /// 记录"已加钱"(本地已执行加金币)。如:杀鱼 KillFish 加完、进房 JoinGame 设置初始金币 [System.Diagnostics.Conditional("ENABLE_LOG_FILE")] public static void LogGoldFlowAppliedGain(string eventName, string description) { #if ENABLE_LOG_FILE WriteGoldFlow("已加钱", eventName, description); #endif } /// 通用金币流水记录(无关扣/加,如退出房间)。需要明确分类时请用上面4个方法 [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 /// 金币流水核心写入(私有)。所有公开 LogGoldFlow* 方法最终都走这里 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 运行时 /// /// 记录鱼生命周期事件。instId 传 0 表示不需要实例ID。 /// [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 /// 鱼生命周期核心写入(私有) 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; } } /// /// 通过 /api/Room/Assign 获取房间专属 socket 地址后连接 game socket /// 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); }); } /// /// 快速加入:用 /api/Room/QuickJoin 返回的 url 直接连 game socket(无需再调 Assign) /// /// QuickJoin 返回的房间专属 socket 地址 /// 连接成功后的回调(一般是 JoinRoom(roomId, userId, -1)) 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; public VideoClip startVideoClip; IEnumerator yanchi(LoadingPanel 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); var v = UIManager.Instance.ShowLoadPanel(startVideoClip); //UIManager.Instance.ShowAsync(UIPanel.LoginPanel, null, (a) => //{ // v.LoadBar(100); //}, 28); StartCoroutine(yanchi(v)); btn_Skip.onClick.AddListener(() => { UIManager.Instance.StopShowPanel(); //UIManager.Instance.ShowAsync(UIPanel.LoginPanel, null, null, 0); 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; // 切换到登录界面 UIManager.Instance.ShowAsync(UIPanel.LoginPanel, null, (panel) => { onDone?.Invoke(); }); } }