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
+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();
}
}