using System.Collections.Generic; using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using Object = UnityEngine.Object; namespace AssetDependencyGraph { public class AssetGraphView : GraphView { public AssetGraphView() { SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); this.AddManipulator(new FreehandSelector()); VisualElement background = new VisualElement { style = { backgroundColor = new Color(0.17f, 0.17f, 0.17f, 1f) } }; Insert(0, background); background.StretchToParentSize(); } } public class AssetGroup { public AssetNode AssetNode; public Group GroupNode = new Group(); public Node MainGraphNode = new Node(); public Rect MainGraphNodeLastPosition = new Rect(); public List AssetGraphNodes = new List(); public List AssetGraphConnections = new List(); public List DependenciesForPlacement = new List(); } public class AssetDependencyGraph : EditorWindow { private const float NodeWidth = 300.0f; DependencyAnalyzer da = new DependencyAnalyzer(); AssetDependencyGraphDB db = new AssetDependencyGraphDB("", "CCS20190109", "localhost"); Dictionary type2Toogle = new(); (string assetType, bool show)[] assetTypeHidenTogleItems = new[] { ("Executable" , true), ("UnityAssembly", true), ("SourceFile", true), ("MakeFile", true), ("DatFile", true), ("AudioClip", true), ("VideoClip", true), ("Texture", false), ("Shader", false), ("ComputeShader", false), ("ShaderHeader", false), ("Binary", true), ("TextFile", true), ("Excel", true), ("UnknowFileType", false), }; Toggle AlignmentToggle; private GraphView graphView; private readonly List selectedObjects = new List(); private readonly List assetGroups = new List(); private readonly Dictionary fullPathNodeLookup = new Dictionary(); [MenuItem("Window/Asset Dependency Graph")] public static void CreateTestGraphViewWindow() { var window = GetWindow(true); window.titleContent = new GUIContent("Asset Dependency Graph"); } public void OnEnable() { CreateGraph(); } public void OnDisable() { rootVisualElement.Remove(graphView); } void CreateGraph() { graphView = new AssetGraphView { name = "Asset Dependency Graph", }; VisualElement toolbar = CreateToolbar(); VisualElement toolbar2 = CreateFilterbar(); rootVisualElement.Add(toolbar); rootVisualElement.Add(toolbar2); rootVisualElement.Add(graphView); graphView.StretchToParentSize(); toolbar.BringToFront(); toolbar2.BringToFront(); } VisualElement CreateToolbar() { var toolbar = new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 0, backgroundColor = new Color(0.25f, 0.25f, 0.25f, 0.75f) } }; var options = new VisualElement { style = { alignContent = Align.Center } }; toolbar.Add(options); toolbar.Add(new Button(ExploreAsset) { text = "Explore Asset", }); toolbar.Add(new Button(ClearGraph) { text = "Clear" }); toolbar.Add(new Button(ResetGroups) { text = "Reset Groups" }); toolbar.Add(new Button(ResetAllNodes) { text = "Reset Nodes" }); toolbar.Add(new Button(() => { da.Analyze(Application.dataPath); }) { text = "Analyze Asset" }); var ts = new ToolbarSearchField(); ts.RegisterValueChangedCallback(x => { if (string.IsNullOrEmpty(x.newValue)) { graphView.FrameAll(); return; } graphView.ClearSelection(); graphView.graphElements.ToList().ForEach(y => { if (y is Node node && y.title.IndexOf(x.newValue, System.StringComparison.OrdinalIgnoreCase) >= 0) { graphView.AddToSelection(node); } }); graphView.FrameSelection(); }); toolbar.Add(ts); AlignmentToggle = new Toggle(); AlignmentToggle.text = "Horizontal Layout"; AlignmentToggle.value = true; AlignmentToggle.RegisterValueChangedCallback(x => { ResetAllNodes(); }); toolbar.Add(AlignmentToggle); return toolbar; } VisualElement CreateFilterbar() { var toolbar = new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 0, backgroundColor = new Color(0.25f, 0.25f, 0.25f, 0.75f) } }; var options = new VisualElement { style = { alignContent = Align.Center } }; toolbar.Add(options); toolbar.Add(new Label("Filters: ")); foreach (var pair in assetTypeHidenTogleItems) { var assetTypeTogle = new Toggle(); assetTypeTogle.text = "Hide " + pair.assetType; assetTypeTogle.value = pair.show; assetTypeTogle.RegisterValueChangedCallback(x => { FilterAssetGroups(); }); toolbar.Add(assetTypeTogle); type2Toogle[pair.assetType] = assetTypeTogle; } return toolbar; } private void ExploreAsset() { Object[] objs = Selection.objects; foreach (var obj in objs) { //Prevent readding same object if (selectedObjects.Contains(obj)) { Debug.Log("Object already loaded"); return; } selectedObjects.Add(obj); AssetGroup AssetGroup = new AssetGroup(); AssetGroup.AssetNode = db.Find(AssetDatabase.GetAssetPath(obj).ToUniversalPath()); assetGroups.Add(AssetGroup); // assetPath will be empty if obj is null or isn't an asset (a scene object) if (obj == null) return; AssetGroup.GroupNode = new Group { title = obj.name }; PopulateGroup(AssetGroup, obj, new Rect(10, graphView.contentRect.height / 2, 0, 0)); } } void PopulateGroup(AssetGroup AssetGroup, Object obj, Rect position) { if (obj == null) { obj = AssetDatabase.LoadMainAssetAtPath(AssetGroup.AssetNode.Self.Path); if (obj == null) { Debug.Log("Object doesn't exist anymore"); return; } } AssetGroup.MainGraphNode = CreateNode(AssetGroup, AssetGroup.AssetNode, obj, true); AssetGroup.MainGraphNode.userData = 0; AssetGroup.MainGraphNode.SetPosition(position); if (!graphView.Contains(AssetGroup.GroupNode)) { graphView.AddElement(AssetGroup.GroupNode); } graphView.AddElement(AssetGroup.MainGraphNode); AssetGroup.GroupNode.AddElement(AssetGroup.MainGraphNode); CreateDependencyNodes(AssetGroup, AssetGroup.AssetNode, AssetGroup.MainGraphNode, AssetGroup.GroupNode, 1); CreateDependentNodes(AssetGroup, AssetGroup.AssetNode, AssetGroup.MainGraphNode, AssetGroup.GroupNode, -1); AssetGroup.AssetGraphNodes.Add(AssetGroup.MainGraphNode); AssetGroup.GroupNode.capabilities &= ~Capabilities.Deletable; AssetGroup.GroupNode.Focus(); AssetGroup.MainGraphNode.RegisterCallback( UpdateGroupDependencyNodePlacement, AssetGroup ); } //Recreate the groups but use the already created groups instead of new ones void FilterAssetGroups() { //first collect the main node's position and then clear the graph foreach (var AssetGroup in assetGroups) { AssetGroup.MainGraphNodeLastPosition = AssetGroup.MainGraphNode.GetPosition(); } fullPathNodeLookup.Clear(); foreach (var AssetGroup in assetGroups) { //clear the nodes and dependencies after getting the position of the main node CleanGroup(AssetGroup); PopulateGroup(AssetGroup, null, AssetGroup.MainGraphNodeLastPosition); } } void CleanGroup(AssetGroup assetGroup) { if (assetGroup.AssetGraphConnections.Count > 0) { foreach (var edge in assetGroup.AssetGraphConnections) { graphView.RemoveElement(edge); } } assetGroup.AssetGraphConnections.Clear(); foreach (var node in assetGroup.AssetGraphNodes) { graphView.RemoveElement(node); } assetGroup.AssetGraphNodes.Clear(); assetGroup.DependenciesForPlacement.Clear(); } private void CreateDependencyNodes(AssetGroup assetGroup, AssetNode asssetNode, Node selfGraphNode, Group groupGraphNode, int depth) { foreach (var dependAssetId in asssetNode.Dependencies) { AssetNode dependAssetNode = db.Find(dependAssetId.Path); var typeName = dependAssetNode.AssetType; //filter out selected asset types if (FilterType(typeName)) { continue; } Node dependGraphNode = CreateNode(assetGroup, dependAssetNode, AssetDatabase.LoadMainAssetAtPath(dependAssetId.Path.ToUnityRelatePath()), false); if (!assetGroup.AssetGraphNodes.Contains(dependGraphNode)) { dependGraphNode.userData = depth; } //CreateDependencyNodes(assetGroup, dependAssetNode, dependGraphNode, groupGraphNode, depth + 1); //if the node doesnt exists yet, put it in the group if (!graphView.Contains(dependGraphNode)) { graphView.AddElement(dependGraphNode); assetGroup.DependenciesForPlacement.Add(dependGraphNode); groupGraphNode.AddElement(dependGraphNode); } else { //TODO: if it already exists, put it in a separate group for shared assets //Check if the dependencyNode is in the same group or not //if it's a different group move it to a new shared group /* if (SharedToggle.value) { if (!assetGroup.m_AssetNodes.Contains(dependencyNode)) { if (assetGroup.SharedGroup == null) { assetGroup.SharedGroup = new AssetGroup(); AssetGroups.Add(assetGroup.SharedGroup); assetGroup.SharedGroup.assetPath = assetGroup.assetPath; assetGroup.SharedGroup.groupNode = new Group { title = "Shared Group" }; assetGroup.SharedGroup.mainNode = dependencyNode; assetGroup.SharedGroup.mainNode.userData = 0; } if (!m_GraphView.Contains(assetGroup.SharedGroup.groupNode)) { m_GraphView.AddElement(assetGroup.SharedGroup.groupNode); } //add the node to the group and remove it from the previous group assetGroup.m_AssetNodes.Remove(dependencyNode); //assetGroup.groupNode.RemoveElement(dependencyNode); assetGroup.m_DependenciesForPlacement.Remove(dependencyNode); assetGroup.SharedGroup.m_DependenciesForPlacement.Add(dependencyNode); if (!assetGroup.SharedGroup.groupNode.ContainsElement(dependencyNode)) { assetGroup.SharedGroup.groupNode.AddElement(dependencyNode); } assetGroup.SharedGroup.m_AssetNodes.Add(dependencyNode); } }*/ } Edge edge = CreateEdge(dependGraphNode, selfGraphNode); assetGroup.AssetGraphConnections.Add(edge); assetGroup.AssetGraphNodes.Add(dependGraphNode); } } private void CreateDependentNodes(AssetGroup assetGroup, AssetNode asssetNode, Node selfGraphNode, Group groupGraphNode, int depth) { foreach (var dependAssetId in asssetNode.Dependent) { AssetNode dependAssetNode = db.Find(dependAssetId.Path); var typeName = dependAssetNode.AssetType; //filter out selected asset types if (FilterType(typeName)) { continue; } Node dependentGraphNode = CreateNode(assetGroup, dependAssetNode, AssetDatabase.LoadMainAssetAtPath(dependAssetId.Path.ToUnityRelatePath()), false); if (!assetGroup.AssetGraphNodes.Contains(dependentGraphNode)) { dependentGraphNode.userData = depth; } //CreateDependencyNodes(assetGroup, dependAssetNode, dependGraphNode, groupGraphNode, depth - 1); //if the node doesnt exists yet, put it in the group if (!graphView.Contains(dependentGraphNode)) { graphView.AddElement(dependentGraphNode); assetGroup.DependenciesForPlacement.Add(dependentGraphNode); groupGraphNode.AddElement(dependentGraphNode); } else { //TODO: if it already exists, put it in a separate group for shared assets //Check if the dependencyNode is in the same group or not //if it's a different group move it to a new shared group /* if (SharedToggle.value) { if (!assetGroup.m_AssetNodes.Contains(dependencyNode)) { if (assetGroup.SharedGroup == null) { assetGroup.SharedGroup = new AssetGroup(); AssetGroups.Add(assetGroup.SharedGroup); assetGroup.SharedGroup.assetPath = assetGroup.assetPath; assetGroup.SharedGroup.groupNode = new Group { title = "Shared Group" }; assetGroup.SharedGroup.mainNode = dependencyNode; assetGroup.SharedGroup.mainNode.userData = 0; } if (!m_GraphView.Contains(assetGroup.SharedGroup.groupNode)) { m_GraphView.AddElement(assetGroup.SharedGroup.groupNode); } //add the node to the group and remove it from the previous group assetGroup.m_AssetNodes.Remove(dependencyNode); //assetGroup.groupNode.RemoveElement(dependencyNode); assetGroup.m_DependenciesForPlacement.Remove(dependencyNode); assetGroup.SharedGroup.m_DependenciesForPlacement.Add(dependencyNode); if (!assetGroup.SharedGroup.groupNode.ContainsElement(dependencyNode)) { assetGroup.SharedGroup.groupNode.AddElement(dependencyNode); } assetGroup.SharedGroup.m_AssetNodes.Add(dependencyNode); } }*/ } Edge edge = CreateEdge(selfGraphNode, dependentGraphNode); assetGroup.AssetGraphConnections.Add(edge); assetGroup.AssetGraphNodes.Add(dependentGraphNode); } } Edge CreateEdge(Node dependencyNode, Node parentNode) { Edge edge = new Edge { input = dependencyNode.inputContainer[0] as Port, output = parentNode.outputContainer[0] as Port, }; edge.input?.Connect(edge); edge.output?.Connect(edge); dependencyNode.RefreshPorts(); graphView.AddElement(edge); edge.capabilities &= ~Capabilities.Deletable; return edge; } private Node CreateNode(AssetGroup assetGroup, AssetNode assetNode, Object obj, bool isMainNode) { Node resultNode; string fullPath = assetNode.Self.Path; if (fullPathNodeLookup.TryGetValue(fullPath, out resultNode)) { //----not sure what this is, the more dependencies the further removed on the chart? //int currentDepth = (int)resultNode.userData; //resultNode.userData = currentDepth + 1; return resultNode; } if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var assetGuid, out long _)) { var objNode = new Node { title = obj.name, style = { width = NodeWidth } }; objNode.extensionContainer.style.backgroundColor = new Color(0.24f, 0.24f, 0.24f, 0.8f); #region Select button objNode.titleContainer.Add(new Button(() => { Selection.activeObject = obj; EditorGUIUtility.PingObject(obj); }) { style = { height = 16.0f, alignSelf = Align.Center, alignItems = Align.Center }, text = "Select" }); objNode.titleContainer.Add(new Button(() => { // assetNode }) { style = { height = 16.0f, alignSelf = Align.Center, alignItems = Align.Center }, text = "Delete" }); #endregion #region Padding var infoContainer = new VisualElement { style = { paddingBottom = 4.0f, paddingTop = 4.0f, paddingLeft = 4.0f, paddingRight = 4.0f } }; #endregion #region Asset Path, removed to improve visibility with large amount of assets // infoContainer.Add(new Label { // text = assetPath, //#if UNITY_2019_1_OR_NEWER // style = { whiteSpace = WhiteSpace.Normal } //#else // style = { wordWrap = true } //#endif // }); #endregion #region Asset type var typeName = assetNode.AssetType; var typeLabel = new Label { text = $"Type: {typeName}", }; infoContainer.Add(typeLabel); objNode.extensionContainer.Add(infoContainer); #endregion var typeContainer = new VisualElement { style = { paddingBottom = 4.0f, paddingTop = 4.0f, paddingLeft = 4.0f, paddingRight = 4.0f, backgroundColor = GetColorByAssetType(typeName) } }; objNode.extensionContainer.Add(typeContainer); #region Node Icon, replaced with color //Texture assetTexture = AssetPreview.GetAssetPreview(obj); //if (!assetTexture) // assetTexture = AssetPreview.GetMiniThumbnail(obj); //if (assetTexture) //{ // AddDivider(objNode); // objNode.extensionContainer.Add(new Image // { // image = assetTexture, // scaleMode = ScaleMode.ScaleToFit, // style = // { // paddingBottom = 4.0f, // paddingTop = 4.0f, // paddingLeft = 4.0f, // paddingRight = 4.0f // } // }); //} #endregion // Ports var dependentAmount = 0; foreach (var item in assetNode.Dependent) { if (item.AssetType != "Folder") { ++dependentAmount; } } if (assetNode.Dependent.Count > 0) { Port port = objNode.InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(Object)); port.Add(new Button(() => { CreateDependentNodes(assetGroup, assetNode, fullPathNodeLookup[fullPath], assetGroup.GroupNode, (int)fullPathNodeLookup[fullPath].userData - 1); EditorApplication.delayCall += () => ResetAllNodes(); }) { style = { height = 16.0f, alignSelf = Align.Center, alignItems = Align.Center }, text = "展开" }); port.portName = dependentAmount + "个引用"; objNode.inputContainer.Add(port); } var dependencyAmount = assetNode.Dependencies.Count; if (dependencyAmount > 0) { Port port = objNode.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Object)); port.Add(new Button(() => { CreateDependencyNodes(assetGroup, assetNode, fullPathNodeLookup[fullPath], assetGroup.GroupNode, (int)fullPathNodeLookup[fullPath].userData + 1); EditorApplication.delayCall += () => ResetAllNodes(); }) { style = { height = 16.0f, alignSelf = Align.Center, alignItems = Align.FlexEnd }, text = "展开" }); port.portName = dependencyAmount + "个依赖"; objNode.outputContainer.Add(port); objNode.RefreshPorts(); } resultNode = objNode; resultNode.RefreshExpandedState(); resultNode.RefreshPorts(); resultNode.capabilities &= ~Capabilities.Deletable; resultNode.capabilities |= Capabilities.Collapsible; } fullPathNodeLookup[fullPath] = resultNode; return resultNode; } bool FilterType(string type) { if (type2Toogle.TryGetValue(type, out var result)) { return result.value; } return false; } StyleColor GetColorByAssetType(string typeName) { switch (typeName) { case "MonoScript": return Color.black; case "Material": return new Color(0.1f, 0.5f, 0.1f); //green case "Texture2D": return new Color(0.5f, 0.1f, 0.1f); //red case "RenderTexture": return new Color(0.8f, 0.1f, 0.1f); //red case "Shader": return new Color(0.1f, 0.1f, 0.5f); //dark blue case "ComputeShader": return new Color(0.1f, 0.1f, 0.5f); //dark blue case "GameObject": return new Color(0f, 0.8f, 0.7f); //light blue case "AnimationClip": return new Color(1, 0.7f, 1); //pink case "AnimatorController": return new Color(1, 0.7f, 0.8f); //pink case "AudioClip": return new Color(1, 0.8f, 0); //orange case "AudioMixerController": return new Color(1, 0.8f, 0); //orange case "Font": return new Color(0.9f, 1, 0.9f); //light green case "TMP_FontAsset": return new Color(0.9f, 1, 0.9f); //light green case "Mesh": return new Color(0.5f, 0, 0.5f); //purple case "TerrainLayer": return new Color(0.5f, 0.8f, 0f); //green default: break; } return CustomColor(typeName); } //Add custom assets here StyleColor CustomColor(string assetType) { switch (assetType) { case "GearObject": return new Color(0.9f, 0, 0.9f); //pink case "TalentObject": return new Color(0.9f, 0, 0.9f); // case "AbilityInfo": return new Color(0.9f, 0, 0.9f); // case "HealthSO": return new Color(0.9f, 0, 0.9f); // default: break; } //standard color return new Color(0.24f, 0.24f, 0.24f, 0.8f); } private static void AddDivider(Node objNode) { var divider = new VisualElement { name = "divider" }; divider.AddToClassList("horizontal"); objNode.extensionContainer.Add(divider); } private void ClearGraph() { selectedObjects.Clear(); foreach (var assetGroup in assetGroups) { EmptyGroup(assetGroup); } fullPathNodeLookup.Clear(); assetGroups.Clear(); } void EmptyGroup(AssetGroup assetGroup) { if (assetGroup.AssetGraphConnections.Count > 0) { foreach (var edge in assetGroup.AssetGraphConnections) { graphView.RemoveElement(edge); } } assetGroup.AssetGraphConnections.Clear(); foreach (var node in assetGroup.AssetGraphNodes) { graphView.RemoveElement(node); } assetGroup.AssetGraphNodes.Clear(); assetGroup.DependenciesForPlacement.Clear(); graphView.RemoveElement(assetGroup.GroupNode); assetGroup.GroupNode = null; } private void UpdateGroupDependencyNodePlacement(GeometryChangedEvent e, AssetGroup assetGroup) { assetGroup.MainGraphNode.UnregisterCallback( UpdateGroupDependencyNodePlacement ); ResetNodes(assetGroup); } void ResetAllNodes() { foreach (var assetGroup in assetGroups) { ResetNodes(assetGroup); } } //Reset the node positions of the given group void ResetNodes(AssetGroup assetGroup) { // The current y offset in per depth var depthOffset = new Dictionary(); foreach (var node in assetGroup.DependenciesForPlacement) { int depth = (int)node.userData; if (!depthOffset.ContainsKey(depth)) depthOffset.Add(depth, 0.0f); if (AlignmentToggle.value) { depthOffset[depth] += node.layout.height; } else { depthOffset[depth] += node.layout.width; } } // Move half of the node into negative y space so they're on either size of the main node in y axis var depths = new List(depthOffset.Keys); foreach (int depth in depths) { if (depth == 0) continue; float offset = depthOffset[depth]; depthOffset[depth] = (0f - offset / 2.0f); } Rect mainNodeRect = assetGroup.MainGraphNode.GetPosition(); foreach (var node in assetGroup.DependenciesForPlacement) { int depth = (int)node.userData; if (AlignmentToggle.value) { node.SetPosition(new Rect(mainNodeRect.x + node.layout.width * 1.5f * depth, mainNodeRect.y + depthOffset[depth], 0, 0)); } else { node.SetPosition(new Rect(mainNodeRect.x + depthOffset[depth], mainNodeRect.y + node.layout.height * 1.5f * depth, 0, 0)); } if (AlignmentToggle.value) { depthOffset[depth] += node.layout.height; } else { depthOffset[depth] += node.layout.width; } } } //fix the position of the groups so they dont overlap void ResetGroups() { float y = 0; float x = 0; foreach (var assetGroup in assetGroups) { if (AlignmentToggle.value) { Rect pos = assetGroup.GroupNode.GetPosition(); pos.x = x; assetGroup.GroupNode.SetPosition(pos); x += assetGroup.GroupNode.GetPosition().width; } else { Rect pos = assetGroup.GroupNode.GetPosition(); pos.y = y; assetGroup.GroupNode.SetPosition(pos); y += assetGroup.GroupNode.GetPosition().height; } } } } }