Files
2026-07-10 10:24:29 +08:00

647 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}