- 引入 UnityGameFramework

- 使用 Luban 替代原来的 UGF 的数据表
This commit is contained in:
huangjun
2026-08-30 18:08:55 +08:00
parent 42ad134759
commit 92bf3e9097
1925 changed files with 362268 additions and 4859 deletions
@@ -0,0 +1,83 @@
using GameFramework;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityGameFramework.Editor.ResourceTools;
namespace SepCore.Editor
{
public sealed class DefaultBuildEventHandler : IBuildEventHandler
{
public bool ContinueOnFailure
{
get
{
return false;
}
}
public void OnPreprocessAllPlatforms(string productName, string companyName, string gameIdentifier, string gameFrameworkVersion, string unityVersion, string applicableGameVersion, int internalResourceVersion,
Platform platforms, AssetBundleCompressionType assetBundleCompression, string compressionHelperTypeName, bool additionalCompressionSelected, bool forceRebuildAssetBundleSelected, string buildEventHandlerTypeName, string outputDirectory, BuildAssetBundleOptions buildAssetBundleOptions,
string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, string buildReportPath)
{
string streamingAssetsPath = GameFramework.Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "StreamingAssets"));
string[] fileNames = Directory.GetFiles(streamingAssetsPath, "*", SearchOption.AllDirectories);
foreach (string fileName in fileNames)
{
if (fileName.Contains(".gitkeep"))
{
continue;
}
File.Delete(fileName);
}
GameFramework.Utility.Path.RemoveEmptyDirectory(streamingAssetsPath);
}
public void OnPostprocessAllPlatforms(string productName, string companyName, string gameIdentifier, string gameFrameworkVersion, string unityVersion, string applicableGameVersion, int internalResourceVersion,
Platform platforms, AssetBundleCompressionType assetBundleCompression, string compressionHelperTypeName, bool additionalCompressionSelected, bool forceRebuildAssetBundleSelected, string buildEventHandlerTypeName, string outputDirectory, BuildAssetBundleOptions buildAssetBundleOptions,
string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, string buildReportPath)
{
}
public void OnPreprocessPlatform(Platform platform, string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath)
{
}
public void OnBuildAssetBundlesComplete(Platform platform, string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, AssetBundleManifest assetBundleManifest)
{
}
public void OnOutputUpdatableVersionListData(Platform platform, string versionListPath, int versionListLength, int versionListHashCode, int versionListCompressedLength, int versionListCompressedHashCode)
{
}
public void OnPostprocessPlatform(Platform platform, string workingPath, bool outputPackageSelected, string outputPackagePath, bool outputFullSelected, string outputFullPath, bool outputPackedSelected, string outputPackedPath, bool isSuccess)
{
if (!outputPackageSelected)
{
return;
}
if (platform != Platform.Windows)
{
return;
}
string streamingAssetsPath = GameFramework.Utility.Path.GetRegularPath(Path.Combine(Application.dataPath, "StreamingAssets"));
string[] fileNames = Directory.GetFiles(outputPackagePath, "*", SearchOption.AllDirectories);
foreach (string fileName in fileNames)
{
string destFileName = GameFramework.Utility.Path.GetRegularPath(Path.Combine(streamingAssetsPath, fileName.Substring(outputPackagePath.Length)));
FileInfo destFileInfo = new FileInfo(destFileName);
if (!destFileInfo.Directory.Exists)
{
destFileInfo.Directory.Create();
}
File.Copy(fileName, destFileName);
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 64311189c3f9ae140b59a31db9831950
timeCreated: 1528026151
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using GameFramework;
using System.IO;
using UnityEngine;
using UnityGameFramework.Editor;
using UnityGameFramework.Editor.ResourceTools;
namespace SepCore.Editor
{
public static class GameFrameworkConfigs
{
[BuildSettingsConfigPath] public static string BuildSettingsConfig =
GameFramework.Utility.Path.GetRegularPath(Path.Combine(Application.dataPath,
"GameMain/Configs/BuildSettings.xml"));
[ResourceCollectionConfigPath] public static string ResourceCollectionConfig =
GameFramework.Utility.Path.GetRegularPath(Path.Combine(Application.dataPath,
"GameMain/Configs/ResourceCollection.xml"));
[ResourceEditorConfigPath] public static string ResourceEditorConfig =
GameFramework.Utility.Path.GetRegularPath(Path.Combine(Application.dataPath,
"GameMain/Configs/ResourceEditor.xml"));
[ResourceBuilderConfigPath] public static string ResourceBuilderConfig =
GameFramework.Utility.Path.GetRegularPath(Path.Combine(Application.dataPath,
"GameMain/Configs/ResourceBuilder.xml"));
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 36618c709c84656438dd91e565226606
timeCreated: 1528026150
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using System.IO;
namespace SepCore.Editor
{
[InitializeOnLoad]
public class SceneSwitchLeft
{
static SceneSwitchLeft()
{
// 注册到全局工具栏绘制事件
SceneView.duringSceneGui += OnSceneGUI;
}
static void OnSceneGUI(SceneView sceneView)
{
// 在 Scene 视图的左上角绘制一个下拉菜单
Handles.BeginGUI();
GUILayout.BeginArea(new Rect(10, 10, 200, 100));
if (EditorGUILayout.DropdownButton(new GUIContent("快速切换场景"), FocusType.Passive,
EditorStyles.toolbarDropDown))
{
ShowSceneMenu();
}
GUILayout.EndArea();
Handles.EndGUI();
}
static void ShowSceneMenu()
{
GenericMenu menu = new GenericMenu();
// 查找项目中所有启用(Enabled)的场景
string[] sceneGuids = AssetDatabase.FindAssets("t:Scene");
foreach (string guid in sceneGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
string name = Path.GetFileNameWithoutExtension(path);
menu.AddItem(new GUIContent(name), false, () =>
{
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
EditorSceneManager.OpenScene(path);
}
});
}
menu.ShowAsContext();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7f70b2d5386e4941ab913255891bb99
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
{
"name": "SepCore.Editor",
"rootNamespace": "",
"references": [
"GUID:363c5eb08ff8e6a439b85e37b8c20d96",
"GUID:a2d8a19598eca814496b089021d08d60",
"GUID:a04b272484018094ab427253f20c416c",
"GUID:11ec41dbc3557e6499b2845ba531a9b0",
"GUID:73a59a40d41e9ce48b3e0515aeec4217"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0bf8adba443c0d04ba34d7379815d50d
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b109521fca5d74d5bbdc6f3b44fc87d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,963 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using SepCore.UI;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.Compilation;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace SepCore.Editor
{
public static class UIAssetsTools
{
public const string GeneratedNamespace = "SepCore.UI";
public const string GeneratedDirectory = "Assets/GameMain/Scripts/UI";
private const string PendingGenerationKey = "UI.Serialization.PendingGeneration";
private static bool finalizeQueued;
private static readonly HashSet<string> CSharpKeywords = new HashSet<string>(StringComparer.Ordinal)
{
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class",
"const", "continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event",
"explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", "if",
"implicit", "in", "int", "interface", "internal", "is", "lock", "long", "namespace", "new",
"null", "object", "operator", "out", "override", "params", "private", "protected", "public",
"readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static",
"string", "struct", "switch", "this", "throw", "true", "try", "typeof", "uint", "ulong",
"unchecked", "unsafe", "ushort", "using", "virtual", "void", "volatile", "while"
};
[Serializable]
private sealed class PendingGenerationBatch
{
public List<PendingGeneration> entries = new List<PendingGeneration>();
public string primaryScriptPath;
}
[Serializable]
private sealed class PendingGeneration
{
public string rootGlobalId;
public string prefabAssetPath;
public string prefabRootRelativePath;
public string scenePath;
public string hierarchyPath;
public string viewClassFullName;
public string formClassFullName;
public string viewScriptPath;
public string formScriptPath;
}
private sealed class GenerationUnit
{
public UISerializationRoot root;
public string viewClassName;
public string formClassName;
public string viewScriptPath;
public string formScriptPath;
public string viewSource;
public string formSource;
public bool viewChanged;
public bool formChanged;
public bool SourceChanged => viewChanged || formChanged;
}
private sealed class Binding
{
public Component component;
public UISerializationRoot nestedRoot;
public string variableName;
public string csharpTypeName;
}
[MenuItem("Assets/Create View Script", false, 2000)]
private static void GenerateFromAssetsMenu()
{
GenerateWithDialog(GetSelectedRoot());
}
[MenuItem("Assets/Create View Script", true)]
private static bool ValidateAssetsMenu()
{
return GetSelectedRoot() != null;
}
[MenuItem("GameObject/Create View Script", false, 30)]
private static void GenerateFromGameObjectMenu()
{
GenerateWithDialog(GetSelectedRoot());
}
[MenuItem("GameObject/Create View Script", true)]
private static bool ValidateGameObjectMenu()
{
return GetSelectedRoot() != null;
}
[MenuItem("CONTEXT/UISerializationRoot/Create View Script")]
private static void GenerateFromComponentMenu(MenuCommand command)
{
GenerateWithDialog(command.context as UISerializationRoot);
}
public static void GenerateWithDialog(UISerializationRoot root)
{
try
{
Generate(root, true);
}
catch (Exception exception)
{
Debug.LogException(exception);
EditorUtility.DisplayDialog("Create UI View Script", exception.Message, "OK");
}
}
public static string Generate(UISerializationRoot root, bool confirmOverwrite)
{
if (root == null)
{
throw new InvalidOperationException("Select a GameObject or Prefab with UISerializationRoot.");
}
List<UISerializationRoot> generationRoots = CollectGenerationRoots(root);
List<GenerationUnit> units = BuildGenerationUnits(generationRoots);
GenerationUnit primaryUnit = units[units.Count - 1];
List<string> changedExistingFiles = units
.Where(unit => unit.SourceChanged)
.SelectMany(unit => new[] { unit.viewScriptPath, unit.formScriptPath })
.Where(File.Exists)
.ToList();
if (confirmOverwrite && changedExistingFiles.Count > 0)
{
string message = "Overwrite the following generated View/Form scripts?\n\n" +
string.Join("\n", changedExistingFiles.ToArray());
if (!EditorUtility.DisplayDialog("Regenerate UI View/Form", message, "Overwrite", "Cancel"))
{
return null;
}
}
EnsureFolder(GeneratedDirectory);
SaveDirtyRootAssets(units);
var pendingBatch = new PendingGenerationBatch
{
primaryScriptPath = primaryUnit.viewScriptPath
};
for (int i = 0; i < units.Count; i++)
{
pendingBatch.entries.Add(CreatePendingGeneration(units[i]));
}
SessionState.SetString(PendingGenerationKey, JsonUtility.ToJson(pendingBatch));
bool anySourceChanged = false;
for (int i = 0; i < units.Count; i++)
{
GenerationUnit unit = units[i];
if (!unit.SourceChanged)
{
continue;
}
EnsureFolder(Path.GetDirectoryName(unit.viewScriptPath));
if (unit.viewChanged)
{
File.WriteAllText(unit.viewScriptPath, unit.viewSource, new UTF8Encoding(false));
anySourceChanged = true;
}
if (unit.formChanged)
{
File.WriteAllText(unit.formScriptPath, unit.formSource, new UTF8Encoding(false));
anySourceChanged = true;
}
}
if (anySourceChanged)
{
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
UnityEngine.Object scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(primaryUnit.viewScriptPath);
if (scriptAsset != null)
{
EditorGUIUtility.PingObject(scriptAsset);
}
if (anySourceChanged)
{
CompilationPipeline.RequestScriptCompilation();
}
else
{
QueuePendingGeneration();
}
Debug.Log("Generated " + units.Count + " UI View/Form script pair(s); primary script: " +
primaryUnit.viewScriptPath);
return primaryUnit.viewScriptPath;
}
public static string GetViewClassName(UISerializationRoot root)
{
if (root == null)
{
return string.Empty;
}
return GetBaseName(root) + "View";
}
public static string GetFormClassName(UISerializationRoot root)
{
if (root == null)
{
return string.Empty;
}
return GetBaseName(root) + "Form";
}
public static string GetBaseName(UISerializationRoot root)
{
if (root == null)
{
return string.Empty;
}
string sourceName = root.gameObject.name.Replace("(Clone)", string.Empty).Trim();
if (sourceName.EndsWith("Form", StringComparison.Ordinal) && sourceName.Length > 4)
{
sourceName = sourceName.Substring(0, sourceName.Length - 4);
}
else if (sourceName.EndsWith("UI", StringComparison.Ordinal) && sourceName.Length > 2)
{
sourceName = sourceName.Substring(0, sourceName.Length - 2);
}
string identifier = ToPascalIdentifier(sourceName);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The UI root name cannot be converted to a C# class name.");
}
return identifier;
}
public static bool IsValidIdentifier(string value)
{
if (string.IsNullOrEmpty(value) || CSharpKeywords.Contains(value))
{
return false;
}
if (!char.IsLetter(value[0]) && value[0] != '_')
{
return false;
}
for (int i = 1; i < value.Length; i++)
{
if (!char.IsLetterOrDigit(value[i]) && value[i] != '_')
{
return false;
}
}
return true;
}
[InitializeOnLoadMethod]
private static void InitializePendingGeneration()
{
QueuePendingGeneration();
}
[DidReloadScripts]
private static void OnScriptsReloaded()
{
QueuePendingGeneration();
}
private static void QueuePendingGeneration()
{
if (finalizeQueued || string.IsNullOrEmpty(SessionState.GetString(PendingGenerationKey, string.Empty)))
{
return;
}
finalizeQueued = true;
EditorApplication.delayCall += RunQueuedPendingGeneration;
}
private static void RunQueuedPendingGeneration()
{
finalizeQueued = false;
FinalizePendingGeneration();
}
private static void FinalizePendingGeneration()
{
string json = SessionState.GetString(PendingGenerationKey, string.Empty);
if (string.IsNullOrEmpty(json))
{
return;
}
PendingGenerationBatch batch = JsonUtility.FromJson<PendingGenerationBatch>(json);
if (batch == null || batch.entries == null || batch.entries.Count == 0)
{
SessionState.EraseString(PendingGenerationKey);
Debug.LogError("The pending UI View generation data is invalid.");
return;
}
var resolvedViewTypes = new List<Type>(batch.entries.Count);
var resolvedFormTypes = new List<Type>(batch.entries.Count);
for (int i = 0; i < batch.entries.Count; i++)
{
PendingGeneration pending = batch.entries[i];
Type viewType = FindViewType(pending.viewClassFullName);
if (viewType == null)
{
Debug.LogError("Generated UI view type was not found after compilation: " + pending.viewClassFullName);
return;
}
Type formType = FindViewType(pending.formClassFullName);
if (formType == null)
{
Debug.LogError("Generated UI form type was not found after compilation: " + pending.formClassFullName);
return;
}
resolvedViewTypes.Add(viewType);
resolvedFormTypes.Add(formType);
}
SessionState.EraseString(PendingGenerationKey);
UnityEngine.Object primaryTarget = null;
for (int i = 0; i < batch.entries.Count; i++)
{
PendingGeneration pending = batch.entries[i];
Type viewType = resolvedViewTypes[i];
Type formType = resolvedFormTypes[i];
if (!string.IsNullOrEmpty(pending.prefabAssetPath))
{
primaryTarget = ApplyToPrefab(pending, viewType, formType);
continue;
}
UISerializationRoot root = ResolveSceneRoot(pending);
if (root == null)
{
throw new InvalidOperationException(
"Could not resolve the UI root after compilation. The scripts were generated at " +
pending.viewScriptPath + " and " + pending.formScriptPath + ".");
}
ApplyGeneratedComponents(root, viewType, formType, true);
EditorSceneManager.MarkSceneDirty(root.gameObject.scene);
primaryTarget = root.gameObject;
}
if (primaryTarget != null)
{
Selection.activeObject = primaryTarget;
EditorGUIUtility.PingObject(primaryTarget);
}
Debug.Log("Attached and populated " + batch.entries.Count + " generated UI View/Form pair(s).");
}
private static List<UISerializationRoot> CollectGenerationRoots(UISerializationRoot selectedRoot)
{
bool persistentContext = EditorUtility.IsPersistent(selectedRoot);
var result = new List<UISerializationRoot>();
var visited = new HashSet<UISerializationRoot>();
CollectGenerationRootsRecursive(selectedRoot, persistentContext, visited, result);
return result;
}
private static void CollectGenerationRootsRecursive(
UISerializationRoot current,
bool persistentContext,
HashSet<UISerializationRoot> visited,
List<UISerializationRoot> result)
{
current = GetCanonicalRoot(current, persistentContext);
if (current == null || !visited.Add(current))
{
return;
}
current.RefreshItems();
for (int i = 0; i < current.NestedViewReferences.Count; i++)
{
UISerializationRoot nestedRoot = current.NestedViewReferences[i].Root;
if (nestedRoot != null)
{
CollectGenerationRootsRecursive(nestedRoot, persistentContext, visited, result);
}
}
result.Add(current);
}
private static UISerializationRoot GetCanonicalRoot(UISerializationRoot root, bool persistentContext)
{
if (!persistentContext || root == null)
{
return root;
}
UISerializationRoot source = PrefabUtility.GetCorrespondingObjectFromSource(root);
return source != null ? source : root;
}
private static List<GenerationUnit> BuildGenerationUnits(List<UISerializationRoot> roots)
{
var units = new List<GenerationUnit>(roots.Count);
var classOwners = new Dictionary<string, UISerializationRoot>(StringComparer.Ordinal);
for (int i = 0; i < roots.Count; i++)
{
UISerializationRoot root = roots[i];
root.RefreshItems();
string viewClassName = GetViewClassName(root);
string formClassName = GetFormClassName(root);
string baseName = GetBaseName(root);
UISerializationRoot existingOwner;
if (classOwners.TryGetValue(viewClassName, out existingOwner) && existingOwner != root)
{
throw new InvalidOperationException(
"Multiple serialization roots generate the same View class '" + viewClassName +
"'. Rename one of the UI roots.");
}
classOwners[viewClassName] = root;
if (classOwners.TryGetValue(formClassName, out existingOwner) && existingOwner != root)
{
throw new InvalidOperationException(
"Multiple serialization roots generate the same Form class '" + formClassName +
"'. Rename one of the UI roots.");
}
classOwners[formClassName] = root;
List<Binding> bindings = CollectBindings(root);
string folderPath = GeneratedDirectory + "/" + baseName;
string viewScriptPath = folderPath + "/" + viewClassName + ".cs";
string formScriptPath = folderPath + "/" + formClassName + ".cs";
string viewSource = BuildViewSource(viewClassName, bindings);
string formSource = BuildFormSource(formClassName, viewClassName);
units.Add(new GenerationUnit
{
root = root,
viewClassName = viewClassName,
formClassName = formClassName,
viewScriptPath = viewScriptPath,
formScriptPath = formScriptPath,
viewSource = viewSource,
formSource = formSource,
viewChanged = !File.Exists(viewScriptPath) || File.ReadAllText(viewScriptPath) != viewSource,
formChanged = !File.Exists(formScriptPath) || File.ReadAllText(formScriptPath) != formSource
});
}
return units;
}
private static List<Binding> CollectBindings(UISerializationRoot root)
{
var bindings = new List<Binding>();
var names = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < root.SerializationItems.Count; i++)
{
UISerializationItem item = root.SerializationItems[i];
if (item == null)
{
continue;
}
item.RefreshComponents();
for (int j = 0; j < item.ComponentReferences.Count; j++)
{
UISerializationItem.ComponentReference reference = item.ComponentReferences[j];
if (reference == null || !reference.GenerateReference)
{
continue;
}
Component component = reference.Component;
string variableName = NormalizeAndValidateVariableName(reference.VariableName, item.transform, names);
if (component == null)
{
throw new InvalidOperationException("A selected UI reference is missing on " + GetHierarchyPath(item.transform) + ".");
}
if (component.GetType().Namespace != null && component.GetType().Namespace.StartsWith("UnityEditor", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Editor-only components cannot be referenced by a runtime View: " + component.GetType().FullName + ".");
}
bindings.Add(new Binding
{
component = component,
variableName = variableName,
csharpTypeName = GetCSharpTypeName(component.GetType())
});
}
}
for (int i = 0; i < root.NestedViewReferences.Count; i++)
{
UISerializationRoot.NestedViewReference reference = root.NestedViewReferences[i];
if (reference == null || !reference.GenerateReference)
{
continue;
}
UISerializationRoot nestedRoot = reference.Root;
if (nestedRoot == null)
{
throw new InvalidOperationException("A nested UI View reference is missing below " + GetHierarchyPath(root.transform) + ".");
}
string variableName = NormalizeAndValidateVariableName(reference.VariableName, nestedRoot.transform, names);
bindings.Add(new Binding
{
nestedRoot = nestedRoot,
variableName = variableName,
csharpTypeName = "global::" + GeneratedNamespace + "." + GetViewClassName(nestedRoot)
});
}
return bindings;
}
private static string NormalizeAndValidateVariableName(string value, Transform owner, HashSet<string> names)
{
string variableName = value == null ? string.Empty : value.Trim();
if (!IsValidIdentifier(variableName))
{
throw new InvalidOperationException(
"Invalid C# field name '" + variableName + "' on " + GetHierarchyPath(owner) + ".");
}
if (!names.Add(variableName))
{
throw new InvalidOperationException("Duplicate UI field name '" + variableName + "'.");
}
if (typeof(MonoBehaviour).GetMember(variableName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Length > 0)
{
throw new InvalidOperationException("UI field name '" + variableName + "' conflicts with a MonoBehaviour member.");
}
return variableName;
}
private static string BuildViewSource(string className, List<Binding> bindings)
{
var builder = new StringBuilder(512 + bindings.Count * 96);
builder.AppendLine("// <auto-generated />");
builder.AppendLine("using UnityEngine;");
builder.AppendLine();
builder.Append("namespace ").AppendLine(GeneratedNamespace);
builder.AppendLine("{");
builder.AppendLine(" [DisallowMultipleComponent]");
builder.Append(" public partial class ").Append(className).AppendLine(" : MonoBehaviour");
builder.AppendLine(" {");
for (int i = 0; i < bindings.Count; i++)
{
Binding binding = bindings[i];
builder.Append(" [SerializeField] public ")
.Append(binding.csharpTypeName)
.Append(' ')
.Append(binding.variableName)
.AppendLine(";");
}
builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString();
}
private static string BuildFormSource(string className, string viewClassName)
{
var builder = new StringBuilder(256);
builder.AppendLine("// <auto-generated />");
builder.AppendLine("using UnityEngine;");
builder.AppendLine();
builder.Append("namespace ").AppendLine(GeneratedNamespace);
builder.AppendLine("{");
builder.AppendLine(" [DisallowMultipleComponent]");
builder.Append(" public partial class ").Append(className).AppendLine(" : UGuiForm");
builder.AppendLine(" {");
builder.Append(" [SerializeField] private ").Append(viewClassName).AppendLine(" _view = null;");
builder.AppendLine();
builder.Append(" public ").Append(viewClassName).AppendLine(" View => _view;");
builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString();
}
private static PendingGeneration CreatePendingGeneration(GenerationUnit unit)
{
UISerializationRoot root = unit.root;
bool isPrefabAsset = EditorUtility.IsPersistent(root) && PrefabUtility.IsPartOfPrefabAsset(root);
string prefabAssetPath = isPrefabAsset ? AssetDatabase.GetAssetPath(root.gameObject) : string.Empty;
string relativePath = string.Empty;
if (isPrefabAsset)
{
GameObject prefabRoot = AssetDatabase.LoadAssetAtPath<GameObject>(prefabAssetPath);
if (prefabRoot == null)
{
throw new InvalidOperationException("Could not load Prefab asset: " + prefabAssetPath);
}
relativePath = GetRelativePath(prefabRoot.transform, root.transform);
}
GlobalObjectId globalObjectId = GlobalObjectId.GetGlobalObjectIdSlow(root);
return new PendingGeneration
{
rootGlobalId = globalObjectId.ToString(),
prefabAssetPath = prefabAssetPath,
prefabRootRelativePath = relativePath,
scenePath = root.gameObject.scene.path,
hierarchyPath = GetHierarchyPath(root.transform),
viewClassFullName = GeneratedNamespace + "." + unit.viewClassName,
formClassFullName = GeneratedNamespace + "." + unit.formClassName,
viewScriptPath = unit.viewScriptPath,
formScriptPath = unit.formScriptPath
};
}
private static UnityEngine.Object ApplyToPrefab(PendingGeneration pending, Type viewType, Type formType)
{
GameObject contents = PrefabUtility.LoadPrefabContents(pending.prefabAssetPath);
try
{
Transform rootTransform = string.IsNullOrEmpty(pending.prefabRootRelativePath)
? contents.transform
: contents.transform.Find(pending.prefabRootRelativePath);
UISerializationRoot root = rootTransform == null ? null : rootTransform.GetComponent<UISerializationRoot>();
if (root == null)
{
throw new InvalidOperationException(
"The Prefab no longer contains the expected UISerializationRoot at '" +
pending.prefabRootRelativePath + "': " + pending.prefabAssetPath);
}
ApplyGeneratedComponents(root, viewType, formType, false);
PrefabUtility.SaveAsPrefabAsset(contents, pending.prefabAssetPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(contents);
}
AssetDatabase.ImportAsset(pending.prefabAssetPath, ImportAssetOptions.ForceUpdate);
return AssetDatabase.LoadAssetAtPath<GameObject>(pending.prefabAssetPath);
}
public static void ApplyGeneratedComponentsToRoot(UISerializationRoot root)
{
if (root == null)
{
return;
}
root.RefreshItems();
for (int i = 0; i < root.NestedViewReferences.Count; i++)
{
UISerializationRoot nestedRoot = root.NestedViewReferences[i].Root;
if (nestedRoot != null)
{
ApplyGeneratedComponentsToRoot(nestedRoot);
}
}
Type viewType = FindViewType(GeneratedNamespace + "." + GetViewClassName(root));
Type formType = FindViewType(GeneratedNamespace + "." + GetFormClassName(root));
if (viewType == null || formType == null)
{
return;
}
ApplyGeneratedComponents(root, viewType, formType, false);
}
private static void ApplyGeneratedComponents(UISerializationRoot root, Type viewType, Type formType,
bool useUndo)
{
Component view = ApplyViewReferences(root, viewType, useUndo);
Component form = root.GetComponent(formType);
if (form == null)
{
form = useUndo ? Undo.AddComponent(root.gameObject, formType) : root.gameObject.AddComponent(formType);
}
else if (useUndo)
{
Undo.RecordObject(form, "Populate UI form view reference");
}
var serializedForm = new SerializedObject(form);
serializedForm.UpdateIfRequiredOrScript();
SerializedProperty viewProperty = serializedForm.FindProperty("_view");
if (viewProperty == null)
{
throw new InvalidOperationException("Generated field was not found: _view on " + formType.FullName + ".");
}
viewProperty.objectReferenceValue = view;
serializedForm.ApplyModifiedProperties();
EditorUtility.SetDirty(form);
if (useUndo && PrefabUtility.IsPartOfPrefabInstance(form))
{
PrefabUtility.RecordPrefabInstancePropertyModifications(form);
}
}
private static Component ApplyViewReferences(UISerializationRoot root, Type viewType, bool useUndo)
{
root.RefreshItems();
List<Binding> bindings = CollectBindings(root);
Component view = root.GetComponent(viewType);
if (view == null)
{
view = useUndo ? Undo.AddComponent(root.gameObject, viewType) : root.gameObject.AddComponent(viewType);
}
else if (useUndo)
{
Undo.RecordObject(view, "Populate UI view references");
}
var serializedView = new SerializedObject(view);
serializedView.UpdateIfRequiredOrScript();
for (int i = 0; i < bindings.Count; i++)
{
Binding binding = bindings[i];
SerializedProperty property = serializedView.FindProperty(binding.variableName);
if (property == null)
{
throw new InvalidOperationException("Generated field was not found: " + binding.variableName + ".");
}
UnityEngine.Object target = binding.component;
if (binding.nestedRoot != null)
{
string nestedViewFullName = GeneratedNamespace + "." + GetViewClassName(binding.nestedRoot);
Type nestedViewType = FindViewType(nestedViewFullName);
if (nestedViewType == null)
{
throw new InvalidOperationException("Nested UI view type was not found: " + nestedViewFullName + ".");
}
target = binding.nestedRoot.GetComponent(nestedViewType);
if (target == null)
{
throw new InvalidOperationException(
"Nested UI root '" + GetHierarchyPath(binding.nestedRoot.transform) +
"' does not have its generated View component '" + nestedViewFullName + "'.");
}
}
property.objectReferenceValue = target;
}
serializedView.ApplyModifiedProperties();
EditorUtility.SetDirty(view);
if (useUndo && PrefabUtility.IsPartOfPrefabInstance(view))
{
PrefabUtility.RecordPrefabInstancePropertyModifications(view);
}
return view;
}
private static Type FindViewType(string fullName)
{
return TypeCache.GetTypesDerivedFrom<MonoBehaviour>()
.FirstOrDefault(type => type.FullName == fullName);
}
private static UISerializationRoot ResolveSceneRoot(PendingGeneration pending)
{
GlobalObjectId parsed;
if (GlobalObjectId.TryParse(pending.rootGlobalId, out parsed))
{
UnityEngine.Object resolved = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(parsed);
UISerializationRoot root = resolved as UISerializationRoot;
if (root != null)
{
return root;
}
GameObject resolvedObject = resolved as GameObject;
if (resolvedObject != null)
{
root = resolvedObject.GetComponent<UISerializationRoot>();
if (root != null)
{
return root;
}
}
}
UISerializationRoot[] loadedRoots = Resources.FindObjectsOfTypeAll<UISerializationRoot>();
for (int i = 0; i < loadedRoots.Length; i++)
{
UISerializationRoot root = loadedRoots[i];
if (root != null && !EditorUtility.IsPersistent(root) && root.gameObject.scene.path == pending.scenePath &&
GetHierarchyPath(root.transform) == pending.hierarchyPath)
{
return root;
}
}
return null;
}
private static UISerializationRoot GetSelectedRoot()
{
GameObject selected = Selection.activeGameObject;
if (selected == null)
{
selected = Selection.activeObject as GameObject;
}
return selected == null ? null : selected.GetComponent<UISerializationRoot>();
}
private static string GetCSharpTypeName(Type type)
{
if (type.IsArray)
{
return GetCSharpTypeName(type.GetElementType()) + "[]";
}
if (type.IsGenericType)
{
string definition = type.GetGenericTypeDefinition().FullName ?? type.Name;
int arityMarker = definition.IndexOf('`');
if (arityMarker >= 0)
{
definition = definition.Substring(0, arityMarker);
}
string arguments = string.Join(", ", type.GetGenericArguments().Select(GetCSharpTypeName).ToArray());
return "global::" + definition.Replace('+', '.') + "<" + arguments + ">";
}
return "global::" + (type.FullName ?? type.Name).Replace('+', '.');
}
private static void SaveDirtyRootAssets(List<GenerationUnit> units)
{
var savedPaths = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < units.Count; i++)
{
UISerializationRoot root = units[i].root;
if (root == null || !EditorUtility.IsPersistent(root))
{
continue;
}
string assetPath = AssetDatabase.GetAssetPath(root);
if (string.IsNullOrEmpty(assetPath) || !savedPaths.Add(assetPath))
{
continue;
}
AssetDatabase.SaveAssetIfDirty(AssetDatabase.GUIDFromAssetPath(assetPath));
}
}
private static string ToPascalIdentifier(string value)
{
var builder = new StringBuilder(value.Length + 8);
bool capitalizeNext = true;
for (int i = 0; i < value.Length; i++)
{
char current = value[i];
if (!char.IsLetterOrDigit(current) && current != '_')
{
capitalizeNext = true;
continue;
}
if (builder.Length == 0 && char.IsDigit(current))
{
builder.Append('_');
}
builder.Append(capitalizeNext ? char.ToUpperInvariant(current) : current);
capitalizeNext = false;
}
return builder.ToString();
}
private static string GetRelativePath(Transform root, Transform target)
{
if (root == target)
{
return string.Empty;
}
var parts = new Stack<string>();
Transform current = target;
while (current != null && current != root)
{
parts.Push(current.name);
current = current.parent;
}
if (current != root)
{
throw new InvalidOperationException("The serialization root is not inside the expected Prefab asset.");
}
return string.Join("/", parts.ToArray());
}
private static string GetHierarchyPath(Transform target)
{
string path = target.name;
Transform current = target.parent;
while (current != null)
{
path = current.name + "/" + path;
current = current.parent;
}
return path;
}
private static void EnsureFolder(string folder)
{
string[] parts = folder.Split('/');
string current = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, parts[i]);
}
current = next;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48c1b6017a08547aa94ad17c9611d7b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using SepCore.UI;
using UnityEditor;
using UnityEngine;
namespace SepCore.Editor
{
[CustomEditor(typeof(UISerializationItem))]
internal sealed class UISerializationItemEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
var item = (UISerializationItem)target;
item.RefreshComponents();
serializedObject.UpdateIfRequiredOrScript();
SerializedProperty references = serializedObject.FindProperty("componentReferences");
EditorGUILayout.LabelField("Components", EditorStyles.boldLabel);
var selectedNames = new HashSet<string>();
for (int i = 0; i < references.arraySize; i++)
{
SerializedProperty entry = references.GetArrayElementAtIndex(i);
SerializedProperty component = entry.FindPropertyRelative("component");
SerializedProperty generate = entry.FindPropertyRelative("generateReference");
SerializedProperty variableName = entry.FindPropertyRelative("variableName");
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
generate.boolValue = EditorGUILayout.Toggle(generate.boolValue, GUILayout.Width(18f));
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(component.objectReferenceValue, typeof(Component), true);
}
EditorGUILayout.EndHorizontal();
if (generate.boolValue)
{
variableName.stringValue = EditorGUILayout.TextField("Variable Name", variableName.stringValue);
string normalized = variableName.stringValue == null ? string.Empty : variableName.stringValue.Trim();
if (!UIAssetsTools.IsValidIdentifier(normalized))
{
EditorGUILayout.HelpBox("Enter a valid C# field name.", MessageType.Error);
}
else if (!selectedNames.Add(normalized))
{
EditorGUILayout.HelpBox("This field name is already selected on this item.", MessageType.Error);
}
}
EditorGUILayout.EndVertical();
}
if (GUILayout.Button("Refresh Components"))
{
Undo.RecordObject(item, "Refresh UI serialization components");
item.RefreshComponents();
serializedObject.UpdateIfRequiredOrScript();
}
serializedObject.ApplyModifiedProperties();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5f17238526324f3d98afa8e8c999814
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,211 @@
using System.Collections.Generic;
using SepCore.UI;
using UnityEditor;
using UnityEngine;
namespace SepCore.Editor
{
[CustomEditor(typeof(UISerializationRoot))]
internal sealed class UISerializationRootEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
var root = (UISerializationRoot)target;
root.RefreshItems();
serializedObject.UpdateIfRequiredOrScript();
int referenceCount = 0;
bool hasValidationError = false;
var selectedNames = new HashSet<string>();
for (int i = 0; i < root.SerializationItems.Count; i++)
{
UISerializationItem item = root.SerializationItems[i];
if (item == null)
{
continue;
}
for (int j = 0; j < item.ComponentReferences.Count; j++)
{
UISerializationItem.ComponentReference reference = item.ComponentReferences[j];
if (reference.GenerateReference)
{
referenceCount++;
string variableName = reference.VariableName == null ? string.Empty : reference.VariableName.Trim();
if (!UIAssetsTools.IsValidIdentifier(variableName) || !selectedNames.Add(variableName))
{
hasValidationError = true;
}
}
}
}
SerializedProperty nestedViews = serializedObject.FindProperty("nestedViewReferences");
for (int i = 0; i < nestedViews.arraySize; i++)
{
SerializedProperty entry = nestedViews.GetArrayElementAtIndex(i);
if (entry.FindPropertyRelative("generateReference").boolValue)
{
referenceCount++;
}
}
EditorGUILayout.LabelField("View Class", UIAssetsTools.GetViewClassName(root));
EditorGUILayout.LabelField("Form Class", UIAssetsTools.GetFormClassName(root));
EditorGUILayout.LabelField("Owned Items", root.SerializationItems.Count.ToString());
EditorGUILayout.LabelField("Nested Views", nestedViews.arraySize.ToString());
EditorGUILayout.LabelField("Generated References", referenceCount.ToString());
EditorGUILayout.Space(4f);
EditorGUILayout.LabelField("Owned Items", EditorStyles.boldLabel);
for (int i = 0; i < root.SerializationItems.Count; i++)
{
UISerializationItem item = root.SerializationItems[i];
if (item == null)
{
continue;
}
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
EditorGUILayout.LabelField(GetRelativePath(root.transform, item.transform));
if (GUILayout.Button("Select", GUILayout.Width(56f)))
{
Selection.activeGameObject = item.gameObject;
EditorGUIUtility.PingObject(item.gameObject);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(4f);
EditorGUILayout.LabelField("Nested Views", EditorStyles.boldLabel);
if (nestedViews.arraySize == 0)
{
EditorGUILayout.HelpBox("Nested UISerializationRoot components will appear here and form serialization boundaries.", MessageType.Info);
}
for (int i = 0; i < nestedViews.arraySize; i++)
{
SerializedProperty entry = nestedViews.GetArrayElementAtIndex(i);
SerializedProperty nestedRootProperty = entry.FindPropertyRelative("root");
SerializedProperty generate = entry.FindPropertyRelative("generateReference");
SerializedProperty variableName = entry.FindPropertyRelative("variableName");
var nestedRoot = nestedRootProperty.objectReferenceValue as UISerializationRoot;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
generate.boolValue = EditorGUILayout.Toggle(generate.boolValue, GUILayout.Width(18f));
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(nestedRoot, typeof(UISerializationRoot), true);
}
if (nestedRoot != null && GUILayout.Button("Select", GUILayout.Width(56f)))
{
Selection.activeGameObject = nestedRoot.gameObject;
EditorGUIUtility.PingObject(nestedRoot.gameObject);
}
EditorGUILayout.EndHorizontal();
if (nestedRoot != null)
{
EditorGUILayout.LabelField("View Class", UIAssetsTools.GetViewClassName(nestedRoot));
}
if (generate.boolValue)
{
variableName.stringValue = EditorGUILayout.TextField("Variable Name", variableName.stringValue);
string normalized = variableName.stringValue == null ? string.Empty : variableName.stringValue.Trim();
if (!UIAssetsTools.IsValidIdentifier(normalized))
{
hasValidationError = true;
EditorGUILayout.HelpBox("Enter a valid C# field name.", MessageType.Error);
}
else if (!selectedNames.Add(normalized))
{
hasValidationError = true;
EditorGUILayout.HelpBox("This field name conflicts with another generated reference.", MessageType.Error);
}
}
EditorGUILayout.EndVertical();
}
if (hasValidationError)
{
EditorGUILayout.HelpBox("Fix invalid or duplicate generated field names before creating View scripts.", MessageType.Error);
}
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space(4f);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Refresh Items"))
{
Undo.RecordObject(root, "Refresh UI serialization items");
root.RefreshItems();
}
using (new EditorGUI.DisabledScope(hasValidationError))
{
if (GUILayout.Button("Create View & Form Scripts"))
{
UIAssetsTools.GenerateWithDialog(root);
}
}
EditorGUILayout.EndHorizontal();
}
private static string GetRelativePath(Transform root, Transform target)
{
if (target == root)
{
return root.name;
}
string path = target.name;
Transform current = target.parent;
while (current != null && current != root)
{
path = current.name + "/" + path;
current = current.parent;
}
return path;
}
}
[InitializeOnLoad]
internal static class UISerializationRootWatcher
{
private static bool refreshQueued;
static UISerializationRootWatcher()
{
EditorApplication.hierarchyChanged += QueueRefresh;
}
private static void QueueRefresh()
{
if (refreshQueued || EditorApplication.isPlayingOrWillChangePlaymode)
{
return;
}
refreshQueued = true;
EditorApplication.delayCall += RefreshLoadedRoots;
}
private static void RefreshLoadedRoots()
{
refreshQueued = false;
UISerializationRoot[] roots = Resources.FindObjectsOfTypeAll<UISerializationRoot>();
for (int i = 0; i < roots.Length; i++)
{
UISerializationRoot root = roots[i];
if (root == null || EditorUtility.IsPersistent(root) || !root.gameObject.scene.IsValid() || !root.gameObject.scene.isLoaded)
{
continue;
}
root.RefreshItems();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c6a8249053ff4cf0a368b8a25b9946b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c30c9f4e90334fe0b836c0f7cf5cc75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: