Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace XGame
{
public class AESManager
{
private static readonly byte[] Key = Encoding.UTF8.GetBytes("01fsd56#@9AB183F"); // 16字节的密钥
private static readonly byte[] IV = Encoding.UTF8.GetBytes("01fsd56789A*%#3F"); // 16字节的初始向量
private static byte[] XWKey;
private static byte[] XWIV;
static AESManager()
{
Set(ChangeBytes(Encoding.UTF8.GetBytes("#@pOrhgf2026091211325890vciu%c-X")));
}
public static void Set(byte[] data)
{
if (data.Length != 32)
throw new Exception("AES Key or IV length must be 32 characters");
XWKey = new byte[16];
Array.Copy(data, 0, XWKey, 0, 16);
XWIV = new byte[16];
Array.Copy(data, 16, XWIV, 0, 16);
}
public static byte[] ChangeBytes(byte[] src, byte xor = 0xb4)
{
for (int i = 0; i < src.Length; i++)
{
src[i] = (byte)(src[i] ^ xor);
}
return src;
}
public static string Encrypt(string plainText)
{
string encryptedText = Convert.ToBase64String(Encrypt(Encoding.UTF8.GetBytes(plainText)));
return encryptedText;
}
public static byte[] Encrypt(byte[] plainBytes, bool bXW = true)
{
using (Aes aes = Aes.Create())
{
if (bXW)
{
aes.Key = XWKey;
aes.IV = XWIV;
}
else
{
aes.Key = Key;
aes.IV = IV;
}
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] encryptedBytes = null;
using (var ms = new System.IO.MemoryStream())
{
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(plainBytes, 0, plainBytes.Length);
cs.FlushFinalBlock();
}
encryptedBytes = ms.ToArray();
}
return encryptedBytes;
}
}
public static byte[] Decrypt(byte[] encryptedBytes, bool bXW = true)
{
using (Aes aes = Aes.Create())
{
if (bXW)
{
aes.Key = XWKey;
aes.IV = XWIV;
}
else
{
aes.Key = Key;
aes.IV = IV;
}
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
byte[] decryptedBytes = null;
using (var ms = new System.IO.MemoryStream(encryptedBytes))
{
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
byte[] decryptedData = new byte[encryptedBytes.Length];
int bytesRead = cs.Read(decryptedData, 0, decryptedData.Length);
if (bytesRead == decryptedData.Length)
return decryptedData;
// 如果你知道解密后的数据的确切大小,你可以根据需要进行截取
byte[] trimmedData = new byte[bytesRead];
Array.Copy(decryptedData, trimmedData, bytesRead);
return trimmedData;
}
}
}
}
public static string Decrypt(string encryptedText)
{
string decryptedText = Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(encryptedText)));
return decryptedText;
}
}
// 使用示例:
//string originalText = "Hello, World!";
//string encryptedText = AESExample.Encrypt(originalText);
//string decryptedText = AESExample.Decrypt(encryptedText);
//Console.WriteLine("Original Text: " + originalText);
//Console.WriteLine("Encrypted Text: " + encryptedText);
//Console.WriteLine("Decrypted Text: " + decryptedText);
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d2fd95f487f29d408cf0205e1fafa15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
using System;
using UnityEngine;
namespace XGame
{
public static class AppConst
{
#if LOCAL_HTTP
#if UNITY_WEBGL
public static string UpdatePath = "D:/HTTP/Web/StreamingAssets/";//
public static string UpdateServer = "http://127.0.0.1/StreamingAssets/";
#else
public static string UpdatePath = "d:/HTTP/update/";//
public static string UpdateServer = "http://127.0.0.1/Web/StreamingAssets/";
#endif//
public static string WebGL = "D:/HTTP/Web/StreamingAssets";//"d:/HTTP/update/webglbase";
#else
public static string UpdatePath = "E:/HTTP/60S/";//"\\\\192.168.9.231\\g\\App\\up\\";//
public static string UpdateServer = "http://192.168.51.100/60S/";//"http://192.168.9.231/up/";//"http://127.0.0.1/update/"; //
public static string WebGL = "\\\\192.168.9.231\\g\\App\\up\\wg";//webglbase";//
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f46db62dac1cc8458a29210db32c2a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 10d0fd33d2c048aebf93520dd8099ebe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XGame
{
[Serializable]
public class CharacterConfigData
{
public string defaultCharacterId;
public Vector3 spawnPosition;
public Vector3 spawnEuler;
public List<CharacterConfigItem> characters = new List<CharacterConfigItem>();
public CharacterConfigItem GetDefault()
{
CharacterConfigItem item = Find(defaultCharacterId);
return item ?? (characters.Count > 0 ? characters[0] : null);
}
public CharacterConfigItem Find(string id)
{
if (string.IsNullOrEmpty(id))
{
return null;
}
for (int i = 0; i < characters.Count; i++)
{
CharacterConfigItem item = characters[i];
if (item != null && string.Equals(item.id, id, StringComparison.OrdinalIgnoreCase))
{
return item;
}
}
return null;
}
}
[Serializable]
public class CharacterConfigItem
{
public string id;
public string displayName;
public string prefabPath;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a02dad566ae24fdcb50e5fbc6e4c42c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,324 @@
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UObject = UnityEngine.Object;
namespace XGame
{
public class CharacterSwitchController : MonoBehaviour
{
private const string ConfigResourcePath = "Config/CharacterConfig";
private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId";
private const string PrefabPath = "Assets/Game/Art/UI/Prefab/UI_CharacterSwitch.prefab";
private CharacterConfigData config;
private CharacterConfigItem currentConfig;
private GameObject currentCharacter;
private Transform characterRoot;
private string errorText;
// UGUI 引用(替换原 IMGUI 绘制)
private GameObject uiPanel; // 实例化的 UI 根
private GameObject selectPanel; // Panel_Select
private Transform listChars; // List_Chars
private GameObject itemTemplate; // Item_Char
private TMP_Text errorLabel; // Txt_Error
private TMP_Text toggleLabel; // Txt_Toggle
private readonly List<Button> itemButtons = new List<Button>();
private readonly List<string> itemIds = new List<string>();
public string CurrentCharacterId
{
get { return currentConfig == null ? string.Empty : currentConfig.id; }
}
private void Start()
{
LoadConfig();
EnsureRoot();
LoadInitialSelection();
StartCoroutine(LoadUIAsync());
}
public void SwitchTo(string characterId)
{
if (config == null)
{
LoadConfig();
}
CharacterConfigItem next = config == null ? null : config.Find(characterId);
if (next == null)
{
errorText = "Character not found: " + characterId;
Debug.LogWarning(errorText);
return;
}
SwitchTo(next);
}
private void LoadConfig()
{
TextAsset textAsset = Resources.Load<TextAsset>(ConfigResourcePath);
if (textAsset == null)
{
errorText = "Missing character config: Resources/" + ConfigResourcePath + ".json";
Debug.LogError(errorText);
return;
}
try
{
config = JsonUtility.FromJson<CharacterConfigData>(textAsset.text);
}
catch (Exception ex)
{
errorText = "Parse character config failed: " + ex.Message;
Debug.LogError(errorText);
config = null;
}
if (config == null || config.characters == null || config.characters.Count == 0)
{
errorText = "Character config is empty.";
Debug.LogError(errorText);
config = null;
}
}
private void EnsureRoot()
{
if (characterRoot != null)
{
return;
}
GameObject rootObject = GameObject.Find("CharacterRoot");
if (rootObject == null)
{
rootObject = new GameObject("CharacterRoot");
}
characterRoot = rootObject.transform;
if (characterRoot.parent != transform)
{
characterRoot.SetParent(transform, true);
}
}
private void LoadInitialSelection()
{
if (config == null)
{
return;
}
string selectedId = PlayerPrefs.GetString(SelectedCharacterKey, string.Empty);
CharacterConfigItem item = config.Find(selectedId) ?? config.GetDefault();
if (item != null)
{
currentConfig = item;
PlayerPrefs.SetString(SelectedCharacterKey, item.id);
PlayerPrefs.Save();
}
}
private void SwitchTo(CharacterConfigItem item)
{
if (currentCharacter != null)
{
Destroy(currentCharacter);
currentCharacter = null;
}
currentConfig = item;
errorText = string.Empty;
PlayerPrefs.SetString(SelectedCharacterKey, item.id);
PlayerPrefs.Save();
RefreshLobbyAvatar();
}
private void RefreshLobbyAvatar()
{
LobbyWorldController lobby = FindObjectOfType<LobbyWorldController>();
if (lobby != null)
{
lobby.RefreshSelectedFigure();
}
}
// ---------------- UGUI ----------------
// 异步加载 UI 预设:按需从 CDN 下载,避免同步加载读到旧 APK 底包。
private IEnumerator LoadUIAsync()
{
string prefabPath = PrefabPath;
if (prefabPath.StartsWith("Assets/"))
{
prefabPath = prefabPath.Substring(7);
}
UObject asset = null;
yield return XResLoader.coLoadRes(prefabPath, typeof(GameObject), o => asset = o);
GameObject prefab = asset as GameObject;
if (prefab == null)
{
Debug.LogError("Load UI_CharacterSwitch failed: " + PrefabPath);
yield break;
}
uiPanel = Instantiate(prefab);
// 角色切换是普通功能界面 → 挂到 UIRoot/Normal 层(见 UI 挂载规则)
UIManager.AttachTo(UILayer.Normal, uiPanel);
CacheUI();
BuildCharacterButtons();
}
private void CacheUI()
{
if (uiPanel == null)
{
return;
}
Transform sel = uiPanel.transform.FindTransform("Panel_Select");
selectPanel = sel == null ? null : sel.gameObject;
listChars = uiPanel.transform.FindTransform("List_Chars");
Transform item = uiPanel.transform.FindTransform("Item_Char");
itemTemplate = item == null ? null : item.gameObject;
Transform err = uiPanel.transform.FindTransform("Txt_Error");
errorLabel = err == null ? null : err.GetComponent<TMP_Text>();
Transform tog = uiPanel.transform.FindTransform("Txt_Toggle");
toggleLabel = tog == null ? null : tog.GetComponent<TMP_Text>();
Transform toggleBtn = uiPanel.transform.FindTransform("Btn_Toggle");
if (toggleBtn != null)
{
Button b = toggleBtn.GetComponent<Button>();
if (b != null)
{
b.onClick.RemoveListener(OnToggleClicked);
b.onClick.AddListener(OnToggleClicked);
}
}
if (selectPanel != null)
{
selectPanel.SetActive(false);
}
UpdateToggleLabel();
}
private void BuildCharacterButtons()
{
itemButtons.Clear();
itemIds.Clear();
if (itemTemplate == null || listChars == null || config == null || config.characters == null)
{
return;
}
for (int i = 0; i < config.characters.Count; i++)
{
CharacterConfigItem item = config.characters[i];
if (item == null)
{
continue;
}
GameObject row = Instantiate(itemTemplate, listChars);
row.name = "Item_Char_" + item.id;
row.SetActive(true);
Transform label = row.transform.FindTransform("Txt_Char");
if (label != null)
{
TMP_Text t = label.GetComponent<TMP_Text>();
if (t != null)
{
t.text = string.IsNullOrEmpty(item.displayName) ? item.id : item.displayName;
}
}
Button btn = row.GetComponent<Button>();
if (btn != null)
{
string captured = item.id;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => OnCharClicked(captured));
itemButtons.Add(btn);
itemIds.Add(item.id);
}
}
RefreshSelection();
RefreshError();
}
private void OnToggleClicked()
{
if (selectPanel == null)
{
return;
}
selectPanel.SetActive(!selectPanel.activeSelf);
}
private void OnCharClicked(string characterId)
{
SwitchTo(characterId);
RefreshSelection();
RefreshError();
UpdateToggleLabel();
if (selectPanel != null)
{
selectPanel.SetActive(false);
}
}
// 高亮当前选中项(当前项绿色加粗,其余白色)
private void RefreshSelection()
{
for (int i = 0; i < itemButtons.Count; i++)
{
Button btn = itemButtons[i];
if (btn == null)
{
continue;
}
bool selected = currentConfig != null && currentConfig.id == itemIds[i];
TMP_Text t = btn.GetComponentInChildren<TMP_Text>(true);
if (t != null)
{
t.color = selected ? new Color(0.34f, 0.84f, 0.55f, 1f) : new Color(0.96f, 0.97f, 1f, 1f);
t.fontStyle = selected ? FontStyles.Bold : FontStyles.Normal;
}
}
}
private void RefreshError()
{
if (errorLabel == null)
{
return;
}
errorLabel.text = errorText ?? string.Empty;
errorLabel.gameObject.SetActive(!string.IsNullOrEmpty(errorText));
}
private void UpdateToggleLabel()
{
if (toggleLabel == null)
{
return;
}
toggleLabel.text = string.IsNullOrEmpty(CurrentCharacterId) ? "角色" : "角色: " + CurrentCharacterId;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7aa234eb9d494595a42577fc322ede94
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: de9ed964a5dc65a4184f678cb4aca8d9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace XGame
{
// 账号鉴权 HTTP 客户端:POST /login、/register,拿 {token,pid}。
public static class AuthClient
{
[Serializable] class AuthReq { public string username; public string password; }
[Serializable] class AuthResp { public string token; public int pid; }
public static IEnumerator Login(string user, string pwd, Action<bool, string, int, string> done)
=> Post("/login", user, pwd, done);
public static IEnumerator Register(string user, string pwd, Action<bool, string, int, string> done)
=> Post("/register", user, pwd, done);
static IEnumerator Post(string path, string user, string pwd, Action<bool, string, int, string> done)
{
string url = GlobalData.ProductionAuthBase + path;
string body = JsonUtility.ToJson(new AuthReq { username = user, password = pwd });
using (var req = new UnityWebRequest(url, "POST"))
{
req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
string err = req.responseCode == 401 ? "账号或密码错误"
: req.responseCode == 409 ? "用户名已被占用"
: ("网络错误: " + req.error);
done(false, null, 0, err);
yield break;
}
AuthResp resp = null;
try { resp = JsonUtility.FromJson<AuthResp>(req.downloadHandler.text); } catch { }
if (resp == null || string.IsNullOrEmpty(resp.token))
{
done(false, null, 0, "响应解析失败");
yield break;
}
done(true, resp.token, resp.pid, null);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1658466d5399e3241b93b004568852d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42ca20d79634abb42b014dd1d9fcfb0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f5f6a77b5a84e6fa8d4a3a4bc53c0f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,107 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace XGame
{
// UI 层级,对应 UIRoot 下的四个子节点;顺序即渲染顺序(后者在上):HUD < Normal < Dialog < Tips
public enum UILayer
{
HUD = 0, // 抬头信息:血条、状态、摇杆、小地图等常驻 HUD
Normal = 1, // 普通功能界面:背包、设置、商城、登录/大厅等常规面板
Dialog = 2, // 弹出对话框:确认框等模态弹窗
Tips = 3, // 弹出文字信息:飘字、Toast 轻提示
}
/// <summary>
/// UI 挂载管理:维护一个统一的 UI 画布 UIRoot,其下有 HUD/Normal/Dialog/Tips 层级节点,
/// 把 UI 预设按类型挂到对应层。规则见 Doc/Rule/UnityProject.md「UI 挂载规则」。
///
/// UIRoot 本身是 Canvas(ScreenSpaceOverlay + ScaleWithScreenSize 2048x1024)。这很关键:
/// 面板预设自带的 Canvas 挂进来后变成"嵌套子画布",嵌套 Canvas 不会驱动其 RectTransform
/// 面板根节点在预设里设置的 Anchor/尺寸才会生效(否则顶层 Overlay 画布会把根强制拉成全屏、锚点失效)。
/// 参考分辨率与各面板自带 CanvasScaler 一致(2048x1024, match 0.5),故缩放不变。
/// </summary>
public static class UIManager
{
private const string UIRootName = "UIRoot";
private static readonly string[] LayerNames = { "HUD", "Normal", "Dialog", "Tips" };
private static readonly Vector2 DesignResolution = new Vector2(2048f, 1024f);
/// <summary>把 UI 挂到指定层(worldPositionStays=false 保留本地布局/锚点)。</summary>
public static void AttachTo(UILayer layer, GameObject ui, bool worldPositionStays = false)
{
if (ui == null)
{
return;
}
RectTransform layerRt = GetLayer(layer);
if (layerRt != null)
{
ui.transform.SetParent(layerRt, worldPositionStays);
}
}
/// <summary>获取指定层的 RectTransform(确保 UIRoot 画布与该层存在)。</summary>
public static RectTransform GetLayer(UILayer layer)
{
Transform root = EnsureUIRoot();
string name = LayerNames[(int)layer];
Transform t = root.Find(name);
if (t == null)
{
t = CreateFullStretchChild(root, name);
}
return t as RectTransform;
}
private static Transform EnsureUIRoot()
{
EnsureEventSystem();
GameObject go = GameObject.Find(UIRootName);
if (go == null)
{
go = new GameObject(UIRootName, typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
Canvas canvas = go.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 100; // 位于主界面之上
CanvasScaler scaler = go.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = DesignResolution;
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
// 按 HUD→Normal→Dialog→Tips 顺序创建,子节点顺序即渲染层级(后建的在上)
for (int i = 0; i < LayerNames.Length; i++)
{
CreateFullStretchChild(go.transform, LayerNames[i]);
}
}
return go.transform;
}
private static void EnsureEventSystem()
{
if (EventSystem.current != null || Object.FindObjectOfType<EventSystem>() != null)
{
return;
}
new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
}
private static Transform CreateFullStretchChild(Transform parent, string name)
{
GameObject go = new GameObject(name, typeof(RectTransform));
RectTransform rt = go.GetComponent<RectTransform>();
rt.SetParent(parent, false);
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.localScale = Vector3.one;
return go.transform;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1c655b3a9179e3479e4066be0525792
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+311
View File
@@ -0,0 +1,311 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.UI;
namespace XGame
{
class Game : MonoBehaviour
{
static public Game Instance;
static public AssetBundle m_shaderBundleLoader;
static public XWebSocket m_WebSocket = null;
private static readonly Dictionary<string, Shader> s_RuntimeShaders = new Dictionary<string, Shader>();
private static readonly Color RuntimeAmbientSky = new Color(0.82f, 0.88f, 0.95f, 1f);
private static readonly Color RuntimeAmbientEquator = new Color(0.62f, 0.68f, 0.76f, 1f);
private static readonly Color RuntimeAmbientGround = new Color(0.34f, 0.32f, 0.28f, 1f);
private static readonly Color RuntimeSunColor = new Color(1f, 0.95686275f, 0.8392157f, 1f);
private const float RuntimeAmbientIntensity = 1.65f;
private const float RuntimeSunIntensity = 2.2f;
private const string RuntimeSunName = "RuntimeDirectionalLight";
private static readonly int RuntimeAmbientEnabledId = Shader.PropertyToID("_XRuntimeAmbientEnabled");
private static readonly int RuntimeAmbientSkyId = Shader.PropertyToID("_XRuntimeAmbientSky");
private static readonly int RuntimeAmbientEquatorId = Shader.PropertyToID("_XRuntimeAmbientEquator");
private static readonly int RuntimeAmbientGroundId = Shader.PropertyToID("_XRuntimeAmbientGround");
private void Awake()
{
//加载(添加)一个loading场景作为永久使用,(Test)
//XResLoader.AddScene("res/scene/loading/loading", (UnityEngine.GameObject[] objs) => {
// if (objs == null || objs[0] == null)
// {
// //Debug.Log("Loading Scene Load Failed!");
// }
// else
// {
// //Test
// foreach (var obj in objs)
// {
// (obj as UnityEngine.GameObject).SetActive(false);
// }
// }
// //添加场景后续可以通过场景名操作
// //XResLoader.AddScene("res/scene/s001/scene001", (UnityEngine.GameObject[] objs) =>
// //{
// // if (objs == null || objs[0] == null)
// // {
// // //Debug.Log("Loading Scene Load Failed!");
// // }
// // else
// // {
// // }
// //});
//});
}
public IEnumerator Init()
{
/*string url;
//test Dynamic RenderPipeline (ok)
XResLoader.LoadRes("res/RenderAsset/XRendePipline1.asset", typeof(RenderPipelineAsset), objs =>
{
if (objs == null || objs[0] == null)
{
Debug.Log($"res/RenderAsset/XRendePipline1.asset");
}
else
{
//GameObject obj = GameObject.Instantiate(objs[0], gameObject.transform) as GameObject;
RenderPipelineAsset rpasset = objs[0] as RenderPipelineAsset;
UniversalRenderPipelineAsset urpAsset = rpasset as UniversalRenderPipelineAsset;
QualitySettings.renderPipeline = rpasset;
GraphicsSettings.renderPipelineAsset = rpasset;
Debug.Log($"res/RenderAsset/XRendePipline1.asset : {rpasset.name} msaa : {urpAsset.msaaSampleCount}");
}
}, null);//*/
#if !UNITY_WEBGL
string sUpdatePath = Application.persistentDataPath;
#else
string sUpdatePath = Application.streamingAssetsPath;
#endif
// 加载 shader 变体集合(ShaderVariantCollection)并 WarmUp。
// 真机从 AssetBundle 加载的预设(如 Main)所用 shader,其光照变体在构建时可能被剔除,
// 导致平行光等不起效。把变体打进 game_shader.unity3d 并在此 WarmUp,可避免被剔除/运行时卡顿。
// 变体集合通过菜单 Tools/Shader 生成到 Assets/Game/Shader/Game.shadervariants(活跃流程会打进 game_shader.unity3d)。
DateTime swStart = DateTime.Now;
m_shaderBundleLoader = null;
s_RuntimeShaders.Clear();
Debug.Log($"###shader bundle md5 XWorld={XResLoader.GetFileMd5("XWorld", "game_shader.unity3d")}");
yield return XResLoader.coLoadAB("game_shader.unity3d", (ab) =>
{
m_shaderBundleLoader = ab;
if (ab == null) return;
Shader[] shaders = ab.LoadAllAssets<Shader>();
foreach (Shader shader in shaders)
{
if (shader == null)
{
continue;
}
s_RuntimeShaders[shader.name] = shader;
}
Shader xTexShader = GetRuntimeShader("Test/XTex");
Debug.Log($"###load Shader Count = {shaders.Length}, Test/XTex={(xTexShader != null ? xTexShader.name : "null")} supported={(xTexShader != null && xTexShader.isSupported)}");
ShaderVariantCollection[] svcs = ab.LoadAllAssets<ShaderVariantCollection>();
Debug.Log($"###load SVC Count = {svcs.Length}");
int i = 0;
foreach (var svc in svcs)
{
Debug.Log($"WarmUp{++i} : {svc.name} (variants={svc.variantCount})");
svc.WarmUp();
}
Debug.Log($"Shader WarmUp Finish! {(DateTime.Now - swStart).TotalMilliseconds} ms");
});
/*
// coLoadAB 在包缺失时不会回调 action,这里兜底提示(非致命,跳过 WarmUp 即可)
if (m_shaderBundleLoader == null)
Debug.LogWarning("未找到 shader 变体包 game_shader.unity3d,已跳过 WarmUp。" +
"若真机光照异常,请先用菜单 Tools/Shader 生成变体集合到 Assets/Game/Shader,并重打更新包发布。");
// 重建基于天空盒的环境光探针(SH)。场景环境光为 Skybox 模式且未烘焙时,
// 构建里不会自动生成该探针(编辑器会自动算,所以编辑器看着正常),真机会丢失基础环境光、整体偏暗。
// 调一次 DynamicGI.UpdateEnvironment() 即可在运行时从当前天空盒重新生成,恢复环境光。
ApplyRuntimeLighting("init");
*/
if (m_shaderBundleLoader == null)
Debug.LogWarning("Shader bundle game_shader.unity3d was not found; skipped WarmUp.");
ApplyRuntimeLighting("init");
StartCoroutine(CoReapplyRuntimeLighting());
/*/test 加载些东西 此场景可以
yield return XResLoader.coLoadRes("Assets/Arts/ScenePrefab/s001.prefab", typeof(GameObject), obj =>
{
if (obj == null )
{
Debug.Log($"Load Assets/Arts/ScenePrefab/s001 Failed");
}
else
{
GameObject objInst = GameObject.Instantiate(obj, gameObject.transform) as GameObject;
}
});
//
GameObject obj = GameObject.Find("Scene_21010100_Stone_005");
if (obj != null)
{
Debug.Log("Scene_21010100_Stone_005 ok");
}//*/
//Assets\Arts\Role\CharaterPrefab
//yield return XResLoader.coLoadRes("Assets/Arts/Role/CharaterPrefab/11501100_l.prefab", typeof(GameObject), obj =>
//{
// if (obj == null)
// {
// Debug.Log($"Load Assets/Arts/Role/CharaterPrefab/11501100_l Failed");
// }
// else
// {
// GameObject objInst = GameObject.Instantiate(obj, gameObject.transform) as GameObject;
// }
//});
/*/test net
string addr = "wss://192.168.10.186:4009";//"wss://app.xworld.link:16888";//"wss://app.xworld.link:16887";//"ws://192.168.10.186:2009";// "ws://127.0.0.1:16888";// 192.168.51.105
int port = 0;
lsocket.connect(addr, port, (GYWebSocket sock, string err) =>
{//连接结果
m_WebSocket = sock;
if (sock == null)
{
Debug.LogError($"connect {addr} Failed!");
}
else
{
//发送信息
byte[] byteArray;
//string abc = "123_test_msg_string";
//byteArray = System.Text.Encoding.ASCII.GetBytes(abc);
byteArray = new byte[] { 0x00 ,0x0B, 0x30, 0x15, 0x03, 0x16, 0x01, 0x51, 0x06, 0x02, 0x07, 0x01, 0x02 };
m_WebSocket.send(byteArray, (int nData, string str) =>
{
Debug.Log($"Send {nData} Bytes ");
}, ref err );
}
});//*/
yield break;
}
public static Shader GetRuntimeShader(string shaderName)
{
if (string.IsNullOrEmpty(shaderName))
{
return null;
}
Shader shader;
s_RuntimeShaders.TryGetValue(shaderName, out shader);
return shader;
}
private static IEnumerator CoReapplyRuntimeLighting()
{
yield return null;
ApplyRuntimeLighting("next-frame");
yield return new WaitForEndOfFrame();
ApplyRuntimeLighting("end-of-frame");
yield return new WaitForSeconds(0.5f);
ApplyRuntimeLighting("delay-0.5");
yield return new WaitForSeconds(1.5f);
ApplyRuntimeLighting("delay-2.0");
}
private static void ApplyRuntimeLighting(string reason)
{
RenderSettings.ambientMode = AmbientMode.Trilight;
RenderSettings.ambientSkyColor = RuntimeAmbientSky;
RenderSettings.ambientEquatorColor = RuntimeAmbientEquator;
RenderSettings.ambientGroundColor = RuntimeAmbientGround;
RenderSettings.ambientIntensity = RuntimeAmbientIntensity;
RenderSettings.reflectionIntensity = Mathf.Max(RenderSettings.reflectionIntensity, 0.8f);
Shader.SetGlobalFloat(RuntimeAmbientEnabledId, 1f);
Shader.SetGlobalColor(RuntimeAmbientSkyId, RuntimeAmbientSky * RuntimeAmbientIntensity);
Shader.SetGlobalColor(RuntimeAmbientEquatorId, RuntimeAmbientEquator * RuntimeAmbientIntensity);
Shader.SetGlobalColor(RuntimeAmbientGroundId, RuntimeAmbientGround * RuntimeAmbientIntensity);
// The runtime ambient probe is authored here. DynamicGI.UpdateEnvironment can
// rebuild it from the skybox later on some platforms and overwrite these values.
RenderSettings.ambientProbe = BuildRuntimeAmbientProbe();
SphericalHarmonicsL2 probe = RenderSettings.ambientProbe;
Light sun = EnsureRuntimeDirectionalLight();
Debug.Log($"[Game] Runtime lighting applied({reason}). ambientMode={RenderSettings.ambientMode}, " +
$"intensity={RenderSettings.ambientIntensity}, sky={RenderSettings.ambientSkyColor}, " +
$"sun={(sun != null ? sun.name : "null")} enabled={(sun != null && sun.enabled)} " +
$"sunIntensity={(sun != null ? sun.intensity : 0f)} skybox={(RenderSettings.skybox != null ? RenderSettings.skybox.name : "null")} " +
$"probeL00=({probe[0, 0]:F3},{probe[1, 0]:F3},{probe[2, 0]:F3})");
}
private static SphericalHarmonicsL2 BuildRuntimeAmbientProbe()
{
SphericalHarmonicsL2 probe = new SphericalHarmonicsL2();
probe.AddAmbientLight(RuntimeAmbientEquator * RuntimeAmbientIntensity);
probe.AddDirectionalLight(new Vector3(0.15f, 1f, 0.25f).normalized, RuntimeAmbientSky, 0.85f);
probe.AddDirectionalLight(new Vector3(-0.25f, -1f, -0.1f).normalized, RuntimeAmbientGround, 0.28f);
return probe;
}
private static Light EnsureRuntimeDirectionalLight()
{
Light sun = RenderSettings.sun;
if (!IsDirectionalLight(sun))
{
GameObject sunObj = GameObject.Find("DirectionalLight") ?? GameObject.Find(RuntimeSunName);
sun = sunObj != null ? sunObj.GetComponent<Light>() : null;
}
if (!IsDirectionalLight(sun))
{
foreach (Light light in FindObjectsOfType<Light>())
{
if (IsDirectionalLight(light))
{
sun = light;
break;
}
}
}
if (sun == null)
{
GameObject sunObj = new GameObject(RuntimeSunName);
sun = sunObj.AddComponent<Light>();
}
sun.gameObject.SetActive(true);
sun.enabled = true;
sun.type = LightType.Directional;
sun.color = RuntimeSunColor;
sun.intensity = Mathf.Max(sun.intensity, RuntimeSunIntensity);
sun.shadows = LightShadows.None;
sun.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
RenderSettings.sun = sun;
return sun;
}
private static bool IsDirectionalLight(Light light)
{
return light != null && light.type == LightType.Directional;
}
private void Update()
{
//获得消息
if (Input.GetKey(KeyCode.H))
{
Camera camera = UnityEngine.Camera.main;
var forward = camera.transform.forward;
Debug.Log($"H Key Up {forward.x},{forward.y},{forward.z}");
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2006a749bbc91644cb3e14eb932c24a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b332266ed87bf384bbadf0876f867d80
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+9
View File
@@ -0,0 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameData
{
public static int EquipmentCount = 5;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5a6da866b38f0945b3c4af37d906a3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+646
View File
@@ -0,0 +1,646 @@
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using System.Collections.Generic;
using XGame;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using XWorld;
using UnityEngine.UI;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering;
/// <summary>
/// 游戏启动器
/// </summary>
public class GameLauncher : MonoBehaviour
{
private const int AndroidTargetFrameRate = 60;
class SphereObj
{
public GameObject obj;
public Transform tr;
public Vector3 pos;
public Vector3 vec;
};
List<SphereObj> sphereObjs = new List<SphereObj> ();
private NativeArray<float3> positions = new NativeArray<float3>();
private NativeArray<float3> velocities = new NativeArray<float3>();
public int objectCount = 1;
public float speed = 1.0f;
public static GameLauncher Instance;
string m_sFPS = "FPS";
float m_fLastTime = 0;
int m_nCount = 0;
GUIStyle m_Style = new GUIStyle();
//系统ui组件
GameObject m_PathObj;
InputField m_InputField;
Button m_LocalButton;
Button m_ExitButton;
static public float m_fSize = 75;
//内网 DevDiscovery(替代老 Broadcast/UDP9999),StartGame 等其完成后回填 CurNode/CurCDN
private XGame.MiniGame.DevServerDiscovery m_LanDiscovery;
private const string LocalClientIdKey = "LocalClientId";
private void Awake()
{
ConfigureFrameRate();
Instance = this;
gameObject.name = "Main";//第一时间改为标准名称
InitStart();
DontDestroyOnLoad(gameObject);
}
private void Start()
{
//ud 设置字体
m_Style.normal.background = Texture2D.blackTexture;
m_Style.normal.textColor = new Color(1, 1, 1);
m_Style.fontSize = 24;
m_Style.fontStyle = FontStyle.Bold;
}
/// <summary>
/// 初始化管理器等(原来直接挂在GameManager这个节点下)
/// </summary>
void InitStart()
{
#if UNITY_EDITOR
System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcessesByName("XNode");
if (processes.Length ==0)
{
System.Threading.Thread.Sleep(1000);
}
#endif
//初始化
GlobalData.bWX = false;// LoadDll.m_bInWX;
#if UNITY_IOS
GlobalData.CurPlatform = "ios";
#elif UNITY_ANDROID
GlobalData.CurPlatform = "android";
#elif UNITY_WEBGL
GlobalData.CurPlatform = "webgl";
#else
GlobalData.CurPlatform = "pc";
#endif
#if UNITY_EDITOR
GlobalData.CurPlatform = "pc";
#endif
#if UNITY_WEBGL
Debug.Log("WEBGL平台");
Shader.EnableKeyword("_PLATFORM_WEBGL");
#else
Debug.Log("非WEBGL平台");
Shader.DisableKeyword("_PLATFORM_WEBGL");
#endif
//XGame.UpdateModule module = gameObject.AddComponent<XGame.UpdateModule>();
//Debug.Log("GameLauncher : Add UpdateModule");
//除了WebGL都要进入更新流程,后再开始游戏
//
//开始流程
#if XW_DEVTEST
if (gameObject.GetComponent<XGame.MiniGame.DevServerSwitchUI>() == null)
{
gameObject.AddComponent<XGame.MiniGame.DevServerSwitchUI>();
}
#endif
StartCoroutine(this.StartGame());
#if UWA
//ud 211112 增加uwa组件
GameObject uwa = Instantiate(Resources.Load("UWA_Launcher") as GameObject);
if(uwa != null)
uwa.transform.SetParent(gameObject.transform);
#endif
#if UWA_AUTO
GameObject uwa = Instantiate(Resources.Load("UWA_LauncherAuto") as GameObject);
if(uwa != null)
uwa.transform.SetParent(gameObject.transform);
#endif
}
IEnumerator StartGame()
{
SystemUIInit();
UpdateNodeAddress();
//内网模式:等 DevDiscovery 完成并回填 CurNode/CurCDN(须在 XResLoader.Init / 全量热更之前)
if (GlobalData.bLocal && m_LanDiscovery != null)
{
float deadline = Time.realtimeSinceStartup + 2.0f;
while (m_LanDiscovery.IsRunning && Time.realtimeSinceStartup < deadline)
yield return null;
var found = m_LanDiscovery.TakeResults();
m_LanDiscovery = null;
if (found.Count > 0)
{
var r = found[0];
GlobalData.CurNode = WithPid(r.GatewayUrl);
GlobalData.CurCDN = r.ResourceBaseUrl; // discovery 下发的是 CDN 根
Debug.Log($"[GameLauncher] LAN discovered {GlobalData.CurNode} {GlobalData.CurCDN}");
}
else
{
Debug.LogWarning("[GameLauncher] LAN discovery found no server");
}
}
else if (GlobalData.CurNode == null)
{//等待网络获得节点(外网)
yield return new WaitForSeconds(0.5f);
}
//大厅/基础资源(XWorld) base:编辑器走本地 CDN(CurCDN),其它 app 端走生产(与 bLocal 无关)。UpdateModule 内部再 +<platform>/
string lobbyRoot = GlobalData.bEditor ? NormalizeCdnUrl(GlobalData.CurCDN) : GlobalData.ProductionResBase;
XGame.AppConst.UpdateServer = lobbyRoot + "XWorld/";
Debug.Log($"UpdateNodeAddress {GlobalData.CurNode} {GlobalData.CurCDN} update={XGame.AppConst.UpdateServer}");
//重新启用旧全量热更链路(大厅相关热更):从 <CurCDN>/XWorld/<platform>/ 增量下载
#if UNITY_WEBGL
{
XGame.UpdateModule upd = gameObject.GetComponent<XGame.UpdateModule>();
if (upd == null) upd = gameObject.AddComponent<XGame.UpdateModule>();
//传非空空回调:UpdateModule.DownLoadAssets 多处无守卫地调 onDownloadComplete(),传 null 会 NRE。我们靠 yield 等待,不依赖回调
yield return upd.UpdateResAllWebGL(() => { });
}
#else
{
XGame.UpdateModule upd = gameObject.GetComponent<XGame.UpdateModule>();
if (upd == null) upd = gameObject.AddComponent<XGame.UpdateModule>();
//传非空空回调(同上,避免 DownLoadAssets 内 onDownloadComplete() 为 null 时 NRE
yield return upd.UpdateResAll(() => { });
upd.SetAllResHash();
}
#endif
//画质控制
//RenderTextureDescriptor descriptor = new RenderTextureDescriptor(Screen.width, Screen.height, RenderTextureFormat.Default);
//bool isRenderScaleSupported = SystemInfo.SupportsRenderTextureFormat(descriptor.colorFormat) && SystemInfo.supportedRenderTargetCount > 0;
//int TargetWidth = 720;
QualitySettings.SetQualityLevel(3);//最高画质VeryHigh 3
ConfigureFrameRate();
EnforceNativeRenderScale();
/*if (UnityEngine.Screen.width > TargetWidth)
{
if (isRenderScaleSupported)
{
UniversalRenderPipelineAsset URPAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
URPAsset.renderScale = Math.Min((float)TargetWidth / (float)UnityEngine.Screen.width, 1);
URPAsset.upscalingFilter = UpscalingFilterSelection.Linear;
Debug.Log($"renderScale {URPAsset.renderScale} {Screen.width}x{Screen.height}");
}
}//*/
gameObject.AddComponent<ConsoleWindow>();
//gameObject.AddComponent<TerrainManager>();
Component obj = gameObject.GetComponent<ConsoleWindow>();
//初始化加载模块(静态)
yield return XResLoader.Init();
//单例
Game game = gameObject.AddComponent<Game>();
yield return game.Init();
EnforceNativeRenderScale();
if (gameObject.GetComponent<CharacterSwitchController>() == null)
{
gameObject.AddComponent<CharacterSwitchController>();
}
//如果仍然没有,则连接默认节点(内网回退本机 WS 网关;外网走生产 WSS 网关。均不再走已废弃的 tcp 7777)
if (GlobalData.CurNode == null || GlobalData.CurNode == "")
GlobalData.CurNode = GlobalData.bLocal ? WithPid("ws://127.0.0.1:5005/ws") : GlobalData.ProductionGatewayUrl;
yield return CSharpClientApp.Init();
//yield return TerrainManager.Init();
#if UNITY_EDITOR
//ConsoleWindow.ConnectConsoleServer(SystemInfo.deviceName);
#endif
//加载基础资源lua
yield break;
}
//更新内外网网络连接
void UpdateNodeAddress()
{
#if UNITY_EDITOR
if (!XData.HasKey("XWorldLocal"))
{
GlobalData.bLocal = true;
XData.SetInt("XWorldLocal", 1);
XData.Save();
}
#endif
if (XData.HasKey("XWorldLocal"))
{
GlobalData.bLocal = XData.GetInt("XWorldLocal") == 1;
}
#if XW_DEVTEST
if (XGame.MiniGame.MiniGameTestOverride.IsActive)
{
GlobalData.CurNode = AddDevPid(XGame.MiniGame.MiniGameTestOverride.GatewayUrl);
GlobalData.CurCDN = NormalizeCdnUrl(XGame.MiniGame.MiniGameTestOverride.CdnBase);
Debug.Log($"Using dev test override: {GlobalData.CurNode} {GlobalData.CurCDN}");
return;
}
#endif
if (GlobalData.bLocal)
{
//if (XData.HasKey("CurNode"))
//{
// string savedNode = XData.GetString("CurNode");
// if (!string.IsNullOrWhiteSpace(savedNode) &&
// (savedNode.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) ||
// savedNode.StartsWith("wss://", StringComparison.OrdinalIgnoreCase)))
// {
// GlobalData.CurNode = savedNode;
// GlobalData.CurCDN = NormalizeCdnUrl(XData.GetString("CurCDN"));
// Debug.Log($"Using local Gateway node: {GlobalData.CurNode} {GlobalData.CurCDN}");
// return;
// }
//}
//内网发现:用已有 DevDiscoveryUDP 48923,二进制+token),结果在 StartGame 里等待回填。
//服务端:dotnet run --project Server/Gateway.Runner -- --lan --devToken xw-dev
m_LanDiscovery = new XGame.MiniGame.DevServerDiscovery(GlobalData.LanDiscoveryToken);
m_LanDiscovery.Begin(1500);
Debug.Log("[GameLauncher] LAN discovery started (DevDiscovery, token=" + GlobalData.LanDiscoveryToken + ")");
}
else
{
//如果有保存当前节点,则直接连接
if (XData.HasKey("CurNode"))
{
GlobalData.CurNode = XData.GetString("CurNode");
GlobalData.CurCDN = NormalizeCdnUrl(XData.GetString("CurCDN"));
//检查链接是否有效
Debug.Log($"Check CurNode {GlobalData.CurNode} {GlobalData.CurCDN}");
}
else
{
GlobalData.CurNode = GlobalData.ProductionGatewayUrl;
GlobalData.CurCDN = NormalizeCdnUrl(GlobalData.ProductionResBase);
Debug.Log($"Using default nodes: {GlobalData.CurNode} {GlobalData.CurCDN}");
}
}
}
//内网网关地址补设备级稳定 pid,避免两个端都用 pid=1 被服务端当作同一玩家重连。
static string WithPid(string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl)) return gatewayUrl;
int pid = GetLocalPlayerId();
int queryStart = gatewayUrl.IndexOf('?');
if (queryStart < 0)
{
return gatewayUrl + "?pid=" + pid;
}
string head = gatewayUrl.Substring(0, queryStart);
string query = gatewayUrl.Substring(queryStart + 1);
string[] parts = query.Split('&');
bool replaced = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].StartsWith("pid=", StringComparison.OrdinalIgnoreCase))
{
parts[i] = "pid=" + pid;
replaced = true;
break;
}
}
return head + "?" + (replaced ? string.Join("&", parts) : query + "&pid=" + pid);
}
static int GetLocalPlayerId()
{
string id = XData.GetString(LocalClientIdKey, string.Empty);
if (string.IsNullOrEmpty(id))
{
id = SystemInfo.deviceUniqueIdentifier;
if (string.IsNullOrEmpty(id) || id == "unsupported")
{
id = Guid.NewGuid().ToString("N");
}
XData.SetString(LocalClientIdKey, id);
XData.Save();
}
unchecked
{
uint hash = 2166136261u;
for (int i = 0; i < id.Length; i++)
{
hash ^= id[i];
hash *= 16777619u;
}
return (int)(hash % 2000000000u) + 1;
}
}
static string NormalizeCdnUrl(string cdn)
{
string url = string.IsNullOrWhiteSpace(cdn) ? "http://127.0.0.1:15081/" : cdn.Trim();
if (url.StartsWith("http://file://", StringComparison.OrdinalIgnoreCase))
{
url = url.Substring("http://".Length);
}
else if (url.StartsWith("https://file://", StringComparison.OrdinalIgnoreCase))
{
url = url.Substring("https://".Length);
}
if (url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
url = NormalizeFileUrl(url);
}
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
url = "http://" + url;
}
if (!url.EndsWith("/"))
{
url += "/";
}
return url;
}
static string NormalizeFileUrl(string url)
{
string rawPath = url.Substring("file://".Length).Replace('\\', '/');
while (rawPath.StartsWith("/") && rawPath.Length > 2 && rawPath[2] == ':')
{
rawPath = rawPath.Substring(1);
}
string localPath = Uri.UnescapeDataString(rawPath).Replace('/', Path.DirectorySeparatorChar);
try
{
return new Uri(localPath).AbsoluteUri;
}
catch
{
return "file:///" + rawPath.Replace("#", "%23");
}
}
static string NormalizeNodeAddress(string node, int defaultPort)
{
string address = string.IsNullOrWhiteSpace(node) ? $"127.0.0.1:{defaultPort}" : node.Trim();
if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
{
address = address.Substring("http://".Length);
}
else if (address.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
address = address.Substring("https://".Length);
}
int slash = address.IndexOf('/');
if (slash >= 0)
{
address = address.Substring(0, slash);
}
int colon = address.LastIndexOf(':');
string host = colon >= 0 ? address.Substring(0, colon) : address;
if (string.IsNullOrWhiteSpace(host))
{
host = "127.0.0.1";
}
return $"{host}:{defaultPort}";
}
#if XW_DEVTEST
static string AddDevPid(string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl))
{
return gatewayUrl;
}
return WithPid(gatewayUrl);
}
#endif
private void SystemUIInit()
{
//用按钮区域
Transform objTR = GameObject.Find("Main/Touch/OpenPath").transform;
Button button = objTR.GetComponent<Button>();
//获取多个系统ui组件
//搜索Main下的所有子节点(包括隐藏节点)
Transform mainTR = GameObject.Find("Main").transform;
foreach (Transform child in mainTR)
{
if(child.name == "Path")
{
m_PathObj = child.gameObject;
}
}
foreach (Transform child in m_PathObj.transform)
{
if(child.name == "InputField")
{
m_InputField = child.GetComponent<InputField>();
}
if(child.name == "Local")
{
m_LocalButton = child.GetComponent<Button>();
}
if(child.name == "Exit")
{
m_ExitButton = child.GetComponent<Button>();
}
}
m_LocalButton.onClick.AddListener(()=>{
GlobalData.bLocal = !GlobalData.bLocal;
XData.SetInt("XWorldLocal", GlobalData.bLocal ? 1 : 0);
//根据是否内网,设置按钮文本为Local或Online,并记录到XData里
if(GlobalData.bLocal)
{
m_LocalButton.GetComponentInChildren<TMPro.TMP_Text>(true).text = "Local";
XData.SetInt("XWorldLocal", 1);
XData.Save();
//更新节点地址
UpdateNodeAddress();
//重启lua
CSharpClientApp.RestartCurrent();
}
else
{
m_LocalButton.GetComponentInChildren<TMPro.TMP_Text>(true).text = "Online";
XData.SetInt("XWorldLocal", 0);
XData.Save();
//更新节点地址
UpdateNodeAddress();
//重启lua
CSharpClientApp.RestartCurrent();
}
});
button.onClick.AddListener(()=>{
//显示Main下的Path节点
m_PathObj.SetActive(true);
bool bLocal = GlobalData.bLocal;
if (bLocal)
{
m_LocalButton.GetComponentInChildren<TMPro.TMP_Text>(true).text = "Local";
}
//Path下的Exit按钮,按下则隐藏obj
m_ExitButton.onClick.AddListener(()=>{
if(m_PathObj != null)
m_PathObj.SetActive(false);
});
});
}
private static void ConfigureFrameRate()
{
#if UNITY_ANDROID && !UNITY_EDITOR
QualitySettings.vSyncCount = 0;
//OnDemandRendering.renderFrameInterval = 1;
Application.targetFrameRate = AndroidTargetFrameRate;
Debug.Log($"[GameLauncher] Frame pacing target={Application.targetFrameRate}, displayRefresh={GetDisplayRefreshRate():0.##}, vSync={QualitySettings.vSyncCount}");
#endif
}
#if UNITY_ANDROID && !UNITY_EDITOR
private static double GetDisplayRefreshRate()
{
#if UNITY_2022_2_OR_NEWER
RefreshRate refreshRate = Screen.currentResolution.refreshRateRatio;
if (refreshRate.denominator > 0)
{
return (double)refreshRate.numerator / refreshRate.denominator;
}
#endif
return Screen.currentResolution.refreshRate;
}
#endif
private static void EnforceNativeRenderScale()
{
UniversalRenderPipelineAsset urpAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
if (urpAsset == null)
{
return;
}
if (Mathf.Abs(urpAsset.renderScale - 1f) > 0.001f)
{
urpAsset.renderScale = 1f;
urpAsset.upscalingFilter = UpscalingFilterSelection.Linear;
}
}
private void OnGUI()
{
//if (GUI.Button(new Rect(30, Screen.height - 48, 60, 30), "Test"))
//{
// DateTime currentTime = DateTime.Now;
// Debug.Log($"当前时间:{currentTime}");
//}
GUI.Label(new Rect(100, Screen.height - 40, 300, 300), m_sFPS, m_Style);
//if (GUI.Button(new Rect(50, Screen.height - 48, 60, 30), "更新"))
//{//运行期热更只能下载更新和使用新的资源,dll那些不能运行期换
// Debug.LogWarning("Update更新中");
// LoadDll.Instance.StartDownload();
//}
}
[BurstCompile]
struct MoveJob : IJobParallelFor
{
public float deltaTime;
public float speed;
public NativeArray<float3> positions;
public NativeArray<float3> velocities;
public void Execute(int indexOut)
{
int index = indexOut;
//for (int i = 0; i < 10; ++i)
{
//int index = indexOut * 10 + i;
if (positions[index].x > 75 || positions[index].x < -75)
{
positions[index] = -velocities[index];
}
if (positions[index].z > 75 || positions[index].z < -75)
{
velocities[index] = -velocities[index];
}
positions[index] += velocities[index] * speed;//deltaTime
}
}
}
private void Update()
{
m_nCount++;
if (m_fLastTime == 0)
{
m_fLastTime = Time.realtimeSinceStartup;
}
else
{
float CurTime = Time.realtimeSinceStartup;
if (CurTime - m_fLastTime > 1.0f)
{
m_sFPS = $"{m_nCount} | {XResLoader.m_displayProgress}%";
m_nCount = 0;
m_fLastTime = CurTime;
}
}
/*
float deltaTime = Time.deltaTime;
var job = new MoveJob
{
deltaTime = deltaTime,
positions = positions,
velocities = velocities,
speed = speed
};
JobHandle jobHandle = job.Schedule<MoveJob>(objectCount, 64);
jobHandle.Complete();
for (int i = 0; i < objectCount; ++i)
{
sphereObjs[i].obj.transform.position = positions[i];
}//*/
foreach (var i in sphereObjs)
{
i.pos += i.vec * 0.1f;
if (i.pos.x > m_fSize || i.pos.x < -m_fSize)
{
i.vec.x = -i.vec.x;
}
if (i.pos.z > m_fSize || i.pos.z < -m_fSize)
{
i.vec.z = -i.vec.z;
}
i.tr.position = i.pos;
}
}
private void OnDestroy()
{
if(positions.IsCreated)
positions.Dispose();
if(velocities.IsCreated)
velocities.Dispose();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14d4e45a2df651c418793abbedf8fb9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+29
View File
@@ -0,0 +1,29 @@
using System.Collections.Generic;
public class XNode
{
public string XID;
public string Node;
public string CDN;
public string Name;
}
public static class GlobalData
{
public static bool bWX;//是否微信小游戏内
public static bool bLocal = false;//是否测试内网
#if UNITY_EDITOR
public static bool bEditor = true;//是否编辑器
#else
public static bool bEditor = false;
#endif
public static string CurCDN = "http://127.0.0.1:15081/";//当前资源位置(CDN根)。默认本机,discovery 失败时回退到此,配合本机 Cdn.Runner 同机可用
public static string CurPlatform;//当前平台,pc android ios webgl
public static string CurXID;//当前节点(游戏),xid //影响资源位置
public static string CurNode;//当前节点(游戏),xid 或 http:// 或 xw://
public static string Token;//当前token
public const string LanDiscoveryToken = "xw-dev";//内网 DevDiscovery 鉴权 token,须与服务端 --devToken 一致
public const string ProductionResBase = "https://www.xworld.ren/game/60S/";//生产资源根:大厅走 <根>/XWorld/<platform>/,小游戏走 <根>/minigame/<id>/<ver>/(经 Caddy TLS
public const string ProductionGatewayUrl = "wss://game.xworld.ren/ws";//生产大厅/对局网关(WSS,经 Caddy 反代到内网 :5005ConnectLobby 会补 ?token=
public const string ProductionAuthBase = "https://game.xworld.ren";//登录/注册 HTTP 端点(与 WSS 同主机,经 Caddy)
public static Dictionary<string, XNode> XNodes = new Dictionary<string, XNode>();//当前节点列表
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78f3f8b24cf1a8f4b8c875388764365d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2352c49b9ad5dbc4c80de7da3a9c87b4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,81 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace XGame
{
public class TerrainManager : MonoBehaviour
{
public static TerrainManager m_Instance = null;
//string heightMap = "res/scene/ground/create/height01.png";
//string mixMap = "res/scene/ground/create/mix03.png";//"res/scene/ground/create/mix01.tga";
string[] baseMap = { "res/scene/ground/mat/Ground_005_T.png", "res/scene/ground/mat/Ground_007_T.png",
"res/scene/ground/mat/Ground_G003_T.png", "res/scene/ground/mat/Ground_003_T.tga" };
string[] normalMap = { "res/scene/ground/mat/Ground_005_M.tga", "res/scene/ground/mat/Ground_G001_M.tga",
"res/scene/ground/mat/Ground_G003_M.tga", "res/scene/ground/mat/Ground_003_M.tga" };
private TerrainObject m_TerrainObject;
void Awake()
{
m_Instance = this;
Debug.Log("TerrainManager Awake");
}
public static IEnumerator Init()
{
if(m_Instance == null)
Debug.Log("TerrainManager m_Instance == null");
yield return m_Instance.LocalInit();
}
private IEnumerator LocalInit()
{
m_TerrainObject = new TerrainObject();
//yield return m_TerrainObject.CreateGroundObjectFromPic(0, 0, heightMap, mixMap, baseMap, normalMap, gameObject.transform);
yield break;
}
//heightMap 高度图
//mixMap 混合图
//baseMap 基础地表纹理图列表 4-16张
public static IEnumerator coCreateMap(string heightMap, string mixMap, string[] baseMap, Action<UnityEngine.Object[]> action)
{
List<UnityEngine.Object> result = new List<UnityEngine.Object>();
yield return XResLoader.coLoadAllRes(new string[] { heightMap, mixMap }, typeof(GameObject), objs =>
{
if (objs == null || objs[0] == null)
{
Debug.Log($"Load heightMap, mixMap Failed");
}
else
{
result.Add(objs[0]);//heightMap
result.Add(objs[1]);//mixMap
}
});
yield return XResLoader.coLoadAllRes(baseMap, typeof(GameObject), objs =>
{
if (objs == null || objs[0] == null)
{
Debug.Log($"Load heightMap, mixMap Failed");
}
else
{
for (int i = 0; i < objs.Length; ++i)
{
result.Add(objs[i]);//baseMap
}
}
});
action(result.ToArray());
}
void LoadGround(float xpos, float zpos)
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 801b8e092355f2c498ccec7520699259
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,394 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using UnityEngine;
using UnityEngine.Networking;
using XLua;
using static XLua.LuaEnv;
namespace XGame
{
public class LuaManager : MonoBehaviour
{
public static LuaEnv m_LuaEnv = null;// new LuaEnv();
public static LuaEnv.CustomLoader m_CustomLoader;
private const string LUA_SUFFIX = ".lua";
private const string BYTES_SUFFIX = ".bytes";
private const float INTERVAL = 1.0f;
//private bool m_bStart = false;
private float m_Timestamp = 0;
public static LuaManager m_Instance = null;
//系统lua
private AssetBundle m_LuaAB = null;
private LuaTable m_LuaMain = null;
private Action m_MainUpdate = null;
private Action m_MainLateUpdate = null;
private Action m_MainLogicUpdate = null;
private Action m_MainGUI = null;
private Action m_MainQuit = null;
//XWorld lua
private Dictionary<string, AssetBundle> m_LuaXWABs = new Dictionary<string, AssetBundle>();//xid, ab
private string m_curXID;
private void Awake()
{
m_Instance = this;
}
private void Start()
{
}
private IEnumerator LoadBaseLuaAB()
{
yield return XResLoader.GetFileDataOrigin("XWorld","bs.unity3d", (uwr) =>
{
if (uwr == null)
{
Debug.LogError($"Load AB Error: bs.unity3d");
}
else
{
byte[] decrypt = AESManager.Decrypt(uwr.downloadHandler.data);//bs.unity3d
m_LuaAB = AssetBundle.LoadFromMemory(decrypt);
Debug.Log("m_LuaAB Load OK!");
}
});
}
//Key : local或xid
public IEnumerator LoadAB(string Path, string Xid = null)
{
yield return XResLoader.coLoadFile(Path, (data) =>
{
if (data == null)
{
Debug.LogError($"Load AB Error: {Path}");
}
else
{
byte[] decrypt = AESManager.Decrypt(data);//bs.unity3d
AssetBundle ab = AssetBundle.LoadFromMemory(decrypt);
if (Xid != null)
m_LuaXWABs[Xid] = ab;
else
m_LuaXWABs[GlobalData.CurNode] = ab;
}
});
yield break;
}
public void ReleaseLua(string Xid)
{
if (Xid == null)
{//基础Lua,可能不需要即时释放
//释放lua
//。。。
//m_LuaAB.Unload(true);
}
else
{
if (m_LuaXWABs.ContainsKey(Xid))
{
AssetBundle ab = null;
if(m_LuaXWABs.TryGetValue(Xid, out ab))
{
ab.Unload(true);
m_LuaXWABs.Remove(Xid);
}
}
}
}
private IEnumerator LocalInit()
{
m_CustomLoader = (ref string path) =>
{//加载器 默认先从基础文件找,然后再去当前库找
string originalFixedPath = path.Replace('.', '/');
string fixedPath = originalFixedPath.ToLower();
//Debug.Log($"Base Loader : {fixedPath}");
byte[] content = null;
#if UNITY_EDITOR
//编辑器查找底层lua本地文件
if (File.Exists($"assets/lua/{fixedPath}.lua"))
{
content = System.IO.File.ReadAllBytes($"assets/lua/{fixedPath}.lua");
if (content != null)
{
return content;
}
}
#endif
TextAsset textAsset;
//然后查找底层ab
//根据文件路径在Assets/lua/路径里(或者luabytes里)加载lua
//先加载系统lua文件
string luaPath = $"assets/l/{fixedPath}.bytes";
if (m_LuaAB != null)
{
textAsset = m_LuaAB.LoadAsset<TextAsset>(luaPath);//
if (textAsset != null)
{
content = textAsset.bytes;
return content;
//Debug.LogError($"lua {path} not found!");
}
}
//Debug.LogError($"m_LuaAB {luaPath} not found!");
//非系统lua则寻找节点lua
//编辑器找本地
#if UNITY_EDITOR
//编辑器查找节点lua本地文件
//luaPath = $"assets/{GlobalData.CurNode}/script/{fixedPath}.lua";
luaPath = $"{Application.dataPath}/../../Lua/{fixedPath}.lua";
if (File.Exists(luaPath))
{
content = System.IO.File.ReadAllBytes(luaPath);
if (content != null)
{
return content;
}
}
luaPath = $"{Application.dataPath}/lua/{originalFixedPath}.lua";
if (File.Exists(luaPath))
{
content = System.IO.File.ReadAllBytes(luaPath);
if (content != null)
{
return content;
}
}
luaPath = $"{Application.dataPath}/lua/{fixedPath}.lua";
if (File.Exists(luaPath))
{
content = System.IO.File.ReadAllBytes(luaPath);
if (content != null)
{
return content;
}
}
luaPath = $"{Application.dataPath}/../../{originalFixedPath}.lua";
if (File.Exists(luaPath))
{
content = System.IO.File.ReadAllBytes(luaPath);
if (content != null)
{
return content;
}
}
luaPath = $"{Application.dataPath}/../../{fixedPath}.lua";
if (File.Exists(luaPath))
{
content = System.IO.File.ReadAllBytes(luaPath);
if (content != null)
{
return content;
}
}
#endif
luaPath = $"assets/l/{fixedPath}.bytes";
//Debug.Log($"lua {path} size:{textAsset.bytes.Length}!");
AssetBundle luaAB = null;
if (m_curXID != null && m_LuaXWABs.ContainsKey(m_curXID))
{
luaAB = m_LuaXWABs[m_curXID];
if (luaAB != null)
{
textAsset = m_LuaAB.LoadAsset<TextAsset>(luaPath);//
if (textAsset != null)
{
return textAsset.bytes;
}
}
}
foreach (var ab in m_LuaXWABs)
{
if (ab.Value != null && ab.Value != luaAB)
{
textAsset = m_LuaAB.LoadAsset<TextAsset>(luaPath);//
if (textAsset != null)
{
return textAsset.bytes;
}
}
}
return null;
};
yield return RestartLua();
yield break;
}
public IEnumerator RestartLua()
{
if (m_LuaEnv != null)
{
m_LuaEnv.Dispose();
m_LuaEnv = null;
if (m_LuaAB != null)
{
m_LuaAB.Unload(true);
m_LuaAB = null;
}
m_LuaMain = null;
m_MainUpdate = null;
m_MainLateUpdate = null;
m_MainLogicUpdate = null;
m_MainGUI = null;
m_MainQuit = null;
GC.Collect();
}
//修改默认加载位置xid或local
m_LuaEnv = new LuaEnv();//*/
if (m_LuaAB == null)
{
Debug.Log("Init m_LuaAB");
yield return LoadBaseLuaAB();
}
m_LuaEnv.AddLoader(m_CustomLoader);
//默认函数
m_LuaEnv.Global.Set("XWorldGetTime", (System.Func<double>)Utility.XWorldGetTime);
Debug.Log($"LuaManager Init 'XWMain' {m_LuaEnv.Memroy}KB");
//无需使用lua
yield break;
//默认lua入口文件
m_LuaEnv.DoString("require'XWBaseLua.XWMain'");
Debug.Log("require'XWBaseLua.XWMain'");
m_LuaMain = m_LuaEnv.Global.Get<LuaTable>("XWMain");
if (m_LuaMain == null)
{
Debug.LogError("Error: Lua 'Main' Not Found !");
yield break;
}
//m_LuaEnv.DoString("_G.Main=nil");//全局的置空
Action initFunc = m_LuaMain.Get<Action>("XWInit");
if (initFunc != null)
{
initFunc();
}
//m_LuaEnv.DoString("MaPlayerPrefsin.Init()");
m_MainUpdate = m_LuaMain.Get<Action>("XWUpdate");
m_MainLateUpdate = m_LuaMain.Get<Action>("XWLateUpdate");
m_MainLogicUpdate = m_LuaMain.Get<Action>("XWFixedUpdate");
m_MainGUI = m_LuaMain.Get<Action>("XWGUI");
m_MainQuit = m_LuaMain.Get<Action>("XWQuit");
//Debug.Log($"'Main' Loaded {m_LuaEnv.Memroy}KB");
//m_bStart = true;
//加载默认节点
string xid = XData.GetString("XWorldID");
//默认加载Lua开发模块
//Action<string> AddModule = m_LuaMain.Get<Action<string>>("AddModule");
//AddModule("client/client");
#if UNITY_EDITOR
#endif
yield break;
}
public static IEnumerator Init()
{
yield return m_Instance.LocalInit();
}
private void Update()
{
float now = Time.time;
if (m_MainUpdate != null)
{
try
{
m_MainUpdate();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
if (!(now > m_Timestamp + INTERVAL))
{
return;
}
m_Timestamp = now;
if(m_LuaEnv != null)
m_LuaEnv.Tick();
}
private void LateUpdate()
{
if (m_MainLateUpdate != null)
{
try
{
m_MainLateUpdate();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
private void FixedUpdate()
{ //
if (m_MainLogicUpdate != null)
{
m_MainLogicUpdate();
}
}
private void OnGUI()
{
if (m_MainGUI != null)
{
try
{
m_MainGUI();
}
catch (Exception e)
{
Debug.LogError(e);
}
}
}
public void DoString(string sLuaString)
{
m_LuaEnv.DoString(sLuaString);
}
private void OnApplicationQuit()
{
if(m_MainQuit != null)
m_MainQuit();
}
#if UNITY_EDITOR
//热更新lua
private void OnLuaFileChange(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !filePath.EndsWith(LUA_SUFFIX, StringComparison.OrdinalIgnoreCase))
{
return;
}
//重载文件
//filePath = filePath.Replace('\\', '/');
//int startIndex = filePath.IndexOf(LuaEditorBaseUrl) + 7;
//filePath = filePath.Substring(startIndex, filePath.Length - startIndex - LUA_SUFFIX.Length);
//Invoke("ToolManager.ReloadLuaFile", filePath);
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 077d0e0d320bc3d41866b4ef3c249888
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
using System.Threading;
using UnityEngine;
using XLua;
using System.IO;
class LuaServer : MonoBehaviour
{
public static LuaEnv m_LuaEnv = new LuaEnv();
private Thread _thread;
private bool _isThreadRunning;
private string _result;
private float _timeBegin = 0;
void Start()
{
//初始化
m_LuaEnv.AddLoader((ref string path) =>
{//加载器 默认先从基础文件找,然后再去当前库找
string fixedPath = path.Replace('.', '/');
//Debug.Log($"Base Loader : {fixedPath}");
#if UNITY_EDITOR
byte[] content = null;
//编辑器查找底层lua本地文件
if (File.Exists($"assets/lua/{fixedPath}.lua"))
{
content = System.IO.File.ReadAllBytes($"assets/lua/{fixedPath}.lua");
if (content != null)
{
return content;
}
}
string luaPath = $"../Lua/{fixedPath}.lua";
if (File.Exists(luaPath))
{
content = System.IO.File.ReadAllBytes(luaPath);
if (content != null)
{
return content;
}
}
#endif
return null;
});
m_LuaEnv.DoString("require'server/xwserver'");
// 启动子线程
_thread = new Thread(Run);
_isThreadRunning = true;
_thread.Start();
_timeBegin = Time.realtimeSinceStartup;
}
void Update()
{
// 在主线程中处理子线程的结果
if (!_isThreadRunning && _result != null)
{
Debug.Log("Result from thread: " + _result);
_result = null; // 清除结果,避免重复处理
}
}
void Run()
{
// 模拟耗时操作
while (_thread != null)
{
Thread.Sleep(66);
if (Time.realtimeSinceStartup - _timeBegin > 60)//60秒退出
{
_result = "Work completed!";
_isThreadRunning = false;
}
}
}
void OnDestroy()
{
// 确保在销毁时停止子线程
if (_thread != null && _thread.IsAlive)
{
_thread.Abort();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3539fd3d393cf014e89a926abd9a95ba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,15 @@
//节点运行
class NodeRunner
{
int Start(string NodeName)
{ //打开对应游戏节点
return 0;
}
bool Close(int NodeID)
{
return false;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2ac8b7067c1411149aa2001c879a8c69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec896450a5a2cd54b9eed305afae7ae7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,719 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;
using System.Net.WebSockets;
using System.Threading;
using UnityEngine.Networking.PlayerConnection;
using System.Text;
using UnityWebSocket;
using WebSocketState = System.Net.WebSockets.WebSocketState;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
namespace XGame
{
public class XWebSocket
{
private IWebSocket m_WebSocket;//第三方webGL库
string m_Url;
private Socket m_TcpSocket = null;
private Thread m_TcpThread = null;
private bool m_UseTcp = false;
private readonly object m_BufferLock = new object();
private Action<XWebSocket, string> m_Callback = null;
public ClientWebSocket m_wSocket;
public CancellationToken m_Token;
private bool m_bRun = false;
private byte[] m_byteBuffer = new byte[65536];
private byte[] m_byteTemp = new byte[4096];
private ArraySegment<byte> m_bufSegment;
private int m_bufCurPos = 0;
//float heartTime = 0;
//float receiveMsgTime = 0;
//float reconnectCount = 0;
public bool IsConected()
{
if (m_bRun && ((m_UseTcp && m_TcpSocket != null) || (!m_UseTcp && m_WebSocket != null)))
return true;
else
return false;
}
public int GetRecvSize() { lock (m_BufferLock) { return m_bufCurPos; } }
public bool isDataRecv() { lock (m_BufferLock) { return (m_bufCurPos > 0) ? true : false; } }
public void Connect(string url, Action<XWebSocket, string> callback)
{
m_Url = url;
m_Callback = callback;
//m_Url = "wss://echo.websocket.events";
m_bRun = true;
m_WebSocket = new UnityWebSocket.WebSocket(m_Url);
m_WebSocket.OnOpen += Socket_OnOpen;
m_WebSocket.OnMessage += Socket_OnMessage;
m_WebSocket.OnClose += Socket_OnClose;
m_WebSocket.OnError += Socket_OnError;
m_WebSocket.ConnectAsync();
}
public void ConnectTcp(string host, int port, Action<XWebSocket, string> callback)
{
m_UseTcp = true;
m_Url = $"{host}:{port}";
m_Callback = callback;
try
{
m_TcpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
m_TcpSocket.Connect(host, port);
m_bRun = true;
m_TcpThread = new Thread(TcpReceiveLoop);
m_TcpThread.IsBackground = true;
m_TcpThread.Start();
Debug.Log($"Connected tcp://{m_Url}");
m_Callback(this, null);
}
catch (Exception e)
{
m_bRun = false;
Debug.Log($"Tcp connect error: {e.Message}");
m_Callback(null, e.Message);
}
}
public void Socket_OnOpen(object sender, OpenEventArgs e)
{
if (e != null)
Debug.Log($"Connected {m_Url} Result : {sender.ToString()}");
else
Debug.Log(string.Format("Connected: {0}", m_Url));
m_Callback(this, null);
}
public void Socket_OnMessage(object sender, UnityWebSocket.MessageEventArgs e)
{
//if (e.IsBinary)
//{
// Debug.Log(string.Format("C# Receive Bytes ({1}): {0}", e.Data, e.RawData.Length));
//}
//else if (e.IsText)
//{
// Debug.Log(string.Format("C# Receive: {0}", e.Data));
//}
//ArraySegment<byte> byteReal = new ArraySegment<byte>(m_byteTemp);
//Debug.Log($"Buffer = {m_byteTemp.Length} ; Segment = {byteReal.Count}");
//复制到m_byteBuffer
AddBuffer(e.RawData);
//receiveCount += 1;
}
public void Socket_OnClose(object sender, CloseEventArgs e)
{
m_bRun = false;
Debug.Log(string.Format("Closed: StatusCode: {0}, Reason: {1}", e.StatusCode, e.Reason));
}
public void Socket_OnError(object sender, ErrorEventArgs e)
{
m_bRun = false;
Debug.Log(string.Format("Error: {0}", e.Message));
}
//public async void Connect(string addr, int port, Action<XWebSocket, string> callback)
//{
// m_wSocket = new ClientWebSocket();
// m_Token = new CancellationToken();
// Uri url = new Uri("wss://echo.websocket.events");//"ws://121.40.165.18:8800");// $"ws://{addr}:{port}/xxx/xxx");
// Debug.Log($"CS Connect {url.ToString()}");
// m_bufSegment = new ArraySegment<byte>(m_byteBuffer);
// await m_wSocket.ConnectAsync(url, m_Token);
// if (m_wSocket.State == WebSocketState.Open)
// {
// Debug.Log($"--------连接成功 : {url.ToString()}");
// m_bRun = true;
// callback(this, null);
// }
// else
// {
// Debug.Log($"--------连接失败 : {url.ToString()}");
// callback(this, $"Connect Error : {m_wSocket.State}");
// }
// while (m_bRun)
// {
// //var result = new byte[1024];
// ArraySegment<byte> byteReal = new ArraySegment<byte>(m_byteTemp);
// WebSocketReceiveResult result = await m_wSocket.ReceiveAsync(byteReal, new CancellationToken());//接受数据
// Debug.Log($"Buffer = {m_byteTemp.Length} ; Segment = {byteReal.Count}");
// //复制到m_byteBuffer
// AddBuffer(byteReal);
// //var str = Encoding.UTF8.GetString(m_byteTemp, 0, m_byteTemp.Length);
// //Debug.Log(str);
// }
//}
void AddBuffer(byte[] buffer)
{
lock (m_BufferLock)
{
if (m_bufCurPos + buffer.Length > m_byteBuffer.Length)
{
m_bufCurPos = 0;
}
Array.ConstrainedCopy(buffer, 0, m_byteBuffer, m_bufCurPos, buffer.Length);
m_bufCurPos = m_bufCurPos + buffer.Length;
m_bufSegment = new ArraySegment<byte>(m_byteBuffer, 0, m_bufCurPos);
}
}
public byte[] GetBuffer()
{
lock (m_BufferLock)
{
return m_bufSegment.ToArray();
}
}
public byte[] recv(ref string errorMsg)
{
if (!m_bRun)
{//已经断线了
errorMsg = null;
Debug.Log($"net disconnected !");
return null;
}
if (m_bufCurPos == 0)
{//不需要获取数据
errorMsg = "";
return null;
}
byte[] retBuf;
lock (m_BufferLock)
{
retBuf = m_bufSegment.ToArray();
m_bufCurPos = 0;
m_bufSegment = new ArraySegment<byte>(m_byteBuffer, 0, 0);
}
//Debug.Log($"c# recv: {retBuf.Length}");
return retBuf;
}
public byte[] Recv(ref string errorMsg)
{
return recv(ref errorMsg);
}
//async void SendAsync(byte[] sendData, Action<int, string> callback)
//{
//await m_wSocket.SendAsync(new ArraySegment<byte>(sendData), WebSocketMessageType.Binary, false, m_Token);
//if (m_wSocket.State != WebSocketState.Open)
//{
// callback(0, null);
//}
//else
// callback(sendData.Length, null);
//}
public int send(byte[] sendData, Action<int, string> callback, ref string errorMsg )
{
//Debug.Log($"c# send {sendData.Length}");加入了consoleWindows后不能再这里打印
if (m_UseTcp)
{
int sent = 0;
while (sent < sendData.Length)
{
sent += m_TcpSocket.Send(sendData, sent, sendData.Length - sent, SocketFlags.None);
}
}
else
{
m_WebSocket.SendAsync(sendData);
}
errorMsg = null;
return sendData.Length;
}
public int Send(byte[] sendData, Action<int, string> callback, ref string errorMsg)
{
return send(sendData, callback, ref errorMsg);
}
public void close()
{
m_bRun = false;
if (m_UseTcp)
{
m_TcpSocket?.Close();
}
else
{
m_WebSocket.CloseAsync();
}
}
public bool status(ref string errorMsg)
{
return m_bRun;
}
public void settimeout(int timeout)
{
}
private void TcpReceiveLoop()
{
byte[] buffer = new byte[4096];
while (m_bRun)
{
try
{
int count = m_TcpSocket.Receive(buffer);
if (count <= 0)
{
break;
}
byte[] data = new byte[count];
Array.ConstrainedCopy(buffer, 0, data, 0, count);
AddBuffer(data);
Debug.Log($"Tcp received {count} bytes");
}
catch (Exception e)
{
if (m_bRun)
{
Debug.Log($"Tcp receive error: {e.Message}");
}
break;
}
}
m_bRun = false;
}
/*
private void WebSocketReceive(object sender, MessageEventArgs e)
{
//优化逻辑
//heartTime = 0;
receiveMsgTime = Time.realtimeSinceStartup;
if (e.IsText)
{
string[] temp = e.Data.Split('.');
//发送接口需要返回的消息
SocketCode socketCode = (SocketCode)int.Parse(temp[0]);
if (msgResponseDic.ContainsKey(socketCode))
{
List<Action<string>> actions = msgResponseDic[socketCode];
for (int i = 0; i < actions.Count; i++)
{
actions[i](temp[1]);
}
msgResponseDic.Remove(socketCode);
}
if (msgListenerDic.ContainsKey(socketCode))
{
List<Action<string>> actions = msgListenerDic[socketCode];
for (int i = 0; i < actions.Count; i++)
{
actions[i](temp[1]);
}
}
Debug.LogError(string.Format("接收到消息: {0}", e.Data));
}
else if (e.IsBinary)
{
Debug.LogError(string.Format("接受到消息 Bytes ({1}): {0}", e.Data, e.RawData.Length));
}
}
private void WebSocketError(object sender, ErrorEventArgs e)
{
connected = false;
throw new NotImplementedException();
}
private void WebSocketOpen(object sender, OpenEventArgs e)
{
reconnectCount = 0;
receiveMsgTime = Time.realtimeSinceStartup;
connected = true;
Debug.LogError(string.Format("连接成功,IP:{0}", ip));
}
private void WebSocketClose(object sender, CloseEventArgs e)
{
connected = false;
Debug.LogError(string.Format("连接关闭: StatusCode: {0}, Reason: {1}", e.StatusCode, e.Reason));
}//*/
}
public class lsocket : MonoBehaviour
{
//private WebSocket m_wSocket = new WebSocket();
public lsocket Instance;
private void Awake()
{
Instance = this;
}
public static float gettime()
{
return (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000;
}
//外部接口
public static void connect(string addr, int port, Action<XWebSocket, string> callback )
{
XWebSocket socket = new XWebSocket();
bool forceTcp = false;
if (addr.StartsWith("http://"))
addr = addr.Substring("http://".Length);
else if (addr.StartsWith("https://"))
addr = addr.Substring("https://".Length);
else if (addr.StartsWith("tcp://"))
{
addr = addr.Substring("tcp://".Length);
forceTcp = true;
}
if (!addr.StartsWith("ws://") && !addr.StartsWith("wss://"))
{
int slash = addr.IndexOf("/");
if (slash >= 0)
addr = addr.Substring(0, slash);
string host = addr;
int tcpPort = port;
int colon = addr.LastIndexOf(":");
if (colon >= 0)
{
host = addr.Substring(0, colon);
int.TryParse(addr.Substring(colon + 1), out tcpPort);
}
if (forceTcp || host == "localhost" || host == "127.0.0.1" || tcpPort == 7777 || tcpPort == 7788)
{
Debug.Log($"C# Connect tcp://{host}:{tcpPort}");
socket.ConnectTcp(host, tcpPort, callback);
return;
}
}
if (addr.StartsWith("ws://") || addr.StartsWith("wss://"))
{
if (port != 0)
addr = $"{addr}:{port}";
Debug.Log($"C# Connect {addr}");
socket.Connect(addr, callback);
return;
}
//socket.Connect(addr, port, callback);
//if (port != 0)
int pos = addr.IndexOf(":");
string str;
if (pos == -1)
str = addr;
else
str = addr.Substring(0, pos);
string regexStrIPV4 = (@"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$");
if (Regex.IsMatch(str, regexStrIPV4) && addr != "0.0.0.0")
{//ip用ws
addr = $"ws://{addr}";
}
else
{//域名的用wss
addr = $"wss://{addr}";
if (port != 0)
port += 2000;//wss连接的端口规则
}
// if (!addr.StartsWith("wss://") && !addr.StartsWith("ws://"))
// {
//#if UNITY_EDITOR
// addr = $"ws://{addr}";
//#else
// if(!LoadDll.m_bInWX)
// {
// addr = $"wss://{addr}";
// if(port != 0)
// port += 2000;//wss连接的端口规则
// }
// else
// {
// addr = $"wss://{addr}";
// if(port != 0)
// port += 2000;//wss连接的端口规则
// }
//#endif
// //addr = $"ws://{addr}";
// }
if (port != 0)
addr = $"{addr}:{port}";
//addr = "wss://g7h5.guangyv.com:4201";
Debug.Log($"C# Connect {addr}");
socket.Connect(addr, callback);
}
//传入多个WebSocket引用,返回有数据处理的, dataSize为零就不需要处理
public static XWebSocket[] select(XWebSocket[] readSockets, XWebSocket[] writeSockets, int Param, ref XWebSocket[] retWriteSockets, ref int dataSize)
{
List<XWebSocket> retReadSockets = new List<XWebSocket>();
string error = null;
bool bOK = true;
dataSize = 0;
for (int i = 0; i < readSockets.Length; ++i)
{
if (!readSockets[i].status(ref error))
{
bOK = false;
break;
}
int recvSize = readSockets[i].GetRecvSize();
if (recvSize > 0)
{
retReadSockets.Add(readSockets[i]);
dataSize += recvSize;
}
}
if (!bOK)
return null;
retWriteSockets = writeSockets;
return retReadSockets.ToArray();
}
}
}
/*
public class WebSocketLogic : MonoBehaviour
{
public async void WebSocket()
{
try
{
ClientWebSocket ws = new ClientWebSocket();
CancellationToken ct = new CancellationToken();
//添加header
//ws.Options.SetRequestHeader("X-Token", "eyJhbGciOiJIUzI1N");
Uri url = new Uri("ws://121.40.165.18:8800/v1/test/test");
await ws.ConnectAsync(url, ct);
await ws.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes("hello")), WebSocketMessageType.Binary, true, ct); //发送数据
while (true)
{
var result = new byte[1024];
await ws.ReceiveAsync(new ArraySegment(result), new CancellationToken());//接受数据
var str = Encoding.UTF8.GetString(result, 0, result.Length);
Debug.Log(str);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private WebSocketLogic() { }
private WebSocketLogic _instance;
public WebSocketLogic Instance
{
get
{
if (_instance == null)
{
_instance = new WebSocketLogic();
}
return _instance;
}
private set { }
}
WebSocket webSocket;
string ip;
public void Init(string _ip)
{
inited = true;
ip = _ip;
}
public void Connect()
{
RemoveHandle();
webSocket = new WebSocket(ip);
webSocket.OnClose += WebSocketClose;
webSocket.OnOpen += WebSocketOpen;
webSocket.OnError += WebSocketError;
webSocket.OnMessage += WebSocketReceive;
webSocket.ConnectAsync();
}
public void AddListener(SocketCode socketCode, Action<string> callback)
{
if (msgListenerDic.ContainsKey(socketCode))
{
List<Action<string>> actions = msgListenerDic[socketCode];
for (int i = 0; i < actions.Count; i++)
{
if (actions[i] == callback)
{
Debug.LogError("重复注册监听,消息号:" + socketCode);
return;
}
}
msgListenerDic[socketCode].Add(callback);
}
else
{
List<Action<string>> actions = new List<Action<string>>();
actions.Add(callback);
msgListenerDic.Add(socketCode, actions);
}
}
public void RemoveListener(SocketCode socketCode, Action<string> callback)
{
if (msgListenerDic.ContainsKey(socketCode))
{
List<Action<string>> actions = msgListenerDic[socketCode];
for (int i = 0; i < actions.Count; i++)
{
if (actions[i] == callback)
{
actions.RemoveAt(i);
return;
}
}
Debug.LogError("移除监听出错,未包注册监听消息函数,消息号:" + socketCode);
}
else
{
Debug.LogError("移除监听出错,未包注册监听,消息号:" + socketCode);
}
}
void RemoveHandle()
{
msgResponseDic.Clear();
webSocket = null;
webSocket.OnClose -= WebSocketClose;
webSocket.OnOpen -= WebSocketOpen;
webSocket.OnError -= WebSocketError;
webSocket.OnMessage -= WebSocketReceive;
}
public void Send(SocketCode socketCode, string msg, Action<string> callback)
{
if (callback != null)
{
if (msgResponseDic.ContainsKey(socketCode))
{
msgResponseDic[socketCode].Add(callback);
}
else
{
List<Action<string>> actions = new List<Action<string>>();
actions.Add(callback);
msgResponseDic.Add(socketCode, actions);
}
}
webSocket.SendAsync(string.Format("{0}/{1}", socketCode, msg));
}
private void WebSocketReceive(object sender, MessageEventArgs e)
{
//优化逻辑
//heartTime = 0;
receiveMsgTime = Time.realtimeSinceStartup;
if (e.IsText)
{
string[] temp = e.Data.Split('.');
//发送接口需要返回的消息
SocketCode socketCode = (SocketCode)int.Parse(temp[0]);
if (msgResponseDic.ContainsKey(socketCode))
{
List<Action<string>> actions = msgResponseDic[socketCode];
for (int i = 0; i < actions.Count; i++)
{
actions[i](temp[1]);
}
msgResponseDic.Remove(socketCode);
}
if (msgListenerDic.ContainsKey(socketCode))
{
List<Action<string>> actions = msgListenerDic[socketCode];
for (int i = 0; i < actions.Count; i++)
{
actions[i](temp[1]);
}
}
Debug.LogError(string.Format("接收到消息: {0}", e.Data));
}
else if (e.IsBinary)
{
Debug.LogError(string.Format("接受到消息 Bytes ({1}): {0}", e.Data, e.RawData.Length));
}
}
private void WebSocketError(object sender, ErrorEventArgs e)
{
connected = false;
throw new NotImplementedException();
}
private void WebSocketOpen(object sender, OpenEventArgs e)
{
reconnectCount = 0;
receiveMsgTime = Time.realtimeSinceStartup;
connected = true;
Debug.LogError(string.Format("连接成功,IP:{0}", ip));
}
private void WebSocketClose(object sender, CloseEventArgs e)
{
connected = false;
Debug.LogError(string.Format("连接关闭: StatusCode: {0}, Reason: {1}", e.StatusCode, e.Reason));
}
float heartTime = 0;
float receiveMsgTime = 0;
float reconnectCount = 0;
private void Update()
{
if (!inited)
{
return;
}
heartTime = heartTime + Time.deltaTime;
if (heartTime >= 5.0f && connected == true)
{
heartTime = 0.0f;
Send(SocketCode.Heart, "heartbeat", null);
}
if (receiveMsgTime != 0 && Time.realtimeSinceStartup - receiveMsgTime > 6.0f)
{
if (reconnectCount > 5)
{
Debug.LogError("重新连接失败!");
return;
}
receiveMsgTime = Time.realtimeSinceStartup;
connected = false;
webSocket.CloseAsync();
Connect();
reconnectCount = reconnectCount + 1;
Debug.LogError(string.Format("连接断开,尝试第{0}次重新连接!", reconnectCount));
}
}
public void Clear()
{
connected = false;
inited = false;
msgResponseDic.Clear();
msgListenerDic.Clear();
}
}//*/
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c5dd4f573cd35648b6e164cc3ec5b8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a3aa1eef02c8ee428446f9e2605293f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using System;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
namespace XGame.MiniGame
{
// 发布产物签名验证(设计 §7):用内置 RSA 公钥(modulus+exponent, base64)验 files.txt 的 digest 签名。
// 公钥未配置(空)时跳过验签(仅本地开发);正式包必须填入与发布方私钥配对的公钥。
// 用 RSAParameters+ImportParametersnetstandard2.0 起即有,Unity mono 普遍支持),避免 ImportFromPem(net5+)。
public static class ClientManifestVerifier
{
// TODO(发布前必填):从发布方 RSA 密钥对导出的公钥分量 base64。
// 例:RSAParameters p = rsa.ExportParameters(false); Modulus=Convert.ToBase64String(p.Modulus); Exponent=Convert.ToBase64String(p.Exponent)("AQAB")
private const string PublicKeyModulusB64 = "";
private const string PublicKeyExponentB64 = "";
public static bool IsConfigured =>
PublicKeyModulusB64.Length > 0 && PublicKeyExponentB64.Length > 0;
// 验 sig(对 digest 的 UTF8 字节做 RSA-SHA256-PKCS1 签名,与发布工具 ManifestSigner.Sign 一致)
public static bool Verify(string digest, byte[] signature)
{
if (!IsConfigured)
{
Debug.LogWarning("[ClientManifestVerifier] 未配置公钥,跳过验签(仅限开发)");
return true;
}
if (signature == null || signature.Length == 0) return false;
try
{
var p = new RSAParameters
{
Modulus = Convert.FromBase64String(PublicKeyModulusB64),
Exponent = Convert.FromBase64String(PublicKeyExponentB64),
};
using (var rsa = RSA.Create())
{
rsa.ImportParameters(p);
return rsa.VerifyData(Encoding.UTF8.GetBytes(digest), signature,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
catch (Exception e)
{
Debug.LogError("[ClientManifestVerifier] 验签异常: " + e.Message);
return false;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e28d9299ac5c6ff4482881be5072577a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;
using XWorld.Framework.Protocol;
namespace XGame.MiniGame
{
public sealed class DevServerDiscovery
{
private readonly string _token;
private readonly string _extraProbeIp;
private readonly object _lock = new object();
private readonly List<DevDiscoveryReply> _results = new List<DevDiscoveryReply>();
private Thread _thread;
private volatile bool _running;
public DevServerDiscovery(string token)
{
_token = token;
// 跨网段联调:广播/定向广播不被路由器转发(RFC 2644),组播亦实测不通(路由器未开组播路由),
// 唯一可靠路径是向已知开发机 IP 单播。IP 来自 persistentDataPath/dev_server.txt——底包首启弹框
// 录入(见 Base 层 DevServerConfig/DevServerPrompt)。persistentDataPath 不能在后台线程访问,
// 故在主线程构造时读好存字段,Run(后台线程)只用字段。
_extraProbeIp = ReadConfiguredProbeIp();
}
public bool IsRunning => _running;
public void Begin(int durationMs = 1200)
{
if (_running) return;
_running = true;
_thread = new Thread(() => Run(durationMs)) { IsBackground = true };
_thread.Start();
}
private void Run(int durationMs)
{
UdpClient udp = null;
try
{
udp = new UdpClient(AddressFamily.InterNetwork);
udp.EnableBroadcast = true;
udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
udp.Client.Bind(new IPEndPoint(IPAddress.Any, 0));
udp.Client.ReceiveTimeout = 250;
uint nonce = unchecked((uint)Guid.NewGuid().GetHashCode());
byte[] query = DevDiscoveryCodec.EncodeQuery(new DevDiscoveryQuery
{
ProtoVersion = DevDiscovery.ProtoVersion,
Nonce = nonce,
Token = _token,
});
// 同机的 UDP 广播常收不到(取决于网卡是否回环广播),故同时向 127.0.0.1 单播;
// 并在窗口内周期重发,防响应器(网关)刚起来还没监听。
// 255.255.255.255(限制广播)在部分 Android WiFi 驱动上会被丢弃,故再按各网卡 IP|~mask 补发子网定向广播(如 192.168.1.255)。
List<IPEndPoint> targetList = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.Broadcast, DevDiscovery.Port),
new IPEndPoint(IPAddress.Loopback, DevDiscovery.Port),
};
foreach (IPAddress bc in GetDirectedBroadcasts())
targetList.Add(new IPEndPoint(bc, DevDiscovery.Port));
if (!string.IsNullOrEmpty(_extraProbeIp) && IPAddress.TryParse(_extraProbeIp, out IPAddress extra))
targetList.Add(new IPEndPoint(extra, DevDiscovery.Port));
IPEndPoint[] targets = targetList.ToArray();
Debug.Log("[DevTest] targets(" + targets.Length + "): " + string.Join(", ", Array.ConvertAll(targets, t => t.ToString())));
DateTime deadline = DateTime.UtcNow.AddMilliseconds(durationMs);
DateTime nextSend = DateTime.MinValue;
while (_running && DateTime.UtcNow < deadline)
{
if (DateTime.UtcNow >= nextSend)
{
foreach (IPEndPoint t in targets)
{
try { udp.Send(query, query.Length, t); } catch { }
}
nextSend = DateTime.UtcNow.AddMilliseconds(300);
}
IPEndPoint from = new IPEndPoint(IPAddress.Any, 0);
byte[] data;
try { data = udp.Receive(ref from); }
catch (SocketException) { continue; }
if (!DevDiscoveryCodec.TryDecodeReply(data, out var reply)) continue;
if (reply.ProtoVersion != DevDiscovery.ProtoVersion) continue;
if (reply.Nonce != nonce) continue;
if (reply.Token != _token) continue;
lock (_lock)
{
if (!_results.Exists(r => r.GatewayUrl == reply.GatewayUrl))
{
_results.Add(reply);
}
}
}
}
catch (Exception e)
{
Debug.LogWarning("[DevTest] discovery thread error: " + e.Message);
}
finally
{
try { udp?.Close(); } catch { }
_running = false;
}
}
// 枚举本机所有 Up 状态 IPv4 网卡,按 broadcast = ip | ~mask 算出各自的子网定向广播地址。
// 关键:Android/Mono 下 IPv4Mask 常拿不到(返回 null/0.0.0.0 甚至抛异常),故对私有网段按 /24 兜底,
// 且逐地址 try/catch,任何一块网卡取掩码失败都不中断整体枚举。
private static List<IPAddress> GetDirectedBroadcasts()
{
List<IPAddress> list = new List<IPAddress>();
NetworkInterface[] nics;
try { nics = NetworkInterface.GetAllNetworkInterfaces(); }
catch (Exception e) { Debug.LogWarning("[DevTest] GetAllNetworkInterfaces failed: " + e.Message); return list; }
foreach (NetworkInterface ni in nics)
{
if (ni.OperationalStatus != OperationalStatus.Up) continue;
if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue;
IPInterfaceProperties props;
try { props = ni.GetIPProperties(); }
catch { continue; }
foreach (UnicastIPAddressInformation ua in props.UnicastAddresses)
{
if (ua.Address.AddressFamily != AddressFamily.InterNetwork) continue; // 只处理 IPv4
byte[] ip = ua.Address.GetAddressBytes();
if (ip.Length != 4) continue;
byte[] mk = null;
try { IPAddress m = ua.IPv4Mask; if (m != null) mk = m.GetAddressBytes(); }
catch { mk = null; } // Android 上访问 IPv4Mask 可能抛,兜住
// 掩码不可用(null/长度不对/全 0)时,私有网段按 /24 兜底(最常见的家用/办公 WiFi),公网地址不猜。
bool maskBad = mk == null || mk.Length != 4 || (mk[0] == 0 && mk[1] == 0 && mk[2] == 0 && mk[3] == 0);
if (maskBad)
{
if (!IsPrivateV4(ip)) continue;
mk = new byte[] { 255, 255, 255, 0 };
}
byte[] bc = new byte[4];
for (int i = 0; i < 4; i++) bc[i] = (byte)(ip[i] | (~mk[i] & 0xFF));
IPAddress bcAddr = new IPAddress(bc);
list.Add(bcAddr);
Debug.Log("[DevTest] nic=" + ni.Name + " ip=" + ua.Address + " mask=" + string.Join(".", mk) + " bc=" + bcAddr);
}
}
return list;
}
private static bool IsPrivateV4(byte[] ip)
{
if (ip[0] == 10) return true; // 10.0.0.0/8
if (ip[0] == 192 && ip[1] == 168) return true; // 192.168.0.0/16
if (ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31) return true; // 172.16.0.0/12
return false;
}
public List<DevDiscoveryReply> TakeResults()
{
lock (_lock)
{
return new List<DevDiscoveryReply>(_results);
}
}
public void Stop()
{
_running = false;
}
// 读 dev_server.txt 首行并校验为 IPv4,失败一律 null。与 Base 层 DevServerConfig.Load() 语义一致——
// XWorld.Link 未引用 Base 程序集,按本项目“内联+两边同步”惯例复制最小实现,改格式时两边须同步。
private static string ReadConfiguredProbeIp()
{
try
{
string path = System.IO.Path.Combine(Application.persistentDataPath, "dev_server.txt");
if (!System.IO.File.Exists(path)) return null;
string line = System.IO.File.ReadAllText(path).Trim();
if (line.Length == 0) return null;
if (!IPAddress.TryParse(line, out IPAddress ip)) return null;
if (ip.AddressFamily != AddressFamily.InterNetwork) return null; // 只认 IPv4
return ip.ToString();
}
catch (Exception)
{
return null;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42eb5dab97c9d9b4aa2540b4096eacd0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,146 @@
#if XW_DEVTEST
using System.Collections.Generic;
using UnityEngine;
using XGame;
using XWorld.Framework.Protocol;
namespace XGame.MiniGame
{
public sealed class DevServerSwitchUI : MonoBehaviour
{
public string Token = "xw-dev";
public bool DiscoverOnStart = true;
private DevServerDiscovery _discovery;
private List<DevDiscoveryReply> _found = new List<DevDiscoveryReply>();
private bool _prompting;
private void Start()
{
if (DiscoverOnStart && !MiniGameTestOverride.IsActive) BeginDiscovery();
}
private void OnDestroy()
{
_discovery?.Stop();
}
public void BeginDiscovery()
{
_found.Clear();
_prompting = false;
_discovery = new DevServerDiscovery(Token);
_discovery.Begin(1200);
}
private void Update()
{
if (_discovery != null && !_discovery.IsRunning && !_prompting)
{
_found = _discovery.TakeResults();
_discovery = null;
if (_found.Count > 0) _prompting = true;
}
}
private void OnGUI()
{
if (MiniGameTestOverride.IsActive)
{
GUI.color = new Color(1f, 0.85f, 0.2f);
GUILayout.BeginArea(new Rect(8, 8, 760, 30));
GUILayout.BeginHorizontal(GUI.skin.box);
GUILayout.Label($"DEV TEST -> {MiniGameTestOverride.ServerName} ({MiniGameTestOverride.GatewayUrl})");
if (GUILayout.Button("Reset", GUILayout.Width(90)))
{
MiniGameTestOverride.Clear();
GlobalData.CurNode = XData.GetString("CurNode");
GlobalData.CurCDN = XData.GetString("CurCDN");
CSharpClientApp.RestartCurrent();
}
GUILayout.EndHorizontal();
GUILayout.EndArea();
GUI.color = Color.white;
return;
}
if (!_prompting) return;
GUILayout.BeginArea(new Rect(8, 8, 760, 52 + _found.Count * 34), GUI.skin.box);
GUILayout.Label("LAN dev server found. Switch gateway and minigame resources?");
for (int i = 0; i < _found.Count; i++)
{
DevDiscoveryReply r = _found[i];
GUILayout.BeginHorizontal();
GUILayout.Label($"{r.ServerName} {r.GatewayUrl}");
if (GUILayout.Button("Switch", GUILayout.Width(80)))
{
MiniGameTestOverride.Apply(r.GatewayUrl, r.ResourceBaseUrl, r.ServerName);
GlobalData.CurNode = WithPid(r.GatewayUrl);
GlobalData.CurCDN = r.ResourceBaseUrl;
CSharpClientApp.RestartCurrent();
_prompting = false;
}
GUILayout.EndHorizontal();
}
if (GUILayout.Button("Ignore", GUILayout.Width(80))) _prompting = false;
GUILayout.EndArea();
}
private static string WithPid(string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl))
{
return gatewayUrl;
}
int pid = GetLocalPlayerId();
int queryStart = gatewayUrl.IndexOf('?');
if (queryStart < 0)
{
return gatewayUrl + "?pid=" + pid;
}
string head = gatewayUrl.Substring(0, queryStart);
string query = gatewayUrl.Substring(queryStart + 1);
string[] parts = query.Split('&');
bool replaced = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].StartsWith("pid=", System.StringComparison.OrdinalIgnoreCase))
{
parts[i] = "pid=" + pid;
replaced = true;
break;
}
}
return head + "?" + (replaced ? string.Join("&", parts) : query + "&pid=" + pid);
}
private static int GetLocalPlayerId()
{
const string key = "LocalClientId";
string id = XData.GetString(key, string.Empty);
if (string.IsNullOrEmpty(id))
{
id = SystemInfo.deviceUniqueIdentifier;
if (string.IsNullOrEmpty(id) || id == "unsupported")
{
id = System.Guid.NewGuid().ToString("N");
}
XData.SetString(key, id);
XData.Save();
}
unchecked
{
uint hash = 2166136261u;
for (int i = 0; i < id.Length; i++)
{
hash ^= id[i];
hash *= 16777619u;
}
return (int)(hash % 2000000000u) + 1;
}
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2cdf2cc29e809b04784da6c402184a82
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
using System;
using UnityEngine;
using XWorld.Framework;
// Alias to resolve ambiguity between XWorld.Framework.ILogger and UnityEngine.ILogger
using IFwkLogger = XWorld.Framework.ILogger;
namespace XGame.MiniGame
{
// 注入给 IGameClient 的上下文:资源/日志/发送/退出
public sealed class GameClientCtx : IGameClientCtx
{
public IAssetLoader Assets { get; }
public IFwkLogger Logger { get; }
private readonly Action<NetMessage> _send;
private readonly Action _exit;
public GameClientCtx(IAssetLoader assets, IFwkLogger logger, Action<NetMessage> send, Action exit)
{
Assets = assets; Logger = logger; _send = send; _exit = exit;
}
public void Send(NetMessage message) => _send(message);
public void Exit() => _exit();
}
// 简单 Unity 日志器(实现框架 ILogger
public sealed class UnityLogger : IFwkLogger
{
private readonly string _prefix;
public UnityLogger(string prefix = "[minigame]") { _prefix = prefix; }
public void Info(string m) => Debug.Log($"{_prefix} {m}");
public void Warn(string m) => Debug.LogWarning($"{_prefix} {m}");
public void Error(string m) => Debug.LogError($"{_prefix} {m}");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc08e7a5bc4eb6140bcb9f5af3c33c15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,68 @@
using System;
using System.IO;
using System.Reflection;
using UnityEngine;
using XWorld.Framework;
namespace XGame.MiniGame
{
// 从代码 ABmanifest.CodeAb)取出 Core/Client 热更 DLL 字节并加载,反射出 IGameClient 入口工厂
public sealed class MiniGameAssemblyLoader
{
public Func<IGameClient> CreateClient { get; private set; }
public string Error { get; private set; }
// localDir: MiniGameDownloader.LocalDirmanifest: 解析好的 game.json
public bool Load(string localDir, MiniGameManifest manifest)
{
string abPath = localDir + manifest.CodeAb;
if (!File.Exists(abPath))
{ Error = $"缺少代码 AB: {abPath}(需要 codeAb 统一 AB 格式,旧 .bytes 包不再支持)"; return false; }
AssetBundle ab = null;
try
{
ab = MiniGamePlatform.LoadAssetBundle(abPath);
if (ab == null) { Error = "代码 AB 打开失败: " + abPath; return false; }
byte[] coreBytes = LoadDllBytes(ab, manifest.CoreDll);
byte[] clientBytes = LoadDllBytes(ab, manifest.ClientDll);
if (coreBytes == null || clientBytes == null)
{
Error = $"代码 AB 内缺少 {manifest.CoreDll}/{manifest.ClientDll} 的 .bytesAB 内资产: {string.Join(", ", ab.GetAllAssetNames())}";
return false;
}
// Core 先于 Client 加载(Client 依赖 Core
Assembly.Load(coreBytes);
Assembly clientAsm = Assembly.Load(clientBytes);
Type entry = clientAsm.GetType(manifest.ClientEntryType);
if (entry == null) { Error = "找不到入口类型 " + manifest.ClientEntryType; return false; }
if (!typeof(IGameClient).IsAssignableFrom(entry))
{ Error = manifest.ClientEntryType + " 未实现 IGameClient"; return false; }
CreateClient = () => (IGameClient)Activator.CreateInstance(entry);
return true;
}
catch (Exception e)
{
Error = "加载小游戏程序集失败: " + e;
Debug.LogError("[MiniGameAssemblyLoader] " + Error);
return false;
}
finally
{
// DLL 字节已复制进托管内存,AB 本体可卸;false=不销毁已取出的对象
if (ab != null) ab.Unload(false);
}
}
// "X.dll.bytes" 导入后资产名为 "X.dll"Unity 剥最后一个扩展名);两种写法都试
private static byte[] LoadDllBytes(AssetBundle ab, string dllName)
{
TextAsset ta = ab.LoadAsset<TextAsset>(dllName) ?? ab.LoadAsset<TextAsset>(dllName + ".bytes");
return ta != null ? ta.bytes : null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a6ccf4c9865f0a34481f34cf3070e12c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XWorld.Framework;
namespace XGame.MiniGame
{
// IAssetLoader over 已下载的小游戏资源 ABmanifest.Assets);首次 Load 时开全部包并缓存,退出统一卸载
public sealed class MiniGameAssetLoader : IAssetLoader
{
private readonly string _localDir;
private readonly List<string> _abNames;
private readonly List<AssetBundle> _opened = new List<AssetBundle>();
private bool _openedAll;
public MiniGameAssetLoader(string localDir, MiniGameManifest manifest)
{ _localDir = localDir; _abNames = manifest.Assets; }
// path: 打包时的资产路径(如 "Assets/MiniGames/RockPaperScissors/res/UI/Prefab/UI_RockPaperScissors.prefab"
// 或裸资产名("UI_RockPaperScissors");找不到回调 null。
public void Load(string path, Action<object> onLoaded)
{
EnsureOpened();
foreach (var ab in _opened)
{
UnityEngine.Object obj = ab.LoadAsset(path);
if (obj == null) obj = ab.LoadAsset(Path.GetFileNameWithoutExtension(path));
if (obj != null) { onLoaded?.Invoke(obj); return; }
}
Debug.LogError("[MiniGameAssetLoader] 资产未找到: " + path);
onLoaded?.Invoke(null);
}
// 小游戏退出时由 MiniGameHost 调用;true=连已实例化引用的资产一起卸
public void UnloadAll()
{
foreach (var ab in _opened) if (ab != null) ab.Unload(true);
_opened.Clear();
_openedAll = false;
}
private void EnsureOpened()
{
if (_openedAll) return;
_openedAll = true;
foreach (var name in _abNames)
{
string p = _localDir + name;
if (!File.Exists(p)) { Debug.LogError("[MiniGameAssetLoader] 资源 AB 缺失: " + p); continue; }
var ab = MiniGamePlatform.LoadAssetBundle(p);
if (ab == null) { Debug.LogError("[MiniGameAssetLoader] 资源 AB 打开失败: " + p); continue; }
_opened.Add(ab);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21a3112de2a41a245a2a4cc7f082ee4f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,168 @@
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace XGame.MiniGame
{
// 下载并校验某小游戏版本的全部文件;产出本地目录 + 解析好的 manifest
public sealed class MiniGameDownloader
{
public string LocalDir { get; private set; }
public MiniGameManifest Manifest { get; private set; }
public string Error { get; private set; }
private readonly string _cdnBase; // 例如 http://.../minigame/
private readonly string _gameId;
private readonly int _version;
public MiniGameDownloader(string cdnBase, string gameId, int version)
{
_cdnBase = NormalizeCdnBase(cdnBase);
_gameId = gameId; _version = version;
LocalDir = $"{Application.persistentDataPath}/minigame/{gameId}/{version}/";
}
private string RemoteBase => $"{_cdnBase}{_gameId}/{_version}/{MiniGamePlatform.Name}/";
public IEnumerator Run(Action<bool> onDone)
{
Error = null;
Directory.CreateDirectory(LocalDir);
// 1) game.json(每次拉取,文件小)
string gameJson = null;
yield return GetText($"{RemoteBase}game.json", t => gameJson = t);
if (gameJson == null) { Fail("下载 game.json 失败"); onDone(false); yield break; }
try { Manifest = MiniGameManifest.ParseGameJson(gameJson); }
catch (Exception e) { Fail("game.json 解析失败: " + e.Message); onDone(false); yield break; }
if (Manifest.GameId != _gameId || Manifest.Version != _version)
{ Fail("game.json 与请求的 gameId/version 不符"); onDone(false); yield break; }
if (Manifest.MinFrameworkVersion > XWorld.Framework.FrameworkInfo.Version)
{ Fail($"小游戏要求最小框架版本 {Manifest.MinFrameworkVersion},当前框架 {XWorld.Framework.FrameworkInfo.Version}"); onDone(false); yield break; }
// 2) files.txtmd5 清单)
string filesTxt = null;
yield return GetText($"{RemoteBase}files.txt", t => filesTxt = t);
if (filesTxt == null) { Fail("下载 files.txt 失败"); onDone(false); yield break; }
Manifest.LoadFilesList(filesTxt);
// 验签(设计 §7):下载 files.txt.sig,校验清单签名后才信任 md5 列表
byte[] sig = null;
yield return GetBytes($"{RemoteBase}files.txt.sig", b => sig = b);
if (ClientManifestVerifier.IsConfigured)
{
if (sig == null) { Fail("缺少签名文件 files.txt.sig"); onDone(false); yield break; }
if (!ClientManifestVerifier.Verify(Manifest.Digest(), sig))
{ Fail("清单签名验证失败,拒绝加载"); onDone(false); yield break; }
}
// 3) 按清单逐个增量下载(md5 命名;本地已存在且 md5 匹配则跳过)
foreach (var kv in Manifest.Files)
{
string name = kv.Key; string md5 = kv.Value.Md5;
string localPath = LocalDir + name;
if (File.Exists(localPath) && Md5OfFile(localPath) == md5) continue;
string remote = $"{RemoteBase}{Md5Name(name, md5)}";
byte[] data = null;
yield return GetBytes(remote, b => data = b);
if (data == null) { Fail("下载失败: " + remote); onDone(false); yield break; }
if (Md5OfBytes(data) != md5) { Fail("md5 不符: " + name); onDone(false); yield break; }
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
File.WriteAllBytes(localPath, data);
}
onDone(true);
}
private void Fail(string msg) { Error = msg; Debug.LogError("[MiniGameDownloader] " + msg); }
private static string NormalizeCdnBase(string cdnBase)
{
string url = string.IsNullOrWhiteSpace(cdnBase) ? "http://127.0.0.1:15081/" : cdnBase.Trim();
if (url.StartsWith("http://file://", StringComparison.OrdinalIgnoreCase))
{
url = url.Substring("http://".Length);
}
else if (url.StartsWith("https://file://", StringComparison.OrdinalIgnoreCase))
{
url = url.Substring("https://".Length);
}
if (url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
url = NormalizeFileUrl(url);
}
else if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
url = "http://" + url;
}
return url.EndsWith("/") ? url : url + "/";
}
private static string NormalizeFileUrl(string url)
{
string rawPath = url.Substring("file://".Length).Replace('\\', '/');
while (rawPath.StartsWith("/") && rawPath.Length > 2 && rawPath[2] == ':')
{
rawPath = rawPath.Substring(1);
}
string localPath = Uri.UnescapeDataString(rawPath).Replace('/', Path.DirectorySeparatorChar);
try
{
return new Uri(localPath).AbsoluteUri;
}
catch
{
return "file:///" + rawPath.Replace("#", "%23");
}
}
// 与 LoadDll.GetFileMd5Name 同款:name.ext -> name_md5.ext
private static string Md5Name(string filename, string md5)
{
int pos = filename.LastIndexOf('.');
return pos >= 0 ? $"{filename.Substring(0, pos)}_{md5}{filename.Substring(pos)}"
: $"{filename}_{md5}";
}
private static string Md5OfBytes(byte[] data)
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var hash = md5.ComputeHash(data);
var sb = new System.Text.StringBuilder(hash.Length * 2);
foreach (var b in hash) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
private static string Md5OfFile(string path) => Md5OfBytes(File.ReadAllBytes(path));
private IEnumerator GetText(string url, Action<string> onText)
{
using (var uwr = UnityWebRequest.Get(url))
{
yield return uwr.SendWebRequest();
onText(uwr.result == UnityWebRequest.Result.Success ? uwr.downloadHandler.text : null);
if (uwr.result != UnityWebRequest.Result.Success)
Debug.LogError($"[MiniGameDownloader] GET 失败 {url}: {uwr.error}");
}
}
private IEnumerator GetBytes(string url, Action<byte[]> onBytes)
{
using (var uwr = UnityWebRequest.Get(url))
{
yield return uwr.SendWebRequest();
onBytes(uwr.result == UnityWebRequest.Result.Success ? uwr.downloadHandler.data : null);
if (uwr.result != UnityWebRequest.Result.Success)
Debug.LogError($"[MiniGameDownloader] GET 失败 {url}: {uwr.error}");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 640fac2ffb7e60b49876a8afdd842f8f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,378 @@
using System;
using System.Collections;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace XGame.MiniGame
{
// 一次小游戏会话的宿主。EnterGame 启动整条链路;每帧驱动;ExitGame 收尾。
public sealed class MiniGameHost : MonoBehaviour
{
// 标准 UGUI 结算弹窗预设(由 Doc/UIPrefabCreater/UI_MiniGameResult.json 经管线生成,进 game_art_ui_prefab AB
private const string ResultDialogPrefabPath = "Assets/Game/Art/UI/Prefab/UI_MiniGameResult.prefab";
private IGameClient _client;
private MiniGameNetChannel _net;
private GameClientCtx _ctx;
private MiniGameAssetLoader _assets;
private bool _running;
private bool _waitingForResultConfirm;
private string _gameId;
private int _version;
private string _roomId;
private int _selfPlayerId;
private GameObject _resultDialog;
public Action OnGameExited;
// 入口:cdnBase 小游戏 CDN 根;sock 已连接的 XWebSocket(来自大厅/或本宿主新建)
public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock)
=> EnterGame(cdnBase, gameId, version, sock, true);
public void EnterGame(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest)
{
_gameId = gameId; _version = version;
// FIX: XWCoroutine.StartCo is a static method (not Instance.StartCo)
XWCoroutine.StartCo(CoEnter(cdnBase, gameId, version, sock, sendMatchRequest));
}
// 大厅链路:MatchFound 由大厅通道消费(sendMatchRequest=false 时宿主收不到),
// 由外部把房间信息注入,否则结算弹框无法判断胜负(_selfPlayerId 恒 0)。
public void SetMatchInfo(string roomId, int selfPlayerId)
{
_roomId = roomId;
_selfPlayerId = selfPlayerId;
}
private IEnumerator CoEnter(string cdnBase, string gameId, int version, XWebSocket sock, bool sendMatchRequest)
{
// 1) 下载 + 校验
var dl = new MiniGameDownloader(cdnBase, gameId, version);
bool ok = false;
yield return dl.Run(r => ok = r);
if (!ok) { Debug.LogError("[MiniGameHost] 下载失败: " + dl.Error); yield break; }
// 2) 加载 Core/Client DLL,反射入口
var asmLoader = new MiniGameAssemblyLoader();
if (!asmLoader.Load(dl.LocalDir, dl.Manifest))
{ Debug.LogError("[MiniGameHost] 加载程序集失败: " + asmLoader.Error); yield break; }
// 3) 网络通道
_net = new MiniGameNetChannel(sock);
_net.OnGameMessage = msg => { if (_client != null) SafeCall(() => _client.OnNetMessage(msg), "OnNetMessage"); };
_net.OnFrameworkFrame = OnFrameworkFrame;
// 4) 反射实例化 IGameClient + 注入 ctx
_client = asmLoader.CreateClient();
_assets = new MiniGameAssetLoader(dl.LocalDir, dl.Manifest);
_ctx = new GameClientCtx(
_assets,
new UnityLogger($"[{gameId}]"),
msg => { if (_net != null) _net.SendGame(msg); },
() => ExitGame());
if (sendMatchRequest)
{
_net.SendFramework(FrameworkOpcode.MatchRequest,
new MatchRequestMsg { GameId = gameId, Version = version }.Encode());
}
// 6) OnEnter 接管
// 注意:OnEnter 在 MatchFound 到达之前调用,房间可能尚未就绪;小游戏应等待后续网络消息再依赖房间状态
SafeCall(() => _client.OnEnter(_ctx), "OnEnter");
_running = true;
Debug.Log("[MiniGameHost] 小游戏已进入: " + gameId);
}
private void OnFrameworkFrame(Frame f)
{
switch ((FrameworkOpcode)f.Opcode)
{
case FrameworkOpcode.MatchFound:
MatchFoundMsg found = MatchFoundMsg.Decode(f.Payload);
_roomId = found.RoomId;
_selfPlayerId = found.SelfPlayerId;
Debug.Log("[MiniGameHost] 房间就绪: " + _roomId);
break;
case FrameworkOpcode.RoomEnd:
Debug.Log("[MiniGameHost] 房间结束,弹结算框");
ShowResultDialog(RoomEndMsg.Decode(f.Payload));
break;
// Snapshot/Heartbeat 等:本步骤交给小游戏自行通过 Game 帧处理;如需框架级快照可在此扩展
}
}
private void Update()
{
if (!_running) return;
_net?.Pump();
if (_client != null) SafeCall(() => _client.OnUpdate(Time.deltaTime), "OnUpdate");
}
public void ExitGame()
{
if (!_running && _client == null) return;
DestroyResultDialog();
_waitingForResultConfirm = false;
_running = false;
if (_client != null) { SafeCall(() => _client.OnExit(), "OnExit"); _client = null; }
_net = null;
_ctx = null;
if (_assets != null) { _assets.UnloadAll(); _assets = null; } // 卸载本小游戏资源 AB
Debug.Log("[MiniGameHost] 已退出小游戏: " + _gameId);
OnGameExited?.Invoke();
// 返回大厅:交回 Lua game_module_runner 或大厅 UI(按现有大厅返回流程)
}
private void ShowResultDialog(RoomEndMsg roomEnd)
{
if (_waitingForResultConfirm)
{
return;
}
_waitingForResultConfirm = true;
_running = false;
DestroyResultDialog();
EnsureEventSystem();
string resultText = DecodeResultBlob(roomEnd.ResultBlob);
string title = ResultTitle(roomEnd, resultText);
string message = ResultMessage(resultText);
// 项目规则:资源一律异步加载(同步只读本地不下载);预设不可用时回退代码构建——结算窗口宁丑不缺
XWCoroutine.StartCo(CoShowResultDialog(title, message));
}
private IEnumerator CoShowResultDialog(string title, string message)
{
GameObject prefab = null;
yield return XResLoader.coLoadRes(ResultDialogPrefabPath, typeof(GameObject), obj => prefab = obj as GameObject);
if (!_waitingForResultConfirm)
{
yield break; // 加载期间用户已退出(ExitGame),不再弹结算
}
if (prefab == null || !TryShowResultDialogFromPrefab(prefab, title, message))
{
Debug.LogWarning("[MiniGameHost] 结算预设不可用,回退代码构建: " + ResultDialogPrefabPath);
ShowResultDialogFallback(title, message);
}
}
// 标准 UGUI 结算预设(Doc/UIPrefabCreater/UI_MiniGameResult.json 生成),按名绑定节点
private bool TryShowResultDialogFromPrefab(GameObject prefab, string title, string message)
{
GameObject instance = Instantiate(prefab);
instance.name = "MiniGameResultDialog";
TextMeshProUGUI titleText = FindChildComponent<TextMeshProUGUI>(instance.transform, "Txt_Title");
TextMeshProUGUI messageText = FindChildComponent<TextMeshProUGUI>(instance.transform, "Txt_Message");
Button okButton = FindChildComponent<Button>(instance.transform, "Btn_Ok");
if (titleText == null || messageText == null || okButton == null)
{
Destroy(instance); // 预设结构不符(旧资源包),回退代码构建
return false;
}
titleText.text = title;
messageText.text = message;
okButton.onClick.AddListener(ExitGame);
_resultDialog = instance;
return true;
}
private static T FindChildComponent<T>(Transform root, string name) where T : Component
{
Transform[] transforms = root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < transforms.Length; i++)
{
if (transforms[i].name == name)
{
return transforms[i].GetComponent<T>();
}
}
return null;
}
// 回退路径:预设缺失/结构不符时代码构建(旧底包兼容),样式从简
private void ShowResultDialogFallback(string title, string message)
{
_resultDialog = new GameObject("MiniGameResultDialog");
Canvas canvas = _resultDialog.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 30000;
_resultDialog.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
_resultDialog.AddComponent<GraphicRaycaster>();
GameObject mask = CreateUiObject("Mask", _resultDialog.transform);
Image maskImage = mask.AddComponent<Image>();
maskImage.color = new Color(0f, 0f, 0f, 0.58f);
Stretch(mask.GetComponent<RectTransform>());
GameObject panel = CreateUiObject("Panel", mask.transform);
Image panelImage = panel.AddComponent<Image>();
panelImage.color = new Color(0.08f, 0.09f, 0.11f, 0.96f);
RectTransform panelRect = panel.GetComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.pivot = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(520f, 300f);
panelRect.anchoredPosition = Vector2.zero;
Text titleText = CreateText("Title", panel.transform, title, 34, FontStyle.Bold, TextAnchor.MiddleCenter);
RectTransform titleRect = titleText.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0f, 1f);
titleRect.anchorMax = new Vector2(1f, 1f);
titleRect.pivot = new Vector2(0.5f, 1f);
titleRect.offsetMin = new Vector2(32f, -92f);
titleRect.offsetMax = new Vector2(-32f, -28f);
Text messageText = CreateText("Message", panel.transform, message, 22, FontStyle.Normal, TextAnchor.MiddleCenter);
RectTransform messageRect = messageText.GetComponent<RectTransform>();
messageRect.anchorMin = new Vector2(0f, 0f);
messageRect.anchorMax = new Vector2(1f, 1f);
messageRect.offsetMin = new Vector2(42f, 98f);
messageRect.offsetMax = new Vector2(-42f, -106f);
Button okButton = CreateButton(panel.transform, "确定");
RectTransform buttonRect = okButton.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0f);
buttonRect.anchorMax = new Vector2(0.5f, 0f);
buttonRect.pivot = new Vector2(0.5f, 0f);
buttonRect.sizeDelta = new Vector2(180f, 56f);
buttonRect.anchoredPosition = new Vector2(0f, 34f);
okButton.onClick.AddListener(ExitGame);
}
private string ResultTitle(RoomEndMsg roomEnd, string resultText)
{
if (roomEnd.WinnerPlayerId == 0)
{
// 有结算文本 = 游戏明确上报了"无胜者"→ 平局;无文本 = 旧模块/异常结束,保持中性标题(见 RoomEndResult 字段约定)
return string.IsNullOrEmpty(resultText) ? "游戏结束" : "平局";
}
if (_selfPlayerId == 0)
{
// 身份未知(未收到 MatchFound 也未注入):不猜胜负,给中性标题
return "游戏结束";
}
if (roomEnd.WinnerPlayerId == _selfPlayerId)
{
return "胜利";
}
return "失败";
}
private string ResultMessage(string resultText)
{
// 游戏上报的结算文本(如"最终比分 2 : 1")优先;裸 pid 对玩家无意义,不再显示
if (!string.IsNullOrEmpty(resultText))
{
return resultText;
}
return string.IsNullOrEmpty(_roomId) ? $"{_gameId}@{_version}" : $"房间 {_roomId}";
}
private static string DecodeResultBlob(byte[] resultBlob)
{
if (resultBlob == null || resultBlob.Length == 0)
{
return string.Empty;
}
if (resultBlob.Length > 4096)
{
return string.Empty; // 结算文本不应这么大,视为异常数据
}
try
{
string text = Encoding.UTF8.GetString(resultBlob);
for (int i = 0; i < text.Length; i++)
{
if (char.IsControl(text[i]) && text[i] != '\n' && text[i] != '\r' && text[i] != '\t')
{
return string.Empty;
}
}
return text;
}
catch
{
return string.Empty;
}
}
private void DestroyResultDialog()
{
if (_resultDialog != null)
{
Destroy(_resultDialog);
_resultDialog = null;
}
}
private static void EnsureEventSystem()
{
if (EventSystem.current != null)
{
return;
}
GameObject eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
private static GameObject CreateUiObject(string name, Transform parent)
{
GameObject obj = new GameObject(name);
obj.transform.SetParent(parent, false);
obj.AddComponent<RectTransform>();
return obj;
}
private static void Stretch(RectTransform rect)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
}
private static Text CreateText(string name, Transform parent, string content, int fontSize, FontStyle style, TextAnchor alignment)
{
GameObject obj = CreateUiObject(name, parent);
Text text = obj.AddComponent<Text>();
text.text = content;
// Unity 2022.2+ 移除了内置 Arial 字体,必须用 LegacyRuntime.ttf,否则抛 ArgumentException
text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
text.fontSize = fontSize;
text.fontStyle = style;
text.alignment = alignment;
text.color = Color.white;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Truncate;
return text;
}
private static Button CreateButton(Transform parent, string label)
{
GameObject obj = CreateUiObject("ButtonOk", parent);
Image image = obj.AddComponent<Image>();
image.color = new Color(0.2f, 0.55f, 1f, 1f);
Button button = obj.AddComponent<Button>();
Text text = CreateText("Text", obj.transform, label, 24, FontStyle.Bold, TextAnchor.MiddleCenter);
Stretch(text.GetComponent<RectTransform>());
return button;
}
private static void SafeCall(Action a, string where)
{
try { a(); }
catch (Exception e) { Debug.LogError($"[MiniGameHost] 小游戏 {where} 抛异常: {e}"); }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9b0a79a6e2896646839f46e8d3e65b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
namespace XGame.MiniGame
{
// game.json 客户端视图(字段名大小写不敏感解析)
public sealed class MiniGameManifest
{
public string GameId;
public int Version;
public string ClientEntryType;
public string CoreDll;
public string ClientDll;
public string CodeAb; // 代码 AB 文件名(统一 ABCore/Client DLL 的 .bytes 都在里面)
public int MinFrameworkVersion;
public List<string> Assets = new List<string>();
// files.txt 解析出的 name->(md5,size),用于增量下载校验
public Dictionary<string, FileEntry> Files = new Dictionary<string, FileEntry>();
public sealed class FileEntry { public string Md5; public int Size; }
// 极简手写 JSON 解析(避免引入 Unity JsonUtility 的字段限制;只认本 schema 的扁平字段 + assets 字符串数组)
// 注意:生产可换成项目已用的 JSON 库;此实现仅解析本 schema,键大小写不敏感。
public static MiniGameManifest ParseGameJson(string json)
{
if (string.IsNullOrEmpty(json)) throw new FormatException("game.json 为空");
var m = new MiniGameManifest
{
GameId = JsonMini.GetString(json, "gameId"),
Version = JsonMini.GetInt(json, "version"),
ClientEntryType = JsonMini.GetString(json, "clientEntryType"),
CoreDll = JsonMini.GetString(json, "coreDll"),
ClientDll = JsonMini.GetString(json, "clientDll"),
CodeAb = JsonMini.GetString(json, "codeAb"),
MinFrameworkVersion = JsonMini.GetInt(json, "minFrameworkVersion"),
};
m.Assets = JsonMini.GetStringArray(json, "assets");
if (string.IsNullOrEmpty(m.GameId)) throw new FormatException("game.json 缺少 gameId");
if (string.IsNullOrEmpty(m.ClientEntryType)) throw new FormatException("game.json 缺少 clientEntryType");
if (string.IsNullOrEmpty(m.CoreDll) || string.IsNullOrEmpty(m.ClientDll)) throw new FormatException("game.json 缺少 coreDll/clientDll");
if (string.IsNullOrEmpty(m.CodeAb))
throw new FormatException("game.json 缺少 codeAb(统一 AB 新格式;旧 .bytes 直下包不再支持,请用 XWorld/生成小游戏更新 重新发布)");
if (m.Version <= 0) throw new FormatException("game.json version 必须为正");
return m;
}
// 与发布工具 FilesManifest.Digest() 完全一致:按 name 的 Ordinal 排序,拼 name=md5;
public string Digest()
{
var keys = new System.Collections.Generic.List<string>(Files.Keys);
keys.Sort(System.StringComparer.Ordinal);
var sb = new System.Text.StringBuilder();
foreach (var k in keys) sb.Append(k).Append('=').Append(Files[k].Md5).Append(';');
return sb.ToString();
}
// files.txt 行格式(沿用 dllfiles):{'name','md5',size},
public void LoadFilesList(string content)
{
Files.Clear();
if (string.IsNullOrEmpty(content)) return;
foreach (var raw in content.Split('\n'))
{
string line = raw.Trim();
if (line.Length < 5) continue;
// 去掉首 { 与尾 }, ,再去引号
int lb = line.IndexOf('{'); int rb = line.LastIndexOf('}');
if (lb < 0 || rb <= lb) continue;
string inner = line.Substring(lb + 1, rb - lb - 1).Replace("'", "");
string[] p = inner.Split(',');
if (p.Length < 3) continue;
int size; int.TryParse(p[2].Trim(), out size);
Files[p[0].Trim()] = new FileEntry { Md5 = p[1].Trim(), Size = size };
}
}
}
// 极小 JSON 取值器(仅供本 schema;非通用)
internal static class JsonMini
{
public static string GetString(string json, string key)
{
int i = FindKey(json, key); if (i < 0) return null;
int c = json.IndexOf(':', i); if (c < 0) return null;
int q1 = json.IndexOf('"', c + 1); if (q1 < 0) return null;
int q2 = json.IndexOf('"', q1 + 1); if (q2 < 0) return null;
return json.Substring(q1 + 1, q2 - q1 - 1);
}
public static int GetInt(string json, string key)
{
int i = FindKey(json, key); if (i < 0) return 0;
int c = json.IndexOf(':', i); if (c < 0) return 0;
int j = c + 1; while (j < json.Length && (json[j] == ' ' || json[j] == '\t')) j++;
int s = j; while (j < json.Length && (char.IsDigit(json[j]) || json[j] == '-')) j++;
int v; int.TryParse(json.Substring(s, j - s), out v); return v;
}
public static System.Collections.Generic.List<string> GetStringArray(string json, string key)
{
var list = new System.Collections.Generic.List<string>();
int i = FindKey(json, key); if (i < 0) return list;
int lb = json.IndexOf('[', i);
if (lb < 0) return list; // 键存在但无数组
int rb = json.IndexOf(']', lb);
if (rb <= lb) return list;
string inner = json.Substring(lb + 1, rb - lb - 1);
foreach (var part in inner.Split(','))
{
int q1 = part.IndexOf('"'); int q2 = part.LastIndexOf('"');
if (q1 >= 0 && q2 > q1) list.Add(part.Substring(q1 + 1, q2 - q1 - 1));
}
return list;
}
private static int FindKey(string json, string key)
{
string needle = "\"" + key + "\"";
int from = 0;
while (true)
{
int idx = json.IndexOf(needle, from, StringComparison.OrdinalIgnoreCase);
if (idx < 0) return -1;
int after = idx + needle.Length;
int j = after;
while (j < json.Length && (json[j] == ' ' || json[j] == '\t' || json[j] == '\r' || json[j] == '\n')) j++;
if (j < json.Length && json[j] == ':') return idx; // 确认是键
from = idx + 1; // 否则是值内嵌文本,继续找
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36963803cf761dd41a7103ff14254909
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace XGame.MiniGame
{
// 客户端到服务端的帧通道。每帧轮询底层 socket,解码出的帧分发:
// Game 帧 -> OnGameMessage(NetMessage)(交给 IGameClient.OnNetMessage
// Framework 帧 -> OnFrameworkFrame(Frame)(宿主处理 MatchFound/RoomEnd 等)
public sealed class MiniGameNetChannel
{
private readonly XWebSocket _sock;
private readonly List<byte> _recvBuffer = new List<byte>(4096);
public Action<NetMessage> OnGameMessage;
public Action<Frame> OnFrameworkFrame;
public MiniGameNetChannel(XWebSocket sock) { _sock = sock; }
public bool IsConnected => _sock != null && _sock.IsConected();
// 业务输入:IGameClient 调 ctx.Send(NetMessage) -> 这里包成 Game 帧发出
public void SendGame(NetMessage msg)
{
byte[] frame = FrameCodec.Encode(new Frame(Channel.Game, msg.Opcode, msg.Payload));
SendRaw(frame);
}
public void SendFramework(FrameworkOpcode op, byte[] payload)
{
byte[] frame = FrameCodec.Encode(new Frame(Channel.Framework, (ushort)op, payload));
SendRaw(frame);
}
private void SendRaw(byte[] frame)
{
string err = null;
_sock.send(frame, null, ref err);
if (!string.IsNullOrEmpty(err)) Debug.LogError("[MiniGameNetChannel] send err: " + err);
}
// 每帧由宿主调用:把底层收到的字节解出并分发。
// XWebSocket 会把多条 WS 消息追加到同一个 bufferFrame 自带 payloadLen
// 所以这里按 FrameCodec 的线网格式从累计字节流中拆出 0..N 帧。
public void Pump()
{
if (_sock == null) return;
string err = null;
while (_sock.isDataRecv())
{
byte[] data = _sock.recv(ref err);
if (!string.IsNullOrEmpty(err)) { Debug.LogError("[MiniGameNetChannel] recv err: " + err); break; }
if (data == null || data.Length == 0) break;
_recvBuffer.AddRange(data);
}
while (TryPopFrame(out var frameBytes))
{
Frame f;
try { f = FrameCodec.Decode(frameBytes); }
catch (FormatException) { Debug.LogWarning("[MiniGameNetChannel] 畸形帧,丢弃"); continue; }
if (f.Channel == Channel.Game) OnGameMessage?.Invoke(new NetMessage(f.Opcode, f.Payload));
else OnFrameworkFrame?.Invoke(f);
}
}
private bool TryPopFrame(out byte[] frameBytes)
{
frameBytes = null;
if (_recvBuffer.Count < 4) return false; // channel + opcode + at least 1-byte payload len
int pos = 3;
uint payloadLen = 0;
int shift = 0;
while (true)
{
if (pos >= _recvBuffer.Count) return false;
if (shift > 28)
{
Debug.LogWarning("[MiniGameNetChannel] payloadLen varuint too long, clear buffer");
_recvBuffer.Clear();
return false;
}
byte b = _recvBuffer[pos++];
if (shift == 28 && (b & 0x70) != 0)
{
Debug.LogWarning("[MiniGameNetChannel] payloadLen overflow, clear buffer");
_recvBuffer.Clear();
return false;
}
payloadLen |= (uint)(b & 0x7F) << shift;
if ((b & 0x80) == 0) break;
shift += 7;
}
if (payloadLen > 1024 * 1024)
{
Debug.LogWarning("[MiniGameNetChannel] frame too large, clear buffer");
_recvBuffer.Clear();
return false;
}
int totalLen = pos + (int)payloadLen;
if (_recvBuffer.Count < totalLen) return false;
frameBytes = _recvBuffer.GetRange(0, totalLen).ToArray();
_recvBuffer.RemoveRange(0, totalLen);
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b4684221b289ce4aa15c841e44b355b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System.IO;
using UnityEngine;
namespace XGame.MiniGame
{
// 小游戏 CDN 的平台段(与 LoadDll.sPlatform / CDN 目录命名一致),及平台相关的本地 AB 打开方式
public static class MiniGamePlatform
{
#if UNITY_ANDROID
public const string Name = "android";
#elif UNITY_IOS
public const string Name = "ios";
#elif UNITY_WEBGL
public const string Name = "webgl";
#else
public const string Name = "pc";
#endif
// WebGL 无同步文件系统 mmapLoadFromFile 不可用 → 读字节走 LoadFromMemory;其余平台 LoadFromFile 更省内存
public static AssetBundle LoadAssetBundle(string localPath)
{
#if UNITY_WEBGL
return AssetBundle.LoadFromMemory(File.ReadAllBytes(localPath));
#else
return AssetBundle.LoadFromFile(localPath);
#endif
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62694f8496a2a0149a14149079e3ae93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
#if XW_DEVTEST
using UnityEngine;
namespace XGame.MiniGame
{
public static class MiniGameTestOverride
{
private const string KActive = "xw_devtest_active";
private const string KGateway = "xw_devtest_gateway";
private const string KCdn = "xw_devtest_cdn";
private const string KName = "xw_devtest_name";
public static bool IsActive => PlayerPrefs.GetInt(KActive, 0) == 1;
public static string GatewayUrl => PlayerPrefs.GetString(KGateway, "");
public static string CdnBase => PlayerPrefs.GetString(KCdn, "");
public static string ServerName => PlayerPrefs.GetString(KName, "");
public static void Apply(string gatewayUrl, string cdnBase, string serverName)
{
PlayerPrefs.SetInt(KActive, 1);
PlayerPrefs.SetString(KGateway, gatewayUrl ?? "");
PlayerPrefs.SetString(KCdn, cdnBase ?? "");
PlayerPrefs.SetString(KName, serverName ?? "");
PlayerPrefs.Save();
Debug.Log($"[DevTest] switched to LAN dev server {serverName}: {gatewayUrl}");
}
public static void Clear()
{
PlayerPrefs.DeleteKey(KActive);
PlayerPrefs.DeleteKey(KGateway);
PlayerPrefs.DeleteKey(KCdn);
PlayerPrefs.DeleteKey(KName);
PlayerPrefs.Save();
Debug.Log("[DevTest] reset to normal server config");
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51a4bc69d945cbe458536264e75c64a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XWorld.Framework;
using XWorld.Framework.Protocol;
namespace XGame.MiniGame
{
public sealed class PcLobbySmokeLauncher : MonoBehaviour
{
public string GatewayUrl = "ws://127.0.0.1:5005/ws?pid=1";
public string CdnBase = "";
public bool AutoStart;
private readonly List<GameInfoMsg> _games = new List<GameInfoMsg>();
private string _status = "idle";
private XWebSocket _socket;
private MiniGameNetChannel _lobbyNet;
private MiniGameHost _gameHost;
private bool _lobbyActive;
private bool _matching;
private float _matchElapsed;
private void Start()
{
#if XW_DEVTEST
if (GetComponent<DevServerSwitchUI>() == null) gameObject.AddComponent<DevServerSwitchUI>();
#endif
if (AutoStart) ConnectLobby();
}
private void OnDestroy()
{
_socket?.close();
_socket = null;
}
private void Update()
{
if (_lobbyActive) _lobbyNet?.Pump();
if (_matching) _matchElapsed += Time.deltaTime;
}
private void OnGUI()
{
if (!_lobbyActive && _matching) return;
GUILayout.BeginArea(new Rect(16, 16, 640, 260), GUI.skin.box);
GUILayout.Label("PC C# Lobby");
GUILayout.Label("Status: " + _status);
GUILayout.Label("Gateway URL");
GatewayUrl = GUILayout.TextField(GatewayUrl);
GUILayout.Label("CDN Base");
CdnBase = GUILayout.TextField(string.IsNullOrEmpty(CdnBase) ? DefaultCdnBase() : CdnBase);
GUILayout.BeginHorizontal();
if (GUILayout.Button(_socket == null ? "Enter Lobby" : "Refresh Games", GUILayout.Width(140)))
{
if (_socket == null) ConnectLobby();
else RequestGameList();
}
GUI.enabled = _socket != null && !_matching && _games.Count > 0;
if (GUILayout.Button("Random Match", GUILayout.Width(140))) RandomMatch();
GUI.enabled = true;
GUILayout.EndHorizontal();
if (_matching) GUILayout.Label("Matching: " + _matchElapsed.ToString("0.0") + "s / AI fill after server timeout");
GUILayout.Label("Discovered games:");
if (_games.Count == 0) GUILayout.Label("(none)");
for (int i = 0; i < _games.Count; i++)
{
var g = _games[i];
GUILayout.Label("- " + g.GameId + " v" + g.Version + " players " + g.MinPlayers + "-" + g.MaxPlayers);
}
GUILayout.EndArea();
}
public void ConnectLobby()
{
string url = ResolvedGatewayUrl();
_status = "connecting " + url;
_socket?.close();
_socket = new XWebSocket();
_socket.Connect(url, (sock, err) =>
{
if (!string.IsNullOrEmpty(err) || sock == null)
{
_status = "connect failed: " + err;
Debug.LogError("[PcLobby] " + _status);
return;
}
_status = "lobby connected";
_lobbyNet = new MiniGameNetChannel(sock);
_lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame;
_lobbyActive = true;
_matching = false;
RequestGameList();
});
}
private void RequestGameList()
{
_status = "requesting game list";
_lobbyNet?.SendFramework(FrameworkOpcode.GameListRequest, Array.Empty<byte>());
}
private void RandomMatch()
{
_status = "matching random game";
_matching = true;
_matchElapsed = 0f;
_lobbyNet?.SendFramework(FrameworkOpcode.RandomMatchRequest, Array.Empty<byte>());
}
private void OnLobbyFrameworkFrame(Frame frame)
{
switch ((FrameworkOpcode)frame.Opcode)
{
case FrameworkOpcode.GameListResponse:
var list = GameListResponseMsg.Decode(frame.Payload);
_games.Clear();
_games.AddRange(list.Games);
_status = "games discovered: " + _games.Count;
break;
case FrameworkOpcode.MatchAssigned:
var assigned = MatchAssignedMsg.Decode(frame.Payload);
_status = "assigned " + assigned.GameId + " v" + assigned.Version + ", loading hot DLL";
EnterAssignedGame(assigned.GameId, assigned.Version);
break;
case FrameworkOpcode.Error:
var err = ErrorMsg.Decode(frame.Payload);
_status = "server error " + err.Code + ": " + err.Message;
_matching = false;
break;
}
}
private void EnterAssignedGame(string gameId, int version)
{
_lobbyActive = false;
_lobbyNet = null;
_gameHost = gameObject.GetComponent<MiniGameHost>();
if (_gameHost == null) _gameHost = gameObject.AddComponent<MiniGameHost>();
_gameHost.OnGameExited = () =>
{
_status = "returned to lobby";
_matching = false;
_lobbyNet = new MiniGameNetChannel(_socket);
_lobbyNet.OnFrameworkFrame = OnLobbyFrameworkFrame;
_lobbyActive = true;
RequestGameList();
};
_gameHost.EnterGame(ResolvedCdnBase(), gameId, version, _socket, false);
}
private string ResolvedCdnBase()
{
#if XW_DEVTEST
if (MiniGameTestOverride.IsActive && !string.IsNullOrEmpty(MiniGameTestOverride.CdnBase))
return MiniGameTestOverride.CdnBase;
#endif
return string.IsNullOrEmpty(CdnBase) ? DefaultCdnBase() : CdnBase;
}
private string ResolvedGatewayUrl()
{
#if XW_DEVTEST
if (MiniGameTestOverride.IsActive && !string.IsNullOrEmpty(MiniGameTestOverride.GatewayUrl))
{
string g = MiniGameTestOverride.GatewayUrl;
return WithLocalPid(g);
}
#endif
return WithLocalPid(GatewayUrl);
}
private static string WithLocalPid(string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl))
{
return gatewayUrl;
}
int pid = GetLocalPlayerId();
int queryStart = gatewayUrl.IndexOf('?');
if (queryStart < 0)
{
return gatewayUrl + "?pid=" + pid;
}
string head = gatewayUrl.Substring(0, queryStart);
string query = gatewayUrl.Substring(queryStart + 1);
string[] parts = query.Split('&');
bool replaced = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].StartsWith("pid=", StringComparison.OrdinalIgnoreCase))
{
parts[i] = "pid=" + pid;
replaced = true;
break;
}
}
return head + "?" + (replaced ? string.Join("&", parts) : query + "&pid=" + pid);
}
private static int GetLocalPlayerId()
{
const string key = "LocalClientId";
string id = XData.GetString(key, string.Empty);
if (string.IsNullOrEmpty(id))
{
id = SystemInfo.deviceUniqueIdentifier;
if (string.IsNullOrEmpty(id) || id == "unsupported")
{
id = Guid.NewGuid().ToString("N");
}
XData.SetString(key, id);
XData.Save();
}
unchecked
{
uint hash = 2166136261u;
for (int i = 0; i < id.Length; i++)
{
hash ^= id[i];
hash *= 16777619u;
}
return (int)(hash % 2000000000u) + 1;
}
}
private static string DefaultCdnBase()
{
// Editor: <repo>/Client/Assets -> <repo>/CDN/minigame/(由 XWorld/生成小游戏更新 菜单产出)
var assets = new DirectoryInfo(Application.dataPath);
var client = assets.Parent;
var repo = client?.Parent;
string root = repo != null ? repo.FullName : Directory.GetCurrentDirectory();
return "file:///" + root.Replace('\\', '/') + "/CDN/minigame/";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 234ccb579ede8ab48ad4f7dc368c851f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,146 @@
using System;
using System.IO;
using UnityEngine;
namespace XGame.MiniGame
{
// PC 本地端到端烟测入口:连接本地 Gateway,再用 MiniGameHost 下载/加载 rps 小游戏。
// 挂到任意场景对象后 Play;也可以运行时由 AddComponent 创建。
public sealed class PcMiniGameSmokeLauncher : MonoBehaviour
{
public string GatewayUrl = "ws://127.0.0.1:5005/ws?pid=1";
public string CdnBase = "";
public string GameId = "rps";
public int Version = 1;
public bool AutoStart;
private string _status = "idle";
private XWebSocket _socket;
private MiniGameHost _host;
private void Start()
{
if (AutoStart) StartSmoke();
}
private void OnDestroy()
{
_socket?.close();
_socket = null;
}
private void OnGUI()
{
GUILayout.BeginArea(new Rect(16, 16, 620, 230), GUI.skin.box);
GUILayout.Label("PC MiniGame Smoke");
GUILayout.Label("Status: " + _status);
GUILayout.Label("Gateway URL");
GatewayUrl = GUILayout.TextField(GatewayUrl);
GUILayout.Label("CDN Base");
CdnBase = GUILayout.TextField(string.IsNullOrEmpty(CdnBase) ? DefaultCdnBase() : CdnBase);
GUILayout.BeginHorizontal();
GUILayout.Label("Game", GUILayout.Width(48));
GameId = GUILayout.TextField(GameId, GUILayout.Width(120));
GUILayout.Label("Version", GUILayout.Width(56));
int.TryParse(GUILayout.TextField(Version.ToString(), GUILayout.Width(60)), out Version);
GUILayout.EndHorizontal();
if (GUILayout.Button("Start Smoke")) StartSmoke();
GUILayout.Label("Expected: download rps DLLs -> connect -> MatchFound(+AI) -> snapshots -> RoomEnd.");
GUILayout.EndArea();
}
public void StartSmoke()
{
string gatewayUrl = WithLocalPid(GatewayUrl);
_status = "connecting " + gatewayUrl;
_socket?.close();
_socket = new XWebSocket();
_socket.Connect(gatewayUrl, (sock, err) =>
{
if (!string.IsNullOrEmpty(err) || sock == null)
{
_status = "connect failed: " + err;
Debug.LogError("[PcMiniGameSmoke] " + _status);
return;
}
_status = "connected, entering " + GameId + " v" + Version;
Debug.Log("[PcMiniGameSmoke] " + _status);
_host = gameObject.GetComponent<MiniGameHost>();
if (_host == null) _host = gameObject.AddComponent<MiniGameHost>();
_host.EnterGame(ResolvedCdnBase(), GameId, Version, sock);
});
}
private static string WithLocalPid(string gatewayUrl)
{
if (string.IsNullOrWhiteSpace(gatewayUrl))
{
return gatewayUrl;
}
int pid = GetLocalPlayerId();
int queryStart = gatewayUrl.IndexOf('?');
if (queryStart < 0)
{
return gatewayUrl + "?pid=" + pid;
}
string head = gatewayUrl.Substring(0, queryStart);
string query = gatewayUrl.Substring(queryStart + 1);
string[] parts = query.Split('&');
bool replaced = false;
for (int i = 0; i < parts.Length; i++)
{
if (parts[i].StartsWith("pid=", StringComparison.OrdinalIgnoreCase))
{
parts[i] = "pid=" + pid;
replaced = true;
break;
}
}
return head + "?" + (replaced ? string.Join("&", parts) : query + "&pid=" + pid);
}
private static int GetLocalPlayerId()
{
const string key = "LocalClientId";
string id = XData.GetString(key, string.Empty);
if (string.IsNullOrEmpty(id))
{
id = SystemInfo.deviceUniqueIdentifier;
if (string.IsNullOrEmpty(id) || id == "unsupported")
{
id = Guid.NewGuid().ToString("N");
}
XData.SetString(key, id);
XData.Save();
}
unchecked
{
uint hash = 2166136261u;
for (int i = 0; i < id.Length; i++)
{
hash ^= id[i];
hash *= 16777619u;
}
return (int)(hash % 2000000000u) + 1;
}
}
private string ResolvedCdnBase()
{
return string.IsNullOrEmpty(CdnBase) ? DefaultCdnBase() : CdnBase;
}
private static string DefaultCdnBase()
{
// Editor: <repo>/Client/Assets -> <repo>/CDN/minigame/(由 XWorld/生成小游戏更新 菜单产出)
var assets = new DirectoryInfo(Application.dataPath);
var client = assets.Parent;
var repo = client?.Parent;
string root = repo != null ? repo.FullName : Directory.GetCurrentDirectory();
return "file:///" + root.Replace('\\', '/') + "/CDN/minigame/";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5465b3788fbc36e4997ac55a2f2ac387
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using System;
using UnityEngine;
using XWorld.Framework;
namespace XGame.MiniGame
{
// IAssetLoader over XResLoader(异步按需从 CDN 下载/加载小游戏 AB 资源)
public sealed class UnityAssetLoader : IAssetLoader
{
// path: 形如 "Assets/Game/Art/UI/Prefab/UI_RPS.prefab"XResLoader 内部剥 "Assets/" 前缀并按目录解析 AB。
// 用回调版 LoadRes(内部自驱动协程),可在非协程上下文 fire-and-forget;失败回调 null。
public void Load(string path, Action<object> onLoaded)
{
XResLoader.LoadRes(path, typeof(UnityEngine.Object),
objs => onLoaded?.Invoke(objs != null && objs.Length > 0 ? objs[0] : null));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c672c124aa90a347899d7a5132851ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1658d5a44b110784eafd3333f601e6f4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,635 @@
using System;
using System.Collections;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Windows;
namespace XGame
{
class TerrainBlock
{
public Color32 m_BaseMapIndex;
public Color32[] m_MapIndex;
public Color[] m_MapMix;
public float[] m_Height;
}
class TerrainObject
{
//m_BlockGridSize = 32
//lv0 512*512 = 32*32单块) * 16 * 16 (总共256块)
//lv1 256*256 = 32*32单块) * 8 * 8 64块)
//lv2 128*128 = 32*32单块) * 4 * 4 16块)
//lv3 64*64 = 32*32单块) * 2 * 2 4块)
class MeshObj
{
public GameObject m_Object;
public Mesh m_Mesh;
public MeshObj[] m_MeshSubObj;//四叉分割 32*32 64*64 128*128 256*256 512*512 1024*1024
}
string[] m_strBaseMapList;
// 预先定义 16 种颜色
static Color[] m_Colors = new Color[]
{
new Color(255, 0, 0), // 红色
new Color(255, 128, 0), // 橙色
new Color(255, 255, 0), // 黄色
new Color(128, 255, 0), // 淡绿色
new Color(0, 255, 0), // 绿色
new Color(0, 255, 128), // 青色
new Color(0, 255, 255), // 蓝绿色
new Color(0, 128, 255), // 淡蓝色
new Color(0, 0, 255), // 蓝色
new Color(128, 0, 255), // 紫色
new Color(255, 0, 255), // 粉红色
new Color(255, 0, 128), // 桃红色
new Color(128, 128, 128), // 灰色
new Color(192, 192, 192), // 淡灰色
new Color(255, 255, 255), // 白色
new Color(0, 0, 0), // 黑色
};
GameObject m_GameObject;
Transform m_Transform;
MeshObj m_Top;
MeshObj[] m_BlockMeshObjList;
TerrainBlock[] m_TerrainBlock;
Texture2D[] m_BaseMapTexture;
Texture2D[] m_NormalMapTexture;
Texture2D m_ControlTexture;
Material m_mtlTerrain;
string m_Name;//xxxzzz.grd
static Color32 m_Index;// = new int[4];
static Color m_Degreed;// = new float[4];
static Color[] m_nearestColors = new Color[4];
public static float m_HeightScale = 20.0f;
public static float m_UVScale = 4.0f;//1-32-贴图大小
public static int m_BlockGridSize = 32;//32*32
void SetCorlorBase(Color[] colors)
{
m_Colors = colors;
}
//通过heightMap生成
public IEnumerator CreateGroundObjectFromPic(int xPos, int zPos, string heightMap, string mixMap, string[] baseMap, string[] normalMap, Transform trParent )
{
Debug.Log("CreateGroundObjectFromPic");
if (baseMap.Length != normalMap.Length)
{
Debug.Log("baseMap Count Must = normalMap Count");
yield break;
}
if (baseMap.Length < 4)
{
Debug.Log("baseMap Must >= 4");
yield break;
}
DateTime oriTime = DateTime.Now;
yield return XResLoader.coLoadAllRes(new string[] { "res/scene/ground/mat/Terrain.mat" }, typeof(Material), objs =>
{
Debug.Log("coLoadAllRes Terrain.mat finish");
if (objs != null && objs[0] != null)
{
m_mtlTerrain = objs[0] as Material;
}
});
if(m_mtlTerrain == null)
{
Debug.Log("Terrian Mtl Load Failed : res/scene/ground/mat/Terrain.mat");
yield break;
}
m_BaseMapTexture = new Texture2D[baseMap.Length];
m_NormalMapTexture = new Texture2D[normalMap.Length];
Texture2D texture = null;
Texture2D mixTexture = null;
//byte[] imageData;
//texture = new Texture2D(2, 2);
//imageData = File.ReadAllBytes(mixMap);
//yield return XResLoader.LoadRes(mixMap, typeof(Texture2D), objs =>
yield return XResLoader.coLoadAllRes(new string[] { mixMap }, typeof(Texture2D), objs =>
{
if (objs != null)
{
mixTexture = objs[0] as Texture2D;
if (mixTexture.width != mixTexture.height)
{
Debug.Log("Height Must == Width");
}
if (!mixTexture.isReadable)
{
Debug.Log("mixTexture.isReadable false");
#if UNITY_EDITOR
string texturePath = UnityEditor.AssetDatabase.GetAssetPath(mixTexture);
UnityEditor.TextureImporter textureImporter = (UnityEditor.TextureImporter)UnityEditor.AssetImporter.GetAtPath(texturePath);
textureImporter.isReadable = true;
UnityEditor.AssetDatabase.ImportAsset(texturePath);
#endif
}
}
});
if(mixTexture == null)
yield break;
//texture.LoadImage(imageData);
Texture2D texHeight = null;// = new Texture2D(2, 2);
//imageData = File.ReadAllBytes(heightMap);
//texHeight.LoadImage(imageData);
//yield return XResLoader.LoadRes(heightMap, typeof(Texture2D), objs =>
yield return XResLoader.coLoadAllRes(new string[] { heightMap }, typeof(Texture2D), objs =>
{
if (objs != null)
{
texHeight = objs[0] as Texture2D;
if (texHeight.width != mixTexture.width || texHeight.height != mixTexture.height)
{//比较两个图的大小
Debug.Log("HeightMap Size != MixMap Size");
}
if (!texHeight.isReadable)
{
Debug.Log("texHeight.isReadable false");
#if UNITY_EDITOR
string texturePath = UnityEditor.AssetDatabase.GetAssetPath(texHeight);
UnityEditor.TextureImporter textureImporter = (UnityEditor.TextureImporter)UnityEditor.AssetImporter.GetAtPath(texturePath);
textureImporter.isReadable = true;
UnityEditor.AssetDatabase.ImportAsset(texturePath);
#endif
}
}
});
if (texHeight == null)
yield break;
m_GameObject = new GameObject($"Terrain{xPos}{zPos}");
m_Transform = m_GameObject.transform;
m_Transform.SetParent(trParent);
m_Transform.position = new Vector3(xPos, 0, zPos);
m_strBaseMapList = baseMap;
//baseMap列表生成m_Colors列表
m_Colors = new Color[baseMap.Length];
Color totalColor = Color.black;
int pixelCount = 0;
Debug.Log("baseMap Load");
//XResLoader.LoadRes(baseMap, typeof(Texture2D), objs=>
yield return XResLoader.coLoadAllRes(baseMap, typeof(Texture2D), objs =>
{
Debug.Log($"baseMap load {objs.Length} ");
for (int i = 0; i < objs.Length; ++i)
{
if (objs[i] != null)
m_BaseMapTexture[i] = objs[i] as Texture2D;
else
{
Debug.Log($"m_BaseMapTexture {normalMap[i]} Failed");
}
}
});
Debug.Log("normalMap Load");
yield return XResLoader.coLoadAllRes(normalMap, typeof(Texture2D), objs =>
{
Debug.Log($"normalMap load {objs.Length} ");
for (int i = 0; i < objs.Length; ++i)
{
if (objs[i] != null)
m_NormalMapTexture[i] = objs[i] as Texture2D;
else
{
Debug.Log($"normalMap {normalMap[i]} Failed");
}
}
});
Debug.Log($"m_BaseMapTexture Color {m_BaseMapTexture.Length}");
for (int i = 0; i < m_BaseMapTexture.Length; ++i)
{
texture = m_BaseMapTexture[i];
if (texture == null)
{
Debug.Log($"texture {i} == null");
continue;
}
if (!texture.isReadable)
{
Debug.Log($"texture.isReadable {texture.name} Failed");
#if UNITY_EDITOR
string texturePath = UnityEditor.AssetDatabase.GetAssetPath(texture);
UnityEditor.TextureImporter textureImporter = (UnityEditor.TextureImporter)UnityEditor.AssetImporter.GetAtPath(texturePath);
textureImporter.isReadable = true;
UnityEditor.AssetDatabase.ImportAsset(texturePath);
#endif
}
for (int j = 0; j < texture.width; j = j + 4)
{
for (int k = 0; k < texture.height; k = k + 4)
{
Color pixelColor = texture.GetPixel(j, k);
totalColor += pixelColor;
pixelCount++;
}
}
// 计算平均颜色
m_Colors[i] = totalColor / pixelCount;
}
Debug.Log($"Color List OK {(DateTime.Now - oriTime).TotalMilliseconds}");
//mixMap rgba决定此点是哪四个basemap且分别是多少混合比例
//mix[513*513][4](index, alpha), 32 * 32 缩减到4种地形(g1 g2 g3 g4,
//0-32 33-64 (33列需要特殊处理)
//相邻区块如果没有相同的就把最小那个改为主块的g1
//读取32*32格子关联边的数据提取两个最大的(g1 g2),然后强设置进去只有两种alpha,
//中心点设置为唯一颜色(最大g1
//边界混合问题
int BlockWidth = mixTexture.width / m_BlockGridSize;
int BlockHeight = mixTexture.height / m_BlockGridSize;
int[] BaseMapCount = new int[baseMap.Length];
m_TerrainBlock = new TerrainBlock[BlockWidth* BlockHeight];
for (int j = 0; j < BlockHeight; j++)
{
for (int i = 0; i < BlockWidth; ++i)
{
Color32[] BaseMapIndex = new Color32[m_BlockGridSize * m_BlockGridSize];
Color[] BaseMapMix = new Color[m_BlockGridSize * m_BlockGridSize];
TerrainBlock tBlock = new TerrainBlock();
tBlock.m_MapIndex = BaseMapIndex;
tBlock.m_MapMix = BaseMapMix;
Array.Clear(BaseMapCount, 0, BaseMapCount.Length);
int l, k;
for (k = 0; k < m_BlockGridSize; k++)
{
for (l = 0; l < m_BlockGridSize; l++)
{
Color pixelColor = mixTexture.GetPixel(i* m_BlockGridSize+k, j* m_BlockGridSize+l);
GetNearColorIndex(pixelColor, ref BaseMapIndex[l * m_BlockGridSize + k], ref BaseMapMix[l * m_BlockGridSize + k]);
BaseMapCount[BaseMapIndex[l * m_BlockGridSize + k][0]]++;
BaseMapCount[BaseMapIndex[l * m_BlockGridSize + k][1]]++;
BaseMapCount[BaseMapIndex[l * m_BlockGridSize + k][2]]++;
BaseMapCount[BaseMapIndex[l * m_BlockGridSize + k][3]]++;
}
}
//留下最高数量的四个BaseMapCount[0-3]
Color32 BaseMapIndexMax = new Color32(0,0,0,0);
int[] BaseMapCountMax = new int[4];
for ( k = 0; k < 4; k++)
{
for ( l = 0; l < BaseMapCount.Length; l++)
{
if (BaseMapCountMax[k] < BaseMapCount[l])
{
BaseMapCountMax[k] = BaseMapCount[l];
BaseMapIndexMax[k] = (byte)l; //(byte)l;
}
}
BaseMapCount[BaseMapIndexMax[k]] = 0;//最终选定的最大数位置
}
//全部混合顺序要变为四个统一
byte temp = 0;
float fMix;
for ( k = 0; k < m_BlockGridSize * m_BlockGridSize; k++)
{
for( l = 0; l < 4; ++l)
{
bool bfound = false;
if (BaseMapIndexMax[l] != BaseMapIndex[k][l])
{
if (l == 3)
{
bfound = false;
}
else
for (int m = l+1; m < 4; m++)
{
if (BaseMapIndex[k][m] == BaseMapIndexMax[l])
{//替换
if (l != m)
{
fMix = BaseMapMix[k][m];
BaseMapMix[k][m] = BaseMapMix[k][l];
BaseMapMix[k][l] = fMix;
temp = BaseMapIndex[k][m];
BaseMapIndex[k][m] = BaseMapIndex[k][l];
BaseMapIndex[k][l] = temp;
}
bfound = true;
break;
}
}
if (!bfound)
{
for (int m = l+1; m < 4; m++)
{
if (BaseMapIndexMax[m] == BaseMapIndex[k][l])
{//顺序错了而已
bfound = true;
fMix = BaseMapMix[k][m];
BaseMapMix[k][m] = BaseMapMix[k][l];
BaseMapMix[k][l] = fMix;
temp = BaseMapIndex[k][m];
BaseMapIndex[k][m] = BaseMapIndex[k][l];
BaseMapIndex[k][l] = temp;
if (BaseMapIndexMax[l] != BaseMapIndex[k][l])
{//如果换了后仍然不同,则需要重新判断一遍
m = l;//再判断一遍
bfound = false;
}
}
}
if (!bfound)
{//完全找不到直接替换(不是最大的四个),混合参数BaseMapMix不用改
BaseMapIndex[k][l] = BaseMapIndexMax[l];
}
}
}
}
}
tBlock.m_BaseMapIndex = BaseMapIndexMax;
m_TerrainBlock[j * BlockWidth + i] = tBlock;
}
}
Debug.Log($"Block Create OK{(DateTime.Now - oriTime).TotalMilliseconds}");
//所有区块都初始化好了,但是边缘是可能对不上的,需要修正
//组合出混合控制贴图
m_ControlTexture = new Texture2D(mixTexture.width, mixTexture.height);
for (int j = 0; j < BlockHeight; j++)
{
for (int i = 0; i < BlockWidth; ++i)
{
TerrainBlock tBlock = m_TerrainBlock[j * BlockWidth + i];
int beginX = i * m_BlockGridSize;
int beginY = j * m_BlockGridSize;
for (int y = 0; y < m_BlockGridSize; y++)
{
for (int x = 0; x < m_BlockGridSize; x++)
{
m_ControlTexture.SetPixel(beginX + x, beginY + y, ChangeColor(tBlock.m_MapMix[x + y * m_BlockGridSize]));
}
}
}
}
m_ControlTexture.Apply();
Debug.Log($"ControlTexture Create OK{(DateTime.Now - oriTime).TotalMilliseconds}");
//height map 决定地表多边形的高度值
float[] mapHeight = new float[texHeight.width * texHeight.height];
for (int j = 0; j < texHeight.height; ++j)
{
for (int i = 0; i < texHeight.width; ++i)
{
Color pixelColor = texHeight.GetPixel(i, j);
mapHeight[i + j * texHeight.width] = GetHeight(pixelColor, m_HeightScale);
}
}
Debug.Log($"height map Create OK{(DateTime.Now - oriTime).TotalMilliseconds}");
//构造真正的Mesh
m_BlockMeshObjList = new MeshObj[BlockWidth * BlockHeight];
//修改uv缩放为贴图大小,适应control图,其他图按照缩放比例
m_UVScale = mixTexture.width;
#if !UNITY_WEBGL
Task<int>[] task = new Task<int>[BlockWidth];
for (int i = 0; i < BlockWidth; ++i)
{
task[i] = Task.Run(() => CalMeshTask(m_TerrainBlock, mapHeight, m_BlockMeshObjList, i, BlockWidth));
}
for (int i = 0; i < BlockWidth; ++i)
{
task[i].Wait();
}
#else
for (int i = 0; i < BlockWidth; ++i)// //test
{
CalMeshTask(m_TerrainBlock, mapHeight, m_BlockMeshObjList, i, BlockWidth);
}
#endif
Debug.Log($"Mesh Create OK{(DateTime.Now - oriTime).TotalMilliseconds}");
//区块生成后把所有节点放到总对象下
for (int i = 0; i < m_BlockMeshObjList.Length; ++i)
{
if(m_BlockMeshObjList[i] != null)
m_BlockMeshObjList[i].m_Object.transform.SetParent(m_Transform);
}
}
static float ColorScale = 3f;
Color ChangeColor(Color color)
{ //超过0.66的设置为全色, 0.5以上为增比例,0.5一下为减比例
Color rc = color;
int i = 0, index = -1;
float cmax = 0;
for ( i = 0; i < 4; ++i)
{
if (color[i] > cmax)
{
cmax = color[i];
index = i;
}
//if (color[i] > 0.5f)
//{
// rc[i] = (color[i] - 0.5f) * ColorScale + 0.5f;
//}
//else
//{
// rc[i] = color[i] / ColorScale;
//}
}
rc[index] = rc[index] * ColorScale;
Vector4 v4 = rc;
v4.Normalize();
return v4;
}
//方便大部分平台分线程计算
int CalMeshTask(TerrainBlock[] BlockList, float[] mapHeight, MeshObj[] BlockMeshObjList, int WidthIndex, int BlockWidth)
{
int BlockHeight = BlockWidth;
int mapWidth = BlockWidth * m_BlockGridSize;
for (int j = 0; j < BlockHeight; j++)
{
TerrainBlock tBlock = BlockList[j * BlockWidth + WidthIndex];
MeshObj mObj = new MeshObj();//需要填充的模型对象
BlockMeshObjList[j * BlockWidth + WidthIndex] = mObj;
// 创建一个新的游戏对象
mObj.m_Object = new GameObject($"T{WidthIndex}_{j}");
// 添加MeshFilter组件
MeshFilter meshFilter = mObj.m_Object.AddComponent<MeshFilter>();
// 创建Mesh
Mesh mesh = new Mesh();
mObj.m_Mesh = mesh;
// 设置顶点和三角形数据
Vector3[] vertices = new Vector3[(m_BlockGridSize+1)*(m_BlockGridSize+1)];
Vector3[] normals;// = new Vector3[(m_BlockGridSize + 1) * (m_BlockGridSize + 1)];
Vector2[] uvs = new Vector2[(m_BlockGridSize + 1) * (m_BlockGridSize + 1)];
//Vector2[] uvs2 = new Vector2[(m_BlockGridSize + 1) * (m_BlockGridSize + 1)];
int[] triangles = new int[6* m_BlockGridSize* m_BlockGridSize];
float height = 0;
int x, y;
int beginPos = j * m_BlockGridSize * mapWidth + WidthIndex * m_BlockGridSize;
//顶点 uv
for (int k = 0; k < m_BlockGridSize+1; ++k)
{
for (int l = 0; l < m_BlockGridSize+1; ++l)
{//每个格子是两个三角形
if (WidthIndex == BlockWidth - 1 && l == m_BlockGridSize)
x = l - 1;
else
x = l;
if (j == BlockHeight - 1 && k == m_BlockGridSize)
y = k - 1;
else
y = k;
height = mapHeight[beginPos + mapWidth * y + x];
vertices[k * (m_BlockGridSize+1) + l] = new Vector3(l, height, k);
//normals[k * (m_BlockGridSize + 1) + l] = Vector3.up;//简化
//Control uv
uvs[k * (m_BlockGridSize + 1) + l].x = ((float)l + WidthIndex*m_BlockGridSize) / m_UVScale;
uvs[k * (m_BlockGridSize + 1) + l].y = ((float)k + j*m_BlockGridSize) / m_UVScale;
//Control uv 不需要, 都用比例
//uvs2[k * (m_BlockGridSize + 1) + l] = new Vector2((float)(WidthIndex* BlockWidth+l) / (float)mapWidth, (float)(j * BlockWidth + k)/ (float)mapWidth);
}
}
//法线
//三角形
for (int k = 0; k < m_BlockGridSize; ++k)
{
for (int m = 0; m < m_BlockGridSize; ++m)
{
triangles[6 * (k * m_BlockGridSize + m)] = k * (m_BlockGridSize+1) + m;
triangles[6 * (k * m_BlockGridSize + m)+2] = k * (m_BlockGridSize + 1) + m+1;
triangles[6 * (k * m_BlockGridSize + m)+1] = (k+1) * (m_BlockGridSize + 1) + m;
triangles[6 * (k * m_BlockGridSize + m)+3] = k * (m_BlockGridSize + 1) + m +1;
triangles[6 * (k * m_BlockGridSize + m)+4] = (k+1) * (m_BlockGridSize + 1) + m;
triangles[6 * (k * m_BlockGridSize + m)+5] = (k+1) * (m_BlockGridSize + 1) + m+1;
}
}
// 这里假设已经按照上面的方法设置了顶点和三角形数据
mesh.vertices = vertices;
//mesh.normals = normals;
mesh.triangles = triangles;
mesh.uv = uvs;
//mesh.uv2 = uvs2;//control uv
mesh.RecalculateNormals();
normals = mesh.normals;
for (int k = 0; k < m_BlockGridSize + 1; ++k)
{
normals[k] = Vector3.up;//第一行
normals[m_BlockGridSize * (m_BlockGridSize + 1) + k] = Vector3.up;//最后一行
}
for (int k = 1; k < m_BlockGridSize; ++k)
{
normals[k * (m_BlockGridSize+1)] = Vector3.up;//第一列
normals[(k+1) * (m_BlockGridSize + 1) - 1] = Vector3.up;//最后一列
}
mesh.normals = normals;
// 将Mesh赋值给MeshFilter
meshFilter.mesh = mesh;
// 添加MeshRenderer组件
MeshRenderer meshRenderer = mObj.m_Object.AddComponent<MeshRenderer>();
meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
// 设置材质和渲染模式
// 这里假设已经有一个名为"Material"的材质
//meshRenderer.material = Resources.Load<Material>("Material");
meshRenderer.material = new Material(m_mtlTerrain);
meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
meshRenderer.receiveShadows = true;
meshRenderer.material.SetTexture("_Splat0", m_BaseMapTexture[tBlock.m_MapIndex[0][0]]);
meshRenderer.material.SetTexture("_Normal0", m_NormalMapTexture[tBlock.m_MapIndex[0][0]]);
meshRenderer.material.SetTexture("_Splat1", m_BaseMapTexture[tBlock.m_MapIndex[0][1]]);
meshRenderer.material.SetTexture("_Normal1", m_NormalMapTexture[tBlock.m_MapIndex[0][1]]);
meshRenderer.material.SetTexture("_Splat2", m_BaseMapTexture[tBlock.m_MapIndex[0][2]]);
meshRenderer.material.SetTexture("_Normal2", m_NormalMapTexture[tBlock.m_MapIndex[0][2]]);
meshRenderer.material.SetTexture("_Splat3", m_BaseMapTexture[tBlock.m_MapIndex[0][3]]);
meshRenderer.material.SetTexture("_Control", m_ControlTexture);
Vector4 uvScale = new Vector4(48f, 64f, 48f, 48f); // 假设要设置的值是(1.0, 1.0, 0.0, 0.0)
meshRenderer.material.SetVector("_UvScale0", uvScale);
//位置
mObj.m_Object.transform.position = new Vector3(WidthIndex * m_BlockGridSize, 0, j * m_BlockGridSize);
}
return 0;
}
static float GetHeight(Color color, float HeightScale = 1.0f)
{
return HeightScale > 0 ? GetGrayScale(color) * HeightScale : GetGrayScale(color) * HeightScale;
}
static float GetGrayScale(Color color)
{
return 0.299f * color.r + 0.587f * color.g + 0.114f * color.b;
}
static void GetNearColorIndex(Color color, ref Color32 index, ref Color degreed)
{
// 找到与输入颜色最接近的四个颜色
//
//double[] distances = new double[4];
for (int i = 0; i < 4; ++i)
{
m_nearestColors[i] = m_Colors[0];
m_Degreed[i] = 1000;
}
for (int i = 0; i < m_Colors.Length; i++)
{
float distance = GetDistance(color, m_Colors[i]);
// 将当前颜色与已知的最近的四个颜色进行比较
for (int j = 0; j < m_nearestColors.Length; j++)
{
if (m_nearestColors[j] == null || distance < m_Degreed[j])
{
// 将当前颜色插入到最近的四个颜色中,其他颜色推后
for (int k = m_nearestColors.Length - 2; k >= j; k--)
{
m_nearestColors[k + 1] = m_nearestColors[k];
m_Degreed[k + 1] = m_Degreed[k];
m_Index[k + 1] = m_Index[k];
}
m_nearestColors[j] = m_Colors[i];
m_Degreed[j] = distance;
m_Index[j] = (byte)i;
break;
}
}
}
index = m_Index;
degreed = m_Degreed;
}
// 计算两个颜色之间的距离
static float GetDistance(Color color1, Color color2)
{
// 使用欧几里得距离公式计算两个颜色在 RGB 空间中的距离
float r = color1.r - color2.r;
float g = color1.g - color2.g;
float b = color1.b - color2.b;
return (float)Math.Sqrt(r * r + g * g + b * b);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 488fc6b8c32a8c444872e55fbe677027
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4f0c743d90a643c4812c05536d4e8d77
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,269 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace XGame
{
public class AuthRegisterLoginController : MonoBehaviour
{
private TMP_InputField loginUsernameField;
private TMP_InputField loginPasswordField;
private TMP_InputField registerUsernameField;
private TMP_InputField registerPasswordField;
private TMP_InputField registerConfirmField;
private Toggle toggleRemember;
private Button btnLogin;
private Button btnRegister;
private Button btnGuest;
private Button btnTabLogin;
private Button btnTabRegister;
private GameObject loginForm;
private GameObject registerForm;
private GameObject rememberRow;
private TMP_Text tipsText;
private TMP_Text titleText;
private TMP_Text rememberText;
private string luaModule = "client/login";
private bool isRegisterMode;
private bool isLoading;
public Action<string, string, bool, bool> OnLogin;
public Action<string, string, string> OnRegister;
public Action<string, string, bool, bool> OnGuestLogin;
private void Awake()
{
CacheControls();
WireEvents();
SwitchLogin();
}
public void Bind(string module)
{
if (!string.IsNullOrEmpty(module))
{
luaModule = module;
}
}
public void SetInitialState(string account, bool remember, bool autoLogin, bool agreement)
{
SetInitialState(account, string.Empty, remember, autoLogin, agreement);
}
public void SetInitialState(string account, string password, bool rememberPassword, bool autoLogin, bool agreement)
{
CacheControls();
SetInputText(loginUsernameField, account);
SetInputText(loginPasswordField, rememberPassword ? password : string.Empty);
SetInputText(registerUsernameField, account);
if (toggleRemember != null)
{
toggleRemember.isOn = rememberPassword;
}
if (rememberText != null)
{
rememberText.text = "记住密码";
}
}
public void SetServerInfo(string serverName, string status)
{
}
public void SetVersion(string version)
{
}
public void SetLoading(bool loading)
{
isLoading = loading;
SetButtonEnabled(btnLogin, !loading);
SetButtonEnabled(btnRegister, !loading);
SetButtonEnabled(btnGuest, !loading);
SetButtonEnabled(btnTabLogin, !loading);
SetButtonEnabled(btnTabRegister, !loading);
if (loading)
{
ShowError(isRegisterMode ? "Registering..." : "Logging in...");
}
}
public void ShowError(string message)
{
CacheControls();
if (tipsText != null)
{
tipsText.text = message ?? string.Empty;
}
}
public void Close()
{
Destroy(gameObject);
}
public void ToggleBaseAtlas()
{
}
public void SwitchLogin()
{
isRegisterMode = false;
SetActive(loginForm, true);
SetActive(registerForm, false);
SetActive(rememberRow, true);
SetActive(btnLogin, true);
SetActive(btnGuest, true);
SetActive(btnRegister, false);
if (titleText != null)
{
titleText.text = "Login";
}
ShowError(string.Empty);
}
public void SwitchRegister()
{
isRegisterMode = true;
SetActive(loginForm, false);
SetActive(registerForm, true);
SetActive(rememberRow, false);
SetActive(btnLogin, false);
SetActive(btnGuest, false);
SetActive(btnRegister, true);
if (titleText != null)
{
titleText.text = "Register";
}
ShowError(string.Empty);
}
private void OnLoginClicked()
{
if (isLoading)
{
return;
}
OnLogin?.Invoke(
GetInputText(loginUsernameField),
GetInputText(loginPasswordField),
toggleRemember != null && toggleRemember.isOn,
false);
}
private void OnRegisterClicked()
{
if (isLoading)
{
return;
}
OnRegister?.Invoke(
GetInputText(registerUsernameField),
GetInputText(registerPasswordField),
GetInputText(registerConfirmField));
}
private void OnGuestClicked()
{
if (isLoading)
{
return;
}
OnGuestLogin?.Invoke(
GetInputText(loginUsernameField),
GetInputText(loginPasswordField),
toggleRemember != null && toggleRemember.isOn,
false);
}
private void CacheControls()
{
loginUsernameField = loginUsernameField ? loginUsernameField : Find<TMP_InputField>("LoginUsernameField");
loginPasswordField = loginPasswordField ? loginPasswordField : Find<TMP_InputField>("LoginPasswordField");
registerUsernameField = registerUsernameField ? registerUsernameField : Find<TMP_InputField>("RegisterUsernameField");
registerPasswordField = registerPasswordField ? registerPasswordField : Find<TMP_InputField>("RegisterPasswordField");
registerConfirmField = registerConfirmField ? registerConfirmField : Find<TMP_InputField>("RegisterConfirmField");
toggleRemember = toggleRemember ? toggleRemember : Find<Toggle>("ToggleRemember");
btnLogin = btnLogin ? btnLogin : Find<Button>("BtnLogin");
btnRegister = btnRegister ? btnRegister : Find<Button>("BtnRegister");
btnGuest = btnGuest ? btnGuest : Find<Button>("BtnGuest");
btnTabLogin = btnTabLogin ? btnTabLogin : Find<Button>("BtnTabLogin");
btnTabRegister = btnTabRegister ? btnTabRegister : Find<Button>("BtnTabRegister");
loginForm = loginForm ? loginForm : FindGameObject("LoginForm");
registerForm = registerForm ? registerForm : FindGameObject("RegisterForm");
rememberRow = rememberRow ? rememberRow : FindGameObject("RememberRow");
tipsText = tipsText ? tipsText : Find<TMP_Text>("TextTips");
titleText = titleText ? titleText : Find<TMP_Text>("TextPanelTitle");
rememberText = rememberText ? rememberText : Find<TMP_Text>("TextRemember");
}
private void WireEvents()
{
AddClick(btnLogin, OnLoginClicked);
AddClick(btnRegister, OnRegisterClicked);
AddClick(btnGuest, OnGuestClicked);
AddClick(btnTabLogin, SwitchLogin);
AddClick(btnTabRegister, SwitchRegister);
}
private T Find<T>(string name) where T : Component
{
Transform target = transform.FindTransform(name);
return target == null ? null : target.GetComponent<T>();
}
private GameObject FindGameObject(string name)
{
Transform target = transform.FindTransform(name);
return target == null ? null : target.gameObject;
}
private void AddClick(Button button, UnityEngine.Events.UnityAction action)
{
if (button == null)
{
return;
}
button.onClick.RemoveListener(action);
button.onClick.AddListener(action);
}
private static string GetInputText(TMP_InputField input)
{
return input == null ? string.Empty : input.text;
}
private static void SetInputText(TMP_InputField input, string text)
{
if (input != null)
{
input.text = text ?? string.Empty;
}
}
private static void SetButtonEnabled(Button button, bool enabled)
{
if (button != null)
{
button.interactable = enabled;
}
}
private static void SetActive(Component component, bool active)
{
if (component != null)
{
component.gameObject.SetActive(active);
}
}
private static void SetActive(GameObject gameObject, bool active)
{
if (gameObject != null)
{
gameObject.SetActive(active);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b356a758f3ce46d5bfdb3b544f8f6a07
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,419 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace XGame
{
// 匹配大厅运行时控制器。沿用 AuthRegisterLoginController 模式:
// 按节点名缓存控件、克隆模板行、回调 Lua。只传基本类型跨 XLua。
public class GameLobbyController : MonoBehaviour
{
private Transform listGames;
private GameObject itemTemplate;
private TMP_Text statusText;
private RectTransform panelLobby;
private RectTransform collapseButtonRect;
private Button collapseButton;
private TMP_Text collapseText;
private GameObject titleObject;
private GameObject scrollObject;
private Vector2 expandedPanelSize;
private Vector2 expandedButtonPosition;
private bool hasCollapseMetrics;
private bool leftLayoutApplied;
private bool rootMatchButtonBound;
private bool collapsed;
private string currentStatus = string.Empty;
private string luaModule = "client/baseworld";
public Action<string> OnMatch;
private const float LobbyPanelWidth = 520f;
private const float LobbyPanelHeight = 296f;
private const float LobbyPanelLeft = 24f;
private const float LobbyPanelTop = -24f;
private const float CollapsedPanelWidth = 56f;
private void Awake()
{
CacheControls();
}
public void Bind(string module)
{
if (!string.IsNullOrEmpty(module))
{
luaModule = module;
}
}
// 清空旧克隆行
public void BeginGames()
{
CacheControls();
if (listGames == null)
{
return;
}
for (int i = listGames.childCount - 1; i >= 0; i--)
{
Transform child = listGames.GetChild(i);
if (itemTemplate != null && child == itemTemplate.transform)
{
continue;
}
Destroy(child.gameObject);
}
}
// 逐行追加。gameId 按行捕获用于点击回调
public void AddGame(string gameId, string name, string players)
{
CacheControls();
if (itemTemplate == null || listGames == null)
{
return;
}
GameObject row = Instantiate(itemTemplate, listGames);
row.name = "Item_Game_" + gameId;
row.SetActive(true);
SetChildText(row.transform, "Txt_GameName", name);
SetChildText(row.transform, "Txt_Players", players);
Transform btnTf = row.transform.FindTransform("Btn_Match");
if (btnTf != null)
{
Button btn = btnTf.GetComponent<Button>();
if (btn != null)
{
string captured = gameId;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => OnMatch?.Invoke(captured));
}
}
}
public void EndGames()
{
}
public void SetStatus(string message)
{
CacheControls();
currentStatus = message ?? string.Empty;
if (statusText == null)
{
return;
}
statusText.text = currentStatus;
statusText.gameObject.SetActive(!collapsed && !string.IsNullOrEmpty(currentStatus));
}
public void Close()
{
Destroy(gameObject);
}
private void CacheControls()
{
if (panelLobby == null)
{
Transform t = transform.FindTransform("Panel_Lobby");
panelLobby = t == null ? null : t.GetComponent<RectTransform>();
}
EnsureCollapseButton();
if (collapseButton == null)
{
Transform t = transform.FindTransform("Btn_Collapse");
if (t != null)
{
collapseButtonRect = t.GetComponent<RectTransform>();
collapseButton = t.GetComponent<Button>();
if (collapseButton != null)
{
collapseButton.onClick.RemoveAllListeners();
collapseButton.onClick.AddListener(ToggleCollapsed);
}
}
}
if (collapseText == null)
{
Transform t = transform.FindTransform("Txt_Collapse");
collapseText = t == null ? null : t.GetComponent<TMP_Text>();
}
if (titleObject == null)
{
Transform t = transform.FindTransform("Txt_Title");
titleObject = t == null ? null : t.gameObject;
}
if (scrollObject == null)
{
Transform t = transform.FindTransform("Scroll_Games");
scrollObject = t == null ? null : t.gameObject;
}
if (listGames == null)
{
listGames = transform.FindTransform("List_Games");
}
if (itemTemplate == null)
{
Transform t = transform.FindTransform("Item_Game");
itemTemplate = t == null ? null : t.gameObject;
}
if (statusText == null)
{
Transform t = transform.FindTransform("Txt_Status");
statusText = t == null ? null : t.GetComponent<TMP_Text>();
}
BindRootMatchButton();
ApplyLeftDockLayout();
if (!hasCollapseMetrics && panelLobby != null && collapseButtonRect != null)
{
expandedPanelSize = panelLobby.sizeDelta;
expandedButtonPosition = collapseButtonRect.anchoredPosition;
hasCollapseMetrics = true;
ApplyCollapsedState();
}
}
private void BindRootMatchButton()
{
if (rootMatchButtonBound)
{
return;
}
Transform matchButton = transform.FindTransform("Btn_Match");
if (matchButton == null || itemTemplate != null && matchButton.IsChildOf(itemTemplate.transform))
{
return;
}
Button btn = matchButton.GetComponent<Button>();
if (btn == null)
{
return;
}
rootMatchButtonBound = true;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => OnMatch?.Invoke(string.Empty));
}
private void EnsureCollapseButton()
{
if (transform.FindTransform("Btn_Collapse") != null)
{
return;
}
Transform parent = panelLobby != null && panelLobby.parent != null ? panelLobby.parent : transform;
GameObject buttonGo = new GameObject("Btn_Collapse", typeof(RectTransform), typeof(Image), typeof(Button));
buttonGo.transform.SetParent(parent, false);
RectTransform rt = buttonGo.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0f, 1f);
rt.anchorMax = new Vector2(0f, 1f);
rt.pivot = new Vector2(0f, 1f);
rt.anchoredPosition = new Vector2(LobbyPanelLeft + LobbyPanelWidth, LobbyPanelTop);
rt.sizeDelta = new Vector2(48f, 72f);
Image img = buttonGo.GetComponent<Image>();
Image matchImage = null;
Transform matchButton = transform.FindTransform("Btn_Match");
if (matchButton != null)
{
matchImage = matchButton.GetComponent<Image>();
}
img.sprite = matchImage != null ? matchImage.sprite : null;
img.type = matchImage != null ? matchImage.type : Image.Type.Sliced;
img.color = new Color(0.239f, 0.482f, 1f, 1f);
img.raycastTarget = true;
GameObject textGo = new GameObject("Txt_Collapse", typeof(RectTransform), typeof(TextMeshProUGUI));
textGo.transform.SetParent(buttonGo.transform, false);
RectTransform textRt = textGo.GetComponent<RectTransform>();
textRt.anchorMin = Vector2.zero;
textRt.anchorMax = Vector2.one;
textRt.pivot = new Vector2(0.5f, 0.5f);
textRt.offsetMin = Vector2.zero;
textRt.offsetMax = Vector2.zero;
TextMeshProUGUI text = textGo.GetComponent<TextMeshProUGUI>();
text.text = "<";
text.fontSize = 34f;
text.fontStyle = FontStyles.Bold;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.white;
text.raycastTarget = false;
}
private void ApplyLeftDockLayout()
{
if (leftLayoutApplied)
{
return;
}
leftLayoutApplied = true;
if (panelLobby != null)
{
panelLobby.anchorMin = new Vector2(0f, 1f);
panelLobby.anchorMax = new Vector2(0f, 1f);
panelLobby.pivot = new Vector2(0f, 1f);
panelLobby.anchoredPosition = new Vector2(LobbyPanelLeft, LobbyPanelTop);
panelLobby.sizeDelta = new Vector2(LobbyPanelWidth, LobbyPanelHeight);
}
if (collapseButtonRect != null)
{
collapseButtonRect.anchorMin = new Vector2(0f, 1f);
collapseButtonRect.anchorMax = new Vector2(0f, 1f);
collapseButtonRect.pivot = new Vector2(0f, 1f);
collapseButtonRect.anchoredPosition = new Vector2(LobbyPanelLeft + LobbyPanelWidth, LobbyPanelTop);
collapseButtonRect.sizeDelta = new Vector2(48f, 72f);
}
SetRect("Txt_Title", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -14f), new Vector2(448f, 36f));
SetOffsets("Scroll_Games", new Vector2(16f, 52f), new Vector2(-16f, -58f));
SetRect("Txt_Status", new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 16f), new Vector2(448f, 28f));
SetRect("Item_Game", new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 1f), Vector2.zero, new Vector2(0f, 76f));
SetLayoutElement("Item_Game", 76f);
SetRect("Txt_GameName", new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(18f, 12f), new Vector2(280f, 28f));
SetRect("Txt_Players", new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(18f, -14f), new Vector2(280f, 22f));
SetRect("Btn_Match", new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), new Vector2(-10f, 0f), new Vector2(104f, 46f));
Transform titleText = transform.FindTransform("Txt_Title");
if (titleText != null)
{
TMP_Text text = titleText.GetComponent<TMP_Text>();
if (text != null)
{
text.fontSize = 28f;
}
}
Transform gameNameText = transform.FindTransform("Txt_GameName");
if (gameNameText != null)
{
TMP_Text text = gameNameText.GetComponent<TMP_Text>();
if (text != null)
{
text.fontSize = 22f;
}
}
Transform matchText = transform.FindTransform("Txt_Match");
if (matchText != null)
{
TMP_Text text = matchText.GetComponent<TMP_Text>();
if (text != null)
{
text.fontSize = 24f;
}
}
}
private void ToggleCollapsed()
{
collapsed = !collapsed;
ApplyCollapsedState();
}
private void ApplyCollapsedState()
{
if (!hasCollapseMetrics)
{
return;
}
if (panelLobby != null)
{
panelLobby.sizeDelta = collapsed ? new Vector2(CollapsedPanelWidth, expandedPanelSize.y) : expandedPanelSize;
}
if (collapseButtonRect != null)
{
collapseButtonRect.anchoredPosition = collapsed
? new Vector2(panelLobby != null ? panelLobby.anchoredPosition.x + CollapsedPanelWidth : LobbyPanelLeft + CollapsedPanelWidth, expandedButtonPosition.y)
: expandedButtonPosition;
}
if (collapseText != null)
{
collapseText.text = collapsed ? ">" : "<";
}
if (titleObject != null)
{
titleObject.SetActive(!collapsed);
}
if (scrollObject != null)
{
scrollObject.SetActive(!collapsed);
}
if (statusText != null)
{
statusText.gameObject.SetActive(!collapsed && !string.IsNullOrEmpty(currentStatus));
}
}
private void SetRect(string nodeName, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPosition, Vector2 sizeDelta)
{
Transform t = transform.FindTransform(nodeName);
if (t == null)
{
return;
}
RectTransform rt = t.GetComponent<RectTransform>();
if (rt == null)
{
return;
}
rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax;
rt.pivot = pivot;
rt.anchoredPosition = anchoredPosition;
rt.sizeDelta = sizeDelta;
}
private void SetOffsets(string nodeName, Vector2 offsetMin, Vector2 offsetMax)
{
Transform t = transform.FindTransform(nodeName);
if (t == null)
{
return;
}
RectTransform rt = t.GetComponent<RectTransform>();
if (rt == null)
{
return;
}
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = offsetMin;
rt.offsetMax = offsetMax;
}
private void SetLayoutElement(string nodeName, float height)
{
Transform t = transform.FindTransform(nodeName);
if (t == null)
{
return;
}
LayoutElement layout = t.GetComponent<LayoutElement>();
if (layout == null)
{
return;
}
layout.minHeight = height;
layout.preferredHeight = height;
}
private static void SetChildText(Transform root, string childName, string text)
{
Transform t = root.FindTransform(childName);
if (t != null)
{
TMP_Text label = t.GetComponent<TMP_Text>();
if (label != null)
{
label.text = text ?? string.Empty;
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: edc88be0ce043d84280fa00680180db4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,136 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace XGame
{
public class MatchWaitingController : MonoBehaviour
{
private RectTransform spinnerRect;
private TMP_Text titleText;
private TMP_Text gameText;
private TMP_Text statusText;
private TMP_Text elapsedText;
private TMP_Text closeText;
private Button closeButton;
private string luaModule = "client/baseworld";
private string currentGameId = string.Empty;
private float startedAt;
public Action OnCloseRequested;
private void Awake()
{
CacheControls();
WireEvents();
startedAt = Time.realtimeSinceStartup;
UpdateElapsedText();
}
private void Update()
{
if (spinnerRect != null)
{
spinnerRect.Rotate(0f, 0f, -180f * Time.unscaledDeltaTime);
}
if (elapsedText != null && startedAt > 0f)
{
UpdateElapsedText();
}
}
public void Bind(string module)
{
if (!string.IsNullOrEmpty(module))
{
luaModule = module;
}
}
public void SetWaiting(string gameId, string message)
{
CacheControls();
currentGameId = gameId ?? string.Empty;
startedAt = Time.realtimeSinceStartup;
SetText(titleText, "匹配等待");
SetText(gameText, string.IsNullOrEmpty(currentGameId) ? "正在寻找对局" : currentGameId);
UpdateElapsedText();
SetStatus(string.IsNullOrEmpty(message) ? "正在匹配,请稍候..." : message, false);
}
public void SetStatus(string message)
{
SetStatus(message, false);
}
public void SetStatus(string message, bool canClose)
{
CacheControls();
SetText(statusText, message);
if (closeText != null)
{
closeText.text = canClose ? "返回大厅" : "退出匹配";
}
}
public void Close()
{
Destroy(gameObject);
}
private void CacheControls()
{
spinnerRect = spinnerRect ? spinnerRect : Find<RectTransform>("Img_Spinner");
titleText = titleText ? titleText : Find<TMP_Text>("Txt_Title");
gameText = gameText ? gameText : Find<TMP_Text>("Txt_Game");
statusText = statusText ? statusText : Find<TMP_Text>("Txt_Status");
elapsedText = elapsedText ? elapsedText : Find<TMP_Text>("Txt_Elapsed");
closeButton = closeButton ? closeButton : Find<Button>("Btn_Close");
closeText = closeText ? closeText : Find<TMP_Text>("Txt_Close");
if (closeText == null && closeButton != null)
{
closeText = closeButton.transform.FindTransform("Text")?.GetComponent<TMP_Text>();
}
}
private void WireEvents()
{
if (closeButton == null)
{
return;
}
closeButton.onClick.RemoveListener(OnCloseClicked);
closeButton.onClick.AddListener(OnCloseClicked);
}
private void OnCloseClicked()
{
OnCloseRequested?.Invoke();
}
private T Find<T>(string name) where T : Component
{
Transform target = transform.FindTransform(name);
return target == null ? null : target.GetComponent<T>();
}
private static void SetText(TMP_Text text, string value)
{
if (text != null)
{
text.text = value ?? string.Empty;
}
}
private void UpdateElapsedText()
{
if (elapsedText == null || startedAt <= 0f)
{
return;
}
int seconds = Mathf.Max(0, Mathf.FloorToInt(Time.realtimeSinceStartup - startedAt));
elapsedText.text = string.Format("已匹配时间 {0:00}:{1:00}", seconds / 60, seconds % 60);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fcb2b9a9dce4b24a8df7d942f7d5a63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9bfc74ac6737b304f920503ac335a818
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+638
View File
@@ -0,0 +1,638 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
namespace XGame
{
class UpdateModule : MonoBehaviour
{
public static UpdateModule Instance;
public class ResInfo
{
//public string fileName;
public string md5;
public string md5Path;
public int pack;
public int priority;
public int size;
}
class PackWairDownList
{
public List<string> list = new List<string>();
public int fileSize = 0;
public string md5 = "";
public bool isChange = false;
}
#if UNITY_ANDROID
static string sPlatform = "android";
#elif UNITY_IOS
static string sPlatform = "ios";
#else
static string sPlatform = "pc";
#endif
GameObject m_MainObj;
//更新相关
string UpdateServer;//
string UpdatePath;
string ServerListName = "files";
string UpdateListName = "updatefiles";
string TempListName = "tempfiles";
private Dictionary<string, ResInfo> m_gameResDict = new Dictionary<string, ResInfo>();
private Dictionary<string, ResInfo> m_localResDict = new Dictionary<string, ResInfo>();
private Dictionary<string, ResInfo> m_tempResDict = new Dictionary<string, ResInfo>();
private PackWairDownList[] m_packWairDownList = new PackWairDownList[10];
private int m_wairDownCount = 0;
private int m_downDownCount = 0;
//private bool m_bHotfix = false;
string m_sNewMD5;
string m_sOldMD5;
//bool m_bChange = false;
//UI 相关
Slider m_sliderUpdate;
TextMeshProUGUI m_textUpdate;
XProgress m_Proc;
private void Awake()
{
Instance = this;
#if UNITY_WEBGL
UpdatePath = $"{Application.streamingAssetsPath}/";
UpdateServer = $"{Application.streamingAssetsPath}/";
#else
UpdatePath = Application.persistentDataPath + "/";
UpdateServer = $"{XGame.AppConst.UpdateServer}{sPlatform}/";//$"http://127.0.0.1/update/{sPlatform}/"; //
#endif
m_Proc = new XProgress();
Init();
}
public void Init()
{
for (int i = 0; i < 10; ++i)
{
m_packWairDownList[i] = new PackWairDownList();
}
/*/Test
//默认Main预设会直接挂好,直接使用下面的Update组件
GameObject sliderObj = GameObject.Find("Main(Clone)/Update/Slider");//Base");//
if (sliderObj != null)
{
m_sliderUpdate = sliderObj.transform.GetComponent<Slider>();
}
m_textUpdate = GameObject.Find("Main(Clone)/Update/Text").transform.GetComponent<TextMeshProUGUI>();
//*/
}
public string GetResMD5()
{
return m_sNewMD5;
}
//设置资源管理器的文件列表hash 字典
public void SetAllResHash()
{
if (m_gameResDict.Count == 0)
{//还未加载file.txt
LoadServerResourceMap();
}
string str;
XResLoader.m_ResHash.Clear();
foreach (var dic in m_gameResDict)
{
//保存UpdatePath/res
Hash128 hash = Hash128.Parse(dic.Value.md5);
XResLoader.m_ResHash[$"{AppConst.UpdatePath}{dic.Key}"] = hash;
str = hash.ToString();
}
}
public void AddResHash(Dictionary<string, ResInfo> resList)
{
foreach (var dic in resList)
{
//保存UpdatePath/res
Hash128 hash = Hash128.Parse(dic.Value.md5);
XResLoader.m_ResHash[$"{AppConst.UpdatePath}{dic.Key}"] = hash;
}
}
#if UNITY_WEBGL
public IEnumerator UpdateResAllWebGL(System.Action onDownloadComplete)
{
Dictionary<string, string> dicFilelist = new Dictionary<string, string>();
string downloadServer = UpdateServer;
UnityWebRequest uwr = UnityWebRequest.Get($"{downloadServer}updatever.txt");
yield return uwr.SendWebRequest();
if (uwr.downloadHandler.isDone && uwr.result == UnityWebRequest.Result.Success)
{
m_sNewMD5 = uwr.downloadHandler.text;
int npos = m_sNewMD5.IndexOf(',');
if (npos != -1)//兼容单双md5
m_sNewMD5 = m_sNewMD5.Substring(npos+1, m_sNewMD5.Length - npos -1);
Debug.Log($"Download DataFile {m_sNewMD5} OK");
}
else
{
Debug.LogErrorFormat($"{downloadServer}updatever.txt is not exists");
yield break;
}
}
#endif
public IEnumerator UpdateResAll(System.Action onDownloadComplete)
{
//检查更新
string sNewMD5File = $"{UpdatePath}updatever.txt";
string sOldMD5File = $"{UpdatePath}ver.txt";
if (File.Exists(sNewMD5File))
{
m_sNewMD5 = File.ReadAllText(sNewMD5File);
int npos = m_sNewMD5.IndexOf(',');
if (npos != -1)//兼容单双md5
m_sNewMD5 = m_sNewMD5.Substring(npos + 1, m_sNewMD5.Length - npos - 1);
}
else
{
//直接结束
if(onDownloadComplete != null)
onDownloadComplete();
yield break;
}
if (File.Exists(sOldMD5File))
{
m_sOldMD5 = File.ReadAllText(sOldMD5File);
}
else
{//无文件,说明是第一次需要都更新
//m_bChange = true;
}
if (m_sNewMD5 != m_sOldMD5)
{
//m_bChange = true;
}
else
{
//无差异直接结束
if (onDownloadComplete != null)
onDownloadComplete();
yield break;
}
yield return DownLoadAssets(onDownloadComplete);
}
//进度变化的回调
void UpdateProcess(int curFinish, int totalNum)
{
if(totalNum > 0)
m_Proc.Set(curFinish * 100 / totalNum);
//if(m_sliderUpdate != null)
// m_sliderUpdate.value = curFinish;
}
void OnFileDownloadBegin(string filename, int data)
{
}
void WriteVerFile()
{
using (StreamWriter sw = new StreamWriter($"{UpdatePath}ver.txt"))
{
sw.WriteLine(m_sNewMD5);
}
}
IEnumerator DownLoadAssets(Action onDownloadComplete)
{
bool isVerChange = true;
string[] sNewMd5s = m_sNewMD5.Split(',');
string[] sOldMd5s = { "SDFASD123SFDSerw","gagfewr21dsfa" };
if(!string.IsNullOrEmpty(m_sOldMD5))
sOldMd5s = m_sOldMD5.Split(',');
if (sNewMd5s[0] == sOldMd5s[0])
{//资源md5相同,写ver更新结束
WriteVerFile();
onDownloadComplete();
yield break;
}
//更新流程
//获得版本号updatever.txt,对比本地ver.txt
//yield return CheckPackage(UpdateServer, "updatever.txt", "ver.txt", (isChange, newmd5) =>
//{
// isVerChange = isChange;
// m_packWairDownList[0].md5 = newmd5;
// m_packWairDownList[0].isChange = isChange;
//});
m_packWairDownList[0].md5 = sNewMd5s[0];
m_packWairDownList[0].isChange = true;
//Debug.Log($"Update Res List Begin : {sNewMd5s[0]}");
if (isVerChange)
{
m_gameResDict.Clear();
m_localResDict.Clear();
m_tempResDict.Clear();
//有改变,需要下载并对比列表文件
yield return DownloadFile(UpdateServer, $"{ServerListName}.txt", result =>
{
if (result > 0)
{
System.DateTime dt = System.DateTime.Now;
LoadServerResourceMap();
Debug.LogFormat("LoadServerResourceMap {0} 耗时:{1}ms", ServerListName, (System.DateTime.Now - dt).TotalMilliseconds);
}
else
{
Debug.LogFormat("File not exist : {0}{1}", UpdatePath, ServerListName);
isVerChange = false;
}
});
if (!isVerChange)//失败
{
onDownloadComplete();
yield break;
}
yield return LoadPackageResourceMap();
LoadLocalResourceMap();
int count = 1;
foreach (var gameRes in m_gameResDict)
{
ResInfo localRes = null;
if (m_localResDict.TryGetValue(gameRes.Key, out localRes) && localRes.md5 == gameRes.Value.md5)
{
//m_existFiles[gameRes.Key] = EExistLocation.Local;
}
else
{
if (m_tempResDict.TryGetValue(gameRes.Key, out localRes) && localRes.md5 == gameRes.Value.md5)
{
//m_existFiles[gameRes.Key] = EExistLocation.Local;
}
else
{
var packList = m_packWairDownList[0];//暂时只用一份更新,无分包
packList.list.Add(gameRes.Key);
packList.fileSize += gameRes.Value.size;
}
}
//m_packSizeRecord[gameRes.Value.pack - 1] += gameRes.Value.size;
UpdateProcess(count++, m_gameResDict.Count);
}
//m_sliderUpdate.maxValue = m_packWairDownList[0].list.Count;
//m_sliderUpdate.minValue = 0;
//m_sliderUpdate.value = 0;
m_Proc.Set(0);
//写入ver.txt文件(md5) 这样可以防止以后服务器没更新的时候还下载检查更新
if (m_packWairDownList[0].fileSize <= 0)
{
WriteVerFile();
isVerChange = false;
m_packWairDownList[0].isChange = false;
}
//下载过程
UnityWebRequest uwr;
string downloadServer = UpdateServer;
Debug.LogFormat("downloadServer : {0} Total:{1}", downloadServer, m_packWairDownList[0].list.Count);
int i = 0;
count = m_packWairDownList[0].list.Count;
string tempListFile = UpdatePath + TempListName + ".txt";
StreamWriter fs = null;
if (count > 0)
{
fs = new StreamWriter(tempListFile, true);
}
while (i < count)
{
string namePath = m_packWairDownList[0].list[i];
var path = downloadServer + namePath;
string outfile = UpdatePath + namePath;
uwr = UnityWebRequest.Get(path);
#region
// by xl
string tempFile = outfile + ".temp";
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
XDownloadHandler dh = new XDownloadHandler(outfile);
dh.OnFinish = (int size) =>
{
if (size > 0)
{
if (m_gameResDict.TryGetValue(namePath, out ResInfo resInfo))
{
//没关闭流 可能写不完整
fs?.WriteLine(string.Format("{{'{0}','{1}','{2}'}}",
namePath, resInfo.md5, 1, resInfo.size));
}
++m_downDownCount;
if (m_textUpdate)
{
int percent = 100 * m_downDownCount / count;
m_textUpdate.text = $"{percent}%";//namePath;
}
UpdateProcess(m_downDownCount, m_wairDownCount);
++i;
}
else
{
Debug.LogError("File Miss : " + path);
}
};
uwr.downloadHandler = dh;
#endregion
yield return uwr.SendWebRequest();
if (uwr.result != UnityWebRequest.Result.Success)
{
Debug.LogError("File Download Error : " + path);
}
}
//m_sliderUpdate.value = m_packWairDownList[0].list.Count;
m_packWairDownList[0].list.Clear();
m_packWairDownList[0].fileSize = 0;
m_packWairDownList[0].isChange = false;
//using (StreamWriter sw = new StreamWriter(string.Format("{0}ver.txt", UpdatePath)))
//{
// sw.WriteLine(m_packWairDownList[0].md5);
//}
WriteVerFile();
//if (pack <= 1)
{
fs?.Close();
yield return DownloadFile(downloadServer, ServerListName + ".txt", result =>
{
if (result > 0)
{
if (File.Exists(tempListFile))
{
File.Delete(tempListFile);
}
}
else
{
Debug.LogErrorFormat("{0} is not exists", downloadServer + ServerListName + ".txt");
}
});
m_wairDownCount = 0;
m_downDownCount = 0;
//m_Proc.Set(100);
//m_downEndFun(isDllUpdate ? 1 : 0);
//yield break;
}
}
Debug.Log("Download Finish!");
if(onDownloadComplete != null)
onDownloadComplete();
}
//协程下载文件
IEnumerator DownloadFile(string downloadServer, string fileName, System.Action<int> onFinish)
{
UnityWebRequest uwr;
//Logger.LogFormat("download ServerList {0}", downloadServer + fileName);
uwr = UnityWebRequest.Get(downloadServer + fileName);
// uwr.timeout = m_httpRequestTimeout;
yield return uwr.SendWebRequest();
if (uwr.downloadHandler.isDone && uwr.result == UnityWebRequest.Result.Success)
{
string outfile = UpdatePath + fileName;
string dir = Path.GetDirectoryName(outfile);
//if (!Directory.Exists(dir))
//{
// Directory.CreateDirectory(dir);
//}
if (File.Exists(outfile))
File.Delete(outfile);
File.WriteAllBytes(outfile, uwr.downloadHandler.data);
onFinish(uwr.downloadHandler.data.Length);
}
else
{
//Logger.LogFormat("Download Failed {0}", downloadServer + fileName);
onFinish(0);
//if (RequestErrorCallback != null) { RequestErrorCallback("DownloadFile", uwr.error); }
}
}
IEnumerator CheckPackage(string downloadServer, string fileName, string verName, System.Action<bool, string> onFinish)
{
yield return DownloadFile(downloadServer, fileName, result =>
{
bool ischange = true;
string vermd51 = null, newmd51 = null;
if (result > 0 && File.Exists(UpdatePath + fileName))
{
using (StreamReader sr = new StreamReader(UpdatePath + fileName))
{
string line;
line = sr.ReadLine();
if (line != null || line != "")
{
newmd51 = line;
}
}
if (File.Exists(UpdatePath + verName))
{
using (StreamReader sr = new StreamReader(UpdatePath + verName))
{
string line;
line = sr.ReadLine();
if (line != null || line != "")
{
vermd51 = line;
}
}
}
if (newmd51 != null)
{
if (vermd51 != null && newmd51 == vermd51)
{
ischange = false;
}
}
else
{
ischange = false;
}
}
else
{
ischange = false;
}
onFinish(ischange, newmd51);
});
}
//加载包内文件
IEnumerator LoadPackageResourceMap()
{
string content;
//在协程里,加载完才往下走
yield return XResLoader.OnLoadPackageFile($"{ServerListName}.txt", (data) =>
{
string str;
if (data != null)
{
str = System.Text.Encoding.UTF8.GetString(data);
TextAsset textAsset = new TextAsset(str);//Resources.Load<TextAsset>($"{ServerListName}.txt");
if (textAsset != null)
{
content = textAsset.text;
LoadResourceMap(content, m_localResDict);
}
}
Debug.LogFormat("{0} file(s) exist in game.", m_localResDict.Count);
});
}
void LoadLocalResourceMap()
{
string localListFile = UpdatePath + UpdateListName + ".txt";
string content = "";
if (File.Exists(localListFile))
{
content = File.ReadAllText(localListFile);
LoadResourceMap(content, m_localResDict);
}
}
void LoadTempResourceMap()
{
string tempListFile = UpdatePath + TempListName + ".txt";
string content = "";
if (File.Exists(tempListFile))
{
content = File.ReadAllText(tempListFile);
//此处是为了修复 文件写入不完整的问题
if (!string.IsNullOrEmpty(content) && !content.EndsWith("}"))
{
int lastIdx = content.LastIndexOf("}");
content = content.Substring(0, lastIdx + 2);//包含换行
File.WriteAllText(tempListFile, content);
}
LoadResourceMap(content, m_tempResDict);
}
}
void LoadResourceMap(string content, Dictionary<string, ResInfo> resMap)
{
if (string.IsNullOrEmpty(content))
{
Debug.LogWarningFormat("Can't load file_list");
return;
}
string[] lines = content.Split('\n');
if (lines == null)
{
Debug.LogWarningFormat("Can't load res_list, it is required!");
return;
}
foreach (string line in lines)
{
if (line.Length < 2) { continue; }
string s = line.Substring(1, line.Length - 1 - 2); //去掉开头的{'和结尾的},
s = s.Replace("\'", ""); //把lua的字符串包围符去掉.
string[] strs = s.Split(',');
if (strs.Length > 2)
{
ResInfo resInfo = new ResInfo();
resInfo.md5Path = strs[0];
resInfo.md5 = strs[1];
resInfo.size = int.Parse(strs[2]);
resMap[strs[0]] = resInfo;
}
else
{
Debug.LogWarningFormat("invalid line: ", line);
}
}
}
void LoadServerResourceMap()
{
string listPath = $"{UpdatePath}{ServerListName}.txt";
if (!File.Exists(listPath))
{//本地还没有下载过资源清单(首次启动 / 未触发增量更新),无缓存可读,安全跳过
Debug.LogWarning($"LoadServerResourceMap skip, not found: {listPath}");
return;
}
string content;//= ResourceLoader.instance.LoadAssetString(ServerListName + ".txt");//Android上读的是内部的文件
byte[] data;
long size;
using (var stream = File.OpenRead(listPath))// _{ m_sNewMD5}
{
size = stream.Seek(0, SeekOrigin.End);
data = new byte[size];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(data, 0, data.Length);
content = System.Text.Encoding.UTF8.GetString(data);
}
if (string.IsNullOrEmpty(content))
{
Debug.LogWarningFormat("LoadServerResourceMap Can't load file_list");
}
string[] lines = content.Split('\n');
//Debug.LogFormat("{0} lines in ServerListName", lines.Length);
if (lines != null)
{
foreach (string line in lines)
{
if (line.Length < 2) { continue; }
//fileName, md5, size
//{'ui/copy_fui.ab','2ca8dd785bf3e99decaab1f9e3e4cd22',1257},
string s = line.Substring(1, line.Length - 1 - 2); //去掉开头的{'和结尾的},
s = s.Replace("\'", ""); //把lua的字符串包围符去掉.
string[] strs = s.Split(',');
if (strs.Length > 2)
{
ResInfo resInfo = new ResInfo();
resInfo.md5Path = strs[0];
resInfo.md5 = strs[1];
resInfo.size = int.Parse(strs[2]);
m_gameResDict[strs[0]] = resInfo;
}
else
{
//Logger.LogWarningFormat("invalid line: ", line);
}
}
//Logger.LogFormat("{0} file(s) exist in game.", m_gameResDict.Count);
}
else
{//失败
//throw new InvalidResListException("Can't load server_res_list, it is required!");
}
}
private void Update()
{
//m_Proc.Update();
}
public XProgress GetProgress()
{
return m_Proc;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44860d1246d1f21489648010fb3d95d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// 用于处理文件下载
/// </summary>
internal class XDownloadHandler: DownloadHandlerScript
{
string m_SavePath = "";
string m_TempFilePath = "";
FileStream fs;
public long totalFileLen { get; private set; }
public long downloadedFileLen { get; private set; }
public string fileName { get; private set; }
public string dirPath { get; private set; }
#region
/// <summary>
/// 每次下载到数据后回调进度
/// </summary>
public Action<float,float> OnProgress = null;
/// <summary>
/// 当下载完成后回调下载的文件位置
/// </summary>
public Action<int> OnFinish = null;
#endregion
/// <summary>
/// 初始化下载句柄,定义每次下载的数据上限为1M
/// </summary>
/// <param name="filePath">保存到本地的文件路径</param>
public XDownloadHandler(string filePath) : base(new byte[1024 * 1024])
{
m_SavePath = filePath.Replace('\\', '/');
fileName = m_SavePath.Substring(m_SavePath.LastIndexOf('/') + 1);
dirPath = m_SavePath.Substring(0, m_SavePath.LastIndexOf('/'));
m_TempFilePath = Path.Combine(dirPath, fileName + ".temp");
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
fs = new FileStream(m_TempFilePath, FileMode.Append, FileAccess.Write);
downloadedFileLen = fs.Length;
}
/// <summary>
/// 从网络获取数据时候的回调,每帧调用一次
/// </summary>
/// <param name="data">接收到的数据字节流,总长度为构造函数定义的200kb,并非所有的数据都是新的</param>
/// <param name="dataLength">接收到的数据长度,表示data字节流数组中有多少数据是新接收到的,即0-dataLength之间的数据是刚接收到的</param>
/// <returns>返回true为继续下载,返回false为中断下载</returns>
protected override bool ReceiveData(byte[] data, int dataLength)
{
if (data == null || data.Length == 0)
{
Debug.LogFormat("【下载中】 下载文件{0}中,没有获取到数据,下载终止", fileName);
return false;
}
fs?.Write(data, 0, dataLength);
downloadedFileLen += dataLength;
OnProgress?.Invoke(downloadedFileLen, totalFileLen);
return true;
}
/// <summary>
/// 当接受数据完成时的回调
/// </summary>
protected override void CompleteContent()
{
OnDispose();
//Debug.LogFormat("【下载完成】 完成对{0}文件的下载,保存路径为{1}", fileName, m_SavePath);
if (File.Exists(m_TempFilePath))
{
if (File.Exists(m_SavePath))
File.Delete(m_SavePath);
File.Move(m_TempFilePath, m_SavePath);
}
else
{
Debug.LogFormat("【下载失败】 下载文件{0}时失败", fileName);
}
OnFinish?.Invoke((int)downloadedFileLen);
}
public void OnDispose()
{
fs?.Close();
fs?.Dispose();
}
/// <summary>
/// 请求下载时的第一个回调函数,会返回需要接收的文件总长度
/// </summary>
/// <param name="contentLength">如果是续传,则是剩下的文件大小;本地拷贝则是文件总长度</param>
protected override void ReceiveContentLengthHeader(ulong contentLength)
{
if (contentLength == 0)
{
Debug.Log("【下载已经完成】");
CompleteContent();
}
base.ReceiveContentLengthHeader(contentLength);
totalFileLen = (long)contentLength + downloadedFileLen;
}
public void ErrorDispose()
{
fs.Close();
fs.Dispose();
if (File.Exists(m_TempFilePath))
{
File.Delete(m_TempFilePath);
}
Dispose();
}
public override void Dispose()
{
OnFinish = null;
OnProgress = null;
base.Dispose();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2419a3fbc53a804f98eddcce6dfb04d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 45eab3c84a9cf274dbf5c2d4df375b75
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,602 @@
using UnityEngine;
using System.Runtime.InteropServices;
#if !UNITY_WEBGL
using System.Net;
using System.Net.Sockets;
#endif
//using GYGame;
using System;
using System.Text;
using System.Collections.Generic;
using System.Threading;
using XLua;
namespace XGame
{
public class ConsoleWindow : MonoBehaviour
{
#if UNITY_STANDALONE_OSX //|| UNITY_EDITOR_OSX
const string ConsoleWindowsDLL = "ConsoleWindow";
[DllImport(ConsoleWindowsDLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int ShowConsoleWin();
[DllImport(ConsoleWindowsDLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void ShowErrorMsg(string msg);
[DllImport(ConsoleWindowsDLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void ShowWarningMsg(string msg);
[DllImport(ConsoleWindowsDLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void ShowLogMsg(string msg);
void ShowConsole()
{
ShowConsoleWin();
}
void HandleLog(string message, string stackTrace, LogType type)
{
message = message + "\n";
if (type == LogType.Log)
{
ShowLogMsg(message);
}
else if (type == LogType.Warning)
{
ShowWarningMsg(message);
}
else
{
ShowErrorMsg(message + stackTrace);
}
}
#elif UNITY_STANDALONE_WIN
void ShowConsole()
{
//XConsoleHelper.ClearConsole();
}
void HandleLog(string message, string stackTrace, LogType type)
{
message = message + "\n";
//XConsoleHelper.Write(type, message);
//可以根据Type加前缀
message = message + "\n";
//发送到服务器转外置console
//可以根据Type加前缀
switch (type)
{
case LogType.Error:
message = "Error: " + AppendLuaStackTrace(message);
break;
case LogType.Exception:
message = "Exception: " + AppendLuaStackTrace(message);
break;
case LogType.Warning:
message = "Warning: " + message;
break;
default:
break;
}
Send(message);//*/
}
#else
void ShowConsole()
{
}
void HandleLog(string message, string stackTrace, LogType type)
{
//可以根据Type加前缀
message = message + "\n";
//发送到服务器转外置console
//可以根据Type加前缀
switch (type)
{
case LogType.Error:
message = "Error: " + AppendLuaStackTrace(message);
break;
case LogType.Exception:
message = "Exception: " + AppendLuaStackTrace(message);
break;
case LogType.Warning:
message = "Warning: " + message;
break;
default:
break;
}
Send(message);
}
#endif
//ud 20220106 增加远程信息调试功能
#if !UNITY_WEBGL
static private Socket LogSocket;
static private bool LogSocketConnecting;
static private readonly object LogSocketLock = new object();
const string RemoteConsoleHost = "119.91.115.142";
const int RemoteConsolePort = 15098;
const int RemoteConsoleConnectTimeoutMs = 1500;
#else
static XWebSocket WebSocket;// = new GYWebSocket();
#endif
private const int BuffSize = 10240;
static private string LuaScript = null;
static byte[] MsgBuffer = new byte[64000];
static int BufferPos = 0;
public void Init()
{
bool bConsole = true;//起始不加载false
#if UNITY_EDITOR
bConsole = false;
#endif
//外网包就跳过
//。。。
//#if !UNITY_STANDALONE
if(bConsole)
{
ConnectConsoleServer(SystemInfo.graphicsDeviceName);
}
//#endif
}
[CSharpCallLua]
static public void ConnectConsoleServer(string name)
{
#if UNITY_EDITOR || UNITY_WEBGL //编辑器和WebGL不开
#if UNITY_WEBGL
if (WebSocket == null)
{
string url = $"192.168.51.100:15098";//$"ws://182.61.4.164:15098";//$"ws://119.91.115.142:15098";//
lsocket.connect(url, 0, (ws, err) =>
{
WebSocket = ws;
if (ws != null)
{
//Debug.LogError($"Console Connect OK!");
string msg = "$set~^client$" + name + ",x\r\n";
Send(msg);
}
else
{
Debug.LogError($"Console Connect Err: {err}");
}
});
}
else
{//重设名字
//发送客户端登录(改换idname),"$set~^client$"+初始登录nameSystemInfo.deviceUniqueIdentifier+"|"+账号id
string msg = "$set~^client$" + name + ",x\r\n";
Send(msg);
}
#endif
return;
#else
ConnectConsoleServerAsync(name);
return;
#if false
if (LogSocket == null || !LogSocket.Connected)
{
try
{
//初始化网络,连接服务器,接收或发送给服务器
if (LogSocket == null)
LogSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//LogSocket.Connect("192.168.8.60", 13782);//服务器所在
LogSocket.Connect("119.91.115.142", 15098);//外网调试服
//LogSocket.Connect("192.168.10.166", 8861);
//发送客户端登录(改换idname),"$set~^client$"+初始登录nameSystemInfo.deviceUniqueIdentifier+"|"+账号id
string msg = "$set~^client$" + name + ",x\r\n";
Send(msg);
ReceiveAsync();
if (LogSocket != null && !LogSocket.Connected)
{
LogSocket.Close();
LogSocket = null;
Debug.LogWarning("ConnectConsoleServer Failed!");
}
else
{
Debug.Log("ConnectConsoleServer OK!");
}
}
catch (Exception e)
{
Debug.LogWarning("ConnectConsoleServer Failed : " + e.Message);
}
}
else
{
//发送客户端登录(改换idname),"$set~^client$"+初始登录nameSystemInfo.deviceUniqueIdentifier+"|"+账号id
string msg = "$set~^client$" + name + ",x\r\n";
Send(msg);
}
#endif
#endif
}
#if !UNITY_WEBGL && !UNITY_EDITOR
class ConsoleConnectState
{
public Socket Socket;
public string Name;
public volatile bool TimedOut;
public ConsoleConnectState(Socket socket, string name)
{
Socket = socket;
Name = name;
}
}
static private void ConnectConsoleServerAsync(string name)
{
if (LogSocket != null && LogSocket.Connected)
{
string msg = "$set~^client$" + name + ",x\r\n";
Send(msg);
return;
}
lock (LogSocketLock)
{
if (LogSocketConnecting)
return;
LogSocketConnecting = true;
}
Socket socket = null;
IAsyncResult connectResult = null;
ConsoleConnectState state = null;
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.NoDelay = true;
state = new ConsoleConnectState(socket, name);
LogSocket = socket;
connectResult = socket.BeginConnect(RemoteConsoleHost, RemoteConsolePort, OnConsoleConnected, state);
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
if (connectResult != null && !connectResult.AsyncWaitHandle.WaitOne(RemoteConsoleConnectTimeoutMs))
{
state.TimedOut = true;
SafeClose(socket);
lock (LogSocketLock)
{
if (LogSocket == socket)
LogSocket = null;
LogSocketConnecting = false;
}
Debug.LogWarning("ConnectConsoleServer timeout.");
}
}
finally
{
try
{
if (connectResult != null)
connectResult.AsyncWaitHandle.Close();
}
catch { }
}
});
}
catch (Exception e)
{
SafeClose(socket);
lock (LogSocketLock)
{
if (LogSocket == socket)
LogSocket = null;
LogSocketConnecting = false;
}
Debug.LogWarning("ConnectConsoleServer Failed : " + e.Message);
}
}
static private void OnConsoleConnected(IAsyncResult result)
{
ConsoleConnectState state = result.AsyncState as ConsoleConnectState;
Socket socket = state != null ? state.Socket : null;
try
{
if (socket == null)
return;
socket.EndConnect(result);
if (socket.Connected)
{
LogSocket = socket;
string msg = "$set~^client$" + state.Name + ",x\r\n";
Send(msg);
ReceiveAsync();
Debug.Log("ConnectConsoleServer OK!");
}
}
catch (Exception e)
{
SafeClose(socket);
lock (LogSocketLock)
{
if (LogSocket == socket)
LogSocket = null;
}
if (state == null || !state.TimedOut)
Debug.LogWarning("ConnectConsoleServer Failed : " + e.Message);
}
finally
{
lock (LogSocketLock)
{
if (LogSocket == socket || LogSocket == null)
LogSocketConnecting = false;
}
}
}
static private void SafeClose(Socket socket)
{
try { socket?.Close(); } catch { }
}
#endif
void Update()
{
if (LuaScript != null)
{//在主线程执行代码
string sLua = LuaScript;
LuaScript = null;
//Debug.Log($"Run Lua : {sLua}");
try
{
LuaManager.m_Instance.DoString(sLua);
}
catch (Exception e)
{
Debug.LogWarning("Do Lua Failed : " + e.Message);
}
}
#if UNITY_WEBGL
//WebSocket 每帧要获得是否有信息
if (WebSocket != null)
{
int recLength = WebSocket.GetRecvSize();
if (recLength > 0)
{
string strError = "";
byte[] data = WebSocket.recv(ref strError);
//拆粘包处理
SetData(data, recLength);
}
}
#endif
}
static private void ReceiveAsync()
{
#if !UNITY_WEBGL
if (LogSocket == null || !LogSocket.Connected) return;
var Buff = new byte[BuffSize];
LogSocket.BeginReceive(Buff, 0, BuffSize, SocketFlags.None, OnReceived, Buff);
#endif
}
static public void OnReceived(IAsyncResult result)
{
#if !UNITY_WEBGL
if (LogSocket == null || !LogSocket.Connected)
{
Debug.LogError("Remote Console DisConnected: ");
return;
}
byte[] data = (byte[])result.AsyncState;
int recLength = LogSocket.EndReceive(result, out SocketError error);
if (recLength <= 0)
{
Debug.LogError($"Remote Console Error: {error}");
return;
}
ReceiveAsync();
//拆粘包处理
SetData(data, recLength);
#endif
}
public static void SetData(byte[] data, int recLength)
{
//拆粘包处理
data.CopyTo(MsgBuffer, BufferPos);
recLength = recLength + BufferPos;
int pos = 0;
for (int j = 0; j < 100; ++j)
{
//前两个字节是长度
short len = BitConverter.ToInt16(MsgBuffer, pos);
if (recLength - pos - 2 < len)
{//长度不足
if (pos == 0)
{
BufferPos = recLength;
}
else
{
data.CopyTo(MsgBuffer, BufferPos);
Buffer.BlockCopy(MsgBuffer, pos, MsgBuffer, 0, recLength - pos);
BufferPos = recLength - pos;
}
break;
}
//处理
System.Text.Encoding encoding = System.Text.Encoding.Default;
//这里是接收线程,不能直接执行代码,可能会出错
int luasize = len;
if (MsgBuffer[pos + 2 + len - 1] == '\0')
luasize = len - 1;
if (LuaScript == null)
{//memcpy(msg.sData, p_tsock->buffer + pos + sizeof(unsigned short), len);
LuaScript = encoding.GetString(MsgBuffer, pos + 2, luasize);
}
else
{
LuaScript = LuaScript + encoding.GetString(MsgBuffer, pos + 2, luasize);
}
pos += len + 2;
if (pos == recLength + BufferPos)
{
BufferPos = 0;
break;
}
}
}
public static void Log(LogType logType, string log)
{
#if !UNITY_WEBGL
if (LogSocket == null || !LogSocket.Connected) return;
#else
if (WebSocket == null) return;
#endif
string fLog = "";
switch (logType)
{
case LogType.Error:
fLog = "Error: " + AppendLuaStackTrace(log);
break;
case LogType.Exception:
fLog = "Exception: " + AppendLuaStackTrace(log);
break;
case LogType.Warning:
fLog = "Warning: " + log;
break;
default:
fLog = log;
break;
}
Send(fLog);
}
private static string AppendLuaStackTrace(string message)
{
string luaStackTrace = GetLuaStackTrace();
if (string.IsNullOrEmpty(luaStackTrace))
{
return message;
}
Debug.Log("\nLua Stack:\n" + luaStackTrace);
return message + "\nLua Stack:\n" + luaStackTrace;
}
private static string GetLuaStackTrace()
{
LuaEnv luaEnv = LuaManager.m_LuaEnv;
if (luaEnv == null)
{
return null;
}
try
{
object[] results = luaEnv.DoString("return debug.traceback('', 2)", "ConsoleWindow.GetLuaStackTrace");
if (results != null && results.Length > 0)
{
return results[0] as string;
}
}
catch
{
// Keep logging robust even if the Lua VM is not in a callable state.
}
return null;
}
private static void Send(string msg)
{
#if !UNITY_WEBGL
if (LogSocket != null && LogSocket.Connected)
{
byte[] content = Encoding.Default.GetBytes(msg);
ushort size = (ushort)content.Length;
byte[] bytes = BitConverter.GetBytes(size);
byte[] buff = new byte[bytes.Length + content.Length];
bytes.CopyTo(buff, 0);
content.CopyTo(buff, bytes.Length);
int nSend = LogSocket.Send(buff);
//nSend = size;
}
#else
if (WebSocket != null)
{
byte[] content = Encoding.Default.GetBytes(msg);
ushort size = (ushort)content.Length;
byte[] bytes = BitConverter.GetBytes(size);
byte[] buff = new byte[bytes.Length + content.Length];
bytes.CopyTo(buff, 0);
content.CopyTo(buff, bytes.Length);
string errorMsg = null;
WebSocket.send(buff, null, ref errorMsg);
}
#endif
}
private static void SendBytes(byte[] buff)
{
#if !UNITY_WEBGL
if (LogSocket != null && LogSocket.Connected)
{
ushort size = (ushort)buff.Length;
byte[] bytes = BitConverter.GetBytes(size);
byte[] sendbuf = new byte[bytes.Length + buff.Length];
bytes.CopyTo(sendbuf, 0);
buff.CopyTo(sendbuf, bytes.Length);
LogSocket.Send(sendbuf);
}
#else
if (WebSocket != null)
{
ushort size = (ushort)buff.Length;
byte[] bytes = BitConverter.GetBytes(size);
byte[] sendbuf = new byte[bytes.Length + buff.Length];
bytes.CopyTo(sendbuf, 0);
buff.CopyTo(sendbuf, bytes.Length);
string errorMsg = null;
WebSocket.send(sendbuf, null, ref errorMsg);
}
#endif
}
#if !UNITY_EDITOR
void Awake()
{
Init();
Application.logMessageReceived += HandleLog;
}
void OnDestroy()
{
Application.logMessageReceived -= HandleLog;
//if (LogSocket != null && LogSocket.Connected)
//{
// LogSocket.Disconnect(false);
//}
}
#else
void Awake()
{
Init();
Application.logMessageReceived += HandleLog;
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0ca08f2ff7cc23d4187a29a38ccadce7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,235 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_WEBGL
using WeChatWASM;
#endif
using ZXing;
using ZXing.QrCode;
/// <summary>
/// 扫描图片
/// </summary>
public class ScanQRCode : MonoBehaviour
{
bool isOpen = true; //true当前开启扫描状态 false 当前是关闭扫描状态
Animator ani; //扫描动画
private WebCamTexture m_webCameraTexture;//摄像头实时显示的画面
private BarcodeReader m_barcodeRender; //申请一个读取二维码的变量
//存放二维码的纹理图片
Texture2D encoded;
[Header("显示摄像头画面的RawImage")]
public RawImage m_cameraTexture;
[Header("扫描间隔")]
public float m_delayTime = 3f;
[Header("开启扫描按钮")]
public Button openScanBtn;
[Header("扫描按钮")]
public Button ScanBtn;
void Start()
{
Debug.Log("ScanQRCode.Start()");
/*初始化纹理图片
* 注意:宽高度大小必须是256,
* 否则出现索引超出数组边界错误
*/
encoded = new Texture2D(256, 256);
CreatQr("http://192.168.1.88/T");
ScanBtn.onClick.AddListener(()=>{
StartScanQRCode((rs) =>
{
CreatQr(rs);
});
});
return;
try
{
//摄像机权限
#if UNITY_WEBGL
if (LoadDll.m_bInWX)
{
AppAuthorizeSetting au = WX.GetAppAuthorizeSetting();
if (au.cameraAuthorized != "authorized")
{//未授权
AuthorizeOption opt = new AuthorizeOption();
opt.complete = (rs) => { Debug.Log($"Cam Authorze complete {rs.errMsg}"); };
opt.success = (rs) => { Debug.Log($"Cam Authorze success {rs.errMsg}"); };
opt.fail = (rs) => { Debug.Log($"Cam Authorze fail {rs.errMsg}"); };
WX.Authorize(opt);
return;
}
}
else
#endif
{
Application.RequestUserAuthorization(UserAuthorization.WebCam);
}
InitQRScan();
}
catch(System.Exception e)
{
Debug.Log($"{e.Message}");
}
}
void InitQRScan()
{
WebCamDevice[] tDevices = WebCamTexture.devices; //获取所有摄像头
if (tDevices.Length > 0)
{
string tDeviceName = tDevices[0].name; //获取第一个摄像头,用第一个摄像头的画面生成图片信息
m_webCameraTexture = new WebCamTexture(tDeviceName, 400, 300);//名字,宽,高
m_cameraTexture.texture = m_webCameraTexture; //赋值图片信息
m_webCameraTexture.Play(); //开始实时显示
m_barcodeRender = new BarcodeReader();
}
else
{
Debug.Log("No Camera!");
return;
}
ani = GetComponent<Animator>();
OpenScanQRCode(); //默认不扫描
//按钮监听
openScanBtn.onClick.AddListener(OpenScanQRCode);
}
#region
/// <summary>
/// 创建二维码
/// </summary>
public void CreatQr(string QrCodeStr)
{
if (QrCodeStr != string.Empty)
{
//二维码写入图片
var color32 = Encode(QrCodeStr, encoded.width, encoded.height);
encoded.SetPixels32(color32); //更改纹理的像素颜色
encoded.Apply();
//生成的二维码图片附给RawImage
m_cameraTexture.texture = encoded;
}
else
Debug.Log("没有生成信息");
}
/// <summary>
/// 生成二维码
/// </summary>
/// <param name="textForEncoding">需要生产二维码的字符串</param>
/// <param name="width">宽</param>
/// <param name="height">高</param>
/// <returns></returns>
private static Color32[] Encode(string formatStr, int width, int height)
{
//绘制二维码前进行一些设置
QrCodeEncodingOptions options = new QrCodeEncodingOptions();
//设置字符串转换格式,确保字符串信息保持正确
options.CharacterSet = "UTF-8";
//设置绘制区域的宽度和高度的像素值
options.Width = width;
options.Height = height;
//设置二维码边缘留白宽度(值越大留白宽度大,二维码就减小)
options.Margin = 1;
/*实例化字符串绘制二维码工具
* BarcodeFormat:条形码格式
* Options: 编码格式(支持的编码格式)
*/
var barcodeWriter = new BarcodeWriter { Format = BarcodeFormat.QR_CODE, Options = options };
//进行二维码绘制并进行返回图片的颜色数组信息
return barcodeWriter.Write(formatStr);
}
#endregion
#region
void StartScanQRCode(Action<string> callback)
{
#if UNITY_WEBGL
if (LoadDll.m_bInWX)
{
ScanCodeOption sco = new ScanCodeOption();
sco.success = (rs) => { Debug.Log($"Cam Scan success {rs.result}\n {rs.path}\n {rs.rawData}"); callback(rs.rawData); };
sco.fail = (rs) => { Debug.Log($"Cam Scan fail {rs.errMsg}"); callback(""); };
WX.ScanCode(sco);
Debug.Log("WX Scan ScanCode");
}
else
{
Debug.Log("WebGL Scan Not Ready");
}
#endif
}
//开启关闭扫描二维码
void OpenScanQRCode()
{
if (isOpen)
{
//开启状态,需要关闭扫描
ani.Play("CloseScan", 0, 0);
//CancelInvoke("CheckQRCode");
Debug.Log("CloseScan ");
}
else
{
//关闭状态,需要开启扫描
//开始扫描
ani.Play("OpenScan", 0, 0);
//以秒为单位调用方法
//InvokeRepeating("CheckQRCode", 0, m_delayTime);
Debug.Log("OpenScan ");
}
isOpen = !isOpen;
}
#endregion
#region
/// <summary>
/// 检索二维码方法
/// </summary>
public void CheckQRCode()
{
//存储摄像头画面信息贴图转换的颜色数组
Color32[] m_colorData = m_webCameraTexture.GetPixels32();
//将画面中的二维码信息检索出来
var tResult = m_barcodeRender.Decode(m_colorData, m_webCameraTexture.width, m_webCameraTexture.height);
if (tResult != null)
{
//Application.OpenURL(tResult.Text);
Debug.Log(tResult.Text);
}
else
{
Debug.Log("CheckQRCode Error");
}
}
#endregion
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2cb2f34d7a8b866419efaa2e0b6b30cc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using UnityEngine;
namespace XGame
{
/// <summary>
/// ALgorithm相关的实用函数。
/// </summary>
public static partial class Utility
{
public static void Swap<T>(ref T x, ref T y)
{
T t = x;
x = y;
y = t;
}
/// <summary>
/// 递归求组合(从n个不同元素中取出m个元素组合)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">结果列表</param>
/// <param name="arr">所求数组</param>
/// <param name="n">辅助变量</param>
/// <param name="m">辅助变量</param>
/// <param name="pos">辅助下标存储数组</param>
/// <param name="M">辅助变量</param>
public static void GetCombination<T>(ref List<T[]> list, T[] arr, int n, int m, int[] pos, int M)
{
for (int i = n; i >= m; i--)
{
pos[m - 1] = i - 1;
if (m > 1)
{
GetCombination(ref list, arr, i - 1, m - 1, pos, M);
}
else
{
T[] temp = new T[M];
for (int j = 0; j < M; j++)
{
temp[j] = arr[pos[j]];
}
if (list == null)
{
list = new List<T[]>();
}
list.Add(temp);
}
}
}
/// <summary>
/// 数组中n个元素的组合
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr">所求数组</param>
/// <param name="n">元素个数</param>
/// <returns>组合结果列表</returns>
public static List<T[]> GetCombination<T>(T[] arr, int n)
{
if (arr.Length < n)
{
return null;
}
List<T[]> list = new List<T[]>();
GetCombination(ref list, arr, arr.Length, n, new int[n], n);
return list;
}
/// <summary>
/// 递归求排列(从n个不同元素中取出m个元素排列)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="result">结果列表</param>
/// <param name="list">所求数组</param>
/// <param name="start">起始标号</param>
/// <param name="end">结束标号</param>
public static void GetPermutation<T>(ref List<T[]> list, T[] arr, int start, int end)
{
if (start == end)
{
T[] temp = new T[arr.Length];
arr.CopyTo(temp, 0);
if (list == null)
{
list = new List<T[]>();
}
list.Add(temp);
}
for (int i = start; i <= end; i++)
{
Swap(ref arr[i], ref arr[start]);
GetPermutation(ref list, arr, start + 1, end);
Swap(ref arr[i], ref arr[start]);
}
}
/// <summary>
/// 数组n个元素的排列
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr">所求数组</param>
/// <param name="n">元素个数</param>
/// <returns>排列结果列表</returns>
public static List<T[]> GetPermutation<T>(T[] arr, int n)
{
if (arr.Length < n)
{
return null;
}
List<T[]> list = new List<T[]>();
List<T[]> comList = GetCombination(arr, n);
foreach (var com in comList)
{
List<T[]> perList = new List<T[]>();
GetPermutation(ref perList, com, 0, n - 1);
list.AddRange(perList);
}
return list;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3e3e37167b4bed44b47cbfee57b40fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,526 @@
using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
namespace XGame
{
public class Type_Null
{
public static int Reserve = 0;
}
public static partial class Utility
{
//private static float FLOAT_PRECISION = 1e-5f;
private static Dictionary<string, int> sActorIdDic = new Dictionary<string, int>();
private static Dictionary<string, Type> sTypeDic = new Dictionary<string, Type>();
private static Dictionary<string, Assembly> Asms = new Dictionary<string, Assembly>();
private static Type typeNull = typeof(Type_Null);
public static int GenClientNameId(string actorTypeName)
{
if (sActorIdDic.ContainsKey(actorTypeName) == false)
{
sActorIdDic.Add(actorTypeName, 0);
}
++sActorIdDic[actorTypeName];
return sActorIdDic[actorTypeName];
}
static Type SuperGetType(string sTypeName)
{
Type typeValue;
if (sTypeDic.TryGetValue(sTypeName, out typeValue))
{
if (typeValue == typeNull)
return null;
else
return typeValue;
}
else
{
typeValue = Type.GetType(sTypeName);//底包的类型有就不用下面的麻烦代码
if (typeValue != null)
{
sTypeDic[sTypeName] = typeValue;
//Debug.Log($"SuperGetType sTypeDic 1 : {sTypeName}");
return typeValue;
}
Assembly[] foundAsms = null;
if (Asms.Count == 0)
{
foundAsms = AppDomain.CurrentDomain.GetAssemblies();
foreach (var asm in foundAsms)
{
//只检查GYFramework 和Assembly-CSharp 热更相关的dll
//Debug.Log($"SuperGetType : {sTypeName} ({asm.GetName().Name})");
string asmName = asm.GetName().Name;
if (asmName == "GYFramework" || asmName == "Assembly-CSharp")
{
Asms[asmName] = asm;
}
}
}
foreach (var asm in Asms)
{
typeValue = asm.Value.GetType(sTypeName);
if (typeValue != null)
{
sTypeDic[sTypeName] = typeValue;
//Debug.Log($"SuperGetType sTypeDic 2 : {sTypeName}");
return typeValue;
}
}
//最后全量查找
if (null == foundAsms)
{
foundAsms = AppDomain.CurrentDomain.GetAssemblies();
}
foreach (var asm in foundAsms)
{
string asmName = asm.GetName().Name;
typeValue = asm.GetType(sTypeName);
if (typeValue != null)
{
Asms[asmName] = asm;
sTypeDic[sTypeName] = typeValue;
//Debug.Log($"SuperGetType sTypeDic 3: {sTypeName}");
return typeValue;
}
}
sTypeDic[sTypeName] = typeNull;
//Debug.Log($"SuperGetType Error : {sTypeName} == null");
}
return null;
}
public static Type GetTypeByTypeName(string typeName)
{
System.Type type = SuperGetType(typeName);//Type.GetType(typeName);//
if (null == type)
{
string newTypeName = "XGame" + "." + typeName;
type = SuperGetType(newTypeName); //Type.GetType(newTypeName);//
//if (null == type)
//{
// newTypeName = "XGame" + "." + typeName;
// type = SuperGetType(newTypeName); //Type.GetType(newTypeName);//
//}
}
return type;
}
public static Type GetTypeByTypeName<BaseType>(string typeName) where BaseType : class
{
System.Type type = Type.GetType(typeName);
if (type == null)
{
string newTypeName = "XGame" + "." + typeName;
type = Type.GetType(newTypeName);
//if (type == null)
//{
// newTypeName = "XGame" + "." + typeName;
// type = Type.GetType(newTypeName);
//}
}
if (type == null)
{
Debug.LogError("GetTypeByTypeName typeName:{0} get type return null");
return null;
}
if (!typeof(BaseType).IsAssignableFrom(type))
{
Debug.LogError($"GetTypeByTypeName typeName:{typeName} is not subclass of {typeof(BaseType).FullName}");
return null;
}
return type;
}
/// <summary>
/// 判断指定的类型 <paramref name="type"/> 是否是指定泛型类型的子类型,或实现了指定泛型接口。
/// </summary>
/// <param name="type">需要测试的类型。</param>
/// <param name="generic">泛型接口类型,传入 typeof(IXxx)</param>
/// <returns>如果是泛型接口的子类型,则返回 true,否则返回 false。</returns>
public static bool HasImplementedRawGeneric(this Type type, Type generic)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (generic == null) throw new ArgumentNullException(nameof(generic));
// 测试接口。
var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType);
if (isTheRawGenericType) return true;
// 测试类型。
while (type != null && type != typeof(object))
{
isTheRawGenericType = IsTheRawGenericType(type);
if (isTheRawGenericType) return true;
type = type.BaseType;
}
// 没有找到任何匹配的接口或类型。
return false;
// 测试某个类型是否是指定的原始接口。
bool IsTheRawGenericType(Type test)
=> generic == (test.IsGenericType ? test.GetGenericTypeDefinition() : test);
}
public static object GetPropValue(object obj, String name)
{
foreach (String part in name.Split('.'))
{
if (obj == null)
{
return null;
}
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null)
{
return null;
}
obj = info.GetValue(obj, null);
}
return obj;
}
public static T GetPropValue<T>(object obj, String name)
{
object retval = GetPropValue(obj, name);
if (retval == null)
{
return default(T);
}
// throws InvalidCastException if types are incompatible
return (T)retval;
}
public static object GetFieldValue(object obj, String name)
{
foreach (String part in name.Split('.'))
{
if (obj == null)
{
return null;
}
Type type = obj.GetType();
FieldInfo info = type.GetField(part);
if (info == null)
{
return null;
}
obj = info.GetValue(obj);
}
return obj;
}
public static T GetFieldValue<T>(object obj, String name)
{
object retval = GetFieldValue(obj, name);
if (retval == null)
{
return default(T);
}
// throws InvalidCastException if types are incompatible
return (T)retval;
}
public static T CreateInstance<T>(string typeName, params object[] args) where T : class
{
if (string.IsNullOrEmpty(typeName) || typeName == "")
{
typeName = typeof(T).FullName;
}
Type type = Type.GetType(typeName);
if (type == null)
{
Debug.LogError($"Get Type from type name:{type} return null");
return null;
}
if (typeof(T).IsAssignableFrom(type) == false)
{
Debug.LogError($"{type} is not assignable from {typeof(T).Name}");
return null;
}
T instance = Activator.CreateInstance(type, args) as T;
return instance;
}
public static object CopyObject(object srcObject)
{
Type type = srcObject.GetType();
object newObject = Activator.CreateInstance(type);
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public
| BindingFlags.Instance | BindingFlags.Static);
for (int i = 0; i < fields.Length; ++i)
{
FieldInfo fi = fields[i];
fi.SetValue(newObject, fi.GetValue(srcObject));
}
PropertyInfo[] props = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public
| BindingFlags.Instance | BindingFlags.Static);
for (int i = 0; i < props.Length; ++i)
{
PropertyInfo fi = props[i];
fi.SetValue(newObject, fi.GetValue(srcObject));
}
return newObject;
}
public static bool CopyObject(object srcObject, object targetObject)
{
Type srcType = srcObject.GetType();
Type targetType = targetObject.GetType();
if (srcType != targetType)
{
return false;
}
FieldInfo[] fields = srcType.GetFields(BindingFlags.NonPublic | BindingFlags.Public
| BindingFlags.Instance | BindingFlags.Static);
for (int i = 0; i < fields.Length; ++i)
{
FieldInfo fi = fields[i];
fi.SetValue(targetObject, fi.GetValue(srcObject));
}
PropertyInfo[] props = srcType.GetProperties(BindingFlags.NonPublic | BindingFlags.Public
| BindingFlags.Instance | BindingFlags.Static);
for (int i = 0; i < props.Length; ++i)
{
PropertyInfo fi = props[i];
fi.SetValue(targetObject, fi.GetValue(srcObject));
}
return true;
}
public static object CopyFields(object src)
{
Type t = src.GetType();
object res = Activator.CreateInstance(t);
FieldInfo[] list = t.GetFields();
for (int i = 0; i < list.Length; ++i)
{
FieldInfo fi = list[i];
fi.SetValue(res, fi.GetValue(src));
}
return res;
}
public static T ParseEnum<T>(string value, T defaultValue) where T : struct
{
T v;
return Enum.TryParse<T>(value, true, out v) ? v : defaultValue;
}
public static void ThrowError(string fmt, params object[] args)
{
string msg = String.Format(fmt, args);
throw new Exception(msg);
}
/// <summary>
/// 计算字符串的MD5值
/// </summary>
public static string GetStringMd5(string source)
{
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
byte[] data = System.Text.Encoding.UTF8.GetBytes(source);
byte[] md5Data = md5.ComputeHash(data, 0, data.Length);
md5.Clear();
string destString = "";
for (int i = 0; i < md5Data.Length; i++)
{
destString += System.Convert.ToString(md5Data[i], 16).PadLeft(2, '0');
}
destString = destString.PadLeft(32, '0');
return destString.ToUpper();
}
/// <summary>
/// 计算文件的MD5值
/// </summary>
public static string GetFileMd5(string file)
{
try
{
FileStream fs = new FileStream(file, FileMode.Open);
System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(fs);
fs.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
return sb.ToString();
}
catch (Exception ex)
{
throw new Exception("md5file() fail, error:" + ex.Message);
}
}
public static string GetBytesMd5(byte[] bytes)
{
System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(bytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
return sb.ToString();
}
public static long GetTimeTick()
{
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalMilliseconds);
}
public static double XWorldGetTime()
{
return Time.realtimeSinceStartupAsDouble;
}
public static bool IsPossibleSimulator()
{
if (Application.isEditor)
{
return false;
}
bool isSimulator = false;
isSimulator = isSimulator || SystemInfo.graphicsDeviceName == "Netease";
isSimulator = isSimulator || SystemInfo.deviceModel == "Netease MuMu";
string processorType = SystemInfo.processorType.ToLower();
isSimulator = isSimulator || processorType.Contains("intel") || processorType.Contains("amd");
return isSimulator;
}
// 格式化文件大小
private static string[] sSuffix = { "B", "KB", "MB", "GB", "TB" };
public static string FormatBytesStr(long bytes)
{
float size = Convert.ToSingle(bytes);
int sufIdx = 0;
for(sufIdx = 0; sufIdx < sSuffix.Length; sufIdx++)
{
if(size >= 1024)
{
size = Mathf.Ceil(size / 1024);
continue;
}
else
{
break;
}
}
string format = size + sSuffix[sufIdx];
return format;
}
public static bool Contains<T>(T[] array, T value) where T : class
{
for (int i = 0; i < array.Length; ++i)
{
if (array[i] == value)
{
return true;
}
}
return false;
}
public static bool Contains(string[] array, string value)
{
for (int i = 0; i < array.Length; ++i)
{
if (array[i].CompareTo(value) == 0)
{
return true;
}
}
return false;
}
public static int RandomRangeToInt(int min,int max)
{
return UnityEngine.Random.Range(min,max);
}
public static void CreateMulDirectory(string path)
{
if (Directory.Exists(path))
return;
int n = path.LastIndexOf('/');
if(n == -1)
n = path.LastIndexOf('\\');
if (n == -1)
{
Directory.CreateDirectory(path);
return;
}
string subpath = path.Substring(0, n);
if (Directory.Exists(subpath))
{
Directory.CreateDirectory(path);
}
else
{
CreateMulDirectory(subpath);
Directory.CreateDirectory(path);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13fe870d13c1c494980ac1fb9062d173
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,740 @@
using System;
using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using DG.Tweening;
using XLua;
namespace XGame
{
[LuaCallCSharp]
public static partial class Utility
{
private static readonly List<Transform> mTempList = new List<Transform>();
public static void DestroyUnityObject(UnityEngine.Object unityObject)
{
if (unityObject != null)
{
UnityEngine.Object.Destroy(unityObject);
}
}
public static void DestroyUnityObjectImmediate(UnityEngine.Object unityObject)
{
if (unityObject != null)
{
UnityEngine.Object.DestroyImmediate(unityObject);
}
}
public static GameObject CreateGameObject(GameObject parent, string name)
{
GameObject go = new GameObject(name);
if (parent != null)
{
go.transform.parent = parent.transform;
}
NormalizeTransform(go.transform);
return go;
}
public static GameObject NewGameObeject(string name)
{
return new GameObject(name);
}
//gameobject ops
//create a child gameobject under the parent ,then normilize its transform
public static GameObject CreateChildGameObject(GameObject parent, string name)
{
GameObject go = new GameObject(name);
if (parent != null)
{
go.transform.parent = parent.transform;
}
NormalizeTransform(go.transform);
return go;
}
public static bool DestroyChildren(Transform transform)
{
if (transform == null || transform.childCount == 0)
{
return false;
}
List<Transform> childTransformList = new List<Transform>();
foreach (Transform child in transform)
{
childTransformList.Add(child);
}
for (int i = 0; i < childTransformList.Count; i++)
{
childTransformList[i].parent = null;
DestroyUnityObject(childTransformList[i].gameObject);
}
childTransformList.Clear();
return true;
}
//normalize an transform
public static void NormalizeTransform(Transform transform)
{
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
}
public static T FindObjectInScene<T>() where T : UnityEngine.Object
{
T component = UnityEngine.Object.FindObjectOfType<T>() as T;
return component;
}
public static bool IsActive(this GameObject gameObject)
{
if (gameObject == null)
{
return false;
}
if (gameObject.activeSelf)
{
return gameObject.transform.localScale == Vector3.one;
}
return false;
}
public static void SetActiveByScale(this Component component, bool active)
{
if (null == component) { return; }
component.transform.localScale = active ? Vector3.one : Vector3.zero;
}
public static void SetActiveByScale(this GameObject component, bool active)
{
if (null == component) { return; }
component.transform.localScale = active ? Vector3.one : Vector3.zero;
}
public static void SetActiveEx(this Transform t, bool active)
{
if (null == t) { return; }
t.gameObject.SetActiveEx(active);
}
public static void SetActiveEx(this Component component, bool active)
{
if (null == component) { return; }
if (component.gameObject.activeSelf != active)
{
component.gameObject.SetActive(active);
}
}
public static bool SetActiveEx(this GameObject gameObject, bool active)
{
if (gameObject == null)
{
return false;
}
if (gameObject.activeSelf != active)
{
gameObject.SetActive(active);
}
return true;
}
/// <summary>
/// 通过缩放大小 隐藏
/// </summary>
/// <param name="gameObject"></param>
/// <param name="active"></param>
/// <returns></returns>
public static bool SetActiveExTwo(this GameObject gameObject, bool active)
{
if (gameObject == null)
{
return false;
}
//强制active
if (gameObject.activeSelf == false)
{
gameObject.SetActive(true);
}
if (active)
{
gameObject.transform.localScale = Vector3.one;
}
else
{
gameObject.transform.localScale = Vector3.zero;
}
return true;
}
public static bool SetActiveExTwo(this Component comp, bool active)
{
if (comp == null)
{
return false;
}
SetActiveExTwo(comp.gameObject, active);
return true;
}
public static bool SetActive3(this Transform t, bool active)
{
return SetActive3(t.gameObject, active);
}
/// <summary>
/// 通过把位置挪出屏幕外 隐藏
/// </summary>
/// <param name="gameObject"></param>
/// <param name="active"></param>
/// <returns></returns>
public static bool SetActive3(this GameObject gameObject, bool active)
{
if (gameObject == null)
{
return false;
}
//gameObject.SetActiveEx(active);
//CanvasGroup g = gameObject.GetComponent<CanvasGroup>();
//if (g == null)
//{
// g = gameObject.AddComponent<CanvasGroup>();
//}
//g.alpha = active ? 1 : 0;
//g.interactable = active;
//g.blocksRaycasts = active;
//Canvas[] canvases = gameObject.GetComponentsInChildren<Canvas>();
//int toLayer = LayerMask.NameToLayer(active ? "UI" : "InVisible");
//for (int i = 0; i < canvases.Length; i++)
//{
// canvases[i].gameObject.layer = toLayer;
//}
//MeshRenderer[] mrs = gameObject.GetComponentsInChildren<MeshRenderer>();
//for (int i = 0; i < mrs.Length; i++)
//{
// mrs[i].enabled = active;
//}
//UiEffect[] effects = gameObject.GetComponentsInChildren<UiEffect>();
//for (int i = 0; i < effects.Length; i++)
//{
// effects[i].gameObject.layer = toLayer;
//}
//gameObject.SetActiveEx(active);
Vector3 pos = gameObject.transform.localPosition;
gameObject.transform.localPosition = new Vector3(pos.x, pos.y, active ? 0 : 99999);
return true;
}
/// <summary>
/// 是否隐藏了
/// </summary>
/// <param name="gameObject"></param>
/// <returns></returns>
public static bool IsHide(this GameObject gameObject)
{
return !gameObject.activeSelf || gameObject.transform.localScale == Vector3.zero || gameObject.transform.localPosition.z >= 99999;
}
public static bool IsHide(this Transform t)
{
return !t.gameObject.activeSelf || t.localScale == Vector3.zero || t.localPosition.z >= 99999;
}
public static GameObject[] GetChildren(this GameObject gameObject)
{
List<GameObject> children = new List<GameObject>();
foreach (Transform child in gameObject.transform)
{
children.Add(child.gameObject);
}
return children.ToArray();
}
public static GameObject FindGameObject(this GameObject go, string name)
{
return !go ? null : go.transform.FindGameObject(name);
}
public static GameObject FindGameObject(this Transform tf, string name)
{
Transform targetTransform = tf.FindTransform(name);
return targetTransform ? targetTransform.gameObject : null;
}
public static Transform FindTransform(this GameObject go, string name)
{
return !go ? null : go.transform.FindTransform(name);
}
public static Transform FindTransform(this Transform tf, string name)
{
if (!tf)
{
return null;
}
if (string.IsNullOrEmpty(name))
{
return null;
}
mTempList.Clear();
mTempList.Add(tf);
int index = 0;
while (mTempList.Count > index)
{
Transform transform = mTempList[index++];
for (int i = 0; i < transform.childCount; ++i)
{
Transform childTransform = transform.GetChild(i);
if (childTransform.name == name)
{
return childTransform;
}
mTempList.Add(childTransform);
}
}
return null;
}
public static T FindComponent<T>(this GameObject go, string name) where T : Component
{
if (go == null)
{
return null;
}
return go.transform.FindComponent<T>(name);
}
public static T FindComponent<T>(this Transform tf, string name) where T : Component
{
if (tf == null)
{
return null;
}
Transform target = tf.FindTransform(name);
if (target == null)
{
return null;
}
return target.GetComponent<T>();
}
public static Component GetOrAddComponent(this GameObject GO, string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
Debug.LogError("GetOrAddComponent typeName is null");
return null;
}
Type componentType = Utility.GetTypeByTypeName(typeName);
if (componentType == null)
{
Debug.LogError($"GetOrAddComponent componentType is null, typeName:{typeName}");
return null;
}
Component result = null;
if (!GO) return result;
result = GO.GetComponent(componentType);
if (!result)
{
result = GO.AddComponent(componentType);
}
return result;
}
public static T GetOrAddComponent<T>(this GameObject GO) where T : UnityEngine.Component
{
T result = null;
if (!GO) return result;
result = GO.GetComponent<T>();
if (!result)
{
result = GO.AddComponent<T>();
}
return result;
}
public static GameObject[] GetAllChildrenGameObjects(this GameObject go)
{
Transform[] allTrans = go.GetComponentsInChildren<Transform>();
List<GameObject> allGOs = new List<GameObject>();
for (int i = 0; i < allTrans.Length; ++i)
{
allGOs.Add(allTrans[i].gameObject);
}
return allGOs.ToArray();
}
public static Transform FindSocketTransform(GameObject go, string socketName)
{
if (string.IsNullOrEmpty(socketName))
{
return null;
}
Transform socketTrans = null;
Transform[] childrenTrans = go.transform.GetComponentsInChildren<Transform>(true);
int count = childrenTrans != null ? childrenTrans.Length : 0;
for (int i = 0; i < count; i++)
{
if (childrenTrans[i] != null && childrenTrans[i].name == socketName)
{
socketTrans = childrenTrans[i];
break;
}
}
return socketTrans;
}
public static void SetPosition(this GameObject go, float x, float y, float z)
{
if (go == null)
{
Debug.LogError("Utility SetPosition go is null");
return;
}
go.transform.position = new Vector3(x, y, z);
}
public static void SetPosition(this Transform trans, float x, float y, float z)
{
if (trans == null)
{
Debug.LogError("Utility SetPosition trans is null");
return;
}
trans.position = new Vector3(x, y, z);
}
public static void SetLocalPosition(this GameObject go, float x, float y, float z)
{
if (go == null)
{
Debug.LogError("Utility SetLocalPosition go is null");
return;
}
go.transform.localPosition = new Vector3(x, y, z);
}
public static void SetLocalPosition(this Transform trans, float x, float y, float z)
{
if (trans == null)
{
Debug.LogError("Utility SetLocalPosition trans is null");
return;
}
trans.localPosition = new Vector3(x, y, z);
}
public static void SetRotation(this GameObject go, float x, float y, float z, float w)
{
if (go == null)
{
Debug.LogError("Utility SetRotation go is null");
return;
}
go.transform.rotation = new Quaternion(x, y, z, w);
}
public static void SetRotation(this Transform trans, float x, float y, float z, float w)
{
if (trans == null)
{
Debug.LogError("Utility SetRotation trans is null");
return;
}
trans.rotation = new Quaternion(x, y, z, w);
}
public static void SetLocalRotation(this GameObject go, float x, float y, float z, float w)
{
if (go == null)
{
Debug.LogError("Utility SetLocalRotation go is null");
return;
}
go.transform.localRotation = new Quaternion(x, y, z, w);
}
public static void SetLocalRotation(this Transform trans, float x, float y, float z, float w)
{
if (trans == null)
{
Debug.LogError("Utility SetLocalRotation trans is null");
return;
}
trans.localRotation = new Quaternion(x, y, z, w);
}
public static void SetLocalEulerAngles(this GameObject go, float x, float y, float z)
{
if (go == null)
{
Debug.LogError("Utility SetLocalEulerAngles go is null");
return;
}
go.transform.localEulerAngles = new Vector3(x, y, z);
}
public static void SetLocalEulerAngles(this Transform trans, float x, float y, float z)
{
if (trans == null)
{
Debug.LogError("Utility SetLocalEulerAngles trans is null");
return;
}
trans.localEulerAngles = new Vector3(x, y, z);
}
public static void SetLocalScale(this GameObject go, float x, float y, float z)
{
if (go == null)
{
Debug.LogError("Utility SetLocalScale go is null");
return;
}
go.transform.localScale = new Vector3(x, y, z);
}
public static void SetLocalScale(this Transform trans, float x, float y, float z)
{
if (trans == null)
{
Debug.LogError("Utility SetLocalScale trans is null");
return;
}
trans.localScale = new Vector3(x, y, z);
}
public static void SetLocalScale(this Transform trans, float scale)
{
if (trans == null)
{
Debug.LogError("Utility SetLocalScale trans is null");
return;
}
trans.localScale = new Vector3(scale, scale, scale);
}
public static DG.Tweening.Core.TweenerCore<Vector3, Vector3, DG.Tweening.Plugins.Options.VectorOptions> DOLocalMove(this Transform trans,float x,float y,float z,float duration,bool snapping = false)
{
return trans.DOLocalMove(new Vector3(x, y, z), duration, snapping);
}
#region
public static float GetPosition(this Transform t, out float y, out float z)
{
Vector3 position = t.position;
y = position.y;
z = position.z;
return t.position.x;
}
public static float GetPosition(this GameObject t, out float y, out float z)
{
return t.transform.GetPosition(out y,out z);
}
public static float GetLocalPosition(this Transform t, out float y, out float z)
{
Vector3 position = t.localPosition;
y = position.y;
z = position.z;
return position.x;
}
public static float GetLocalPosition(this GameObject t, out float y, out float z)
{
return t.transform.GetLocalPosition(out y, out z);
}
public static void GetLocalScale(this Transform t, out float x, out float y, out float z)
{
x = t.localScale.x;
y = t.localScale.y;
z = t.localScale.z;
}
public static void GetLocalScale(this GameObject t, out float x, out float y, out float z)
{
t.transform.GetLocalScale(out x,out y, out z);
}
public static void GetRotation(this Transform t, out float x, out float y, out float z, out float w)
{
x = t.rotation.x;
y = t.rotation.y;
z = t.rotation.z;
w = t.rotation.w;
}
public static void GetRotation(this GameObject o, out float x, out float y, out float z, out float w)
{
o.transform.GetRotation(out x, out y, out z, out w);
}
public static void GetLocalRotation(this Transform t, out float x, out float y, out float z, out float w)
{
x = t.localRotation.x;
y = t.localRotation.y;
z = t.localRotation.z;
w = t.localRotation.w;
}
public static void GetLocalRotation(this GameObject o, out float x, out float y, out float z, out float w)
{
o.transform.GetLocalRotation(out x, out y, out z, out w);
}
public static void GetEulerAngle(this Transform t, out float x, out float y, out float z)
{
x = t.eulerAngles.x;
y = t.eulerAngles.y;
z = t.eulerAngles.z;
}
public static void GetEulerAngle(this GameObject o, out float x, out float y, out float z)
{
o.transform.GetEulerAngle(out x, out y, out z);
}
public static void GetLocalEulerAngle(this Transform t, out float x, out float y, out float z)
{
x = t.localEulerAngles.x;
y = t.localEulerAngles.y;
z = t.localEulerAngles.z;
}
public static void GetLocalEulerAngle(this GameObject o, out float x, out float y, out float z)
{
o.transform.GetLocalEulerAngle(out x, out y, out z);
}
public static void GetAnchoredPosition(this RectTransform t, out float x, out float y)
{
x = t.anchoredPosition.x;
y = t.anchoredPosition.y;
}
public static bool IsActive(this Transform t)
{
return t.gameObject.IsActive();
}
#endregion
public static bool Exist(this UnityEngine.Object obj)
{
return obj;
}
public static bool IsUnityObject(object unityObject)
{
if (unityObject != null)
{
if (unityObject is UnityEngine.Object)
{
return true;
}
}
return false;
}
public static Component GetComponent(this Component component, string asm, string typeName)
{
var gen_ret = component.GetComponent(typeName);
if (gen_ret != null)
return gen_ret;
System.Type componentType = Utility.GetTypeByTypeName(typeName);
if (componentType == null)
{
#if ENV_INTRANET
Debug.LogWarning($"GetType('{typeName}') Failed!! componentName:{component.name}");
#endif
return null;
}
gen_ret = component.GetComponent(componentType);
if (gen_ret == null)
{
#if ENV_INTRANET
Debug.LogWarning($"{component.name}.GetComponent({typeName}) Failed!!");
#endif
}
return gen_ret;
}
public static Component GetComponent(this GameObject gameObject, string asm, string typeName)
{
var gen_ret = gameObject.GetComponent(typeName);
if (gen_ret != null)
return gen_ret;
System.Type componentType = Utility.GetTypeByTypeName(typeName);
if (componentType == null)
{
#if ENV_INTRANET
Debug.LogWarning($"GetType('{typeName}') Failed!! objectName:{gameObject.name}");
#endif
return null;
}
gen_ret = gameObject.GetComponent(componentType);
if (gen_ret == null)
{
#if ENV_INTRANET
Debug.LogWarning($"Go.GetComponent({typeName}) Failed!!");
#endif
}
return gen_ret;
}
//防止代码剪裁
//private static void PreventCodeClipping(GameObject obj)
//{
// Empty4Raycast a = obj.GetComponent<Empty4Raycast>();
//}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 33dfc8d859349a34d850ed113ea7dd81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
namespace XWorld
{
//协程等待对话框关闭
public class WaitForDialogClose : CustomYieldInstruction
{
private bool _isClosed;
public override bool keepWaiting => !_isClosed; // 对话框未关闭时持续等待
public void OnDialogClosed() => _isClosed = true;
}
public class WaitDialogController
{
// 关闭事件(网页4的消息管理思想)
public UnityEvent OnClose;
WaitForDialogClose waitObj;
public void CloseDialog()
{
waitObj?.OnDialogClosed();
}
IEnumerator ShowDialogCoroutine(Button CloseButton)
{
CloseButton.onClick.AddListener(() =>
{
CloseDialog();
});
waitObj = new WaitForDialogClose();
yield return waitObj; // 协程在此处暂停,直到对话框关闭
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e75e501321e37b1419f7efa1b3baabe8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+105
View File
@@ -0,0 +1,105 @@
#if UNITY_WEBGL
using WeChatWASM;
#endif
public static class XData
{
//存储
public static void DeleteAll()
{
#if UNITY_WEBGL
if (GlobalData.bWX)
WX.StorageDeleteAllSync();
else
PlayerPrefs.DeleteAll();
#endif
UnityEngine.PlayerPrefs.DeleteAll();
}
public static void DeleteKey(string key)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
WX.StorageDeleteKeySync(key);
else
PlayerPrefs.DeleteKey(key);
#endif
UnityEngine.PlayerPrefs.DeleteKey(key);
}
public static float GetFloat(string key, float defaultValue = 0)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
return WX.StorageGetFloatSync(key, defaultValue);
else
PlayerPrefs.GetFloat(key, defaultValue);
#endif
return UnityEngine.PlayerPrefs.GetFloat(key, defaultValue);
}
public static int GetInt(string key, int defaultValue = 0)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
return WX.StorageGetIntSync(key, defaultValue);
else
return PlayerPrefs.GetInt(key, defaultValue);
#endif
return UnityEngine.PlayerPrefs.GetInt(key, defaultValue);
}
public static string GetString(string key, string defaultValue = "")
{
#if UNITY_WEBGL
if (GlobalData.bWX)
return WX.StorageGetStringSync(key, defaultValue);
else
return PlayerPrefs.GetString(key, defaultValue);
#endif
return UnityEngine.PlayerPrefs.GetString(key, defaultValue);
}
public static bool HasKey(string key)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
return WX.StorageHasKeySync(key);
else
return PlayerPrefs.HasKey(key);
#endif
return UnityEngine.PlayerPrefs.HasKey(key);
}
public static void Save()
{
if (!GlobalData.bWX)
UnityEngine.PlayerPrefs.Save();
//微信不需要
}
public static void SetFloat(string key, float value)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
WX.StorageSetFloatSync(key,value);
else
PlayerPrefs.SetFloat(key, value);
#endif
UnityEngine.PlayerPrefs.SetFloat(key, value);
}
public static void SetInt(string key, int value)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
WX.StorageSetIntSync(key, value);
else
PlayerPrefs.SetInt(key, value);
#endif
UnityEngine.PlayerPrefs.SetInt(key, value);
}
public static void SetString(string key, string value)
{
#if UNITY_WEBGL
if (GlobalData.bWX)
WX.StorageSetStringSync(key, value);
else
PlayerPrefs.SetString(key, value);
#endif
UnityEngine.PlayerPrefs.SetString(key, value);
}
}

Some files were not shown because too many files have changed in this diff Show More