Add prototype gameplay loop with character/battlefield assets and server game APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -255,9 +255,45 @@ namespace AIGame.EditorTools
|
||||
Fail(vr, "events 节点不存在: " + n);
|
||||
}
|
||||
}
|
||||
ValidateNineSliceControls(root, nodes, vr);
|
||||
return vr;
|
||||
}
|
||||
|
||||
private static void ValidateNineSliceControls(JObject root, JArray nodes, ValidationResult vr)
|
||||
{
|
||||
var checkedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (JToken node in nodes)
|
||||
{
|
||||
foreach (JToken comp in (node["components"] as JArray) ?? new JArray())
|
||||
{
|
||||
string type = (string)comp["type"];
|
||||
string[] fields = type == "Image" ? new[] { "sprite" }
|
||||
: type == "Button" ? new[] { "spriteHighlighted", "spritePressed", "spriteDisabled" }
|
||||
: Array.Empty<string>();
|
||||
foreach (string field in fields)
|
||||
{
|
||||
string name = (string)comp[field];
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
string path = (string)root["assets"]?["sprites"]?[name]
|
||||
?? TextureRoot + "/Common/" + name + ".png";
|
||||
if (!UiSpriteNineSliceRules.IsStandardControl(path)) continue;
|
||||
string imageType = (string)comp["imageType"];
|
||||
if (type == "Image" && !string.IsNullOrEmpty(imageType)
|
||||
&& !string.Equals(imageType, "Sliced", StringComparison.OrdinalIgnoreCase))
|
||||
Fail(vr, "标准控件图必须使用 imageType: Sliced: " + (string)node["name"] + " / " + name);
|
||||
if (!checkedPaths.Add(path)) continue;
|
||||
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
|
||||
if (sprite == null)
|
||||
Fail(vr, "标准控件 Sprite 缺失: " + path);
|
||||
else if (sprite.border.x <= 0 || sprite.border.y <= 0 || sprite.border.z <= 0 || sprite.border.w <= 0
|
||||
|| sprite.border.x + sprite.border.z >= sprite.rect.width
|
||||
|| sprite.border.y + sprite.border.w >= sprite.rect.height)
|
||||
Fail(vr, "标准控件 Sprite 的九宫格 Border 无效,请执行 Tools/UI/Apply Standard Control Nine-Slice: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string BuildFromJsonFile(string jsonPath)
|
||||
{
|
||||
ValidationResult vr = Validate(jsonPath);
|
||||
@@ -531,6 +567,9 @@ namespace AIGame.EditorTools
|
||||
Sprite sprite = ResolveSprite(spriteName, fileName);
|
||||
img.sprite = sprite;
|
||||
string imageType = (string)comp["imageType"];
|
||||
if (string.IsNullOrEmpty(imageType) && sprite != null
|
||||
&& UiSpriteNineSliceRules.IsStandardControl(AssetDatabase.GetAssetPath(sprite)))
|
||||
imageType = "Sliced";
|
||||
if (string.Equals(imageType, "Sliced", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
img.type = Image.Type.Sliced;
|
||||
@@ -569,6 +608,16 @@ namespace AIGame.EditorTools
|
||||
Button btn = GetOrAdd<Button>(go);
|
||||
Image target = go.GetComponent<Image>();
|
||||
if (target != null) btn.targetGraphic = target;
|
||||
string targetGraphicName = (string)comp["targetGraphic"];
|
||||
if (!string.IsNullOrEmpty(targetGraphicName))
|
||||
{
|
||||
deferred.Add(() =>
|
||||
{
|
||||
if (!map.TryGetValue(targetGraphicName, out GameObject graphicGo) || graphicGo.GetComponent<Graphic>() == null)
|
||||
throw new InvalidOperationException("Button targetGraphic not found: " + targetGraphicName);
|
||||
btn.targetGraphic = graphicGo.GetComponent<Graphic>();
|
||||
});
|
||||
}
|
||||
if (string.Equals((string)comp["transition"], "SpriteSwap", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
btn.transition = Selectable.Transition.SpriteSwap;
|
||||
@@ -581,6 +630,14 @@ namespace AIGame.EditorTools
|
||||
else
|
||||
{
|
||||
btn.transition = Selectable.Transition.ColorTint;
|
||||
ColorBlock colors = btn.colors;
|
||||
colors.normalColor = ParseColor((string)comp["normalColor"], colors.normalColor);
|
||||
colors.highlightedColor = ParseColor((string)comp["highlightedColor"], colors.highlightedColor);
|
||||
colors.pressedColor = ParseColor((string)comp["pressedColor"], colors.pressedColor);
|
||||
colors.selectedColor = ParseColor((string)comp["selectedColor"], colors.selectedColor);
|
||||
colors.disabledColor = ParseColor((string)comp["disabledColor"], colors.disabledColor);
|
||||
if (comp["fadeDuration"] != null) colors.fadeDuration = (float)comp["fadeDuration"];
|
||||
btn.colors = colors;
|
||||
}
|
||||
// onClick 仅作事件标记,不在 Prefab 写死回调
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using AOT.Boot;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>仅在 Boot 配置为本机 AIServer 时,进入 Play Mode 前启动已有服务。</summary>
|
||||
[InitializeOnLoad]
|
||||
public static class LocalServerAutoStart
|
||||
{
|
||||
[Serializable] class Health { public bool ok; public string service; }
|
||||
|
||||
static LocalServerAutoStart()
|
||||
{
|
||||
EditorApplication.playModeStateChanged += state =>
|
||||
{
|
||||
if (state != PlayModeStateChange.ExitingEditMode) return;
|
||||
var boot = UnityEngine.Object.FindObjectOfType<BootDriver>();
|
||||
if (boot == null) return;
|
||||
var settings = new SerializedObject(boot).FindProperty("settings");
|
||||
if (settings.FindPropertyRelative("HttpBase").stringValue.TrimEnd('/') != "http://127.0.0.1:8080") return;
|
||||
try { UnityEngine.Debug.Log("[LocalAIServer] " + StartLocal()); }
|
||||
catch (Exception e) { UnityEngine.Debug.LogWarning("[LocalAIServer] " + e.Message); }
|
||||
};
|
||||
}
|
||||
|
||||
static bool IsHealthy()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = (HttpWebRequest)WebRequest.Create("http://127.0.0.1:8080/health");
|
||||
request.Timeout = 250; request.ReadWriteTimeout = 250; request.Proxy = null;
|
||||
using (var response = request.GetResponse())
|
||||
using (var reader = new StreamReader(response.GetResponseStream()))
|
||||
{
|
||||
var health = JsonUtility.FromJson<Health>(reader.ReadToEnd());
|
||||
return health != null && health.ok && health.service == "aiserver";
|
||||
}
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
[MenuItem("AIGame/Boot/Server/Start Local AIServer")]
|
||||
public static string StartLocal()
|
||||
{
|
||||
if (IsHealthy()) return "already healthy at 127.0.0.1:8080";
|
||||
string repo = Directory.GetParent(Application.dataPath).Parent.FullName;
|
||||
string exe = Path.Combine(repo, "AIServer/build/Release/aiserver.exe");
|
||||
if (!File.Exists(exe)) throw new FileNotFoundException("Build AIServer first: cmake --build AIServer/build --config Release", exe);
|
||||
var info = new ProcessStartInfo(exe)
|
||||
{
|
||||
Arguments = "--host 127.0.0.1 --port 8080 --daemon --db \"" + Path.Combine(repo, "AIServer/data/aiserver.db") + "\"",
|
||||
WorkingDirectory = Path.Combine(repo, "AIServer"),
|
||||
UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
using (var process = Process.Start(info))
|
||||
{
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
if (IsHealthy()) return "started at 127.0.0.1:8080";
|
||||
if (process.HasExited) throw new Exception("Local AIServer exited; check port 8080 and the database path.");
|
||||
}
|
||||
}
|
||||
throw new Exception("Local AIServer did not become healthy; theme preview remains available without login.");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c394878c52f333f40b441423b6c4cc27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using YooAsset.Editor;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>把 Candy UI Prefab 收进 Art;图片/字体通过 Prefab 依赖进入资源包。</summary>
|
||||
public static class SetupUiBundleCollector
|
||||
{
|
||||
[MenuItem("Tools/UI/Setup UI Bundle Collector")]
|
||||
public static string Setup()
|
||||
{
|
||||
var package = BundleCollectorSettingData.Setting.Packages.FirstOrDefault(p => p.PackageName == "Art");
|
||||
if (package == null) throw new System.InvalidOperationException("Set up the Art package before UI collection.");
|
||||
var group = package.Groups.FirstOrDefault(g => g.GroupName == "UI");
|
||||
if (group == null) group = BundleCollectorSettingData.CreateGroup(package, "UI");
|
||||
group.GroupDesc = "Candy UI";
|
||||
group.AssetTags = "ui";
|
||||
group.ActiveRuleName = nameof(EnableGroup);
|
||||
BundleCollectorSettingData.ModifyGroup(package, group);
|
||||
const string path = "Assets/Bundles/UI/Prefab";
|
||||
var collector = group.Collectors.FirstOrDefault(c => c.CollectPath == path);
|
||||
if (collector == null)
|
||||
{
|
||||
collector = new BundleCollector
|
||||
{
|
||||
CollectPath = path,
|
||||
CollectorGUID = AssetDatabase.AssetPathToGUID(path),
|
||||
CollectorType = ECollectorType.MainAssetCollector,
|
||||
AddressRuleName = nameof(AddressByFileName),
|
||||
PackRuleName = nameof(PackSeparately),
|
||||
FilterRuleName = nameof(CollectPrefab),
|
||||
AssetTags = "ui"
|
||||
};
|
||||
BundleCollectorSettingData.CreateCollector(group, collector);
|
||||
}
|
||||
package.CheckConfigError();
|
||||
BundleCollectorSettingData.SaveFile();
|
||||
return "Art/UI collects Candy UI by prefab name, including sprite/font dependencies.";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ff5ceeb3b52f7042b92bd3bfc260c05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,121 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AIGame.EditorTools
|
||||
{
|
||||
/// <summary>Common 标准控件的九宫格导入规则,基于 Doc/UI_Normal_Set.md。</summary>
|
||||
public static class UiSpriteNineSliceRules
|
||||
{
|
||||
private const string CommonRoot = "Assets/Bundles/UI/Texture/Common/";
|
||||
// W,H,L,R,T,B:设计尺寸与边界;实际切图按尺寸换算,并包含 2px 透明留边。
|
||||
private static readonly Dictionary<string, int[]> Rules = new Dictionary<string, int[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "ui_panel_main", new[] { 480, 256, 40, 40, 40, 40 } },
|
||||
{ "ui_panel_popup", new[] { 360, 240, 36, 36, 36, 36 } },
|
||||
{ "ui_panel_content", new[] { 320, 200, 28, 28, 28, 28 } },
|
||||
{ "ui_panel_header", new[] { 400, 80, 32, 32, 24, 24 } },
|
||||
{ "btn_primary_normal", new[] { 224, 88, 32, 32, 24, 24 } },
|
||||
{ "btn_primary_pressed", new[] { 224, 88, 32, 32, 24, 24 } },
|
||||
{ "btn_primary_disabled", new[] { 224, 88, 32, 32, 24, 24 } },
|
||||
{ "btn_secondary_normal", new[] { 224, 88, 32, 32, 24, 24 } },
|
||||
{ "btn_secondary_pressed", new[] { 224, 88, 32, 32, 24, 24 } },
|
||||
{ "btn_small_normal", new[] { 160, 64, 24, 24, 18, 18 } },
|
||||
{ "btn_small_pressed", new[] { 160, 64, 24, 24, 18, 18 } },
|
||||
{ "scrollbar_track_v", new[] { 28, 160, 8, 8, 16, 16 } },
|
||||
{ "scrollbar_thumb_v", new[] { 28, 80, 8, 8, 16, 16 } },
|
||||
{ "input_bg_normal", new[] { 360, 72, 28, 28, 20, 20 } },
|
||||
{ "input_bg_focus", new[] { 360, 72, 28, 28, 20, 20 } },
|
||||
{ "ui_list_item_normal", new[] { 320, 72, 20, 20, 16, 16 } },
|
||||
{ "ui_list_item_selected", new[] { 320, 72, 20, 20, 16, 16 } },
|
||||
{ "tab_active", new[] { 200, 72, 28, 28, 20, 20 } },
|
||||
{ "tab_inactive", new[] { 200, 72, 28, 28, 20, 20 } },
|
||||
{ "progress_bg", new[] { 400, 32, 16, 16, 10, 10 } },
|
||||
{ "progress_fill", new[] { 400, 32, 16, 16, 10, 10 } },
|
||||
{ "slider_track", new[] { 320, 28, 14, 14, 8, 8 } },
|
||||
{ "slider_fill", new[] { 320, 28, 14, 14, 8, 8 } },
|
||||
{ "toast_bg", new[] { 280, 80, 28, 28, 20, 20 } },
|
||||
{ "toggle_bg_off", new[] { 112, 56, 28, 28, 18, 18 } },
|
||||
{ "toggle_bg_on", new[] { 112, 56, 28, 28, 18, 18 } }
|
||||
};
|
||||
|
||||
public static bool IsStandardControl(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return false;
|
||||
path = path.Replace('\\', '/');
|
||||
return path.StartsWith(CommonRoot, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(Path.GetDirectoryName(path)?.Replace('\\', '/'), CommonRoot.TrimEnd('/'), StringComparison.OrdinalIgnoreCase)
|
||||
&& Rules.ContainsKey(Path.GetFileNameWithoutExtension(path));
|
||||
}
|
||||
|
||||
public static Vector4 GetBorder(string path, int width, int height)
|
||||
{
|
||||
int[] rule = Rules[Path.GetFileNameWithoutExtension(path)];
|
||||
float sx = Mathf.Max(1, width - 4) / (float)rule[0];
|
||||
float sy = Mathf.Max(1, height - 4) / (float)rule[1];
|
||||
// Unity Vector4 顺序为 L,B,R,T,与文档 L,R,T,B 不同。
|
||||
float maxX = Mathf.Max(1, (width - 1) / 2);
|
||||
float maxY = Mathf.Max(1, (height - 1) / 2);
|
||||
return new Vector4(
|
||||
Mathf.Clamp(Mathf.Round(rule[2] * sx) + 2, 1, maxX),
|
||||
Mathf.Clamp(Mathf.Round(rule[5] * sy) + 2, 1, maxY),
|
||||
Mathf.Clamp(Mathf.Round(rule[3] * sx) + 2, 1, maxX),
|
||||
Mathf.Clamp(Mathf.Round(rule[4] * sy) + 2, 1, maxY));
|
||||
}
|
||||
|
||||
public static bool Configure(TextureImporter importer)
|
||||
{
|
||||
if (!IsStandardControl(importer.assetPath)) return false;
|
||||
importer.GetSourceTextureWidthAndHeight(out int width, out int height);
|
||||
Vector4 border = GetBorder(importer.assetPath, width, height);
|
||||
var settings = new TextureImporterSettings();
|
||||
importer.ReadTextureSettings(settings);
|
||||
bool changed = importer.textureType != TextureImporterType.Sprite
|
||||
|| importer.spriteImportMode != SpriteImportMode.Single
|
||||
|| settings.spriteMeshType != SpriteMeshType.FullRect
|
||||
|| importer.spriteBorder != border;
|
||||
if (!changed) return false;
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Single;
|
||||
importer.ReadTextureSettings(settings);
|
||||
settings.spriteMeshType = SpriteMeshType.FullRect;
|
||||
importer.SetTextureSettings(settings);
|
||||
importer.spriteBorder = border;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MenuItem("Tools/UI/Apply Standard Control Nine-Slice")]
|
||||
public static void ApplyFromMenu()
|
||||
{
|
||||
Debug.Log("[UIGen] 九宫格更新: " + ApplyExisting() + " 张标准控件图。");
|
||||
}
|
||||
|
||||
public static int ApplyExisting()
|
||||
{
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
throw new InvalidOperationException("Apply UI sprite borders in Edit Mode.");
|
||||
int changed = 0;
|
||||
foreach (string name in Rules.Keys)
|
||||
{
|
||||
string path = CommonRoot + name + ".png";
|
||||
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
|
||||
if (importer == null || !Configure(importer)) continue;
|
||||
importer.SaveAndReimport();
|
||||
changed++;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UiSpriteNineSlicePostprocessor : AssetPostprocessor
|
||||
{
|
||||
private void OnPreprocessTexture()
|
||||
{
|
||||
UiSpriteNineSliceRules.Configure((TextureImporter)assetImporter);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47e634eecc0295440a1fa7f8a0fe2423
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -25,6 +25,7 @@ namespace AOT.Editor.YooAssetTools
|
||||
public static string BuildAndStage()
|
||||
{
|
||||
SetupM3Character.SetupAll();
|
||||
AIGame.EditorTools.SetupUiBundleCollector.Setup();
|
||||
|
||||
var target = EditorUserBuildSettings.activeBuildTarget;
|
||||
string packageVersion = DateTime.Now.ToString("yyyyMMdd.HHmmss");
|
||||
|
||||
Reference in New Issue
Block a user