Add hot-update boot pipeline, UI bundles/atlas assets, and design docs.
Ignore AIClient/Library and AIClient/HybridCLRData permanently; include HybridCLR/YooAsset project setup and UI tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "AOT.Runtime",
|
||||
"rootNamespace": "AOT",
|
||||
"references": [
|
||||
"YooAsset",
|
||||
"HybridCLR.Runtime",
|
||||
"UnityEngine.UI"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 359f9ae67ab8957448c75b56a2a6a0aa
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf0509e7c014a1e478f821ac118ec9ad
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using YooAsset;
|
||||
|
||||
namespace AOT.Boot
|
||||
{
|
||||
/// <summary>
|
||||
/// 壳层传给热更入口的运行时上下文。
|
||||
/// </summary>
|
||||
public sealed class BootContext
|
||||
{
|
||||
public string AppVersion;
|
||||
public string ResVersion;
|
||||
public string CdnRoot;
|
||||
public string PackageName;
|
||||
public ResourcePackage Package;
|
||||
public string WsUrlTemplate;
|
||||
|
||||
public string BuildWsUrl(string pid, string token = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(WsUrlTemplate))
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
return $"wss://game.xworld.ren/ws?pid={pid}";
|
||||
return $"wss://game.xworld.ren/ws?pid={pid}&token={UnityEngine.Networking.UnityWebRequest.EscapeURL(token)}";
|
||||
}
|
||||
|
||||
var url = WsUrlTemplate.Replace("{pid}", pid ?? string.Empty);
|
||||
if (url.Contains("{token}"))
|
||||
url = url.Replace("{token}", UnityEngine.Networking.UnityWebRequest.EscapeURL(token ?? string.Empty));
|
||||
else if (!string.IsNullOrEmpty(token))
|
||||
url += (url.Contains("?") ? "&" : "?") + "token=" + UnityEngine.Networking.UnityWebRequest.EscapeURL(token);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a57e0242ddf4ff48af5d8e8a763e105
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
using AOT.Boot;
|
||||
using AOT.HotUpdate;
|
||||
using AOT.Patch;
|
||||
using AOT.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AOT.Boot
|
||||
{
|
||||
/// <summary>
|
||||
/// 壳层启动入口:版本检查 → 资源补丁 → HybridCLR/热更入口。
|
||||
/// </summary>
|
||||
public sealed class BootDriver : MonoBehaviour
|
||||
{
|
||||
[SerializeField] BootSettings settings = new BootSettings();
|
||||
|
||||
BootPatchView _view;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Application.targetFrameRate = 60;
|
||||
Screen.sleepTimeout = SleepTimeout.NeverSleep;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (Screen.orientation != ScreenOrientation.Portrait)
|
||||
Screen.orientation = ScreenOrientation.Portrait;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
_view = BootPatchView.Create();
|
||||
StartCoroutine(Bootstrap());
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator Bootstrap()
|
||||
{
|
||||
BootContext ctx = null;
|
||||
string error = null;
|
||||
|
||||
var runner = new PatchRunner(this, settings, _view);
|
||||
yield return runner.Run((c, e) =>
|
||||
{
|
||||
ctx = c;
|
||||
error = e;
|
||||
});
|
||||
|
||||
if (!string.IsNullOrEmpty(error) || ctx == null)
|
||||
{
|
||||
_view?.SetError(error ?? "启动失败");
|
||||
Debug.LogError($"[Boot] {error}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus("加载热更代码…");
|
||||
_view?.SetProgress(0.97f);
|
||||
|
||||
string loadError = null;
|
||||
yield return HotUpdateLoader.LoadAndStart(ctx, settings, e => loadError = e);
|
||||
|
||||
if (!string.IsNullOrEmpty(loadError))
|
||||
{
|
||||
_view?.SetError(loadError);
|
||||
Debug.LogError($"[Boot] {loadError}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetProgress(1f);
|
||||
_view?.SetStatus("热更已启动");
|
||||
Debug.Log($"[Boot] OK app={ctx.AppVersion} res={ctx.ResVersion} cdn={ctx.CdnRoot}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d11b543accc52448b82d773c022254c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using UnityEngine;
|
||||
using YooAsset;
|
||||
|
||||
namespace AOT.Boot
|
||||
{
|
||||
/// <summary>
|
||||
/// Boot 可序列化配置(挂在 BootDriver 上)。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class BootSettings
|
||||
{
|
||||
[Tooltip("壳版本,对应提审包 appVersion")]
|
||||
public string AppVersion = "1.0.0";
|
||||
|
||||
[Tooltip("CDN 总控 version.json")]
|
||||
public string VersionUrl = "https://www.xworld.ren/wechat/version.json";
|
||||
|
||||
[Tooltip("CDN 根(version.json 未下发 cdnRoot 时回退;含 Package 名)")]
|
||||
public string DefaultCdnRoot = "https://www.xworld.ren/wechat/1.0.0/DefaultPackage";
|
||||
|
||||
[Tooltip("YooAsset 默认包名")]
|
||||
public string PackageName = "DefaultPackage";
|
||||
|
||||
[Tooltip("热更 DLL 在包内的定位地址")]
|
||||
public string HotUpdateDllLocation = "HotUpdate.Runtime";
|
||||
|
||||
[Tooltip("WebSocket 模板,{pid}/{token} 会被替换")]
|
||||
public string WsUrlTemplate = "wss://game.xworld.ren/ws?pid={pid}&token={token}";
|
||||
|
||||
[Tooltip("运行模式;编辑器默认 EditorSimulateMode;M1 Offline/Host 验证时切换")]
|
||||
public EPlayMode PlayMode = EPlayMode.EditorSimulateMode;
|
||||
|
||||
[Tooltip("编辑器下跳过 YooAsset 补丁,直接反射加载已编译热更程序集(M0 默认开;M1 验证补丁时关掉)")]
|
||||
public bool EditorDirectHotUpdate = true;
|
||||
|
||||
[Tooltip("version.json 拉取失败时是否允许使用 StreamingAssets 本地回退")]
|
||||
public bool AllowStreamingVersionFallback = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2c6317a2a3e50f4ca2bed34aafc0291
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5891db66601b1c140aaa02117bba81c5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AOT.Boot;
|
||||
using UnityEngine;
|
||||
using YooAsset;
|
||||
#if !UNITY_EDITOR
|
||||
using HybridCLR;
|
||||
#endif
|
||||
|
||||
namespace AOT.HotUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载热更程序集并调用稳定入口 HotUpdate.Entry.Start(BootContext)。
|
||||
/// </summary>
|
||||
public static class HotUpdateLoader
|
||||
{
|
||||
const string EntryTypeName = "HotUpdate.Entry";
|
||||
const string EntryMethodName = "Start";
|
||||
|
||||
public static IEnumerator LoadAndStart(BootContext ctx, BootSettings settings, Action<string> onError)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (settings.EditorDirectHotUpdate || ctx.Package == null)
|
||||
{
|
||||
var err = InvokeEntry(FindLoadedAssembly("HotUpdate.Runtime"), ctx);
|
||||
if (err != null) onError?.Invoke(err);
|
||||
yield break;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ctx.Package == null)
|
||||
{
|
||||
onError?.Invoke("ResourcePackage 为空,无法加载热更 DLL");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var handle = ctx.Package.LoadAssetAsync<RawFileObject>(settings.HotUpdateDllLocation);
|
||||
yield return handle;
|
||||
if (handle.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onError?.Invoke($"加载热更 DLL 失败: {handle.Error}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var raw = handle.GetAssetObject<RawFileObject>();
|
||||
if (raw == null)
|
||||
{
|
||||
onError?.Invoke("热更 DLL RawFileObject 为空");
|
||||
yield break;
|
||||
}
|
||||
|
||||
byte[] dllBytes = raw.GetBytes();
|
||||
handle.Release();
|
||||
|
||||
if (dllBytes == null || dllBytes.Length < 64)
|
||||
{
|
||||
onError?.Invoke($"热更 DLL 字节异常 length={dllBytes?.Length ?? 0}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log($"[HotUpdateLoader] RawFile loaded location={settings.HotUpdateDllLocation} bytes={dllBytes.Length}");
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// 编辑器域内已加载同名程序集,不能重复 Assembly.Load;验证 RawFile 后走反射入口。
|
||||
{
|
||||
var err = InvokeEntry(FindLoadedAssembly("HotUpdate.Runtime"), ctx);
|
||||
if (err != null) onError?.Invoke(err);
|
||||
yield break;
|
||||
}
|
||||
#else
|
||||
// AOT 补充元数据:后续由 HybridCLR Generate 产物批量加载。
|
||||
// HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(metaBytes, HomologousImageMode.SuperSet);
|
||||
|
||||
Assembly asm;
|
||||
try
|
||||
{
|
||||
asm = Assembly.Load(dllBytes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
onError?.Invoke($"Assembly.Load 失败: {e.Message}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var invokeErr = InvokeEntry(asm, ctx);
|
||||
if (invokeErr != null)
|
||||
onError?.Invoke(invokeErr);
|
||||
#endif
|
||||
}
|
||||
|
||||
static Assembly FindLoadedAssembly(string name)
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => string.Equals(a.GetName().Name, name, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
static string InvokeEntry(Assembly asm, BootContext ctx)
|
||||
{
|
||||
if (asm == null)
|
||||
return "未找到热更程序集 HotUpdate.Runtime";
|
||||
|
||||
var type = asm.GetType(EntryTypeName, throwOnError: false);
|
||||
if (type == null)
|
||||
return $"未找到类型 {EntryTypeName}";
|
||||
|
||||
var method = type.GetMethod(EntryMethodName, BindingFlags.Public | BindingFlags.Static);
|
||||
if (method == null)
|
||||
return $"未找到方法 {EntryTypeName}.{EntryMethodName}";
|
||||
|
||||
try
|
||||
{
|
||||
method.Invoke(null, new object[] { ctx });
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var inner = e is TargetInvocationException tie && tie.InnerException != null
|
||||
? tie.InnerException
|
||||
: e;
|
||||
return $"调用热更入口失败: {inner.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6796557e85f4374e95c149ba3b871e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f909e1ffa39b29349b083bc3a520263d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using YooAsset;
|
||||
|
||||
namespace AOT.Patch
|
||||
{
|
||||
/// <summary>
|
||||
/// YooAsset 远端地址查询(CDN)。
|
||||
/// </summary>
|
||||
public sealed class CdnRemoteService : IRemoteService
|
||||
{
|
||||
readonly string _primary;
|
||||
readonly string _fallback;
|
||||
|
||||
public CdnRemoteService(string primaryRoot, string fallbackRoot = null)
|
||||
{
|
||||
_primary = TrimSlash(primaryRoot);
|
||||
_fallback = string.IsNullOrEmpty(fallbackRoot) ? _primary : TrimSlash(fallbackRoot);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetRemoteUrls(string fileName)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
$"{_primary}/{fileName}",
|
||||
$"{_fallback}/{fileName}",
|
||||
};
|
||||
}
|
||||
|
||||
static string TrimSlash(string root)
|
||||
{
|
||||
if (string.IsNullOrEmpty(root)) return string.Empty;
|
||||
return root.TrimEnd('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89730891313aa084e9adf58e6f37542f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using AOT.Boot;
|
||||
using AOT.UI;
|
||||
using UnityEngine;
|
||||
using YooAsset;
|
||||
|
||||
namespace AOT.Patch
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动补丁:version 门控 → YooAsset 初始化/更新 → 回调。
|
||||
/// </summary>
|
||||
public sealed class PatchRunner
|
||||
{
|
||||
readonly MonoBehaviour _host;
|
||||
readonly BootSettings _settings;
|
||||
readonly BootPatchView _view;
|
||||
|
||||
public PatchRunner(MonoBehaviour host, BootSettings settings, BootPatchView view)
|
||||
{
|
||||
_host = host;
|
||||
_settings = settings;
|
||||
_view = view;
|
||||
}
|
||||
|
||||
public IEnumerator Run(Action<BootContext, string> onDone)
|
||||
{
|
||||
_view?.SetStatus("检查版本…");
|
||||
_view?.SetProgress(0.05f);
|
||||
|
||||
VersionInfo version = null;
|
||||
string versionError = null;
|
||||
yield return new VersionClient(_settings.VersionUrl, _settings.AllowStreamingVersionFallback)
|
||||
.Fetch((info, err) =>
|
||||
{
|
||||
version = info;
|
||||
versionError = err;
|
||||
});
|
||||
|
||||
if (version == null)
|
||||
{
|
||||
// 无 CDN 时仍允许编辑器直通;真机 Host/Web 则失败。
|
||||
if (!CanContinueWithoutVersion())
|
||||
{
|
||||
onDone?.Invoke(null, $"获取 version.json 失败: {versionError}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
version = new VersionInfo
|
||||
{
|
||||
appVersionMin = _settings.AppVersion,
|
||||
appVersionMax = _settings.AppVersion,
|
||||
resVersion = "local",
|
||||
cdnRoot = _settings.DefaultCdnRoot,
|
||||
packages = new[] { _settings.PackageName },
|
||||
};
|
||||
_view?.SetStatus($"版本回退本地 ({versionError})");
|
||||
}
|
||||
else
|
||||
{
|
||||
_view?.SetStatus($"远程版本 {version.resVersion}");
|
||||
}
|
||||
|
||||
if (VersionInfo.CompareVersion(_settings.AppVersion, version.appVersionMin) < 0)
|
||||
{
|
||||
onDone?.Invoke(null, $"壳版本过低,请更新微信小游戏包(当前 {_settings.AppVersion} < 最低 {version.appVersionMin})");
|
||||
yield break;
|
||||
}
|
||||
|
||||
string cdnRoot = string.IsNullOrEmpty(version.cdnRoot) ? _settings.DefaultCdnRoot : version.cdnRoot;
|
||||
var ctx = new BootContext
|
||||
{
|
||||
AppVersion = _settings.AppVersion,
|
||||
ResVersion = version.resVersion,
|
||||
CdnRoot = cdnRoot,
|
||||
PackageName = _settings.PackageName,
|
||||
WsUrlTemplate = _settings.WsUrlTemplate,
|
||||
};
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (_settings.EditorDirectHotUpdate)
|
||||
{
|
||||
_view?.SetStatus("编辑器直通热更(跳过资源补丁)");
|
||||
_view?.SetProgress(0.9f);
|
||||
onDone?.Invoke(ctx, null);
|
||||
yield break;
|
||||
}
|
||||
#endif
|
||||
|
||||
_view?.SetStatus("初始化资源系统…");
|
||||
_view?.SetProgress(0.15f);
|
||||
|
||||
if (!YooAssets.IsInitialized)
|
||||
YooAssets.Initialize();
|
||||
|
||||
if (!YooAssets.TryGetPackage(_settings.PackageName, out var package))
|
||||
package = YooAssets.CreatePackage(_settings.PackageName);
|
||||
ctx.Package = package;
|
||||
|
||||
var playMode = ResolvePlayMode();
|
||||
var initOp = CreateInitializeOperation(package, playMode, cdnRoot);
|
||||
if (initOp == null)
|
||||
{
|
||||
onDone?.Invoke(null, $"不支持的 PlayMode: {playMode}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return initOp;
|
||||
if (initOp.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"YooAsset 初始化失败: {initOp.Error}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus("请求资源版本…");
|
||||
_view?.SetProgress(0.35f);
|
||||
var versionOp = package.RequestPackageVersionAsync();
|
||||
yield return versionOp;
|
||||
if (versionOp.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"请求 PackageVersion 失败: {versionOp.Error}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus($"更新清单 {versionOp.PackageVersion}");
|
||||
_view?.SetProgress(0.5f);
|
||||
var manifestOp = package.LoadPackageManifestAsync(new LoadPackageManifestOptions(versionOp.PackageVersion, 60));
|
||||
yield return manifestOp;
|
||||
if (manifestOp.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"更新清单失败: {manifestOp.Error}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus("下载资源…");
|
||||
var downloader = package.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3));
|
||||
if (downloader.TotalDownloadCount > 0)
|
||||
{
|
||||
downloader.DownloadProgressChanged += info =>
|
||||
{
|
||||
_view?.SetProgress(0.5f + info.Progress * 0.4f);
|
||||
_view?.SetStatus($"下载 {info.CurrentDownloadCount}/{info.TotalDownloadCount}");
|
||||
};
|
||||
downloader.StartDownload();
|
||||
yield return downloader;
|
||||
if (downloader.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"下载失败: {downloader.Error}");
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
_view?.SetProgress(0.95f);
|
||||
_view?.SetStatus("资源就绪");
|
||||
onDone?.Invoke(ctx, null);
|
||||
}
|
||||
|
||||
bool CanContinueWithoutVersion()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
return _settings.EditorDirectHotUpdate;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
EPlayMode ResolvePlayMode()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
return _settings.PlayMode;
|
||||
#elif UNITY_WEBGL
|
||||
return EPlayMode.WebPlayMode;
|
||||
#else
|
||||
return _settings.PlayMode == EPlayMode.EditorSimulateMode
|
||||
? EPlayMode.HostPlayMode
|
||||
: _settings.PlayMode;
|
||||
#endif
|
||||
}
|
||||
|
||||
InitializePackageOperation CreateInitializeOperation(ResourcePackage package, EPlayMode playMode, string cdnRoot)
|
||||
{
|
||||
if (playMode == EPlayMode.EditorSimulateMode)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
var buildResult = EditorSimulateBuildInvoker.Build(_settings.PackageName, (int)EBundleType.VirtualAssetBundle);
|
||||
var options = new EditorSimulateModeOptions
|
||||
{
|
||||
EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(buildResult.PackageRootDirectory)
|
||||
};
|
||||
return package.InitializePackageAsync(options);
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (playMode == EPlayMode.OfflinePlayMode)
|
||||
{
|
||||
var options = new OfflinePlayModeOptions
|
||||
{
|
||||
BuiltinFileSystemParameters = FileSystemParameters.CreateDefaultBuiltinFileSystemParameters()
|
||||
};
|
||||
return package.InitializePackageAsync(options);
|
||||
}
|
||||
|
||||
if (playMode == EPlayMode.HostPlayMode)
|
||||
{
|
||||
var remote = new CdnRemoteService(cdnRoot);
|
||||
var options = new HostPlayModeOptions
|
||||
{
|
||||
BuiltinFileSystemParameters = FileSystemParameters.CreateDefaultBuiltinFileSystemParameters(),
|
||||
CacheFileSystemParameters = FileSystemParameters.CreateDefaultSandboxFileSystemParameters(remote)
|
||||
};
|
||||
options.BuiltinFileSystemParameters.AddParameter(EFileSystemParameter.CopyBuiltinPackageManifest, true);
|
||||
return package.InitializePackageAsync(options);
|
||||
}
|
||||
|
||||
if (playMode == EPlayMode.WebPlayMode)
|
||||
{
|
||||
// 微信小游戏正式接入时替换为 WechatFileSystem;M0 先用默认 WebNetwork。
|
||||
var remote = new CdnRemoteService(cdnRoot);
|
||||
var options = new WebPlayModeOptions
|
||||
{
|
||||
WebNetworkFileSystemParameters = FileSystemParameters.CreateDefaultWebNetworkFileSystemParameters(remote, true)
|
||||
};
|
||||
return package.InitializePackageAsync(options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57535d94c1b98984da5c887d5f55657a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace AOT.Patch
|
||||
{
|
||||
public sealed class VersionClient
|
||||
{
|
||||
readonly string _url;
|
||||
readonly bool _allowStreamingFallback;
|
||||
|
||||
public VersionClient(string url, bool allowStreamingFallback)
|
||||
{
|
||||
_url = url;
|
||||
_allowStreamingFallback = allowStreamingFallback;
|
||||
}
|
||||
|
||||
public IEnumerator Fetch(Action<VersionInfo, string> onDone)
|
||||
{
|
||||
string error = null;
|
||||
VersionInfo info = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(_url))
|
||||
{
|
||||
using (var req = UnityWebRequest.Get(_url))
|
||||
{
|
||||
req.timeout = 15;
|
||||
yield return req.SendWebRequest();
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
bool failed = req.result != UnityWebRequest.Result.Success;
|
||||
#else
|
||||
bool failed = req.isNetworkError || req.isHttpError;
|
||||
#endif
|
||||
if (!failed)
|
||||
{
|
||||
info = VersionInfo.FromJson(req.downloadHandler.text);
|
||||
if (info == null)
|
||||
error = "version.json parse failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
error = req.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null && _allowStreamingFallback)
|
||||
{
|
||||
var localPath = Path.Combine(Application.streamingAssetsPath, "cdn", "version.json");
|
||||
yield return ReadLocalText(localPath, text =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
info = VersionInfo.FromJson(text);
|
||||
if (info != null)
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDone?.Invoke(info, error);
|
||||
}
|
||||
|
||||
static IEnumerator ReadLocalText(string path, Action<string> onDone)
|
||||
{
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
using (var req = UnityWebRequest.Get(path))
|
||||
{
|
||||
yield return req.SendWebRequest();
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
bool failed = req.result != UnityWebRequest.Result.Success;
|
||||
#else
|
||||
bool failed = req.isNetworkError || req.isHttpError;
|
||||
#endif
|
||||
onDone?.Invoke(failed ? null : req.downloadHandler.text);
|
||||
}
|
||||
#else
|
||||
if (File.Exists(path))
|
||||
onDone?.Invoke(File.ReadAllText(path));
|
||||
else
|
||||
onDone?.Invoke(null);
|
||||
yield break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c255f1d996335441ae6dccb3c3dc352
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AOT.Patch
|
||||
{
|
||||
[Serializable]
|
||||
public class VersionInfo
|
||||
{
|
||||
public string appVersionMin = "1.0.0";
|
||||
public string appVersionMax = "1.0.0";
|
||||
public string resVersion = "0";
|
||||
public string cdnRoot = "https://www.xworld.ren/wechat/1.0.0";
|
||||
public string[] packages = { "DefaultPackage" };
|
||||
public bool forceUpdate;
|
||||
public string notice = "";
|
||||
|
||||
public static VersionInfo FromJson(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
return JsonUtility.FromJson<VersionInfo>(json);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[VersionInfo] parse failed: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int CompareVersion(string a, string b)
|
||||
{
|
||||
if (string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b)) return 0;
|
||||
if (string.IsNullOrEmpty(a)) return -1;
|
||||
if (string.IsNullOrEmpty(b)) return 1;
|
||||
|
||||
var pa = a.Split('.');
|
||||
var pb = b.Split('.');
|
||||
int n = Mathf.Max(pa.Length, pb.Length);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int va = i < pa.Length && int.TryParse(pa[i], out var xa) ? xa : 0;
|
||||
int vb = i < pb.Length && int.TryParse(pb[i], out var xb) ? xb : 0;
|
||||
if (va != vb) return va.CompareTo(vb);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 925a013a115d7694f8429c8444591d46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb0e5690dd591654997ad561372bd3a1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AOT.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动更新条(运行时生成,竖屏安全区友好)。
|
||||
/// </summary>
|
||||
public sealed class BootPatchView : MonoBehaviour
|
||||
{
|
||||
Text _status;
|
||||
Image _fill;
|
||||
|
||||
public static BootPatchView Create()
|
||||
{
|
||||
var root = new GameObject("BootPatchUI");
|
||||
DontDestroyOnLoad(root);
|
||||
|
||||
var canvasGo = new GameObject("Canvas");
|
||||
canvasGo.transform.SetParent(root.transform, false);
|
||||
var canvas = canvasGo.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 1000;
|
||||
canvasGo.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
var scaler = canvasGo.GetComponent<CanvasScaler>();
|
||||
scaler.referenceResolution = new Vector2(1080, 1920);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
canvasGo.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var bg = CreateUiObject("Background", canvasGo.transform);
|
||||
var bgImage = bg.AddComponent<Image>();
|
||||
bgImage.color = new Color(0.08f, 0.09f, 0.12f, 1f);
|
||||
StretchFull(bg.GetComponent<RectTransform>());
|
||||
|
||||
var title = CreateUiObject("Title", canvasGo.transform);
|
||||
var titleText = title.AddComponent<Text>();
|
||||
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
titleText.text = "AIGame";
|
||||
titleText.alignment = TextAnchor.MiddleCenter;
|
||||
titleText.fontSize = 64;
|
||||
titleText.color = Color.white;
|
||||
var titleRt = title.GetComponent<RectTransform>();
|
||||
titleRt.anchorMin = new Vector2(0.1f, 0.62f);
|
||||
titleRt.anchorMax = new Vector2(0.9f, 0.72f);
|
||||
titleRt.offsetMin = Vector2.zero;
|
||||
titleRt.offsetMax = Vector2.zero;
|
||||
|
||||
var statusGo = CreateUiObject("Status", canvasGo.transform);
|
||||
var status = statusGo.AddComponent<Text>();
|
||||
status.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
status.text = "启动中…";
|
||||
status.alignment = TextAnchor.MiddleCenter;
|
||||
status.fontSize = 28;
|
||||
status.color = new Color(0.85f, 0.88f, 0.92f, 1f);
|
||||
var statusRt = statusGo.GetComponent<RectTransform>();
|
||||
statusRt.anchorMin = new Vector2(0.1f, 0.42f);
|
||||
statusRt.anchorMax = new Vector2(0.9f, 0.5f);
|
||||
statusRt.offsetMin = Vector2.zero;
|
||||
statusRt.offsetMax = Vector2.zero;
|
||||
|
||||
var barBg = CreateUiObject("ProgressBg", canvasGo.transform);
|
||||
var barBgImage = barBg.AddComponent<Image>();
|
||||
barBgImage.color = new Color(1f, 1f, 1f, 0.12f);
|
||||
var barBgRt = barBg.GetComponent<RectTransform>();
|
||||
barBgRt.anchorMin = new Vector2(0.15f, 0.36f);
|
||||
barBgRt.anchorMax = new Vector2(0.85f, 0.39f);
|
||||
barBgRt.offsetMin = Vector2.zero;
|
||||
barBgRt.offsetMax = Vector2.zero;
|
||||
|
||||
var barFill = CreateUiObject("ProgressFill", barBg.transform);
|
||||
var fill = barFill.AddComponent<Image>();
|
||||
fill.color = new Color(0.35f, 0.75f, 0.55f, 1f);
|
||||
var fillRt = barFill.GetComponent<RectTransform>();
|
||||
fillRt.anchorMin = Vector2.zero;
|
||||
fillRt.anchorMax = new Vector2(0f, 1f);
|
||||
fillRt.offsetMin = Vector2.zero;
|
||||
fillRt.offsetMax = Vector2.zero;
|
||||
fillRt.pivot = new Vector2(0f, 0.5f);
|
||||
|
||||
var view = root.AddComponent<BootPatchView>();
|
||||
view._status = status;
|
||||
view._fill = fill;
|
||||
return view;
|
||||
}
|
||||
|
||||
public void SetStatus(string text)
|
||||
{
|
||||
if (_status != null)
|
||||
_status.text = text ?? string.Empty;
|
||||
}
|
||||
|
||||
public void SetProgress(float normalized01)
|
||||
{
|
||||
if (_fill == null) return;
|
||||
float v = Mathf.Clamp01(normalized01);
|
||||
var rt = _fill.rectTransform;
|
||||
rt.anchorMax = new Vector2(v, 1f);
|
||||
}
|
||||
|
||||
public void SetError(string message)
|
||||
{
|
||||
SetStatus(message);
|
||||
if (_fill != null)
|
||||
_fill.color = new Color(0.85f, 0.3f, 0.3f, 1f);
|
||||
}
|
||||
|
||||
static GameObject CreateUiObject(string name, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name, typeof(RectTransform));
|
||||
go.transform.SetParent(parent, false);
|
||||
return go;
|
||||
}
|
||||
|
||||
static void StretchFull(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b952baba9265e524c844c7aa15bf0ebb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user