274 lines
17 KiB
C#
274 lines
17 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AIGame.EditorTools;
|
|
using HotUpdate.UI;
|
|
using Newtonsoft.Json.Linq;
|
|
using TMPro;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.EventSystems;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
|
|
/// <summary>Pipeline-only import, JSON-to-Prefab build, isolated rendering and binding checks.</summary>
|
|
public static class BuildHomeUi
|
|
{
|
|
static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName;
|
|
const string Folder = "Assets/Bundles/UI/Texture/Home";
|
|
static readonly string[] Icons = { "coin", "diamond", "energy", "avatar", "tasks", "gift", "mail", "trophy", "bag", "home", "battle", "pets", "chest", "calendar", "settings", "pinball" };
|
|
static readonly string[] Names = { "UI_MainHUD" };
|
|
static string Design => Path.Combine(Repo, "Doc/Design/UI/Home");
|
|
|
|
public static string Build()
|
|
{
|
|
if (EditorApplication.isPlaying) throw new Exception("Build in Edit Mode.");
|
|
Directory.CreateDirectory(Folder);
|
|
File.Copy(Path.Combine(Design, "home_camp_source.png"), Folder + "/home_camp.png", true);
|
|
var atlas = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
|
try
|
|
{
|
|
if (!atlas.LoadImage(File.ReadAllBytes(Path.Combine(Design, "home_icons_source.png")))) throw new Exception("Invalid icon sheet.");
|
|
for (int i = 0; i < Icons.Length; i++)
|
|
{
|
|
int col = i % 4, row = 3 - i / 4;
|
|
int x = Mathf.RoundToInt(col * atlas.width / 4f), y = Mathf.RoundToInt(row * atlas.height / 4f);
|
|
int w = Mathf.RoundToInt((col + 1) * atlas.width / 4f) - x;
|
|
int h = Mathf.RoundToInt((row + 1) * atlas.height / 4f) - y;
|
|
var icon = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
|
try { icon.SetPixels(CleanSlice(atlas.GetPixels(x, y, w, h), w, h)); icon.Apply(); File.WriteAllBytes(Folder + "/home_ico_" + Icons[i] + ".png", icon.EncodeToPNG()); }
|
|
finally { UnityEngine.Object.DestroyImmediate(icon); }
|
|
}
|
|
}
|
|
finally { UnityEngine.Object.DestroyImmediate(atlas); }
|
|
AssetDatabase.Refresh();
|
|
foreach (var path in Directory.GetFiles(Folder, "*.png"))
|
|
{
|
|
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
|
importer.textureType = TextureImporterType.Sprite;
|
|
importer.spriteImportMode = SpriteImportMode.Single;
|
|
importer.spritePixelsPerUnit = 100;
|
|
var settings = new TextureImporterSettings(); importer.ReadTextureSettings(settings);
|
|
settings.spriteMeshType = SpriteMeshType.FullRect; importer.SetTextureSettings(settings);
|
|
importer.alphaIsTransparency = true; importer.mipmapEnabled = false;
|
|
importer.textureCompression = TextureImporterCompression.Uncompressed;
|
|
importer.filterMode = FilterMode.Bilinear; importer.wrapMode = TextureWrapMode.Clamp;
|
|
importer.maxTextureSize = 2048; importer.SaveAndReimport();
|
|
}
|
|
foreach (var name in Names)
|
|
{
|
|
var path = Path.Combine(Repo, "Doc/UIPrefabCreater", name + ".json");
|
|
var validation = JsonUIPrefabBuilder.Validate(path);
|
|
if (!validation.Ok) throw new Exception(string.Join("\n", validation.Errors));
|
|
if (string.IsNullOrEmpty(JsonUIPrefabBuilder.BuildFromJsonFile(path))) throw new Exception("Build failed: " + name);
|
|
}
|
|
return "Imported 17 Home sprites; built Candy home HUD prefab from JSON.";
|
|
}
|
|
|
|
// Atlas extraction: discard isolated fragments spilling in from adjacent cells.
|
|
// This preserves original icon pixels and alpha; it does not repaint the source artwork.
|
|
static Color[] CleanSlice(Color[] pixels, int width, int height)
|
|
{
|
|
var visited = new bool[pixels.Length];
|
|
var components = new List<List<int>>();
|
|
for (int start = 0; start < pixels.Length; start++)
|
|
{
|
|
if (visited[start] || pixels[start].a < 0.01f) continue;
|
|
var queue = new Queue<int>(); var component = new List<int>(); queue.Enqueue(start); visited[start] = true;
|
|
while (queue.Count > 0)
|
|
{
|
|
int p = queue.Dequeue(); component.Add(p);
|
|
int px = p % width, py = p / width;
|
|
for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++)
|
|
{
|
|
int nx = px + dx, ny = py + dy;
|
|
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
int next = ny * width + nx;
|
|
if (visited[next] || pixels[next].a < 0.01f) continue;
|
|
visited[next] = true; queue.Enqueue(next);
|
|
}
|
|
}
|
|
components.Add(component);
|
|
}
|
|
int largest = components.Count > 0 ? components.Max(c => c.Count) : 0;
|
|
foreach (var c in components.Where(c => c.Count < largest * 0.03f))
|
|
foreach (int p in c) pixels[p] = Color.clear;
|
|
return pixels;
|
|
}
|
|
|
|
public static string Verify()
|
|
{
|
|
int checks = 0;
|
|
Action<bool, string> check = (ok, message) => { if (!ok) throw new Exception(message); checks++; };
|
|
foreach (var name in Names)
|
|
{
|
|
var doc = JObject.Parse(File.ReadAllText(Path.Combine(Repo, "Doc/UIPrefabCreater", name + ".json")));
|
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/" + name + ".prefab");
|
|
check(prefab != null, "Missing prefab " + name);
|
|
var scene = EditorSceneManager.NewPreviewScene();
|
|
var go = UnityEngine.Object.Instantiate(prefab); SceneManager.MoveGameObjectToScene(go, scene);
|
|
try
|
|
{
|
|
foreach (var path in ((JObject)doc["assets"]["sprites"]).Properties())
|
|
check(AssetDatabase.LoadAssetAtPath<Sprite>((string)path.Value) != null, "Missing sprite " + path.Name);
|
|
var images = go.GetComponentsInChildren<Image>(true);
|
|
check(images.All(i => i.sprite != null), "Unresolved prefab image " + name);
|
|
check(go.GetComponentsInChildren<CanvasScaler>().Single().referenceResolution == new Vector2(1080, 1920), "Wrong resolution");
|
|
foreach (var b in doc["bindings"])
|
|
check(go.GetComponentsInChildren<Transform>(true).Any(t => t.name == (string)b), "Missing binding " + b);
|
|
check(!go.GetComponentsInChildren<Transform>(true).Any(t => t.name == "UI_ThemeSwitcher"), "Unexpected theme toolbar");
|
|
var view = go.AddComponent<MainHudView>(); view.Bind();
|
|
string called = null; int invocations = 0, reconnect = 0;
|
|
view.OnHomeActionClicked += k => { called = k; invocations++; };
|
|
view.OnReconnectClicked += () => reconnect++;
|
|
view.Bind(); // Bind is safe to repeat.
|
|
foreach (var e in doc["events"])
|
|
{
|
|
var button = go.GetComponentsInChildren<Button>(true).Single(b => b.name == (string)e["node"]);
|
|
check(button.GetComponent<RectTransform>().sizeDelta.x >= 88 && button.GetComponent<RectTransform>().sizeDelta.y >= 82, "Small touch target " + button.name);
|
|
button.onClick.Invoke();
|
|
var eventName = (string)e["name"];
|
|
if (eventName != "MainHUD.Reconnect")
|
|
check(called == eventName.Substring("MainHUD.".Length), "Wrong click callback: " + eventName);
|
|
}
|
|
check(invocations == 18 && reconnect == 1, "Duplicate or missing event callbacks");
|
|
view.SetStatus("已连接"); view.SetPid(10248); view.ShowReconnect(true, "网络已断开,请重试");
|
|
check(go.transform.Find("Panel_Reconnect").gameObject.activeSelf, "Reconnect not visible");
|
|
check(go.GetComponentsInChildren<TextMeshProUGUI>(true).Single(t => t.name == "Txt_Reconnect").text == "网络已断开,请重试", "Reconnect message missing");
|
|
check(go.GetComponentsInChildren<TextMeshProUGUI>(true).Single(t => t.name == "Txt_Status").text == "已连接", "Status binding broken");
|
|
view.ShowReconnect(false); check(!go.transform.Find("Panel_Reconnect").gameObject.activeSelf, "Reconnect not hidden");
|
|
}
|
|
finally { UnityEngine.Object.DestroyImmediate(go); EditorSceneManager.ClosePreviewScene(scene); }
|
|
}
|
|
foreach (var path in Directory.GetFiles(Folder, "*.png"))
|
|
{
|
|
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
|
check(importer.textureType == TextureImporterType.Sprite && !importer.mipmapEnabled && importer.textureCompression == TextureImporterCompression.Uncompressed, "Wrong sprite settings " + path);
|
|
}
|
|
return checks + " sprite / schema / bindings / touch-target / click / reconnect checks passed.";
|
|
}
|
|
|
|
public static string Preview()
|
|
{
|
|
foreach (var name in Names) Render(name, 1080, 1920, false);
|
|
Render("UI_MainHUD", 1080, 2400, false);
|
|
Render("UI_MainHUD", 1080, 1920, true);
|
|
return "Rendered Candy, tall-screen and reconnect previews: " + Design;
|
|
}
|
|
|
|
public static string PrepareLive()
|
|
{
|
|
var editor = typeof(Editor).Assembly;
|
|
var viewType = editor.GetType("UnityEditor.GameView");
|
|
var window = EditorWindow.GetWindow(viewType);
|
|
var selected = viewType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
SessionState.SetInt("AIGame.Home.PreviousGameViewSize", (int)selected.GetValue(window));
|
|
var sizesType = editor.GetType("UnityEditor.GameViewSizes");
|
|
var sizes = typeof(ScriptableSingleton<>).MakeGenericType(sizesType).GetProperty("instance").GetValue(null);
|
|
var getGroup = sizesType.GetMethod("GetGroup");
|
|
var groupType = sizesType.GetProperty("currentGroupType").GetValue(sizes);
|
|
var group = getGroup.Invoke(sizes, new[] { groupType });
|
|
int count = (int)group.GetType().GetMethod("GetTotalCount").Invoke(group, null);
|
|
int found = -1;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var size = group.GetType().GetMethod("GetGameViewSize").Invoke(group, new object[] { i });
|
|
if ((int)size.GetType().GetProperty("width").GetValue(size) == 1080 && (int)size.GetType().GetProperty("height").GetValue(size) == 1920) { found = i; break; }
|
|
}
|
|
if (found < 0)
|
|
{
|
|
var sizeType = editor.GetType("UnityEditor.GameViewSize"); var kind = editor.GetType("UnityEditor.GameViewSizeType");
|
|
var size = Activator.CreateInstance(sizeType, new object[] { Enum.ToObject(kind, 1), 1080, 1920, "AIGame Home 1080x1920" });
|
|
group.GetType().GetMethod("AddCustomSize").Invoke(group, new[] { size }); found = count;
|
|
}
|
|
selected.SetValue(window, found); window.Repaint();
|
|
return "Game View set to portrait 1080x1920 for live UI validation.";
|
|
}
|
|
|
|
public static string RestoreGameView()
|
|
{
|
|
var type = typeof(Editor).Assembly.GetType("UnityEditor.GameView");
|
|
var window = EditorWindow.GetWindow(type);
|
|
type.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).SetValue(window, SessionState.GetInt("AIGame.Home.PreviousGameViewSize", 0));
|
|
window.Repaint(); return "Restored previous Game View size.";
|
|
}
|
|
|
|
static async Task Until(Func<bool> condition)
|
|
{
|
|
var start = DateTime.UtcNow;
|
|
while (!condition()) { if ((DateTime.UtcNow - start).TotalSeconds > 20) throw new Exception("Live HUD did not become ready."); await Task.Delay(100); }
|
|
}
|
|
|
|
static void Click(Button button)
|
|
{
|
|
Canvas.ForceUpdateCanvases();
|
|
var rect = button.GetComponent<RectTransform>();
|
|
var data = new PointerEventData(EventSystem.current) { position = RectTransformUtility.WorldToScreenPoint(null, rect.TransformPoint(rect.rect.center)), button = PointerEventData.InputButton.Left };
|
|
var hits = new List<RaycastResult>(); EventSystem.current.RaycastAll(data, hits);
|
|
if (hits.Count == 0 || ExecuteEvents.GetEventHandler<IPointerClickHandler>(hits[0].gameObject) != button.gameObject) throw new Exception("Button raycast blocked: " + button.name + "; hits=" + string.Join(",", hits.Take(4).Select(h => h.gameObject.name)));
|
|
ExecuteEvents.Execute(button.gameObject, data, ExecuteEvents.pointerClickHandler);
|
|
}
|
|
|
|
public static async Task<string> VerifyLive()
|
|
{
|
|
if (!EditorApplication.isPlaying) throw new Exception("Enter Play Mode first.");
|
|
await Until(() => UiManager.Instance != null && UiManager.Instance.Get(ConfirmDialogView.PrefabName) != null);
|
|
var hud = UiManager.Instance.Get(MainHudView.PrefabName);
|
|
var dialog = UiManager.Instance.Get(ConfirmDialogView.PrefabName).GetComponent<ConfirmDialogView>();
|
|
if (EventSystem.current == null) throw new Exception("No EventSystem.");
|
|
var flow = UnityEngine.Object.FindObjectOfType<HotUpdate.Gameplay.PrototypeFlow>();
|
|
await Until(() => flow?.Game != null && !flow.IsBusy);
|
|
dialog.Hide();
|
|
flow.Handle("Home");
|
|
await Task.Delay(100);
|
|
var entries = new[] { ("Btn_Adventure", "Stages"), ("Btn_Tasks", "Tasks"), ("Btn_Mail", "Mail"), ("Btn_Rewards", "Chest"), ("Btn_NavPets", "Pets"), ("Btn_NavShop", "Shop"), ("Btn_NavBattle", "Stages"), ("Btn_NavPinball", "Pinball"), ("Btn_Settings", "") };
|
|
foreach (var entry in entries)
|
|
{
|
|
Click(hud.GetComponentsInChildren<Button>().Single(b => b.name == entry.Item1));
|
|
if (entry.Item2.Length > 0) { await Until(() => flow.Page == "UI_Prototype" + entry.Item2); flow.Handle("Home"); await Until(() => flow.Page == "Home"); }
|
|
else { await Until(() => dialog.gameObject.activeSelf); await Task.Delay(100); Click(dialog.GetComponentsInChildren<Button>().Single(b => b.name == "Btn_Ok")); if (dialog.gameObject.activeSelf) throw new Exception("Dialog did not close."); }
|
|
await Task.Delay(150);
|
|
}
|
|
Click(hud.GetComponentsInChildren<Button>().Single(b => b.name == "Btn_NavHome"));
|
|
await Until(() => flow.Page == "UI_PrototypeCamp"); flow.Handle("Home");
|
|
return "9 home feature entries and camp passed live EventSystem raycasts, authored pages and remaining support dialogs; screen=" + Screen.width + "x" + Screen.height + "; theme=" + UiManager.Instance.Theme;
|
|
}
|
|
|
|
static void Render(string name, int width, int height, bool reconnect)
|
|
{
|
|
var scene = EditorSceneManager.NewPreviewScene();
|
|
var previous = RenderTexture.active;
|
|
var rt = new RenderTexture(width, height, 24);
|
|
try
|
|
{
|
|
var cameraGo = new GameObject("HomePreviewCamera", typeof(Camera)); SceneManager.MoveGameObjectToScene(cameraGo, scene);
|
|
var camera = cameraGo.GetComponent<Camera>(); camera.scene = scene;
|
|
camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.92f, 0.95f, 0.97f);
|
|
camera.orthographic = true; camera.orthographicSize = height / 2f; camera.transform.position = new Vector3(0, 0, -100);
|
|
camera.targetTexture = rt;
|
|
var go = UnityEngine.Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Bundles/UI/Prefab/" + name + ".prefab"));
|
|
SceneManager.MoveGameObjectToScene(go, scene);
|
|
var canvas = go.GetComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = camera; canvas.planeDistance = 10;
|
|
var scaler = go.GetComponent<CanvasScaler>(); scaler.enabled = false; canvas.scaleFactor = 1;
|
|
var view = go.AddComponent<MainHudView>(); view.Bind(); view.SetStatus("欢迎回到小镇");
|
|
if (reconnect) view.ShowReconnect(true, "网络连接已断开,正在为你恢复连接…");
|
|
Canvas.ForceUpdateCanvases();
|
|
foreach (var text in go.GetComponentsInChildren<TextMeshProUGUI>(true)) text.ForceMeshUpdate(true);
|
|
Canvas.ForceUpdateCanvases(); camera.Render();
|
|
RenderTexture.active = rt;
|
|
var screenshot = new Texture2D(width, height, TextureFormat.RGB24, false);
|
|
try
|
|
{
|
|
screenshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); screenshot.Apply();
|
|
File.WriteAllBytes(Path.Combine(Design, name + (reconnect ? "_reconnect" : height == 2400 ? "_tall" : "") + ".png"), screenshot.EncodeToPNG());
|
|
}
|
|
finally { UnityEngine.Object.DestroyImmediate(screenshot); }
|
|
}
|
|
finally { RenderTexture.active = previous; EditorSceneManager.ClosePreviewScene(scene); rt.Release(); UnityEngine.Object.DestroyImmediate(rt); }
|
|
}
|
|
}
|