Add prototype gameplay loop with character/battlefield assets and server game APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,32 @@ namespace HotUpdate.UI
|
||||
Button _close;
|
||||
Action _onOk;
|
||||
Action _onCancel;
|
||||
RectTransform _okRect;
|
||||
Vector2 _okAnchorMin, _okAnchorMax, _okPivot, _okPosition;
|
||||
ScrollRect _messageScroll;
|
||||
|
||||
public struct ViewState
|
||||
{
|
||||
public string Title, Message;
|
||||
public bool Visible, ShowCancel;
|
||||
public Action OnOk, OnCancel;
|
||||
}
|
||||
|
||||
public ViewState CaptureState() => new ViewState
|
||||
{
|
||||
Title = _title != null ? _title.text : string.Empty,
|
||||
Message = _message != null ? _message.text : string.Empty,
|
||||
Visible = gameObject.activeSelf,
|
||||
ShowCancel = _cancel != null && _cancel.gameObject.activeSelf,
|
||||
OnOk = _onOk,
|
||||
OnCancel = _onCancel
|
||||
};
|
||||
|
||||
public void RestoreState(ViewState state)
|
||||
{
|
||||
Show(state.Title, state.Message, state.OnOk, state.OnCancel, state.ShowCancel);
|
||||
if (!state.Visible) Hide();
|
||||
}
|
||||
|
||||
public void Bind()
|
||||
{
|
||||
@@ -27,6 +53,15 @@ namespace HotUpdate.UI
|
||||
_ok = FindBtn("Btn_Ok");
|
||||
_cancel = FindBtn("Btn_Cancel");
|
||||
_close = FindBtn("Btn_Close");
|
||||
if (_ok != null)
|
||||
{
|
||||
_okRect = _ok.GetComponent<RectTransform>();
|
||||
_okAnchorMin = _okRect.anchorMin;
|
||||
_okAnchorMax = _okRect.anchorMax;
|
||||
_okPivot = _okRect.pivot;
|
||||
_okPosition = _okRect.anchoredPosition;
|
||||
}
|
||||
_messageScroll = GetComponentInChildren<ScrollRect>(true);
|
||||
|
||||
Wire(_ok, () => { _onOk?.Invoke(); Hide(); });
|
||||
Wire(_cancel, () => { _onCancel?.Invoke(); Hide(); });
|
||||
@@ -45,6 +80,18 @@ namespace HotUpdate.UI
|
||||
_onOk = onOk;
|
||||
_onCancel = onCancel;
|
||||
if (_cancel != null) _cancel.gameObject.SetActive(showCancel);
|
||||
if (_okRect != null)
|
||||
{
|
||||
_okRect.anchorMin = showCancel ? _okAnchorMin : new Vector2(0.5f, _okAnchorMin.y);
|
||||
_okRect.anchorMax = showCancel ? _okAnchorMax : new Vector2(0.5f, _okAnchorMax.y);
|
||||
_okRect.pivot = showCancel ? _okPivot : new Vector2(0.5f, _okPivot.y);
|
||||
_okRect.anchoredPosition = showCancel ? _okPosition : new Vector2(0, _okPosition.y);
|
||||
}
|
||||
if (_messageScroll != null && _messageScroll.content != null)
|
||||
{
|
||||
_messageScroll.StopMovement();
|
||||
_messageScroll.content.anchoredPosition = Vector2.zero;
|
||||
}
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnityEngine.UI;
|
||||
namespace HotUpdate.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UI_MainHUD 运行时绑定。
|
||||
/// 营地主界面:绑定 JSON Prefab 入口、网络状态和断线重连。
|
||||
/// </summary>
|
||||
public sealed class MainHudView : MonoBehaviour
|
||||
{
|
||||
@@ -19,6 +19,7 @@ namespace HotUpdate.UI
|
||||
Button _reconnectBtn;
|
||||
|
||||
public event Action OnReconnectClicked;
|
||||
public event Action<string> OnHomeActionClicked;
|
||||
|
||||
public void Bind()
|
||||
{
|
||||
@@ -35,6 +36,16 @@ namespace HotUpdate.UI
|
||||
_reconnectBtn.onClick.AddListener(() => OnReconnectClicked?.Invoke());
|
||||
}
|
||||
ShowReconnect(false);
|
||||
foreach (var button in GetComponentsInChildren<Button>(true))
|
||||
{
|
||||
if (button == _reconnectBtn) continue;
|
||||
var key = button.name.StartsWith("Btn_Nav", StringComparison.Ordinal)
|
||||
? button.name.Substring(7)
|
||||
: button.name.StartsWith("Btn_", StringComparison.Ordinal) ? button.name.Substring(4) : null;
|
||||
if (key == null) continue;
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => OnHomeActionClicked?.Invoke(key));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStatus(string text)
|
||||
@@ -47,6 +58,54 @@ namespace HotUpdate.UI
|
||||
if (_pid != null) _pid.text = pid > 0 ? $"pid={pid}" : "pid=-";
|
||||
}
|
||||
|
||||
public void SetPrototypeState(Gameplay.PrototypeSave state, Gameplay.PrototypeConfig config = null, long serverNow = 0)
|
||||
{
|
||||
SetText("Txt_Level", "Lv. " + state.level);
|
||||
SetText("Txt_Coin", state.coins.ToString("N0"));
|
||||
SetText("Txt_Diamond", "0");
|
||||
SetText("Txt_Pinballs", state.balls.ToString());
|
||||
SetText("Txt_TaskCaption", "教学目标");
|
||||
SetText("Txt_Task", state.goalClaimed ? "教学目标已完成" : state.cleared ? "领取首通目标奖励" : state.shots == 0 ? "发射第一枚弹珠" : state.level < 2 ? "共享训练至 Lv.2" : "完成教学关卡 1-1");
|
||||
SetText("Txt_AdventureHint", state.cleared ? "首通已完成 · 可重复挑战" : "首通另获糖果 40 · 不消耗体力");
|
||||
if (config != null && state.schema >= 2)
|
||||
{
|
||||
var stage = config.Stage(state.UnlockedStage); bool complete = state.clearedStages[stage.id - 1] == 1;
|
||||
SetText("Txt_Chapter", config.chapter);
|
||||
SetText("Txt_Stage", stage.code + " · " + stage.name);
|
||||
SetText("Txt_Reward_coin", complete ? "已领取" : stage.reward.coins.ToString());
|
||||
SetText("Txt_Reward_pinball", complete ? "已领取" : stage.reward.balls.ToString());
|
||||
SetText("Txt_AdventureHint", complete ? "本轮五关已完成 · 可免费重试" : "首通另获糖果 " + stage.reward.candy + " · 不消耗体力");
|
||||
}
|
||||
foreach (var child in GetComponentsInChildren<Transform>(true))
|
||||
if (child.name.StartsWith("Dot_", StringComparison.Ordinal)) child.gameObject.SetActive(false);
|
||||
if (state.mails != null)
|
||||
foreach (var child in GetComponentsInChildren<Transform>(true))
|
||||
if (child.name == "Dot_Mail") child.gameObject.SetActive(Array.Exists(state.mails, m => !m.expired && serverNow < m.expiresAt && (!m.read || !m.claimed && m.HasAttachment)));
|
||||
if (config?.rewards?.chests != null && state.chests != null)
|
||||
foreach (var child in GetComponentsInChildren<Transform>(true))
|
||||
if (child.name == "Dot_Rewards") child.gameObject.SetActive(state.chestDailyClaimed == 0 || Array.Exists(state.chests, n => n > 0));
|
||||
if (state.schema >= 5 && config?.daily != null)
|
||||
{
|
||||
int ready = 0; bool signin = false;
|
||||
for (int i = 0; i < 3; i++) if (state.dailyClaimed[i] == 0 && state.dailyProgress[i] >= config.daily.tasks[i].target) ready++;
|
||||
for (int i = 0; i < state.signinDays; i++) if (state.signinClaimed[i] == 0) signin = true;
|
||||
foreach (var child in GetComponentsInChildren<Transform>(true))
|
||||
if (child.name == "Dot_Tasks") child.gameObject.SetActive(ready > 0 || state.cleared && !state.goalClaimed);
|
||||
else if (child.name == "Dot_Daily") child.gameObject.SetActive(signin);
|
||||
if (state.goalClaimed)
|
||||
{
|
||||
SetText("Txt_TaskCaption", "每日目标");
|
||||
SetText("Txt_Task", ready > 0 ? ready + "项奖励可领取" : "查看今日任务进度");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetText(string name, string value)
|
||||
{
|
||||
var label = FindTmp(name);
|
||||
if (label != null) label.text = value;
|
||||
}
|
||||
|
||||
public void ShowReconnect(bool visible, string message = null)
|
||||
{
|
||||
if (_reconnectPanel != null) _reconnectPanel.SetActive(visible);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -14,28 +15,46 @@ namespace HotUpdate.UI
|
||||
readonly Dictionary<string, GameObject> _opened = new Dictionary<string, GameObject>(StringComparer.Ordinal);
|
||||
Transform _root;
|
||||
UiPrefabLoader _loader;
|
||||
readonly HashSet<string> _loading = new HashSet<string>(StringComparer.Ordinal);
|
||||
public string Theme { get; private set; } = UiThemeCatalog.Candy;
|
||||
|
||||
public void Init(Transform root, UiPrefabLoader loader)
|
||||
public void Init(Transform root, UiPrefabLoader loader, string theme = UiThemeCatalog.Candy)
|
||||
{
|
||||
Instance = this;
|
||||
_root = root;
|
||||
_loader = loader ?? new UiPrefabLoader();
|
||||
Theme = UiThemeCatalog.Normalize(theme);
|
||||
}
|
||||
|
||||
public GameObject Open(string prefabName, int siblingIndex = -1)
|
||||
public IEnumerator OpenAsync(string prefabName, Action<GameObject> onOpened, int siblingIndex = -1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefabName)) return null;
|
||||
if (string.IsNullOrEmpty(prefabName)) { onOpened?.Invoke(null); yield break; }
|
||||
while (_loading.Contains(prefabName)) yield return null;
|
||||
if (_opened.TryGetValue(prefabName, out var existing) && existing != null)
|
||||
{
|
||||
existing.SetActive(true);
|
||||
return existing;
|
||||
onOpened?.Invoke(existing);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var prefab = _loader.Load(prefabName);
|
||||
_loading.Add(prefabName);
|
||||
GameObject prefab = null;
|
||||
try
|
||||
{
|
||||
var assetName = UiThemeCatalog.Resolve(prefabName, Theme);
|
||||
yield return _loader.LoadAsync(assetName, loaded => prefab = loaded);
|
||||
if (prefab == null && assetName != prefabName)
|
||||
{
|
||||
Debug.LogWarning($"[UI] Missing {assetName}; falling back to {prefabName}. Include both themes in the resource package.");
|
||||
yield return _loader.LoadAsync(prefabName, loaded => prefab = loaded);
|
||||
}
|
||||
}
|
||||
finally { _loading.Remove(prefabName); }
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError($"[UiManager] prefab not found: {prefabName}");
|
||||
return null;
|
||||
onOpened?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var go = Instantiate(prefab, _root, false);
|
||||
@@ -43,13 +62,7 @@ namespace HotUpdate.UI
|
||||
if (siblingIndex >= 0)
|
||||
go.transform.SetSiblingIndex(siblingIndex);
|
||||
_opened[prefabName] = go;
|
||||
return go;
|
||||
}
|
||||
|
||||
public T Open<T>(string prefabName) where T : Component
|
||||
{
|
||||
var go = Open(prefabName);
|
||||
return go != null ? go.GetComponentInChildren<T>(true) : null;
|
||||
onOpened?.Invoke(go);
|
||||
}
|
||||
|
||||
public void Close(string prefabName, bool destroy = false)
|
||||
@@ -72,6 +85,8 @@ namespace HotUpdate.UI
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
_loader?.Dispose();
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,90 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using YooAsset;
|
||||
|
||||
namespace HotUpdate.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// UI Prefab 加载:编辑器 AssetDatabase;运行时优先 DefaultPackage 寻址。
|
||||
/// 异步加载并持有资源句柄,随 UiManager 销毁统一释放。
|
||||
/// </summary>
|
||||
public sealed class UiPrefabLoader
|
||||
public sealed class UiPrefabLoader : IDisposable
|
||||
{
|
||||
public const string PrefabFolder = "Assets/Bundles/UI/Prefab";
|
||||
readonly ResourcePackage _preferredPackage;
|
||||
readonly string _folder;
|
||||
readonly Dictionary<string, GameObject> _cache = new Dictionary<string, GameObject>();
|
||||
readonly List<AssetHandle> _handles = new List<AssetHandle>();
|
||||
bool _disposed;
|
||||
|
||||
public GameObject Load(string prefabName)
|
||||
public UiPrefabLoader(ResourcePackage preferredPackage = null, string folder = PrefabFolder)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefabName)) return null;
|
||||
_preferredPackage = preferredPackage;
|
||||
_folder = folder;
|
||||
}
|
||||
|
||||
public IEnumerator LoadAsync(string prefabName, Action<GameObject> onLoaded)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(prefabName))
|
||||
{
|
||||
onLoaded?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
if (_cache.TryGetValue(prefabName, out var cached) && cached != null)
|
||||
{
|
||||
onLoaded?.Invoke(cached);
|
||||
yield break;
|
||||
}
|
||||
var packages = new List<ResourcePackage>();
|
||||
if (_preferredPackage != null) packages.Add(_preferredPackage);
|
||||
if (YooAssets.IsInitialized)
|
||||
{
|
||||
// DefaultPackage 或任意已初始化包
|
||||
var packages = new[] { "DefaultPackage", "Art" };
|
||||
foreach (var name in packages)
|
||||
foreach (var name in new[] { "DefaultPackage", "Art" })
|
||||
if (YooAssets.TryGetPackage(name, out var pkg) && !packages.Contains(pkg))
|
||||
packages.Add(pkg);
|
||||
}
|
||||
foreach (var package in packages)
|
||||
{
|
||||
if (package.InitializeStatus != EOperationStatus.Succeeded) continue;
|
||||
foreach (var address in new[] { prefabName, $"{_folder}/{prefabName}.prefab" })
|
||||
{
|
||||
if (!YooAssets.TryGetPackage(name, out var pkg)) continue;
|
||||
if (pkg.InitializeStatus != EOperationStatus.Succeeded) continue;
|
||||
var handle = pkg.LoadAssetSync<GameObject>(prefabName);
|
||||
if (!package.IsLocationValid(address)) continue;
|
||||
var handle = package.LoadAssetAsync<GameObject>(address);
|
||||
_handles.Add(handle);
|
||||
yield return handle;
|
||||
if (_disposed) yield break;
|
||||
if (handle.Status == EOperationStatus.Succeeded)
|
||||
return handle.GetAssetObject<GameObject>();
|
||||
{
|
||||
var prefab = handle.GetAssetObject<GameObject>();
|
||||
if (prefab != null)
|
||||
{
|
||||
_cache[prefabName] = prefab;
|
||||
onLoaded?.Invoke(prefab);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
_handles.Remove(handle);
|
||||
handle.Release();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
var path = $"{PrefabFolder}/{prefabName}.prefab";
|
||||
return UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
var editorPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>($"{_folder}/{prefabName}.prefab");
|
||||
if (editorPrefab != null) _cache[prefabName] = editorPrefab;
|
||||
onLoaded?.Invoke(editorPrefab);
|
||||
#else
|
||||
return null;
|
||||
onLoaded?.Invoke(null);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
foreach (var handle in _handles) handle.Release();
|
||||
_handles.Clear();
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace HotUpdate.UI
|
||||
{
|
||||
/// <summary>Candy UI;旧启动配置中的其他主题值回退到 Candy。</summary>
|
||||
public static class UiThemeCatalog
|
||||
{
|
||||
public const string Candy = "candy";
|
||||
|
||||
public static string Normalize(string theme)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(theme)) return Candy;
|
||||
theme = theme.Trim().ToLowerInvariant();
|
||||
if (theme == Candy) return Candy;
|
||||
Debug.LogWarning($"[UI] Unknown theme '{theme}', using candy.");
|
||||
return Candy;
|
||||
}
|
||||
|
||||
public static string Resolve(string logicalName, string theme)
|
||||
{
|
||||
return logicalName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69126c9c801a65e4e8a3c7c0c8bfc88e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user