324 lines
18 KiB
C#
324 lines
18 KiB
C#
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Demo2.Runtime;
|
|
using ShrinkTutorial;
|
|
using TMPro;
|
|
using UnityEditor;
|
|
using UnityEditor.Build.Reporting;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Demo2.Editor
|
|
{
|
|
public static class Demo2SceneBuilder
|
|
{
|
|
private const string Root = "Assets/Demos/Demo2";
|
|
private const string Art = Root + "/Art";
|
|
private const string Ui = Root + "/UI";
|
|
private const string ResourceRoot = Root + "/Resources";
|
|
private const string PrefabPath = Root + "/Prefabs/Demo2Root.prefab";
|
|
private const string ScenePath = Root + "/Demo2.unity";
|
|
private const string UxmlPath = Ui + "/Demo2.uxml";
|
|
private const string UssPath = Ui + "/Demo2.uss";
|
|
private const string PanelSettingsPath = Ui + "/Demo2PanelSettings.asset";
|
|
private const string ThemePath = Ui + "/Demo2RuntimeTheme.tss";
|
|
private const string FontPath = Art + "/NotoSansSC-Variable.ttf";
|
|
private const string TutorialFontPath = Art + "/TutorialChinese SDF.asset";
|
|
|
|
private static readonly string[] MachineNames = { "miner", "conveyor", "splitter", "smelter", "assembler", "loader", "turret", "generator" };
|
|
private static readonly string[] TutorialAnchorIds = { "demo2.miner", "demo2.conveyor", "demo2.grid", "demo2.smelter", "demo2.recipe", "demo2.turret", "demo2.ready", "demo2.reward" };
|
|
|
|
private static Sprite? _floor;
|
|
private static Sprite[] _machineSprites = Array.Empty<Sprite>();
|
|
private static Texture2D? _itemTexture;
|
|
private static Font? _font;
|
|
private static TMP_FontAsset? _tutorialFont;
|
|
|
|
[MenuItem("ShrinkSDK/Demo2/创建规则工厂 Demo")]
|
|
public static void Build()
|
|
{
|
|
EnsureDirectories();
|
|
ConfigureArt();
|
|
LoadAssets();
|
|
BuildTutorialAssets();
|
|
var panelSettings = BuildPanelSettings();
|
|
var root = BuildRoot(panelSettings);
|
|
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
|
|
UnityEngine.Object.DestroyImmediate(root);
|
|
|
|
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath) ?? throw new InvalidOperationException("Demo2 root prefab was not created.");
|
|
PrefabUtility.InstantiatePrefab(prefab, scene);
|
|
var eventSystem = new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
|
|
Undo.RegisterCreatedObjectUndo(eventSystem, "Create Demo2 EventSystem");
|
|
EditorSceneManager.MarkSceneDirty(scene);
|
|
EditorSceneManager.SaveScene(scene, ScenePath);
|
|
AddSceneToBuildSettings();
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
Selection.activeObject = AssetDatabase.LoadAssetAtPath<SceneAsset>(ScenePath);
|
|
Debug.Log($"[Demo2] Created UI Toolkit prefab {PrefabPath} and scene {ScenePath}");
|
|
}
|
|
|
|
[MenuItem("ShrinkSDK/Demo2/构建 Headless Server")]
|
|
public static void BuildHeadlessServer()
|
|
{
|
|
if (!File.Exists(Path.GetFullPath(ScenePath))) Build();
|
|
var output = Path.GetFullPath("Builds/Demo2Headless/Demo2Server.exe");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
|
|
var options = new BuildPlayerOptions
|
|
{
|
|
scenes = new[] { ScenePath },
|
|
locationPathName = output,
|
|
target = BuildTarget.StandaloneWindows64,
|
|
subtarget = (int)StandaloneBuildSubtarget.Server,
|
|
options = BuildOptions.CleanBuildCache
|
|
};
|
|
var report = BuildPipeline.BuildPlayer(options);
|
|
if (report.summary.result != BuildResult.Succeeded)
|
|
throw new InvalidOperationException("Demo2 headless build failed: " + report.summary.result);
|
|
Debug.Log($"[Demo2] Headless server built: {output} ({report.summary.totalSize} bytes)");
|
|
}
|
|
|
|
private static GameObject BuildRoot(PanelSettings panelSettings)
|
|
{
|
|
var root = new GameObject("Demo2Root", typeof(Demo2DemoController), typeof(Demo2HeadlessBootstrap));
|
|
var cameraObject = new GameObject("WorldCamera", typeof(Camera), typeof(AudioListener));
|
|
cameraObject.tag = "MainCamera";
|
|
cameraObject.transform.SetParent(root.transform, false);
|
|
cameraObject.transform.position = new Vector3(16, 10, -20);
|
|
var camera = cameraObject.GetComponent<Camera>();
|
|
camera.orthographic = true;
|
|
camera.orthographicSize = 11.6f;
|
|
camera.clearFlags = CameraClearFlags.SolidColor;
|
|
camera.backgroundColor = Hex("101419");
|
|
|
|
var factoryWorld = new GameObject("FactoryWorld");
|
|
factoryWorld.transform.SetParent(root.transform, false);
|
|
var floorObject = new GameObject("FactoryFloor", typeof(SpriteRenderer));
|
|
floorObject.transform.SetParent(factoryWorld.transform, false);
|
|
floorObject.transform.position = new Vector3(16, 10, 1);
|
|
var floorRenderer = floorObject.GetComponent<SpriteRenderer>();
|
|
floorRenderer.sprite = _floor;
|
|
floorRenderer.sortingOrder = -10;
|
|
if (_floor != null)
|
|
{
|
|
var size = _floor.bounds.size;
|
|
floorObject.transform.localScale = new Vector3(32f / size.x, 20f / size.y, 1);
|
|
}
|
|
|
|
var proxyObject = new GameObject("MachineProxyPool", typeof(Demo2MachineProxyPool));
|
|
proxyObject.transform.SetParent(factoryWorld.transform, false);
|
|
var proxyPool = proxyObject.GetComponent<Demo2MachineProxyPool>();
|
|
proxyPool.SetSprites(_machineSprites);
|
|
var itemObject = new GameObject("ItemBatchRenderer", typeof(Demo2ItemBatchRenderer));
|
|
itemObject.transform.SetParent(factoryWorld.transform, false);
|
|
var itemRenderer = itemObject.GetComponent<Demo2ItemBatchRenderer>();
|
|
SetObject(itemRenderer, "itemTexture", _itemTexture);
|
|
|
|
var uiObject = new GameObject("UI", typeof(UIDocument));
|
|
uiObject.transform.SetParent(root.transform, false);
|
|
var document = uiObject.GetComponent<UIDocument>();
|
|
document.panelSettings = panelSettings;
|
|
document.visualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath) ?? throw new InvalidOperationException("Demo2 UXML is missing.");
|
|
document.sortingOrder = 0;
|
|
|
|
var tutorialAnchorRoot = CreateTutorialAnchors(root.transform);
|
|
var controller = root.GetComponent<Demo2DemoController>();
|
|
SetObject(controller, "worldCamera", camera);
|
|
SetObject(controller, "proxyPool", proxyPool);
|
|
SetObject(controller, "itemBatchRenderer", itemRenderer);
|
|
SetObject(controller, "uiDocument", document);
|
|
SetObject(controller, "uiStyleSheet", AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath));
|
|
SetObject(controller, "titleBackground", _floor);
|
|
SetObject(controller, "uiFont", _font);
|
|
SetObject(controller, "tutorialFont", _tutorialFont);
|
|
SetObject(controller, "tutorialAnchorRoot", tutorialAnchorRoot);
|
|
SetObjectArray(controller, "machineIcons", _machineSprites);
|
|
return root;
|
|
}
|
|
|
|
private static RectTransform CreateTutorialAnchors(Transform parent)
|
|
{
|
|
var root = new GameObject("TutorialAnchors", typeof(RectTransform)).GetComponent<RectTransform>();
|
|
root.SetParent(parent, false);
|
|
root.anchorMin = Vector2.zero;
|
|
root.anchorMax = Vector2.zero;
|
|
root.pivot = Vector2.zero;
|
|
root.anchoredPosition = Vector2.zero;
|
|
root.sizeDelta = new Vector2(1920, 1080);
|
|
foreach (var id in TutorialAnchorIds)
|
|
{
|
|
var child = new GameObject(id, typeof(RectTransform), typeof(ShrinkTutorialAnchor)).GetComponent<RectTransform>();
|
|
child.SetParent(root, false);
|
|
child.anchorMin = Vector2.zero;
|
|
child.anchorMax = Vector2.zero;
|
|
child.pivot = Vector2.zero;
|
|
child.sizeDelta = new Vector2(64, 64);
|
|
child.GetComponent<ShrinkTutorialAnchor>().SetAnchorId(id);
|
|
child.gameObject.SetActive(false);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
private static PanelSettings BuildPanelSettings()
|
|
{
|
|
DeleteAssetIfExists(PanelSettingsPath);
|
|
var settings = ScriptableObject.CreateInstance<PanelSettings>();
|
|
settings.name = "Demo2PanelSettings";
|
|
settings.themeStyleSheet = AssetDatabase.LoadAssetAtPath<ThemeStyleSheet>(ThemePath) ?? throw new InvalidOperationException("Demo2 runtime theme is missing.");
|
|
settings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
|
|
settings.referenceResolution = new Vector2Int(1920, 1080);
|
|
settings.screenMatchMode = PanelScreenMatchMode.MatchWidthOrHeight;
|
|
settings.match = 0.5f;
|
|
settings.sortingOrder = 0;
|
|
settings.clearColor = false;
|
|
AssetDatabase.CreateAsset(settings, PanelSettingsPath);
|
|
if (AssetDatabase.IsValidFolder("Assets/UI Toolkit")) AssetDatabase.DeleteAsset("Assets/UI Toolkit");
|
|
return settings;
|
|
}
|
|
|
|
private static void ConfigureArt()
|
|
{
|
|
AssetDatabase.Refresh();
|
|
foreach (var path in AssetDatabase.FindAssets("t:Texture2D", new[] { Art }).Select(AssetDatabase.GUIDToAssetPath).Where(path => path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
if (AssetImporter.GetAtPath(path) is not TextureImporter importer) continue;
|
|
importer.textureType = TextureImporterType.Sprite;
|
|
importer.spriteImportMode = SpriteImportMode.Single;
|
|
importer.spritePixelsPerUnit = path.EndsWith("factory-floor.png", StringComparison.OrdinalIgnoreCase) ? 64 : 1024;
|
|
importer.mipmapEnabled = false;
|
|
importer.alphaIsTransparency = true;
|
|
importer.textureCompression = TextureImporterCompression.CompressedHQ;
|
|
importer.maxTextureSize = path.EndsWith("factory-floor.png", StringComparison.OrdinalIgnoreCase) ? 2048 : 1024;
|
|
importer.SaveAndReimport();
|
|
}
|
|
if (AssetDatabase.LoadAssetAtPath<Font>(FontPath) == null)
|
|
{
|
|
const string source = "Assets/Demos/ReplacedPerson/Art/NotoSansSC-Variable.ttf";
|
|
if (!AssetDatabase.CopyAsset(source, FontPath)) throw new InvalidOperationException("Could not copy the Chinese UI Toolkit font.");
|
|
}
|
|
if (AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(TutorialFontPath) == null)
|
|
{
|
|
const string source = "Assets/Demos/ReplacedPerson/Art/NotoSansSC SDF.asset";
|
|
if (!AssetDatabase.CopyAsset(source, TutorialFontPath)) throw new InvalidOperationException("Could not copy the Chinese tutorial TMP font.");
|
|
}
|
|
}
|
|
|
|
private static void LoadAssets()
|
|
{
|
|
_font = AssetDatabase.LoadAssetAtPath<Font>(FontPath) ?? throw new InvalidOperationException("Demo2 UI Toolkit font is missing.");
|
|
_tutorialFont = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(TutorialFontPath) ?? throw new InvalidOperationException("Demo2 tutorial font is missing.");
|
|
_floor = AssetDatabase.LoadAssetAtPath<Sprite>(Art + "/factory-floor.png");
|
|
_itemTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(Art + "/material-crate.png");
|
|
_machineSprites = MachineNames.Select(name => AssetDatabase.LoadAssetAtPath<Sprite>($"{Art}/machine-{name}.png")).ToArray()!;
|
|
if (_floor == null || _itemTexture == null || _machineSprites.Any(sprite => sprite == null)) throw new InvalidOperationException("Demo2 art import is incomplete.");
|
|
}
|
|
|
|
private static void EnsureDirectories()
|
|
{
|
|
foreach (var path in new[] { Root, Art, Ui, Root + "/Prefabs", ResourceRoot })
|
|
if (!AssetDatabase.IsValidFolder(path))
|
|
{
|
|
var slash = path.LastIndexOf('/');
|
|
AssetDatabase.CreateFolder(path[..slash], path[(slash + 1)..]);
|
|
}
|
|
}
|
|
|
|
private static void BuildTutorialAssets()
|
|
{
|
|
DeleteAssetIfExists(ResourceRoot + "/Demo2FirstCycleTutorial.asset");
|
|
DeleteAssetIfExists(ResourceRoot + "/Demo2TutorialDatabase.asset");
|
|
DeleteAssetIfExists(ResourceRoot + "/Demo2TutorialSettings.asset");
|
|
var tutorial = ScriptableObject.CreateInstance<ShrinkTutorialData>();
|
|
tutorial.tutorialId = "demo2.first-cycle";
|
|
tutorial.steps = new List<ShrinkTutorialStep>
|
|
{
|
|
TutorialStep("demo2.miner", "demo2.miner-selected", "选择采集器,准备建立新的铁矿入口。", ShrinkTutorialDialogAnchor.Right),
|
|
TutorialStep("demo2.grid", "demo2.miner-placed", "在空网格放置采集器。", ShrinkTutorialDialogAnchor.Top),
|
|
TutorialStep("demo2.conveyor", "demo2.conveyor-selected", "选择传送带,准备连接生产线。", ShrinkTutorialDialogAnchor.Right),
|
|
TutorialStep("demo2.grid", "demo2.conveyor-placed", "在网格放置传送带,把产物送向加工区。", ShrinkTutorialDialogAnchor.Top),
|
|
TutorialStep("demo2.smelter", "demo2.smelter-selected", "选择熔炼机,准备把矿石加工为铁锭。", ShrinkTutorialDialogAnchor.Right),
|
|
TutorialStep("demo2.recipe", "demo2.recipe-selected", "确认当前机器配方。配方决定输入、产出与加工周期。", ShrinkTutorialDialogAnchor.Top),
|
|
TutorialStep("demo2.turret", "demo2.turret-selected", "选择自动炮塔,准备连接防线。", ShrinkTutorialDialogAnchor.Right),
|
|
TutorialStep("demo2.grid", "demo2.turret-placed", "在网格放置炮塔。生产的弹药会在结算阶段自动消耗。", ShrinkTutorialDialogAnchor.Top),
|
|
TutorialStep("demo2.ready", "demo2.production-started", "准备后进入自动生产。周期火力等于载荷乘以倍率。", ShrinkTutorialDialogAnchor.Bottom),
|
|
TutorialStep("demo2.reward", "demo2.rule-chosen", "防守后选择第一张规则牌。规则槽严格从左到右结算。", ShrinkTutorialDialogAnchor.Bottom)
|
|
};
|
|
AssetDatabase.CreateAsset(tutorial, ResourceRoot + "/Demo2FirstCycleTutorial.asset");
|
|
var database = ScriptableObject.CreateInstance<ShrinkTutorialDatabase>();
|
|
var serialized = new SerializedObject(database);
|
|
var tutorials = serialized.FindProperty("tutorials");
|
|
tutorials.arraySize = 1;
|
|
tutorials.GetArrayElementAtIndex(0).objectReferenceValue = tutorial;
|
|
serialized.ApplyModifiedPropertiesWithoutUndo();
|
|
AssetDatabase.CreateAsset(database, ResourceRoot + "/Demo2TutorialDatabase.asset");
|
|
var settings = ScriptableObject.CreateInstance<ShrinkTutorialSettings>();
|
|
settings.name = "Demo2TutorialSettings";
|
|
settings.database = database;
|
|
settings.playerPrefsStorageKey = "Demo2.RuleFactory.Tutorial";
|
|
settings.showSkipButton = true;
|
|
settings.canvasSortingOrder = 5000;
|
|
AssetDatabase.CreateAsset(settings, ResourceRoot + "/Demo2TutorialSettings.asset");
|
|
}
|
|
|
|
private static ShrinkTutorialStep TutorialStep(string anchor, string signal, string text, ShrinkTutorialDialogAnchor dialogAnchor) => new()
|
|
{
|
|
targetMode = ShrinkTutorialTargetMode.AnchorId,
|
|
anchorId = anchor,
|
|
completeCondition = ShrinkTutorialCompleteCondition.CustomEvent,
|
|
customEventName = signal,
|
|
fallbackText = text,
|
|
dialogAnchor = dialogAnchor,
|
|
maskShape = ShrinkTutorialMaskShape.Rect,
|
|
maskPadding = 10,
|
|
waitForTarget = true,
|
|
waitTimeout = 300
|
|
};
|
|
|
|
private static void AddSceneToBuildSettings()
|
|
{
|
|
var scenes = EditorBuildSettings.scenes.ToList();
|
|
if (scenes.All(value => value.path != ScenePath)) scenes.Add(new EditorBuildSettingsScene(ScenePath, true));
|
|
else
|
|
for (var i = 0; i < scenes.Count; i++) if (scenes[i].path == ScenePath) scenes[i] = new EditorBuildSettingsScene(ScenePath, true);
|
|
EditorBuildSettings.scenes = scenes.ToArray();
|
|
}
|
|
|
|
private static void SetObject(UnityEngine.Object target, string propertyName, UnityEngine.Object? value)
|
|
{
|
|
var serialized = new SerializedObject(target);
|
|
var property = serialized.FindProperty(propertyName) ?? throw new InvalidOperationException($"Missing serialized property {target.GetType().Name}.{propertyName}");
|
|
property.objectReferenceValue = value;
|
|
serialized.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
private static void SetObjectArray(UnityEngine.Object target, string propertyName, IReadOnlyList<UnityEngine.Object> values)
|
|
{
|
|
var serialized = new SerializedObject(target);
|
|
var property = serialized.FindProperty(propertyName) ?? throw new InvalidOperationException($"Missing serialized property {target.GetType().Name}.{propertyName}");
|
|
property.arraySize = values.Count;
|
|
for (var i = 0; i < values.Count; i++) property.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
|
|
serialized.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
private static void DeleteAssetIfExists(string path)
|
|
{
|
|
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null) AssetDatabase.DeleteAsset(path);
|
|
}
|
|
|
|
private static Color Hex(string hex)
|
|
{
|
|
if (!ColorUtility.TryParseHtmlString("#" + hex, out var color)) throw new ArgumentException("Invalid color: " + hex);
|
|
return color;
|
|
}
|
|
}
|
|
}
|