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:
udx
2026-11-04 16:18:01 +08:00
co-authored by Cursor
parent 9eec364f30
commit 80678ec499
405 changed files with 48053 additions and 420302 deletions
@@ -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:
+52 -106
View File
@@ -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);
}
}
+16 -1
View File
@@ -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;