From ffcdfa3b774b092c5f4da75376e359f402366f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A3=AE2003?= <2384017988@qq.com> Date: Mon, 13 Jul 2026 00:12:44 +0800 Subject: [PATCH] 1 --- Assets/Framework/Tool/Captcha.meta | 8 + .../Tool/Captcha/CaptchaGenerator.cs | 227 ++++++++++++++++++ .../Tool/Captcha/CaptchaGenerator.cs.meta | 11 + .../Scripts/Net/WebSocketReponseList.cs | 24 +- .../aa/UI/Game/GameMainPanel_FishBase.cs | 5 +- .../aa/UI/Home/Sub/UIHome_BY_ShopPanel.cs | 3 + Assets/MyGame/aa/UI/Home/UIHomeMain.cs | 16 +- .../aa/UI/Login/SubPanel/RegisterPanel.cs | 31 +++ .../aa/UI/Login/SubPanel/RegisterPanel.prefab | 99 +++++++- .../Home/VIP-恢复的/jindutiao.png.meta | 17 +- .../Home/VIP-恢复的/jindutiao_di.png.meta | 17 +- 11 files changed, 441 insertions(+), 17 deletions(-) create mode 100644 Assets/Framework/Tool/Captcha.meta create mode 100644 Assets/Framework/Tool/Captcha/CaptchaGenerator.cs create mode 100644 Assets/Framework/Tool/Captcha/CaptchaGenerator.cs.meta diff --git a/Assets/Framework/Tool/Captcha.meta b/Assets/Framework/Tool/Captcha.meta new file mode 100644 index 000000000..e20a55b13 --- /dev/null +++ b/Assets/Framework/Tool/Captcha.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f790cbebb7c8c304196143de204f5048 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs b/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs new file mode 100644 index 000000000..e990fb9ae --- /dev/null +++ b/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +/// +/// 图形验证码生成器:运行时动态生成验证码图片(Texture2D → Sprite),不依赖任何外部图片资源。 +/// 用法:var sprite = CaptchaGenerator.Generate(width, height, out string code); +/// +public static class CaptchaGenerator +{ + // 验证码字符集(去除易混淆字符 0/O/1/I/l) + private const string CharSet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + + // 干扰线颜色池 + private static readonly Color[] NoiseColors = + { + new Color(0.6f, 0.6f, 0.6f), + new Color(0.4f, 0.4f, 0.8f), + new Color(0.8f, 0.4f, 0.4f), + new Color(0.4f, 0.8f, 0.4f), + new Color(0.8f, 0.8f, 0.2f), + }; + + // 字符颜色池 + private static readonly Color[] TextColors = + { + new Color(0.1f, 0.1f, 0.6f), + new Color(0.6f, 0.1f, 0.1f), + new Color(0.1f, 0.5f, 0.1f), + new Color(0.5f, 0.2f, 0.5f), + new Color(0.2f, 0.2f, 0.2f), + new Color(0.6f, 0.3f, 0.1f), + }; + + /// + /// 生成验证码图片并转换为 Sprite。 + /// + /// 图片宽度 + /// 图片高度 + /// 输出的验证码文本 + /// 验证码字符数(默认4) + /// 验证码 Sprite(可直接赋给 Image) + public static Sprite Generate(int width, int height, out string code, int length = 4) + { + code = RandomCode(length); + var tex = DrawCaptcha(code, width, height); + return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f)); + } + + /// + /// 生成验证码图片并转换为 Sprite(无 out 参数的便捷重载)。 + /// + public static Sprite Generate(int width, int height, int length = 4) + { + return Generate(width, height, out _, length); + } + + /// 生成随机验证码文本 + private static string RandomCode(int length) + { + var sb = new StringBuilder(length); + for (int i = 0; i < length; i++) + sb.Append(CharSet[Random.Range(0, CharSet.Length)]); + return sb.ToString(); + } + + /// 把验证码画到 Texture2D 上 + private static Texture2D DrawCaptcha(string code, int width, int height) + { + // 半透明背景,方便看清楚字符 + var bg = new Color(0.95f, 0.95f, 0.95f, 1f); + var tex = new Texture2D(width, height, TextureFormat.RGBA32, false); + tex.filterMode = FilterMode.Point; + + var pixels = new Color32[width * height]; + + // 填充背景 + for (int i = 0; i < pixels.Length; i++) + pixels[i] = bg; + + tex.SetPixels32(pixels); + + // 画干扰线 + int lineCount = 3 + Random.Range(0, 3); // 3~5 条 + for (int i = 0; i < lineCount; i++) + { + var c = NoiseColors[Random.Range(0, NoiseColors.Length)]; + int x0 = Random.Range(0, width); + int y0 = Random.Range(0, height); + int x1 = Random.Range(0, width); + int y1 = Random.Range(0, height); + DrawLine(tex, x0, y0, x1, y1, c); + } + + // 画干扰点 + int dotCount = 30 + Random.Range(0, 30); // 30~59 个 + for (int i = 0; i < dotCount; i++) + { + var c = NoiseColors[Random.Range(0, NoiseColors.Length)]; + int x = Random.Range(0, width); + int y = Random.Range(0, height); + tex.SetPixel(x, y, c); + } + + // 逐个字符绘制(使用内置 8x8 像素字体) + int charPixelSize = Mathf.Min(height * 2 / 3, width / (code.Length + 1)); // 字符像素大小 + int startX = (width - code.Length * charPixelSize) / 2; + int startY = (height - charPixelSize) / 2; + + for (int i = 0; i < code.Length; i++) + { + var c = TextColors[Random.Range(0, TextColors.Length)]; + int x = startX + i * charPixelSize; + // 每个字符的 y 做轻微随机偏移 + int y = startY + Random.Range(-charPixelSize / 6, charPixelSize / 6); + DrawChar(tex, code[i], x, y, charPixelSize, c); + } + + tex.Apply(); + return tex; + } + + /// 绘制单条直线(Bresenham) + private static void DrawLine(Texture2D tex, int x0, int y0, int x1, int y1, Color c) + { + int dx = Mathf.Abs(x1 - x0), sx = x0 < x1 ? 1 : -1; + int dy = -Mathf.Abs(y1 - y0), sy = y0 < y1 ? 1 : -1; + int err = dx + dy; + while (true) + { + if (x0 >= 0 && x0 < tex.width && y0 >= 0 && y0 < tex.height) + tex.SetPixel(x0, y0, c); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } + } + + /// 用 8x8 像素位图绘制单个字符,可缩放到任意像素大小 + private static void DrawChar(Texture2D tex, char ch, int x, int y, int size, Color c) + { + // 5x7 像素位图(行宽5,行高7)的简单 ASCII 字库 + bool[,] pattern = GetCharPattern(ch); + if (pattern == null) return; + + int pw = pattern.GetLength(0); // 5 + int ph = pattern.GetLength(1); // 7 + float sx = size / (float)pw; + float sy = size / (float)ph; + + for (int py = 0; py < ph; py++) + { + for (int px = 0; px < pw; px++) + { + if (!pattern[px, py]) continue; + // 把位图的一个像素映射到 size×size 区域(从下往上画,因为 Texture2D 原点在左下) + int xStart = Mathf.RoundToInt(x + px * sx); + int xEnd = Mathf.RoundToInt(x + (px + 1) * sx); + int yStart = Mathf.RoundToInt(y + (ph - 1 - py) * sy); + int yEnd = Mathf.RoundToInt(y + (ph - py) * sy); + + for (int yy = yStart; yy < yEnd; yy++) + { + for (int xx = xStart; xx < xEnd; xx++) + { + if (xx >= 0 && xx < tex.width && yy >= 0 && yy < tex.height) + tex.SetPixel(xx, yy, c); + } + } + } + } + } + + /// + /// 简单的 5x7 像素字库(支持 0-9 A-Z,去掉易混淆字符)。 + /// 返回 bool[x, y],x=0..4(列),y=0..6(行,从上到下)。 + /// + private static bool[,] GetCharPattern(char ch) + { + // 每个字符 7 行,每行 5 个像素(从上到下) + // '1' = 亮,'0' = 暗 + string[] rows; + switch (ch) + { + case '2': rows = new[] { "01110", "10001", "00001", "00010", "00100", "01000", "11111" }; break; + case '3': rows = new[] { "01110", "10001", "00001", "00110", "00001", "10001", "01110" }; break; + case '4': rows = new[] { "00010", "00110", "01010", "10010", "11111", "00010", "00010" }; break; + case '5': rows = new[] { "11111", "10000", "11110", "00001", "00001", "10001", "01110" }; break; + case '6': rows = new[] { "00110", "01000", "10000", "11110", "10001", "10001", "01110" }; break; + case '7': rows = new[] { "11111", "00001", "00010", "00100", "01000", "01000", "01000" }; break; + case '8': rows = new[] { "01110", "10001", "10001", "01110", "10001", "10001", "01110" }; break; + case '9': rows = new[] { "01110", "10001", "10001", "01111", "00001", "00010", "01100" }; break; + case 'A': rows = new[] { "01110", "10001", "10001", "11111", "10001", "10001", "10001" }; break; + case 'B': rows = new[] { "11110", "10001", "10001", "11110", "10001", "10001", "11110" }; break; + case 'C': rows = new[] { "01110", "10001", "10000", "10000", "10000", "10001", "01110" }; break; + case 'D': rows = new[] { "11110", "10001", "10001", "10001", "10001", "10001", "11110" }; break; + case 'E': rows = new[] { "11111", "10000", "10000", "11110", "10000", "10000", "11111" }; break; + case 'F': rows = new[] { "11111", "10000", "10000", "11110", "10000", "10000", "10000" }; break; + case 'G': rows = new[] { "01110", "10001", "10000", "10111", "10001", "10001", "01110" }; break; + case 'H': rows = new[] { "10001", "10001", "10001", "11111", "10001", "10001", "10001" }; break; + case 'J': rows = new[] { "00111", "00010", "00010", "00010", "00010", "10010", "01100" }; break; + case 'K': rows = new[] { "10001", "10010", "10100", "11000", "10100", "10010", "10001" }; break; + case 'L': rows = new[] { "10000", "10000", "10000", "10000", "10000", "10000", "11111" }; break; + case 'M': rows = new[] { "10001", "11011", "10101", "10101", "10001", "10001", "10001" }; break; + case 'N': rows = new[] { "10001", "11001", "10101", "10011", "10001", "10001", "10001" }; break; + case 'P': rows = new[] { "11110", "10001", "10001", "11110", "10000", "10000", "10000" }; break; + case 'Q': rows = new[] { "01110", "10001", "10001", "10001", "10101", "10010", "01101" }; break; + case 'R': rows = new[] { "11110", "10001", "10001", "11110", "10100", "10010", "10001" }; break; + case 'S': rows = new[] { "01111", "10000", "10000", "01110", "00001", "10001", "11110" }; break; + case 'T': rows = new[] { "11111", "00100", "00100", "00100", "00100", "00100", "00100" }; break; + case 'U': rows = new[] { "10001", "10001", "10001", "10001", "10001", "10001", "01110" }; break; + case 'V': rows = new[] { "10001", "10001", "10001", "10001", "10001", "01010", "00100" }; break; + case 'W': rows = new[] { "10001", "10001", "10001", "10101", "10101", "11011", "10001" }; break; + case 'X': rows = new[] { "10001", "10001", "01010", "00100", "01010", "10001", "10001" }; break; + case 'Y': rows = new[] { "10001", "10001", "01010", "00100", "00100", "00100", "00100" }; break; + case 'Z': rows = new[] { "11111", "00001", "00010", "00100", "01000", "10000", "11111" }; break; + default: return null; + } + + var pattern = new bool[5, 7]; + for (int y = 0; y < 7; y++) + for (int x = 0; x < 5; x++) + pattern[x, y] = rows[y][x] == '1'; + return pattern; + } +} diff --git a/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs.meta b/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs.meta new file mode 100644 index 000000000..cf854b52d --- /dev/null +++ b/Assets/Framework/Tool/Captcha/CaptchaGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1cba60bc00a4a094a88b8147d9c2fdd5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs index 52c6772b3..2d4b8f9ed 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs @@ -12,6 +12,8 @@ namespace Assets.MyGame.Scripts.Net { public class WebSocketReponseList { + /// 正在加载中的游戏场景 UIPanel 集合,防止房主收到多条 RoomJoinNotice(含人机)时重复创建场景 + private static readonly HashSet _loadingGamePanels = new HashSet(); /// /// 获取房间列表 room /// @@ -94,23 +96,34 @@ namespace Assets.MyGame.Scripts.Net default: break; } - if (UIManager.Instance.Get(uIPanel)==null) + if (UIManager.Instance.Get(uIPanel)==null && !_loadingGamePanels.Contains(uIPanel)) { - UIManager.Instance.ShowAsync(uIPanel, UIPanel.HomeMainPanel, msg); - // 卸载所有已加载的游戏地图场景 + // 标记正在加载,防止房主收到多条 RoomJoinNotice(含人机玩家)时重复创建场景 + _loadingGamePanels.Add(uIPanel); + UIManager.Instance.ShowAsync(uIPanel, UIPanel.HomeMainPanel, msg, (panel) => + { + // 面板创建完成,移除加载标记 + _loadingGamePanels.Remove(uIPanel); + }); + // 卸载所有已加载的游戏地图场景(保证场景唯一性) string[] gameScenes = { "FLZY", "YYSC", "XJMH", "HHWK" }; foreach (var sn in gameScenes) { + // 不卸载即将加载的目标场景 + if (sn == sceneName) continue; var s = SceneManager.GetSceneByName(sn); if (s.IsValid() && s.isLoaded) SceneManager.UnloadSceneAsync(sn); } - SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); + // 目标场景已加载则不重复加载 + var targetScene = SceneManager.GetSceneByName(sceneName); + if (!targetScene.IsValid() || !targetScene.isLoaded) + SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); UIManager.Instance.Close(UIPanel.HomeMainPanel); WebSocketManager.Instance.isGame = true; } - else + else if (UIManager.Instance.Get(uIPanel) != null) { // 面板已存在 → 新玩家加入,转发给现有面板激活座位 var panel = UIManager.Instance.Get(uIPanel); @@ -134,6 +147,7 @@ namespace Assets.MyGame.Scripts.Net } } } + // else: 场景正在加载中(_loadingGamePanels 含此 uIPanel),忽略本次重复通知 } diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs index f11b370a9..d57a270c1 100644 --- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs +++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs @@ -304,7 +304,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base UnLoadResources(); - SceneManager.UnloadSceneAsync(sceneName); + // 场景可能未加载或已卸载,做保护避免重复卸载报错 + var scene = SceneManager.GetSceneByName(sceneName); + if (scene.IsValid() && scene.isLoaded) + SceneManager.UnloadSceneAsync(sceneName); WebSocketManager.Instance.Close(SocketType.game); } diff --git a/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_ShopPanel.cs b/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_ShopPanel.cs index e8985cb97..8bd72f229 100644 --- a/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_ShopPanel.cs +++ b/Assets/MyGame/aa/UI/Home/Sub/UIHome_BY_ShopPanel.cs @@ -85,6 +85,7 @@ public class UIHome_BY_ShopPanel : SubUIBase APIRequestList.GetUser(GameManager.Instance.user.id, (a) => { GameManager.Instance.UpdataUserData(a); + RefreshUI(); }); }); }); @@ -228,6 +229,7 @@ public class UIHome_BY_ShopPanel : SubUIBase APIRequestList.GetUser(GameManager.Instance.user.id, (a) => { GameManager.Instance.UpdataUserData(a); + RefreshUI(); }); }); }); @@ -269,6 +271,7 @@ public class UIHome_BY_ShopPanel : SubUIBase APIRequestList.GetUser(GameManager.Instance.user.id, (a) => { GameManager.Instance.UpdataUserData(a); + RefreshUI(); }); }); }); diff --git a/Assets/MyGame/aa/UI/Home/UIHomeMain.cs b/Assets/MyGame/aa/UI/Home/UIHomeMain.cs index e247f85b1..c86b9f37e 100644 --- a/Assets/MyGame/aa/UI/Home/UIHomeMain.cs +++ b/Assets/MyGame/aa/UI/Home/UIHomeMain.cs @@ -1,4 +1,4 @@ -using Assets.MyGame.Scripts.Net; +using Assets.MyGame.Scripts.Net; using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass; using Assets.MyGame.Scripts.Net.APIRequest.Objs; using Assets.MyGame.Scripts.Net.APIResponses.HomeMain_User; @@ -208,15 +208,25 @@ public class UIHomeMain : UIBase } public void UpdateUserData() { + if (GameManager.Instance?.user?.assets == null) return; + tMP_GoldCoinNum.text = GameManager.Instance.user.assets.gold.ToString(); tMP_DiamondNum.text = GameManager.Instance.user.assets.diamond.ToString(); userName.text = GameManager.Instance.user.BaseInfo.userName; userID.text = GameManager.Instance.user.BaseInfo.number; - img_VIP.sprite = sprite_VIP[GameManager.Instance.user.assets.vipLevel]; + + // VIP 图标越界保护:vipLevel 超出列表范围时用最后一个,避免异常中断刷新 + int vipLevel = GameManager.Instance.user.assets.vipLevel; + if (sprite_VIP != null && sprite_VIP.Count > 0 && img_VIP != null) + { + int idx = Mathf.Clamp(vipLevel, 0, sprite_VIP.Count - 1); + img_VIP.sprite = sprite_VIP[idx]; + } + RefreshAvatar(); RefreshAvatarFrame(); - img_VIP.SetNativeSize(); + if (img_VIP != null) img_VIP.SetNativeSize(); } private void RefreshAvatar() diff --git a/Assets/MyGame/aa/UI/Login/SubPanel/RegisterPanel.cs b/Assets/MyGame/aa/UI/Login/SubPanel/RegisterPanel.cs index e8332498c..ee162f29a 100644 --- a/Assets/MyGame/aa/UI/Login/SubPanel/RegisterPanel.cs +++ b/Assets/MyGame/aa/UI/Login/SubPanel/RegisterPanel.cs @@ -20,6 +20,9 @@ public class RegisterPanel : SubUIBase private string _realQueRenPassword = ""; private Coroutine _maskCoroutine, _maskCoroutine2; + /// 当前验证码(不区分大小写比对) + private string _captchaCode = ""; + public override void Awake() { base.Awake(); @@ -37,8 +40,25 @@ public class RegisterPanel : SubUIBase tMPInput_QueRenPassword.contentType = TMP_InputField.ContentType.Standard; tMPInput_QueRenPassword.onValueChanged.AddListener(v => OnPwdChanged(v, tMPInput_QueRenPassword, ref _realQueRenPassword, ref _maskCoroutine2)); + // 点击验证码图片刷新 + if (img_YZM != null) + { + var yzmBtn = img_YZM.GetComponent