Files
2026-08-17 18:18:13 +08:00

525 lines
34 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ReplacedPerson.Runtime;
using ShrinkApp;
using ShrinkContext.AppAdapter;
using ShrinkModFramework;
using ShrinkTutorial;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.TextCore.LowLevel;
using UnityEngine.UI;
namespace ReplacedPerson.Editor
{
public static class ReplacedPersonSceneBuilder
{
private const string Root = "Assets/Demos/ReplacedPerson";
private const string PrefabRoot = Root + "/Prefabs";
private const string ResourceRoot = Root + "/Resources";
private const string ScenePath = Root + "/ReplacedPerson.unity";
private const string DemoPrefabPath = PrefabRoot + "/ReplacedPersonDemo.prefab";
private const string BattlePrefabPath = PrefabRoot + "/BattleScreen.prefab";
private const string ModalPrefabPath = PrefabRoot + "/ModalOverlay.prefab";
private const string FontPath = Root + "/Art/NotoSansSC-Variable.ttf";
private const string FontAssetPath = Root + "/Art/NotoSansSC SDF.asset";
private static TMP_FontAsset? _font;
private static Sprite? _paper;
private static Sprite? _attack;
private static Sprite? _dodge;
private static Sprite? _counter;
[MenuItem("ShrinkSDK/Demos/Replaced Person/Rebuild Full Demo")]
public static void Rebuild()
{
Directory.CreateDirectory(PrefabRoot);
Directory.CreateDirectory(ResourceRoot);
AssetDatabase.Refresh();
EnsureSdkAssets();
ConfigureSprites();
EnsureFont();
BuildBattlePrefab();
BuildModalPrefab();
BuildDemoPrefab();
BuildTutorialAssets();
BuildScene();
AddToBuildSettings();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"[ReplacedPerson] Demo rebuilt: {ScenePath}");
}
[MenuItem("ShrinkSDK/Demos/Replaced Person/Build Windows Player")]
public static void BuildWindowsPlayer()
{
var output = Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Artifacts", "ReplacedPersonBuild", "ReplacedPerson.exe"));
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions
{
scenes = new[] { ScenePath },
locationPathName = output,
target = BuildTarget.StandaloneWindows64,
options = BuildOptions.Development
});
if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
throw new InvalidOperationException($"Windows build failed: errors={report.summary.totalErrors}, warnings={report.summary.totalWarnings}");
Debug.Log($"[ReplacedPerson] Windows build complete: {output}, warnings={report.summary.totalWarnings}");
}
private static void EnsureSdkAssets()
{
const string appSettingsPath = "Assets/Resources/ShrinkAppSettings.asset";
var appSettings = AssetDatabase.LoadAssetAtPath<ShrinkAppSettings>(appSettingsPath);
var appScript = appSettings == null
? null
: new SerializedObject(appSettings).FindProperty("m_Script")?.objectReferenceValue;
if (appSettings == null || appScript == null)
{
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(appSettingsPath))) AssetDatabase.DeleteAsset(appSettingsPath);
appSettings = ScriptableObject.CreateInstance<ReplacedPersonAppSettings>();
appSettings.hostingMode = ShrinkAppHostingMode.ContextLoader;
appSettings.verboseLogging = false;
AssetDatabase.CreateAsset(appSettings, appSettingsPath);
}
const string modSettingsPath = ResourceRoot + "/ShrinkModFrameworkSettings.asset";
var modSettings = AssetDatabase.LoadAssetAtPath<ShrinkModFrameworkSettings>(modSettingsPath);
if (modSettings == null)
{
modSettings = ScriptableObject.CreateInstance<ShrinkModFrameworkSettings>();
modSettings.verboseLogging = false;
AssetDatabase.CreateAsset(modSettings, modSettingsPath);
}
const string profilePath = "Assets/Resources/ShrinkAppComposition.asset";
var profile = AssetDatabase.LoadAssetAtPath<ShrinkAppCompositionProfile>(profilePath);
if (profile != null)
{
var document = profile.ResolveDocument();
if (document.entries.All(value => !string.Equals(value.id, "demo.replaced-person", StringComparison.OrdinalIgnoreCase)))
document.entries.Add(new ShrinkAppCompositionEntry("demo.replaced-person"));
profile.SetDocument(document);
EditorUtility.SetDirty(profile);
}
AssetDatabase.SaveAssets();
}
private static void ConfigureSprites()
{
var paths = new[]
{
Root + "/Art/ForumOriginal/paper.jpg",
Root + "/Art/ForumOriginal/card-attack-a.png",
Root + "/Art/ForumOriginal/card-dodge-a.png",
Root + "/Art/ForumOriginal/card-counter.png",
Root + "/Art/ForumOriginal/card-back.png"
};
foreach (var path in paths)
{
if (AssetImporter.GetAtPath(path) is not TextureImporter importer) continue;
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Single;
importer.mipmapEnabled = false;
importer.alphaIsTransparency = true;
importer.SaveAndReimport();
}
_paper = AssetDatabase.LoadAssetAtPath<Sprite>(paths[0]);
_attack = AssetDatabase.LoadAssetAtPath<Sprite>(paths[1]);
_dodge = AssetDatabase.LoadAssetAtPath<Sprite>(paths[2]);
_counter = AssetDatabase.LoadAssetAtPath<Sprite>(paths[3]);
}
private static void EnsureFont()
{
DeleteAssetIfExists(FontAssetPath);
var source = AssetDatabase.LoadAssetAtPath<Font>(FontPath);
if (source == null) throw new InvalidOperationException("CJK source font missing: " + FontPath);
_font = TMP_FontAsset.CreateFontAsset(source, 48, 9, GlyphRenderMode.SDFAA, 1024, 1024,
AtlasPopulationMode.Dynamic, true);
_font.name = "NotoSansSC SDF";
_font.atlasPopulationMode = AtlasPopulationMode.Dynamic;
AssetDatabase.CreateAsset(_font, FontAssetPath);
foreach (var atlas in _font.atlasTextures)
{
if (atlas == null || AssetDatabase.Contains(atlas)) continue;
atlas.name = _font.name + " Atlas";
AssetDatabase.AddObjectToAsset(atlas, _font);
}
if (_font.material != null && !AssetDatabase.Contains(_font.material))
{
_font.material.name = _font.name + " Material";
AssetDatabase.AddObjectToAsset(_font.material, _font);
}
EditorUtility.SetDirty(_font);
AssetDatabase.SaveAssets();
}
private static void BuildBattlePrefab()
{
var screen = Screen("BattleScreen");
try
{
var header = Panel(screen.transform, "Header", new Color(0.05f, 0.055f, 0.06f, 0.96f),
new Vector2(0, .92f), Vector2.one);
Button(header.transform, "BackButton", "", new Vector2(0, 0), new Vector2(.07f, 1), 32);
Text(header.transform, "RoundText", "回合 1", 25, TextAlignmentOptions.Center,
new Vector2(.08f, 0), new Vector2(.92f, 1));
var enemy = Panel(screen.transform, "EnemyArea", new Color(.11f, .12f, .12f, .94f),
new Vector2(0, .75f), new Vector2(1, .92f));
Text(enemy.transform, "EnemyStatus", "贪婪 HP 30/30 COST 10/10", 24, TextAlignmentOptions.Left,
new Vector2(.025f, .56f), new Vector2(.98f, .95f));
Text(enemy.transform, "EnemyQueue", "等待敌方锁定", 22, TextAlignmentOptions.Left,
new Vector2(.025f, .08f), new Vector2(.98f, .54f));
Anchor(enemy.gameObject, "rp.matchup");
var effects = FullRect(screen.transform, "EffectArea", new Vector2(.03f, .43f), new Vector2(.97f, .74f));
var layout = effects.gameObject.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 18; layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlWidth = true; layout.childControlHeight = true; layout.childForceExpandWidth = true;
for (var i = 1; i <= 3; i++)
{
var slot = Panel(effects, "Slot" + i, i == 2 ? new Color(.29f, .11f, .10f, .94f) : new Color(.11f, .14f, .14f, .94f), Vector2.zero, Vector2.one);
Text(slot.transform, "Text", $"槽 {i}\n等待拼点", 21, TextAlignmentOptions.Center, Vector2.zero, Vector2.one, new Vector2(20, 16), new Vector2(-20, -16));
}
var log = Panel(screen.transform, "LogPanel", new Color(.04f, .045f, .05f, .90f), new Vector2(.03f, .30f), new Vector2(.97f, .42f));
Text(log.transform, "LogText", "战斗记录", 15, TextAlignmentOptions.TopLeft, Vector2.zero, new Vector2(.48f, 1), new Vector2(16, 10), new Vector2(-12, -10));
Text(log.transform, "CardDetailTitle", "卡牌效果", 18, TextAlignmentOptions.TopLeft,
new Vector2(.51f, .60f), new Vector2(.98f, .94f));
Text(log.transform, "CardDetailText", "悬停、点击或拖动卡牌查看完整效果。", 15, TextAlignmentOptions.TopLeft,
new Vector2(.51f, .06f), new Vector2(.98f, .62f));
var player = Panel(screen.transform, "PlayerArea", new Color(.08f, .085f, .09f, .97f), new Vector2(0, 0), new Vector2(1, .29f));
Text(player.transform, "PlayerStatus", "被替代之人 HP 30/30 COST 10/10", 21, TextAlignmentOptions.Left,
new Vector2(.02f, .82f), new Vector2(.5f, .99f));
Text(player.transform, "SelectionText", "普通牌:未选择\n结束牌:未选择", 15, TextAlignmentOptions.TopLeft,
new Vector2(.51f, .73f), new Vector2(.98f, .99f));
var normalHand = FullRect(player.transform, "NormalHand", new Vector2(.02f, .30f), new Vector2(.72f, .81f));
var normalLayout = normalHand.gameObject.AddComponent<HorizontalLayoutGroup>();
normalLayout.spacing = 8; normalLayout.childControlWidth = false; normalLayout.childControlHeight = true;
normalLayout.childForceExpandWidth = false; normalLayout.padding = new RectOffset(0, 0, 4, 4);
CardTemplate(normalHand, "CardButtonTemplate");
Anchor(normalHand.gameObject, "rp.normal-hand");
var endHand = FullRect(player.transform, "EndHand", new Vector2(.73f, .30f), new Vector2(.98f, .81f));
var endLayout = endHand.gameObject.AddComponent<HorizontalLayoutGroup>();
endLayout.spacing = 8; endLayout.childControlWidth = false; endLayout.childControlHeight = true; endLayout.childForceExpandWidth = false;
CardTemplate(endHand, "CardButtonTemplate");
Anchor(endHand.gameObject, "rp.end-hand");
var actions = FullRect(player.transform, "Actions", new Vector2(.02f, .03f), new Vector2(.98f, .28f));
var actionLayout = actions.gameObject.AddComponent<HorizontalLayoutGroup>();
actionLayout.spacing = 10; actionLayout.childControlHeight = true; actionLayout.childControlWidth = true; actionLayout.childForceExpandWidth = true;
Button(actions, "Reroll1Button", "骰 1", Vector2.zero, Vector2.one, 17);
Button(actions, "Reroll2Button", "骰 2", Vector2.zero, Vector2.one, 17);
Button(actions, "Reroll3Button", "骰 3", Vector2.zero, Vector2.one, 17);
Button(actions, "PassButton", "跳过", Vector2.zero, Vector2.one, 18);
Button(actions, "SubmitButton", "锁定", Vector2.zero, Vector2.one, 19, new Color(.66f, .12f, .10f, 1));
Anchor(actions.gameObject, "rp.reroll");
PrefabUtility.SaveAsPrefabAsset(screen, BattlePrefabPath);
}
finally { UnityEngine.Object.DestroyImmediate(screen); }
}
private static void BuildModalPrefab()
{
var root = Screen("ModalOverlay");
try
{
root.AddComponent<CanvasGroup>();
var scrim = Image(root.transform, "Scrim", null, new Color(0, 0, 0, .72f), Vector2.zero, Vector2.one);
scrim.raycastTarget = true;
var panel = Panel(root.transform, "Panel", new Color(.11f, .12f, .12f, 1), new Vector2(.3f, .32f), new Vector2(.7f, .68f));
Text(panel.transform, "Title", "提示", 30, TextAlignmentOptions.Center, new Vector2(.08f, .67f), new Vector2(.92f, .94f));
Text(panel.transform, "Message", "", 20, TextAlignmentOptions.Center, new Vector2(.08f, .28f), new Vector2(.92f, .68f));
Button(panel.transform, "CloseButton", "确定", new Vector2(.32f, .08f), new Vector2(.68f, .25f), 20);
PrefabUtility.SaveAsPrefabAsset(root, ModalPrefabPath);
}
finally { UnityEngine.Object.DestroyImmediate(root); }
}
private static void BuildDemoPrefab()
{
var root = new GameObject("ReplacedPersonDemo", typeof(RectTransform), typeof(ReplacedPersonDemoController));
try
{
var canvasObject = new GameObject("Canvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvasObject.transform.SetParent(root.transform, false);
var canvas = canvasObject.GetComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.matchWidthOrHeight = .5f;
Stretch(canvasObject.GetComponent<RectTransform>());
var background = Image(canvasObject.transform, "Background", _paper, new Color(.32f, .35f, .36f, 1), Vector2.zero, Vector2.one);
background.preserveAspect = false;
var safe = FullRect(canvasObject.transform, "SafeAreaRoot", Vector2.zero, Vector2.one);
safe.gameObject.AddComponent<ReplacedSafeArea>();
var screens = FullRect(safe, "Screens", Vector2.zero, Vector2.one);
BuildTitle(screens);
BuildChapter(screens);
BuildDeck(screens);
BuildLan(screens);
var battlePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BattlePrefabPath) ?? throw new InvalidOperationException("Battle prefab missing.");
var battle = (GameObject)PrefabUtility.InstantiatePrefab(battlePrefab);
battle.transform.SetParent(screens, false); Stretch(battle.GetComponent<RectTransform>());
BuildReward(screens);
BuildSettings(screens);
PrefabUtility.SaveAsPrefabAsset(root, DemoPrefabPath);
}
finally { UnityEngine.Object.DestroyImmediate(root); }
}
private static void BuildTitle(Transform parent)
{
var screen = Screen("TitleScreen", parent);
Text(screen.transform, "Title", "被替代之人", 70, TextAlignmentOptions.Center, new Vector2(.08f, .70f), new Vector2(.92f, .94f));
Text(screen.transform, "Subtitle", "第一章 · 贪婪", 24, TextAlignmentOptions.Center, new Vector2(.15f, .62f), new Vector2(.85f, .72f));
var art = FullRect(screen.transform, "ArtTriptych", new Vector2(.08f, .20f), new Vector2(.58f, .62f));
var layout = art.gameObject.AddComponent<HorizontalLayoutGroup>(); layout.spacing = 20; layout.childControlHeight = true; layout.childControlWidth = true; layout.childForceExpandWidth = true;
Image(art, "Attack", _attack, Color.white, Vector2.zero, Vector2.one).preserveAspect = true;
Image(art, "Dodge", _dodge, Color.white, Vector2.zero, Vector2.one).preserveAspect = true;
Image(art, "Counter", _counter, Color.white, Vector2.zero, Vector2.one).preserveAspect = true;
var actions = FullRect(screen.transform, "Actions", new Vector2(.64f, .20f), new Vector2(.91f, .61f));
var vertical = actions.gameObject.AddComponent<VerticalLayoutGroup>();
vertical.spacing = 14; vertical.childControlWidth = true; vertical.childControlHeight = true; vertical.childForceExpandHeight = true;
Button(actions, "ContinueButton", "进入章节", Vector2.zero, Vector2.one, 25, new Color(.67f, .13f, .11f, 1));
Button(actions, "DeckButton", "牌组", Vector2.zero, Vector2.one, 22);
Button(actions, "LanButton", "局域网", Vector2.zero, Vector2.one, 22);
Button(actions, "SettingsButton", "设置", Vector2.zero, Vector2.one, 22);
Text(screen.transform, "Footer", "DEMO / WINDOWS PC / v1", 14, TextAlignmentOptions.BottomLeft, new Vector2(.03f, .02f), new Vector2(.7f, .09f));
}
private static void BuildChapter(Transform parent)
{
var screen = Screen("ChapterScreen", parent);
Header(screen.transform, "章节");
var content = FullRect(screen.transform, "Content", new Vector2(.10f, .17f), new Vector2(.90f, .88f));
Text(content, "ChapterLabel", "第一章 · 贪婪", 42, TextAlignmentOptions.TopLeft, new Vector2(0, .77f), Vector2.one);
Text(content, "StoryText", "", 26, TextAlignmentOptions.TopLeft, Vector2.zero, new Vector2(1, .76f));
var footer = FullRect(screen.transform, "Footer", new Vector2(.66f, .05f), new Vector2(.90f, .14f));
Button(footer, "StartBattleButton", "进入第一战", Vector2.zero, Vector2.one, 24, new Color(.67f, .13f, .11f, 1));
}
private static void BuildDeck(Transform parent)
{
var screen = Screen("DeckScreen", parent); Header(screen.transform, "牌组编辑");
var content = FullRect(screen.transform, "Content", new Vector2(.04f, .15f), new Vector2(.96f, .88f));
Text(content, "DeckStatus", "普通牌 12/12", 22, TextAlignmentOptions.Left, new Vector2(0, .91f), Vector2.one);
ScrollList(content, "CurrentDeck", "当前牌组 · 点击移除", new Vector2(0, 0), new Vector2(.48f, .89f));
ScrollList(content, "Collection", "收藏 · 点击加入", new Vector2(.52f, 0), new Vector2(1, .89f));
var footer = FullRect(screen.transform, "Footer", new Vector2(.62f, .04f), new Vector2(.96f, .12f));
var layout = footer.gameObject.AddComponent<HorizontalLayoutGroup>(); layout.spacing = 12; layout.childControlHeight = true; layout.childControlWidth = true;
Button(footer, "ResetButton", "重置", Vector2.zero, Vector2.one, 20);
Button(footer, "SaveButton", "保存", Vector2.zero, Vector2.one, 20, new Color(.67f, .13f, .11f, 1));
}
private static void BuildLan(Transform parent)
{
var screen = Screen("LanScreen", parent); Header(screen.transform, "局域网大厅");
var content = FullRect(screen.transform, "Content", new Vector2(.18f, .16f), new Vector2(.82f, .84f));
Text(content, "StatusText", "尚未连接", 24, TextAlignmentOptions.Center, new Vector2(.05f, .68f), new Vector2(.95f, .92f));
Input(content, "AddressInput", "127.0.0.1", new Vector2(.18f, .49f), new Vector2(.82f, .62f));
Button(content, "HostButton", "创建房间", new Vector2(.18f, .31f), new Vector2(.48f, .43f), 22);
Button(content, "JoinButton", "直接加入", new Vector2(.52f, .31f), new Vector2(.82f, .43f), 22, new Color(.67f, .13f, .11f, 1));
Button(content, "StartMatchButton", "进入对局", new Vector2(.34f, .16f), new Vector2(.66f, .27f), 21, new Color(.67f, .13f, .11f, 1));
Text(content, "DiscoveryText", "局域网发现会自动广播可用房间", 17, TextAlignmentOptions.Center, new Vector2(.1f, .01f), new Vector2(.9f, .14f));
}
private static void BuildReward(Transform parent)
{
var screen = Screen("RewardScreen", parent);
Text(screen.transform, "Title", "结算", 28, TextAlignmentOptions.Center, new Vector2(.2f, .87f), new Vector2(.8f, .96f));
var content = FullRect(screen.transform, "Content", new Vector2(.18f, .22f), new Vector2(.82f, .84f));
Text(content, "ResultText", "胜利", 52, TextAlignmentOptions.Center, new Vector2(.1f, .75f), new Vector2(.9f, .94f));
Text(content, "RewardText", "", 21, TextAlignmentOptions.TopLeft, new Vector2(.05f, .30f), new Vector2(.48f, .73f));
Text(content, "AppearanceText", "当前装饰:无", 20, TextAlignmentOptions.Left, new Vector2(.52f, .61f), new Vector2(.98f, .72f));
var ornaments = FullRect(content, "Ornaments", new Vector2(.52f, .30f), new Vector2(.98f, .59f));
var ornamentLayout = ornaments.gameObject.AddComponent<VerticalLayoutGroup>();
ornamentLayout.spacing = 8; ornamentLayout.childControlWidth = true; ornamentLayout.childControlHeight = true; ornamentLayout.childForceExpandHeight = true;
Button(ornaments, "GoldEyeButton", "金色势利眼", Vector2.zero, Vector2.one, 17);
Button(ornaments, "GoldChainButton", "金链", Vector2.zero, Vector2.one, 17);
Button(ornaments, "WornSuitButton", "褶皱西装", Vector2.zero, Vector2.one, 17);
var footer = FullRect(screen.transform, "Footer", new Vector2(.38f, .08f), new Vector2(.62f, .17f));
Button(footer, "ContinueButton", "继续", Vector2.zero, Vector2.one, 23, new Color(.67f, .13f, .11f, 1));
}
private static void BuildSettings(Transform parent)
{
var screen = Screen("SettingsScreen", parent); Header(screen.transform, "设置");
var content = FullRect(screen.transform, "Content", new Vector2(.24f, .25f), new Vector2(.76f, .78f));
Text(content, "TransportStatus", "LAN 传输:TCP", 28, TextAlignmentOptions.Center, new Vector2(.1f, .68f), new Vector2(.9f, .91f));
Button(content, "TcpButton", "TCP", new Vector2(.12f, .42f), new Vector2(.48f, .61f), 23);
Button(content, "KcpButton", "KCP", new Vector2(.52f, .42f), new Vector2(.88f, .61f), 23);
Text(content, "Note", "1920×1080 基准 · 支持 16:10 与 4:3\n拖牌与点牌共用同一提交队列", 18, TextAlignmentOptions.Center, new Vector2(.06f, .09f), new Vector2(.94f, .34f));
}
private static void Header(Transform screen, string title)
{
var header = Panel(screen, "Header", new Color(.05f, .055f, .06f, .94f), new Vector2(0, .90f), Vector2.one);
Button(header.transform, "BackButton", "", new Vector2(0, 0), new Vector2(.08f, 1), 34);
Text(header.transform, "Title", title, 30, TextAlignmentOptions.Left, new Vector2(.10f, 0), new Vector2(.9f, 1));
}
private static void ScrollList(Transform parent, string name, string title, Vector2 min, Vector2 max)
{
var root = Panel(parent, name, new Color(.08f, .085f, .09f, .94f), min, max);
Text(root.transform, "Title", title, 20, TextAlignmentOptions.Left, new Vector2(.04f, .90f), new Vector2(.96f, .99f));
var viewport = FullRect(root.transform, "Viewport", new Vector2(.03f, .03f), new Vector2(.97f, .88f));
viewport.gameObject.AddComponent<RectMask2D>();
var content = FullRect(viewport, "Content", new Vector2(0, 1), Vector2.one);
content.anchorMin = new Vector2(0, 1); content.anchorMax = new Vector2(1, 1); content.pivot = new Vector2(.5f, 1); content.sizeDelta = new Vector2(0, 0);
var layout = content.gameObject.AddComponent<VerticalLayoutGroup>();
layout.spacing = 6; layout.childControlWidth = true; layout.childControlHeight = true; layout.childForceExpandWidth = true; layout.childForceExpandHeight = false; layout.padding = new RectOffset(4, 4, 4, 4);
var fitter = content.gameObject.AddComponent<ContentSizeFitter>(); fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var row = Button(content, "RowTemplate", "卡牌", Vector2.zero, Vector2.one, 17);
row.gameObject.AddComponent<LayoutElement>().preferredHeight = 54; row.gameObject.SetActive(false);
var scroll = root.gameObject.AddComponent<ScrollRect>(); scroll.viewport = viewport; scroll.content = content; scroll.horizontal = false; scroll.movementType = ScrollRect.MovementType.Clamped;
}
private static void CardTemplate(Transform parent, string name)
{
var button = Button(parent, name, "卡牌", Vector2.zero, Vector2.one, 14, new Color(.90f, .87f, .78f, .98f));
button.gameObject.AddComponent<ReplacedCardDragHandler>();
var layout = button.gameObject.AddComponent<LayoutElement>(); layout.preferredWidth = 132; layout.minWidth = 116;
var rect = button.GetComponent<RectTransform>(); rect.sizeDelta = new Vector2(132, 132);
button.gameObject.SetActive(false);
}
private static GameObject Screen(string name, Transform? parent = null)
{
var result = new GameObject(name, typeof(RectTransform));
if (parent != null) result.transform.SetParent(parent, false);
Stretch(result.GetComponent<RectTransform>());
return result;
}
private static RectTransform FullRect(Transform parent, string name, Vector2 min, Vector2 max)
{
var result = new GameObject(name, typeof(RectTransform)).GetComponent<RectTransform>();
result.SetParent(parent, false); result.anchorMin = min; result.anchorMax = max; result.offsetMin = Vector2.zero; result.offsetMax = Vector2.zero;
return result;
}
private static Image Panel(Transform parent, string name, Color color, Vector2 min, Vector2 max) => Image(parent, name, null, color, min, max);
private static Image Image(Transform parent, string name, Sprite? sprite, Color color, Vector2 min, Vector2 max)
{
var rect = FullRect(parent, name, min, max);
var image = rect.gameObject.AddComponent<Image>(); image.sprite = sprite; image.color = color; image.raycastTarget = false;
return image;
}
private static TMP_Text Text(Transform parent, string name, string value, float size, TextAlignmentOptions alignment,
Vector2 min, Vector2 max, Vector2? offsetMin = null, Vector2? offsetMax = null)
{
var rect = FullRect(parent, name, min, max); rect.offsetMin = offsetMin ?? Vector2.zero; rect.offsetMax = offsetMax ?? Vector2.zero;
var text = rect.gameObject.AddComponent<TextMeshProUGUI>(); text.font = _font; text.text = value; text.fontSize = size; text.alignment = alignment;
text.color = new Color(.92f, .91f, .86f, 1); text.enableWordWrapping = true; text.overflowMode = TextOverflowModes.Ellipsis; text.raycastTarget = false;
return text;
}
private static Button Button(Transform parent, string name, string label, Vector2 min, Vector2 max, float size, Color? color = null)
{
var image = Image(parent, name, null, color ?? new Color(.18f, .19f, .19f, .98f), min, max); image.raycastTarget = true;
var button = image.gameObject.AddComponent<Button>(); button.targetGraphic = image;
Text(image.transform, "Label", label, size, TextAlignmentOptions.Center, Vector2.zero, Vector2.one, new Vector2(8, 4), new Vector2(-8, -4));
return button;
}
private static TMP_InputField Input(Transform parent, string name, string placeholder, Vector2 min, Vector2 max)
{
var background = Image(parent, name, null, new Color(.92f, .90f, .84f, 1), min, max); background.raycastTarget = true;
var area = FullRect(background.transform, "TextArea", new Vector2(.04f, .1f), new Vector2(.96f, .9f)); area.gameObject.AddComponent<RectMask2D>();
var text = Text(area, "Text", string.Empty, 22, TextAlignmentOptions.Left, Vector2.zero, Vector2.one); text.color = Color.black;
var hint = Text(area, "Placeholder", placeholder, 22, TextAlignmentOptions.Left, Vector2.zero, Vector2.one); hint.color = new Color(.2f, .2f, .2f, .45f);
var input = background.gameObject.AddComponent<TMP_InputField>(); input.textViewport = area; input.textComponent = (TMP_Text)text; input.placeholder = hint;
return input;
}
private static void Anchor(GameObject target, string id)
{
var anchor = target.AddComponent<ShrinkTutorialAnchor>(); anchor.SetAnchorId(id);
}
private static void Stretch(RectTransform rect)
{
rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero;
}
private static void BuildTutorialAssets()
{
DeleteAssetIfExists(ResourceRoot + "/ReplacedPersonFirstMatchTutorial.asset");
DeleteAssetIfExists(ResourceRoot + "/ReplacedPersonTutorialDatabase.asset");
DeleteAssetIfExists(ResourceRoot + "/ShrinkTutorialSettings.asset");
DeleteAssetIfExists(ResourceRoot + "/ReplacedPersonTutorialSettings.asset");
var tutorial = ScriptableObject.CreateInstance<ShrinkTutorialData>();
tutorial.tutorialId = "replaced-person.first-match";
tutorial.steps = new List<ShrinkTutorialStep>
{
Step("rp.normal-hand", "rp.card-selected", "选择一张普通牌。牌面顺序会从左到右继承到结束牌。", ShrinkTutorialDialogAnchor.Top),
Step("rp.end-hand", "rp.end-selected", "选择结束牌。它提供三个效果槽,并锁定本次提交。", ShrinkTutorialDialogAnchor.Top),
Step("rp.matchup", "rp.matchup-submitted", "后手能看到先手牌序。对齐克制关系后再锁定。", ShrinkTutorialDialogAnchor.Bottom),
Step("rp.reroll", "rp.reroll-marked", "标记一个效果槽;初次骰点不高于 2 时会自动重掷一次。", ShrinkTutorialDialogAnchor.Top)
};
AssetDatabase.CreateAsset(tutorial, ResourceRoot + "/ReplacedPersonFirstMatchTutorial.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 + "/ReplacedPersonTutorialDatabase.asset");
var settings = ScriptableObject.CreateInstance<ShrinkTutorialSettings>(); settings.database = database; settings.playerPrefsStorageKey = "ReplacedPerson.Tutorial"; settings.showSkipButton = true; settings.canvasSortingOrder = 5000;
settings.name = "ReplacedPersonTutorialSettings";
AssetDatabase.CreateAsset(settings, ResourceRoot + "/ReplacedPersonTutorialSettings.asset");
}
private static ShrinkTutorialStep Step(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 = 3
};
private static void BuildScene()
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(DemoPrefabPath) ?? throw new InvalidOperationException("Demo prefab missing.");
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, scene); instance.name = "ReplacedPersonDemo";
var cameraObject = new GameObject("Main Camera", typeof(Camera), typeof(AudioListener));
cameraObject.tag = "MainCamera";
cameraObject.transform.position = new Vector3(0, 0, -10);
var camera = cameraObject.GetComponent<Camera>();
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(.02f, .025f, .03f, 1);
camera.cullingMask = 0;
camera.orthographic = true;
camera.depth = -100;
new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
EditorSceneManager.SaveScene(scene, ScenePath);
}
private static void AddToBuildSettings()
{
var scenes = EditorBuildSettings.scenes.ToList();
var existing = scenes.FindIndex(value => value.path == ScenePath);
if (existing >= 0) scenes[existing] = new EditorBuildSettingsScene(ScenePath, true);
else scenes.Add(new EditorBuildSettingsScene(ScenePath, true));
EditorBuildSettings.scenes = scenes.ToArray();
}
private static void DeleteAssetIfExists(string path) { if (AssetDatabase.LoadMainAssetAtPath(path) != null) AssetDatabase.DeleteAsset(path); }
}
}