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