Land Explorer in HomeCamp with boot loading, journal UI, and CDN publish tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
"references": [
|
||||
"YooAsset",
|
||||
"HybridCLR.Runtime",
|
||||
"UnityEngine.UI"
|
||||
"UnityEngine.UI",
|
||||
"Unity.TextMeshPro"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace AOT.Boot
|
||||
public sealed class BootDriver : MonoBehaviour
|
||||
{
|
||||
[SerializeField] BootSettings settings = new BootSettings();
|
||||
[SerializeField] BootPatchView loadingPrefab;
|
||||
|
||||
BootPatchView _view;
|
||||
|
||||
@@ -27,7 +28,7 @@ namespace AOT.Boot
|
||||
|
||||
void Start()
|
||||
{
|
||||
_view = BootPatchView.Create();
|
||||
_view = BootPatchView.Create(loadingPrefab);
|
||||
StartCoroutine(Bootstrap());
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ namespace AOT.Boot
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetStatus("加载热更代码…");
|
||||
_view?.SetStatus("正在准备冒险…");
|
||||
_view?.SetProgress(0.97f);
|
||||
|
||||
string loadError = null;
|
||||
@@ -63,12 +64,11 @@ namespace AOT.Boot
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetProgress(1f);
|
||||
_view?.SetStatus("热更已启动");
|
||||
Debug.Log($"[Boot] OK app={ctx.AppVersion} res={ctx.ResVersion} pack={ctx.PackVersion} cdn={ctx.CdnRoot}");
|
||||
|
||||
// Prefabs/atlases load asynchronously; two frames are not a readiness guarantee.
|
||||
_view?.SetStatus("加载主界面…");
|
||||
_view?.SetStatus("正在布置营地…");
|
||||
_view?.SetProgress(.98f);
|
||||
float uiDeadline = Time.realtimeSinceStartup + 120f;
|
||||
while (!ctx.MainUiReady && string.IsNullOrEmpty(ctx.MainUiError) && Time.realtimeSinceStartup < uiDeadline)
|
||||
yield return null;
|
||||
@@ -80,6 +80,8 @@ namespace AOT.Boot
|
||||
yield break;
|
||||
}
|
||||
yield return null;
|
||||
_view?.SetStatus("准备就绪,即将出发");
|
||||
_view?.SetProgress(1f);
|
||||
_view?.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace AOT.Patch
|
||||
|
||||
public IEnumerator Run(Action<BootContext, string> onDone)
|
||||
{
|
||||
_view?.SetStatus("检查版本…");
|
||||
_view?.SetStatus("正在检查更新…");
|
||||
_view?.SetProgress(0.05f);
|
||||
|
||||
VersionInfo version = null;
|
||||
@@ -55,11 +55,11 @@ namespace AOT.Patch
|
||||
cdnRoot = _settings.DefaultCdnRoot,
|
||||
packages = new[] { _settings.PackageName },
|
||||
};
|
||||
_view?.SetStatus($"版本回退本地 ({versionError})");
|
||||
Debug.LogWarning($"[Boot] Local version fallback: {versionError}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_view?.SetStatus($"远程版本 资源 {version.resVersion} 热更 {version.packVersion}");
|
||||
Debug.Log($"[Boot] Remote art={version.resVersion} pack={version.packVersion}");
|
||||
}
|
||||
|
||||
if (VersionInfo.CompareVersion(_settings.AppVersion, version.appVersionMin) < 0)
|
||||
@@ -85,14 +85,14 @@ namespace AOT.Patch
|
||||
#if UNITY_EDITOR
|
||||
if (_settings.EditorDirectHotUpdate)
|
||||
{
|
||||
_view?.SetStatus("编辑器直通热更(跳过资源补丁)");
|
||||
_view?.SetStatus("正在准备冒险…");
|
||||
_view?.SetProgress(0.9f);
|
||||
onDone?.Invoke(ctx, null);
|
||||
yield break;
|
||||
}
|
||||
#endif
|
||||
|
||||
_view?.SetStatus("初始化资源系统…");
|
||||
_view?.SetStatus("正在准备资源…");
|
||||
_view?.SetProgress(0.15f);
|
||||
|
||||
if (!YooAssets.IsInitialized)
|
||||
@@ -125,15 +125,15 @@ namespace AOT.Patch
|
||||
yield break;
|
||||
}
|
||||
|
||||
_view?.SetProgress(0.5f);
|
||||
_view?.SetStatus("下载资源…");
|
||||
_view?.SetProgress(0.4f);
|
||||
_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}");
|
||||
_view?.SetProgress(0.4f + info.Progress * 0.15f);
|
||||
_view?.SetStatus($"正在下载资源… {info.CurrentDownloadCount}/{info.TotalDownloadCount}");
|
||||
};
|
||||
downloader.StartDownload();
|
||||
yield return downloader;
|
||||
@@ -155,7 +155,7 @@ namespace AOT.Patch
|
||||
var companionRoot = cdnRoot.TrimEnd('/');
|
||||
var slash = companionRoot.LastIndexOf('/');
|
||||
companionRoot = companionRoot.Substring(0, slash + 1) + packageName;
|
||||
_view?.SetStatus($"初始化资源包 {packageName}…");
|
||||
_view?.SetStatus("正在准备营地…");
|
||||
if (companion.InitializeStatus != EOperationStatus.Succeeded)
|
||||
{
|
||||
var companionInit = CreateInitializeOperation(companion, playMode, companionRoot);
|
||||
@@ -181,7 +181,12 @@ namespace AOT.Patch
|
||||
var companionDownload = companion.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3));
|
||||
if (companionDownload.TotalDownloadCount > 0)
|
||||
{
|
||||
_view?.SetStatus($"下载资源包 {packageName}…");
|
||||
_view?.SetStatus("正在下载营地资源…");
|
||||
companionDownload.DownloadProgressChanged += info =>
|
||||
{
|
||||
_view?.SetProgress(.55f + info.Progress * .35f);
|
||||
_view?.SetStatus($"正在下载资源… {info.CurrentDownloadCount}/{info.TotalDownloadCount}");
|
||||
};
|
||||
companionDownload.StartDownload();
|
||||
yield return companionDownload;
|
||||
if (companionDownload.Status != EOperationStatus.Succeeded)
|
||||
@@ -209,7 +214,7 @@ namespace AOT.Patch
|
||||
var packageVersion = pinnedVersion;
|
||||
if (string.IsNullOrEmpty(packageVersion))
|
||||
{
|
||||
_view?.SetStatus($"请求{label}版本…");
|
||||
_view?.SetStatus("正在检查资源…");
|
||||
var versionOp = package.RequestPackageVersionAsync();
|
||||
yield return versionOp;
|
||||
if (versionOp.Status != EOperationStatus.Succeeded)
|
||||
@@ -220,7 +225,7 @@ namespace AOT.Patch
|
||||
packageVersion = versionOp.PackageVersion;
|
||||
}
|
||||
|
||||
_view?.SetStatus($"更新{label}清单 {packageVersion}");
|
||||
_view?.SetStatus("正在检查资源…");
|
||||
var manifestOp = package.LoadPackageManifestAsync(new LoadPackageManifestOptions(packageVersion, 60));
|
||||
yield return manifestOp;
|
||||
if (manifestOp.Status != EOperationStatus.Succeeded)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AOT.UI
|
||||
{
|
||||
/// <summary>Fill the screen without stretching the portrait illustration.</summary>
|
||||
[RequireComponent(typeof(Image))]
|
||||
public sealed class BootArtworkCover : MonoBehaviour
|
||||
{
|
||||
void OnEnable() => Fit();
|
||||
void LateUpdate() => Fit();
|
||||
void OnRectTransformDimensionsChange() => Fit();
|
||||
|
||||
void Fit()
|
||||
{
|
||||
var image = GetComponent<Image>();
|
||||
if (image == null || image.sprite == null || transform.parent == null) return;
|
||||
var parent = transform.parent as RectTransform;
|
||||
if (parent == null || parent.rect.width <= 0 || parent.rect.height <= 0) return;
|
||||
var rect = (RectTransform)transform;
|
||||
float aspect = image.sprite.rect.width / image.sprite.rect.height;
|
||||
float width = Mathf.Max(parent.rect.width, parent.rect.height * aspect);
|
||||
Vector2 size = new Vector2(width, width / aspect);
|
||||
rect.anchorMin = rect.anchorMax = rect.pivot = new Vector2(.5f, .5f);
|
||||
rect.anchoredPosition = Vector2.zero;
|
||||
if ((rect.sizeDelta - size).sqrMagnitude > .01f) rect.sizeDelta = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6c80e70df189694cb6c4d032daa6154
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,132 +1,78 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AOT.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动更新条(运行时生成,竖屏安全区友好)。
|
||||
/// </summary>
|
||||
/// <summary>Local JSON-authored loading screen, available before CDN initialization.</summary>
|
||||
public sealed class BootPatchView : MonoBehaviour
|
||||
{
|
||||
Text _status;
|
||||
Image _fill;
|
||||
TMP_Text _status, _percentage;
|
||||
RectTransform _fill;
|
||||
CanvasGroup _group;
|
||||
float _target, _shown;
|
||||
|
||||
public static BootPatchView Create()
|
||||
public static BootPatchView Create(BootPatchView prefab)
|
||||
{
|
||||
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;
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError("[Boot] Loading prefab missing; run BuildBootLoading.Build before exporting.");
|
||||
return null;
|
||||
}
|
||||
var view = Instantiate(prefab);
|
||||
DontDestroyOnLoad(view.gameObject);
|
||||
return view;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_status = transform.Find("LoadingCard/Txt_Status").GetComponent<TMP_Text>();
|
||||
_percentage = transform.Find("LoadingCard/Txt_Percentage").GetComponent<TMP_Text>();
|
||||
_fill = transform.Find("LoadingCard/ProgressTrack/FillClip").GetComponent<RectTransform>();
|
||||
_group = GetComponent<CanvasGroup>();
|
||||
SetStatus("正在准备冒险…");
|
||||
DrawProgress();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
_shown = Mathf.MoveTowards(_shown, _target, Time.unscaledDeltaTime * .65f);
|
||||
DrawProgress();
|
||||
}
|
||||
|
||||
void DrawProgress()
|
||||
{
|
||||
if (_fill != null) _fill.anchorMax = new Vector2(_shown, 1f);
|
||||
if (_percentage != null) _percentage.text = Mathf.FloorToInt(_shown * 100f) + "%";
|
||||
}
|
||||
|
||||
public void SetStatus(string text)
|
||||
{
|
||||
if (_status != null)
|
||||
_status.text = text ?? string.Empty;
|
||||
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 SetProgress(float normalized01) => _target = Mathf.Max(_target, Mathf.Clamp01(normalized01));
|
||||
|
||||
public void SetError(string message)
|
||||
{
|
||||
SetStatus(message);
|
||||
if (_fill != null)
|
||||
_fill.color = new Color(0.85f, 0.3f, 0.3f, 1f);
|
||||
Debug.LogError("[Boot] " + message);
|
||||
SetStatus("加载失败,请重新进入游戏");
|
||||
if (_status != null) _status.color = new Color(.72f, .22f, .18f);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
public void Hide() => StartCoroutine(FadeOut());
|
||||
|
||||
System.Collections.IEnumerator FadeOut()
|
||||
{
|
||||
_shown = _target = 1f; DrawProgress();
|
||||
yield return new WaitForSecondsRealtime(.12f);
|
||||
for (float time = 0; time < .25f; time += Time.unscaledDeltaTime)
|
||||
{
|
||||
if (_group != null) _group.alpha = 1f - time / .25f;
|
||||
yield return null;
|
||||
}
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void DestroyUi()
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public void DestroyUi() => Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace AOT.UI
|
||||
{
|
||||
RectTransform _rect;
|
||||
Vector2 _position;
|
||||
Vector2 _offsetMin, _offsetMax;
|
||||
bool _captured;
|
||||
static readonly float[] NativeInsets = new float[4];
|
||||
static int _frame = -1;
|
||||
@@ -29,13 +30,19 @@ namespace AOT.UI
|
||||
{
|
||||
_rect = (RectTransform)transform;
|
||||
_position = _rect.anchoredPosition;
|
||||
_offsetMin = _rect.offsetMin;
|
||||
_offsetMax = _rect.offsetMax;
|
||||
_captured = true;
|
||||
Apply(CurrentInsets());
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (_captured && _rect != null) _rect.anchoredPosition = _position;
|
||||
if (_captured && _rect != null)
|
||||
{
|
||||
_rect.offsetMin = _offsetMin;
|
||||
_rect.offsetMax = _offsetMax;
|
||||
}
|
||||
_captured = false;
|
||||
}
|
||||
|
||||
@@ -44,6 +51,14 @@ namespace AOT.UI
|
||||
void Apply(Vector4 inset)
|
||||
{
|
||||
if (!_captured || !(_rect.parent is RectTransform parent)) return;
|
||||
// Full-screen authored containers shrink to the safe rectangle. Edge
|
||||
// layouts retain the original offset behavior used by the camp HUD.
|
||||
if (_rect.anchorMin == Vector2.zero && _rect.anchorMax == Vector2.one)
|
||||
{
|
||||
_rect.offsetMin = _offsetMin + new Vector2(parent.rect.width * inset.x, parent.rect.height * inset.y);
|
||||
_rect.offsetMax = _offsetMax - new Vector2(parent.rect.width * inset.z, parent.rect.height * inset.w);
|
||||
return;
|
||||
}
|
||||
Vector2 delta = Vector2.zero;
|
||||
if (Mathf.Approximately(_rect.anchorMin.y, 1) && Mathf.Approximately(_rect.anchorMax.y, 1))
|
||||
delta.y = -parent.rect.height * inset.w;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9db0a60c7f3c25f4f90a7ebc68d220ae
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a54075564ff57941b3f7f9f6bf9492c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b6aac82fe2582d4e89a18c7a0b61d1a
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user