├── .gitignore ├── .npmignore ├── .yamato └── build.yml ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Documentation~ └── playablegraph-visualizer.md ├── Editor.meta ├── Editor ├── Graph.meta ├── Graph │ ├── Graph.cs │ ├── Graph.cs.meta │ ├── Layouts.meta │ ├── Layouts │ │ ├── IGraphLayout.cs │ │ ├── IGraphLayout.cs.meta │ │ ├── ReingoldTilford.cs │ │ └── ReingoldTilford.cs.meta │ ├── Node.cs │ ├── Node.cs.meta │ ├── Renderer.meta │ └── Renderer │ │ ├── DefaultGraphRenderer.cs │ │ ├── DefaultGraphRenderer.cs.meta │ │ ├── IGraphRenderer.cs │ │ └── IGraphRenderer.cs.meta ├── PlayableGraphVisualizer.cs ├── PlayableGraphVisualizer.cs.meta ├── PlayableGraphVisualizerWindow.cs ├── PlayableGraphVisualizerWindow.cs.meta ├── PlayableNodes.meta ├── PlayableNodes │ ├── AnimationClipPlayableNode.cs │ ├── AnimationClipPlayableNode.cs.meta │ ├── AnimationLayerMixerPlayableNode.cs │ ├── AnimationLayerMixerPlayableNode.cs.meta │ ├── PlayableNode.cs │ ├── PlayableNode.cs.meta │ ├── PlayableOutputNode.cs │ ├── PlayableOutputNode.cs.meta │ ├── SharedPlayableNode.cs │ └── SharedPlayableNode.cs.meta ├── Resources.meta ├── Resources │ ├── Node.png │ └── Node.png.meta ├── Unity.PlayableGraphVisualizer.Editor.asmdef └── Unity.PlayableGraphVisualizer.Editor.asmdef.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── GraphVisualizerClient.cs ├── GraphVisualizerClient.cs.meta ├── Unity.PlayableGraphVisualizer.asmdef └── Unity.PlayableGraphVisualizer.asmdef.meta ├── Tests.meta ├── Tests ├── Runtime.meta └── Runtime │ ├── GraphVisualizerClientTests.cs │ ├── GraphVisualizerClientTests.cs.meta │ ├── Unity.PlayableGraphVisualizer.Tests.asmdef │ └── Unity.PlayableGraphVisualizer.Tests.asmdef.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/** 2 | build/** 3 | .build_script/** 4 | node_modules/** 5 | .DS_Store 6 | .npmrc 7 | !Documentation~ 8 | !.Documentation 9 | npm-debug.log 10 | build.sh.meta 11 | build.bat.meta 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | artifacts/** 2 | build/** 3 | .build_script/** 4 | node_modules/** 5 | Documentation/ApiDocs/** 6 | Documentation~/ApiDocs/** 7 | .DS_Store 8 | .npmrc 9 | .npmignore 10 | .gitignore 11 | CONTRIBUTING.md 12 | CONTRIBUTING.md.meta 13 | QAReport.md 14 | QAReport.md.meta 15 | .gitlab-ci.yml 16 | build.sh 17 | build.sh.meta 18 | build.bat 19 | build.bat.meta 20 | -------------------------------------------------------------------------------- /.yamato/build.yml: -------------------------------------------------------------------------------- 1 | editors: 2 | - version: 2019.1 3 | platforms: 4 | - name: win 5 | type: Unity::VM 6 | image: package-ci/win10:latest 7 | flavor: m1.large 8 | - name: mac 9 | type: Unity::VM::osx 10 | image: buildfarm/mac:stable 11 | flavor: m1.mac 12 | --- 13 | {% for editor in editors %} 14 | {% for platform in platforms %} 15 | {{ platform.name }}_{{ editor.version }}: 16 | name : Build and Test version {{ editor.version }} on {{ platform.name }} 17 | agent: 18 | type: {{ platform.type }} 19 | image: {{ platform.image }} 20 | flavor: {{ platform.flavor}} 21 | commands: 22 | - npm install upm-ci-utils -g --registry https://api.bintray.com/npm/unity/unity-npm 23 | - upm-ci package pack 24 | - upm-ci package test --unity-version {{ editor.version }} 25 | triggers: 26 | branches: 27 | only: 28 | - master 29 | artifacts: 30 | logs.zip: 31 | paths: 32 | - "upm-ci~/test-results/**/*" 33 | {% endfor %} 34 | {% endfor %} 35 | 36 | run_preview_verified_staging: 37 | name: Preview and Verified Packages to Staging 38 | agent: 39 | type: Unity::VM 40 | image: package-ci/win10:latest 41 | flavor: m1.large 42 | name: Runner 43 | commands: 44 | - npm install upm-ci-utils -g --registry https://api.bintray.com/npm/unity/unity-npm 45 | - upm-ci package pack 46 | - upm-ci package publish 47 | triggers: 48 | tags: 49 | only: 50 | - /^[vV][0-9]+.[0-9]+.[0-9]+/ 51 | artifacts: 52 | artifacts.zip: 53 | paths: 54 | - "upm-ci~/packages/*.tgz" 55 | dependencies: 56 | {% for editor in editors %} 57 | {% for platform in platforms %} 58 | - .yamato/build.yml#{{ platform.name }}_{{ editor.version }} 59 | {% endfor %} 60 | {% endfor %} 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this package will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.2.1] - 2019-02-07 9 | 10 | ### Added 11 | 12 | - It is possible to hide the legend within the inspector through the window menu 13 | - Runtime tests 14 | 15 | ### Changed 16 | 17 | - Update Yamato config file 18 | 19 | ## [0.2.0] - 2019-01-25 20 | 21 | ### Changed 22 | 23 | - Updated the Yamato CI configuration 24 | - Moved from MIT to UCL license 25 | - Moved menu entry under Window > Analysis 26 | 27 | ## [0.1.0] - 2018-12-20 28 | 29 | ### Added 30 | 31 | - First release of Unity package *PlayableGraph Visualizer*. 32 | - Mirrored the original [graph-visualizer repository](https://github.com/Unity-Technologies/graph-visualizer) and modified it to fulfill the Unity package requirements. 33 | 34 | 35 | [Unreleased]: https://github.cds.internal.unity3d.com/unity/com.unity.playablegraph-visualizer/compare/v0.1.0...HEAD 36 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 082393007b3cfe547958d9d6f7abffd2 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation~/playablegraph-visualizer.md: -------------------------------------------------------------------------------- 1 | # About PlayableGraph Visualizer 2 | 3 | Use the *PlayableGraph Visualizer* package to have a visual representation of the 4 | Playable graphs instantiated in the scene. 5 | 6 | # Installing PlayableGraph Visualizer 7 | 8 | To install this package, follow the instructions in the 9 | [Package Manager documentation](https://docs.unity3d.com/Packages/com.unity.package-manager-ui@latest/index.html). 10 | 11 | # Using PlayableGraph Visualizer 12 | 13 | - Open the PlayableGraph Visualizer in **Window > Analysis > PlayableGraph Visualizer** 14 | - Open any scene that contains at least one `PlayableGraph` 15 | - In the top-left list, select the `PlayableGraph` to display in the window 16 | - Click on the nodes to display more information about the associated Playable handle 17 | 18 | _Note:_ 19 | - You can show just your `PlayableGraph` using `GraphVisualizerClient.Show(PlayableGraph)` in the code 20 | - If your `PlayableGraph` is only available in Play mode, you will not be able to see it in Edit mode 21 | 22 | # Technical details 23 | 24 | ## Requirements 25 | 26 | This version of *PlayableGraph Visualizer* is compatible with the following versions of the Unity Editor: 27 | 28 | * 2018.1 and later (recommended) 29 | 30 | ## Package contents 31 | 32 | The following table indicates the structure of the package: 33 | 34 | |Location|Description| 35 | |---|---| 36 | |`Editor/`|Contains the editor scripts for the new *PlayableGraph Visualizer* window.| 37 | |`Runtime/GraphVisualizerClient.cs`|Contains the class allowing the user to register specific Playable graphs.| 38 | 39 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d39aa905569c24bf2a618d7edb0bb147 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Graph.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 569c993bef0ef6146b1b8cbb904bdfde 3 | folderAsset: yes 4 | timeCreated: 1478032057 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Graph/Graph.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace GraphVisualizer 6 | { 7 | public abstract class Graph : IEnumerable 8 | { 9 | private readonly List m_Nodes = new List(); 10 | 11 | public ReadOnlyCollection nodes 12 | { 13 | get { return m_Nodes.AsReadOnly(); } 14 | } 15 | 16 | protected class NodeWeight 17 | { 18 | public object node { get; set; } 19 | public float weight { get; set; } 20 | } 21 | 22 | // Derived class should specify the children of a given node. 23 | protected abstract IEnumerable GetChildren(Node node); 24 | 25 | // Derived class should implement how to populate this graph (usually by calling AddNodeHierarchy()). 26 | protected abstract void Populate(); 27 | 28 | public void AddNodeHierarchy(Node root) 29 | { 30 | AddNode(root); 31 | 32 | IEnumerable children = GetChildren(root); 33 | if (children == null) 34 | return; 35 | 36 | foreach (Node child in children) 37 | { 38 | root.AddChild(child); 39 | AddNodeHierarchy(child); 40 | } 41 | } 42 | 43 | public void AddNode(Node node) 44 | { 45 | m_Nodes.Add(node); 46 | } 47 | 48 | public void Clear() 49 | { 50 | m_Nodes.Clear(); 51 | } 52 | 53 | public void Refresh() 54 | { 55 | // TODO optimize? 56 | Clear(); 57 | Populate(); 58 | } 59 | 60 | public IEnumerator GetEnumerator() 61 | { 62 | return m_Nodes.GetEnumerator(); 63 | } 64 | 65 | IEnumerator IEnumerable.GetEnumerator() 66 | { 67 | return m_Nodes.GetEnumerator(); 68 | } 69 | 70 | public bool IsEmpty() 71 | { 72 | return m_Nodes.Count == 0; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Editor/Graph/Graph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 147d962da65506442b417599ddc1ca7e 3 | timeCreated: 1478627468 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Graph/Layouts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1ba77f7c2ab935748b7f3d4559ed450c 3 | folderAsset: yes 4 | timeCreated: 1478627467 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Graph/Layouts/IGraphLayout.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace GraphVisualizer 5 | { 6 | // Interface for a generic graph layout. 7 | public interface IGraphLayout 8 | { 9 | IEnumerable vertices { get; } 10 | IEnumerable edges { get; } 11 | 12 | bool leftToRight { get; } 13 | 14 | void CalculateLayout(Graph graph); 15 | } 16 | 17 | public class Edge 18 | { 19 | // Indices in the vertex array of the layout algorithm. 20 | public Edge(Vertex src, Vertex dest) 21 | { 22 | source = src; 23 | destination = dest; 24 | } 25 | 26 | public Vertex source { get; private set; } 27 | 28 | public Vertex destination { get; private set; } 29 | } 30 | 31 | // One vertex is associated to each node in the graph. 32 | public class Vertex 33 | { 34 | // Center of the node in the graph layout. 35 | public Vector2 position { get; set; } 36 | 37 | // The Node represented by the vertex. 38 | public Node node { get; private set; } 39 | 40 | public Vertex(Node node) 41 | { 42 | this.node = node; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Editor/Graph/Layouts/IGraphLayout.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5171cb2fc868cda4bbfd28e24fe5d37c 3 | timeCreated: 1478627880 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Graph/Layouts/ReingoldTilford.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | namespace GraphVisualizer 6 | { 7 | // Implementation of Reingold and Tilford algorithm for graph layout 8 | // "Tidier Drawings of Trees", IEEE Transactions on Software Engineering Vol SE-7 No.2, March 1981 9 | // The implementation has been customized to support graphs with multiple roots and unattached nodes. 10 | public class ReingoldTilford : IGraphLayout 11 | { 12 | // By convention, all graph layout algorithms should have a minimum distance of 1 unit between nodes 13 | private static readonly float s_DistanceBetweenNodes = 1.0f; 14 | 15 | // Used to specify the vertical distance two non-attached trees in the graph. 16 | private static readonly float s_VerticalDistanceBetweenTrees = 3.0f; 17 | 18 | // Used to lengthen the wire when lots of children are connected. If 1, all levels will be evenly separated 19 | private static readonly float s_WireLengthFactorForLargeSpanningTrees = 3.0f; 20 | 21 | private static readonly float s_MaxChildrenThreshold = 6.0f; 22 | 23 | // Helper structure to easily find the vertex associated to a given Node. 24 | private readonly Dictionary m_NodeVertexLookup = new Dictionary(); 25 | 26 | public ReingoldTilford(bool leftToRight = true) 27 | { 28 | this.leftToRight = leftToRight; 29 | } 30 | 31 | public IEnumerable vertices 32 | { 33 | get { return m_NodeVertexLookup.Values; } 34 | } 35 | 36 | public IEnumerable edges 37 | { 38 | get 39 | { 40 | var edgesList = new List(); 41 | foreach (var node in m_NodeVertexLookup) 42 | { 43 | Vertex v = node.Value; 44 | foreach (Node child in v.node.children) 45 | { 46 | edgesList.Add(new Edge(m_NodeVertexLookup[child], v)); 47 | } 48 | } 49 | return edgesList; 50 | } 51 | } 52 | 53 | public bool leftToRight { get; private set; } 54 | 55 | // Main entry point of the algorithm 56 | public void CalculateLayout(Graph graph) 57 | { 58 | m_NodeVertexLookup.Clear(); 59 | foreach (Node node in graph) 60 | { 61 | m_NodeVertexLookup.Add(node, new Vertex(node)); 62 | } 63 | 64 | if (m_NodeVertexLookup.Count == 0) return; 65 | 66 | IList horizontalPositions = ComputeHorizontalPositionForEachLevel(); 67 | 68 | List roots = m_NodeVertexLookup.Keys.Where(n => n.parent == null).ToList(); 69 | 70 | for (int i = 0; i < roots.Count; ++i) 71 | { 72 | RecursiveLayout(roots[i], 0, horizontalPositions); 73 | 74 | if (i > 0) 75 | { 76 | Vector2 previousRootRange = ComputeRangeRecursive(roots[i - 1]); 77 | RecursiveMoveSubtree(roots[i], previousRootRange.y + s_VerticalDistanceBetweenTrees + s_DistanceBetweenNodes); 78 | } 79 | } 80 | } 81 | 82 | // Precompute the horizontal position for each level. 83 | // Levels with few wires (as measured by the maximum number of children for one node) are placed closer 84 | // apart; very cluttered levels are placed further apart. 85 | private float[] ComputeHorizontalPositionForEachLevel() 86 | { 87 | // Gather information about depths. 88 | var maxDepth = int.MinValue; 89 | var nodeDepths = new Dictionary>(); 90 | foreach (Node node in m_NodeVertexLookup.Keys) 91 | { 92 | int d = node.depth; 93 | List nodes; 94 | if (!nodeDepths.TryGetValue(d, out nodes)) 95 | { 96 | nodeDepths[d] = nodes = new List(); 97 | } 98 | nodes.Add(node); 99 | maxDepth = Mathf.Max(d, maxDepth); 100 | } 101 | 102 | // Bake the left to right horizontal positions. 103 | var horizontalPositionForDepth = new float[maxDepth]; 104 | horizontalPositionForDepth[0] = 0; 105 | for (int d = 1; d < maxDepth; ++d) 106 | { 107 | IEnumerable nodesOnThisLevel = nodeDepths[d + 1]; 108 | 109 | int maxChildren = nodesOnThisLevel.Max(x => x.children.Count); 110 | 111 | float wireLengthHeuristic = Mathf.Lerp(1, s_WireLengthFactorForLargeSpanningTrees, 112 | Mathf.Min(1, maxChildren / s_MaxChildrenThreshold)); 113 | 114 | horizontalPositionForDepth[d] = horizontalPositionForDepth[d - 1] + 115 | s_DistanceBetweenNodes * wireLengthHeuristic; 116 | } 117 | 118 | return leftToRight ? horizontalPositionForDepth : horizontalPositionForDepth.Reverse().ToArray(); 119 | } 120 | 121 | // Traverse the graph and place all nodes according to the algorithm 122 | private void RecursiveLayout(Node node, int depth, IList horizontalPositions) 123 | { 124 | IList children = node.children; 125 | foreach (Node child in children) 126 | { 127 | RecursiveLayout(child, depth + 1, horizontalPositions); 128 | } 129 | 130 | var yPos = 0.0f; 131 | if (children.Count > 0) 132 | { 133 | SeparateSubtrees(children); 134 | yPos = GetAveragePosition(children).y; 135 | } 136 | 137 | var pos = new Vector2(horizontalPositions[depth], yPos); 138 | m_NodeVertexLookup[node].position = pos; 139 | } 140 | 141 | private Vector2 ComputeRangeRecursive(Node node) 142 | { 143 | var range = Vector2.one * m_NodeVertexLookup[node].position.y; 144 | foreach (Node child in node.children) 145 | { 146 | Vector2 childRange = ComputeRangeRecursive(child); 147 | range.x = Mathf.Min(range.x, childRange.x); 148 | range.y = Mathf.Max(range.y, childRange.y); 149 | } 150 | return range; 151 | } 152 | 153 | // Determine parent's vertical position based on its children 154 | private Vector2 GetAveragePosition(ICollection children) 155 | { 156 | Vector2 centroid = new Vector2(); 157 | 158 | centroid = children.Aggregate(centroid, (current, n) => current + m_NodeVertexLookup[n].position); 159 | 160 | if (children.Count > 0) 161 | centroid /= children.Count; 162 | 163 | return centroid; 164 | } 165 | 166 | // Separate the given subtrees so they do not overlap 167 | private void SeparateSubtrees(IList subroots) 168 | { 169 | if (subroots.Count < 2) 170 | return; 171 | 172 | Node upperNode = subroots[0]; 173 | 174 | Dictionary upperTreeBoundaries = GetBoundaryPositions(upperNode); 175 | for (int s = 0; s < subroots.Count - 1; s++) 176 | { 177 | Node lowerNode = subroots[s + 1]; 178 | Dictionary lowerTreeBoundaries = GetBoundaryPositions(lowerNode); 179 | 180 | int minDepth = upperTreeBoundaries.Keys.Min(); 181 | if (minDepth != lowerTreeBoundaries.Keys.Min()) 182 | Debug.LogError("Cannot separate subtrees which do not start at the same root depth"); 183 | 184 | int lowerMaxDepth = lowerTreeBoundaries.Keys.Max(); 185 | int upperMaxDepth = upperTreeBoundaries.Keys.Max(); 186 | int maxDepth = System.Math.Min(upperMaxDepth, lowerMaxDepth); 187 | 188 | for (int depth = minDepth; depth <= maxDepth; depth++) 189 | { 190 | float delta = s_DistanceBetweenNodes - (lowerTreeBoundaries[depth].x - upperTreeBoundaries[depth].y); 191 | delta = System.Math.Max(delta, 0); 192 | RecursiveMoveSubtree(lowerNode, delta); 193 | for (int i = minDepth; i <= lowerMaxDepth; i++) 194 | lowerTreeBoundaries[i] += new Vector2(delta, delta); 195 | } 196 | upperTreeBoundaries = CombineBoundaryPositions(upperTreeBoundaries, lowerTreeBoundaries); 197 | } 198 | } 199 | 200 | // Using a Vector2 at each depth to hold the extrema vertical positions 201 | private Dictionary GetBoundaryPositions(Node subTreeRoot) 202 | { 203 | var extremePositions = new Dictionary(); 204 | 205 | IEnumerable descendants = GetSubtreeNodes(subTreeRoot); 206 | 207 | foreach (var node in descendants) 208 | { 209 | int depth = m_NodeVertexLookup[node].node.depth; 210 | float pos = m_NodeVertexLookup[node].position.y; 211 | if (extremePositions.ContainsKey(depth)) 212 | extremePositions[depth] = new Vector2(Mathf.Min(extremePositions[depth].x, pos), 213 | Mathf.Max(extremePositions[depth].y, pos)); 214 | else 215 | extremePositions[depth] = new Vector2(pos, pos); 216 | } 217 | 218 | return extremePositions; 219 | } 220 | 221 | // Includes all descendants and the subtree root itself 222 | private IEnumerable GetSubtreeNodes(Node root) 223 | { 224 | var allDescendants = new List { root }; 225 | foreach (Node child in root.children) 226 | { 227 | allDescendants.AddRange(GetSubtreeNodes(child)); 228 | } 229 | return allDescendants; 230 | } 231 | 232 | // After adjusting a subtree, compute its new boundary positions 233 | private Dictionary CombineBoundaryPositions(Dictionary upperTree, Dictionary lowerTree) 234 | { 235 | var combined = new Dictionary(); 236 | int minDepth = upperTree.Keys.Min(); 237 | int maxDepth = System.Math.Max(upperTree.Keys.Max(), lowerTree.Keys.Max()); 238 | 239 | for (int d = minDepth; d <= maxDepth; d++) 240 | { 241 | float upperBoundary = upperTree.ContainsKey(d) ? upperTree[d].x : lowerTree[d].x; 242 | float lowerBoundary = lowerTree.ContainsKey(d) ? lowerTree[d].y : upperTree[d].y; 243 | combined[d] = new Vector2(upperBoundary, lowerBoundary); 244 | } 245 | return combined; 246 | } 247 | 248 | // Apply a vertical delta to all nodes in a subtree 249 | private void RecursiveMoveSubtree(Node subtreeRoot, float yDelta) 250 | { 251 | Vector2 pos = m_NodeVertexLookup[subtreeRoot].position; 252 | m_NodeVertexLookup[subtreeRoot].position = new Vector2(pos.x, pos.y + yDelta); 253 | 254 | foreach (Node child in subtreeRoot.children) 255 | { 256 | RecursiveMoveSubtree(child, yDelta); 257 | } 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Editor/Graph/Layouts/ReingoldTilford.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 51bcffb11720b2748b8b520d55c94a12 3 | timeCreated: 1478032057 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Graph/Node.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace GraphVisualizer 7 | { 8 | public class Node 9 | { 10 | public object content { get; private set; } 11 | public float weight { get; set; } 12 | public bool active { get; private set; } 13 | public Node parent { get; private set; } 14 | public IList children { get; private set; } 15 | 16 | public Node(object content, float weight = 1.0f, bool active = false) 17 | { 18 | this.content = content; 19 | this.weight = weight; 20 | this.active = active; 21 | children = new List(); 22 | } 23 | 24 | public void AddChild(Node child) 25 | { 26 | if (child == this) throw new Exception("Circular graphs not supported."); 27 | if (child.parent == this) return; 28 | 29 | children.Add(child); 30 | child.parent = this; 31 | } 32 | 33 | public int depth 34 | { 35 | get { return GetDepthRecursive(this); } 36 | } 37 | 38 | private static int GetDepthRecursive(Node node) 39 | { 40 | if (node.parent == null) return 1; 41 | return 1 + GetDepthRecursive(node.parent); 42 | } 43 | 44 | public virtual Type GetContentType() 45 | { 46 | return content == null ? null : content.GetType(); 47 | } 48 | 49 | public virtual string GetContentTypeName() 50 | { 51 | Type type = GetContentType(); 52 | return type == null ? "Null" : type.ToString(); 53 | } 54 | 55 | public virtual string GetContentTypeShortName() 56 | { 57 | return GetContentTypeName().Split('.').Last(); 58 | } 59 | 60 | public override string ToString() 61 | { 62 | return "Node content: " + GetContentTypeName(); 63 | } 64 | 65 | public virtual Color GetColor() 66 | { 67 | Type type = GetContentType(); 68 | if (type == null) 69 | return Color.red; 70 | 71 | string shortName = type.ToString().Split('.').Last(); 72 | float h = (float)Math.Abs(shortName.GetHashCode()) / int.MaxValue; 73 | return Color.HSVToRGB(h, 0.6f, 1.0f); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Editor/Graph/Node.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0a5e939f109486342a621dfffb0e92a4 3 | timeCreated: 1478627468 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Graph/Renderer.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6e6f7481b1110e348aafbed5c3c6f137 3 | folderAsset: yes 4 | timeCreated: 1478627880 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Graph/Renderer/DefaultGraphRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using UnityEditor; 6 | using UnityEngine; 7 | 8 | namespace GraphVisualizer 9 | { 10 | public class DefaultGraphRenderer : IGraphRenderer 11 | { 12 | protected event Action nodeClicked; 13 | private static readonly Color s_EdgeColorMin = new Color(1.0f, 1.0f, 1.0f, 0.1f); 14 | private static readonly Color s_EdgeColorMax = Color.white; 15 | private static readonly Color s_LegendBackground = new Color(0, 0, 0, 0.1f); 16 | 17 | private static readonly float s_BorderSize = 15; 18 | private static readonly float s_LegendFixedOverheadWidth = 100; 19 | private static readonly float s_DefaultMaximumNormalizedNodeSize = 0.8f; 20 | private static readonly float s_DefaultMaximumNodeSizeInPixels = 100.0f; 21 | private static readonly float s_DefaultAspectRatio = 1.5f; 22 | 23 | private static readonly int s_NodeMaxFontSize = 14; 24 | 25 | private GUIStyle m_LegendLabelStyle; 26 | private GUIStyle m_SubTitleStyle; 27 | private GUIStyle m_InspectorStyle; 28 | private GUIStyle m_NodeRectStyle; 29 | 30 | private static readonly int s_ActiveNodeThickness = 2; 31 | private static readonly int s_SelectedNodeThickness = 4; 32 | private static readonly Color s_ActiveNodeColor = Color.white; 33 | private static readonly Color s_SelectedNodeColor = Color.yellow; 34 | 35 | private readonly Dictionary m_LegendForType = new Dictionary(); 36 | 37 | private Node m_SelectedNode; 38 | 39 | private Texture2D m_ColorBar; 40 | Vector2 m_ScrollPos; 41 | 42 | private struct NodeTypeLegend 43 | { 44 | public Color color; 45 | public string label; 46 | } 47 | 48 | public DefaultGraphRenderer() 49 | { 50 | InitializeStyles(); 51 | } 52 | 53 | public void Reset() 54 | { 55 | m_SelectedNode = null; 56 | } 57 | 58 | public void Draw(IGraphLayout graphLayout, Rect drawingArea) 59 | { 60 | GraphSettings defaults; 61 | defaults.maximumNormalizedNodeSize = s_DefaultMaximumNormalizedNodeSize; 62 | defaults.maximumNodeSizeInPixels = s_DefaultMaximumNodeSizeInPixels; 63 | defaults.aspectRatio = s_DefaultAspectRatio; 64 | defaults.showInspector = true; 65 | defaults.showLegend = true; 66 | Draw(graphLayout, drawingArea, defaults); 67 | } 68 | 69 | public void Draw(IGraphLayout graphLayout, Rect totalDrawingArea, GraphSettings graphSettings) 70 | { 71 | var legendArea = new Rect(); 72 | var drawingArea = new Rect(totalDrawingArea); 73 | 74 | PrepareLegend(graphLayout.vertices); 75 | 76 | if (graphSettings.showInspector) 77 | { 78 | legendArea = new Rect(totalDrawingArea) 79 | { 80 | width = Mathf.Max(EstimateLegendWidth(), drawingArea.width * 0.25f) + s_BorderSize * 2 81 | }; 82 | 83 | legendArea.x = drawingArea.xMax - legendArea.width; 84 | drawingArea.width -= legendArea.width; // + s_BorderSize; 85 | 86 | DrawLegend(graphSettings, legendArea); 87 | } 88 | 89 | if (m_SelectedNode != null) 90 | { 91 | Event currentEvent = Event.current; 92 | if (currentEvent.type == EventType.MouseUp && currentEvent.button == 0) 93 | { 94 | Vector2 mousePos = currentEvent.mousePosition; 95 | if (drawingArea.Contains(mousePos)) 96 | { 97 | m_SelectedNode = null; 98 | 99 | if (nodeClicked != null) 100 | nodeClicked(m_SelectedNode); 101 | } 102 | } 103 | } 104 | 105 | DrawGraph(graphLayout, drawingArea, graphSettings); 106 | } 107 | 108 | private void InitializeStyles() 109 | { 110 | m_LegendLabelStyle = new GUIStyle(GUI.skin.label) 111 | { 112 | margin = { top = 0 }, 113 | alignment = TextAnchor.UpperLeft 114 | }; 115 | 116 | m_NodeRectStyle = new GUIStyle 117 | { 118 | normal = 119 | { 120 | background = (Texture2D)Resources.Load("Node"), 121 | textColor = Color.black, 122 | }, 123 | border = new RectOffset(10, 10, 10, 10), 124 | alignment = TextAnchor.MiddleCenter, 125 | wordWrap = true, 126 | clipping = TextClipping.Clip 127 | }; 128 | 129 | m_SubTitleStyle = EditorStyles.boldLabel; 130 | 131 | m_InspectorStyle = new GUIStyle 132 | { 133 | normal = 134 | { 135 | textColor = Color.white, 136 | }, 137 | richText = true, 138 | alignment = TextAnchor.MiddleLeft, 139 | wordWrap = true, 140 | clipping = TextClipping.Clip 141 | }; 142 | } 143 | 144 | private void PrepareLegend(IEnumerable vertices) 145 | { 146 | m_LegendForType.Clear(); 147 | foreach (Vertex v in vertices) 148 | { 149 | if (v.node == null) 150 | continue; 151 | 152 | string nodeType = v.node.GetContentTypeName(); 153 | 154 | if (m_LegendForType.ContainsKey(nodeType)) 155 | continue; 156 | 157 | m_LegendForType[nodeType] = new NodeTypeLegend 158 | { 159 | label = v.node.GetContentTypeShortName(), 160 | color = v.node.GetColor() 161 | }; 162 | } 163 | } 164 | 165 | private float EstimateLegendWidth() 166 | { 167 | float legendWidth = 0; 168 | foreach (NodeTypeLegend legend in m_LegendForType.Values) 169 | { 170 | legendWidth = Mathf.Max(legendWidth, GUI.skin.label.CalcSize(new GUIContent(legend.label)).x); 171 | } 172 | 173 | legendWidth += s_LegendFixedOverheadWidth; 174 | return legendWidth; 175 | } 176 | 177 | public void DrawRect(Rect rect, Color color, string text, bool active, bool selected = false) 178 | { 179 | var originalColor = GUI.color; 180 | 181 | if (selected) 182 | { 183 | GUI.color = s_SelectedNodeColor; 184 | float t = s_SelectedNodeThickness + (active ? s_ActiveNodeThickness : 0.0f); 185 | GUI.Box(new Rect(rect.x - t, rect.y - t, rect.width + 2 * t, rect.height + 2 * t), 186 | string.Empty, m_NodeRectStyle); 187 | } 188 | 189 | if (active) 190 | { 191 | GUI.color = s_ActiveNodeColor; 192 | GUI.Box(new Rect(rect.x - s_ActiveNodeThickness, rect.y - s_ActiveNodeThickness, 193 | rect.width + 2 * s_ActiveNodeThickness, rect.height + 2 * s_ActiveNodeThickness), 194 | string.Empty, m_NodeRectStyle); 195 | } 196 | 197 | // Body + Text 198 | GUI.color = color; 199 | m_NodeRectStyle.fontSize = ComputeFontSize(rect.size, text); 200 | GUI.Box(rect, text, m_NodeRectStyle); 201 | 202 | GUI.color = originalColor; 203 | } 204 | 205 | private void DrawLegend(GraphSettings graphSettings, Rect legendArea) 206 | { 207 | EditorGUI.DrawRect(legendArea, s_LegendBackground); 208 | 209 | // Add a border around legend area 210 | legendArea.x += s_BorderSize; 211 | legendArea.width -= s_BorderSize * 2; 212 | legendArea.y += s_BorderSize; 213 | legendArea.height -= s_BorderSize * 2; 214 | 215 | GUILayout.BeginArea(legendArea); 216 | GUILayout.BeginVertical(); 217 | 218 | if (graphSettings.showInspector) 219 | { 220 | GUILayout.Label("Inspector", m_SubTitleStyle); 221 | 222 | if (m_SelectedNode != null) 223 | { 224 | using (var scrollView = new EditorGUILayout.ScrollViewScope(m_ScrollPos)) 225 | { 226 | m_ScrollPos = scrollView.scrollPosition; 227 | GUILayout.Label(m_SelectedNode.ToString(), m_InspectorStyle); 228 | } 229 | } 230 | else 231 | { 232 | GUILayout.Label("Click on a node\nto display its details."); 233 | } 234 | } 235 | 236 | GUILayout.FlexibleSpace(); 237 | 238 | if (graphSettings.showLegend) 239 | { 240 | GUILayout.Label("Legend", m_SubTitleStyle); 241 | 242 | foreach (var pair in m_LegendForType) 243 | { 244 | DrawLegendEntry(pair.Value.color, pair.Value.label, false); 245 | } 246 | 247 | DrawLegendEntry(Color.gray, "Playing", true); 248 | 249 | GUILayout.Space(20); 250 | 251 | GUILayout.Label("Edge weight", m_SubTitleStyle); 252 | GUILayout.BeginHorizontal(); 253 | GUILayout.Label("0"); 254 | GUILayout.FlexibleSpace(); 255 | GUILayout.Label("1"); 256 | GUILayout.EndHorizontal(); 257 | 258 | DrawEdgeWeightColorBar(legendArea.width); 259 | 260 | GUILayout.Space(20); 261 | } 262 | 263 | GUILayout.EndVertical(); 264 | GUILayout.EndArea(); 265 | } 266 | 267 | private void DrawLegendEntry(Color color, string label, bool active) 268 | { 269 | GUILayout.Space(5); 270 | GUILayout.BeginHorizontal(GUILayout.Height(20)); 271 | 272 | Rect legendIconRect = GUILayoutUtility.GetRect(1, 1, GUILayout.Width(20), GUILayout.Height(20)); 273 | DrawRect(legendIconRect, color, string.Empty, active); 274 | 275 | GUILayout.Label(label, m_LegendLabelStyle); 276 | 277 | GUILayout.EndHorizontal(); 278 | } 279 | 280 | private void DrawEdgeWeightColorBar(float width) 281 | { 282 | const int nbLevels = 64; 283 | 284 | if (m_ColorBar == null) 285 | { 286 | m_ColorBar = new Texture2D(nbLevels, 1) 287 | { 288 | wrapMode = TextureWrapMode.Clamp 289 | }; 290 | 291 | var cols = m_ColorBar.GetPixels(); 292 | for (int x = 0; x < nbLevels; x++) 293 | { 294 | Color c = Color.Lerp(s_EdgeColorMin, s_EdgeColorMax, (float)x / nbLevels); 295 | cols[x] = c; 296 | } 297 | 298 | m_ColorBar.SetPixels(cols); 299 | m_ColorBar.Apply(false); 300 | } 301 | 302 | const int colorbarHeight = 20; 303 | GUI.DrawTexture(GUILayoutUtility.GetRect(width, colorbarHeight), m_ColorBar); 304 | } 305 | 306 | // Draw the graph and returns the selected Node if there's any. 307 | private void DrawGraph(IGraphLayout graphLayout, Rect drawingArea, GraphSettings graphSettings) 308 | { 309 | // add border, except on right-hand side where the legend will provide necessary padding 310 | drawingArea = new Rect(drawingArea.x + s_BorderSize, 311 | drawingArea.y + s_BorderSize, 312 | drawingArea.width - s_BorderSize * 2, 313 | drawingArea.height - s_BorderSize * 2); 314 | 315 | var b = new Bounds(Vector3.zero, Vector3.zero); 316 | foreach (Vertex v in graphLayout.vertices) 317 | { 318 | b.Encapsulate(new Vector3(v.position.x, v.position.y, 0.0f)); 319 | } 320 | 321 | // Increase b by maximum node size (since b is measured between node centers) 322 | b.Expand(new Vector3(graphSettings.maximumNormalizedNodeSize, graphSettings.maximumNormalizedNodeSize, 0)); 323 | 324 | var scale = new Vector2(drawingArea.width / b.size.x, drawingArea.height / b.size.y); 325 | var offset = new Vector2(-b.min.x, -b.min.y); 326 | 327 | Vector2 nodeSize = ComputeNodeSize(scale, graphSettings); 328 | 329 | GUI.BeginGroup(drawingArea); 330 | 331 | foreach (var e in graphLayout.edges) 332 | { 333 | Vector2 v0 = ScaleVertex(e.source.position, offset, scale); 334 | Vector2 v1 = ScaleVertex(e.destination.position, offset, scale); 335 | Node node = e.source.node; 336 | 337 | if (graphLayout.leftToRight) 338 | DrawEdge(v1, v0, node.weight); 339 | else 340 | DrawEdge(v0, v1, node.weight); 341 | } 342 | 343 | Event currentEvent = Event.current; 344 | 345 | bool oldSelectionFound = false; 346 | Node newSelectedNode = null; 347 | 348 | foreach (Vertex v in graphLayout.vertices) 349 | { 350 | Vector2 nodeCenter = ScaleVertex(v.position, offset, scale) - nodeSize / 2; 351 | var nodeRect = new Rect(nodeCenter.x, nodeCenter.y, nodeSize.x, nodeSize.y); 352 | 353 | bool clicked = false; 354 | if (currentEvent.type == EventType.MouseUp && currentEvent.button == 0) 355 | { 356 | Vector2 mousePos = currentEvent.mousePosition; 357 | if (nodeRect.Contains(mousePos)) 358 | { 359 | clicked = true; 360 | currentEvent.Use(); 361 | } 362 | } 363 | 364 | bool currentSelection = (m_SelectedNode != null) 365 | && v.node.content.Equals(m_SelectedNode.content); // Make sure to use Equals() and not == to call any overriden comparison operator in the content type. 366 | 367 | DrawNode(nodeRect, v.node, currentSelection || clicked); 368 | 369 | if (currentSelection) 370 | { 371 | // Previous selection still there. 372 | oldSelectionFound = true; 373 | } 374 | else if (clicked) 375 | { 376 | // Just Selected a new node. 377 | newSelectedNode = v.node; 378 | } 379 | } 380 | 381 | if (newSelectedNode != null) 382 | { 383 | m_SelectedNode = newSelectedNode; 384 | 385 | if (nodeClicked != null) 386 | nodeClicked(m_SelectedNode); 387 | } 388 | else if (!oldSelectionFound) 389 | { 390 | m_SelectedNode = null; 391 | } 392 | 393 | GUI.EndGroup(); 394 | } 395 | 396 | // Apply node constraints to node size 397 | private static Vector2 ComputeNodeSize(Vector2 scale, GraphSettings graphSettings) 398 | { 399 | var extraTickness = (s_SelectedNodeThickness + s_ActiveNodeThickness) * 2.0f; 400 | var nodeSize = new Vector2(graphSettings.maximumNormalizedNodeSize * scale.x - extraTickness, 401 | graphSettings.maximumNormalizedNodeSize * scale.y - extraTickness); 402 | 403 | // Adjust aspect ratio after scaling 404 | float currentAspectRatio = nodeSize.x / nodeSize.y; 405 | 406 | if (currentAspectRatio > graphSettings.aspectRatio) 407 | { 408 | // Shrink x dimension 409 | nodeSize.x = nodeSize.y * graphSettings.aspectRatio; 410 | } 411 | else 412 | { 413 | // Shrink y dimension 414 | nodeSize.y = nodeSize.x / graphSettings.aspectRatio; 415 | } 416 | 417 | // If node size is still too big, scale down 418 | if (nodeSize.x > graphSettings.maximumNodeSizeInPixels) 419 | { 420 | nodeSize *= graphSettings.maximumNodeSizeInPixels / nodeSize.x; 421 | } 422 | 423 | if (nodeSize.y > graphSettings.maximumNodeSizeInPixels) 424 | { 425 | nodeSize *= graphSettings.maximumNodeSizeInPixels / nodeSize.y; 426 | } 427 | 428 | return nodeSize; 429 | } 430 | 431 | private static int ComputeFontSize(Vector2 nodeSize, string text) 432 | { 433 | if (string.IsNullOrEmpty(text)) 434 | return s_NodeMaxFontSize; 435 | 436 | string[] words = text.Split('\n'); 437 | int nbLignes = words.Length; 438 | int longuestWord = words.Max(s => s.Length); 439 | 440 | // Approximate the text rectangle size using magic values. 441 | int width = longuestWord * (int)(0.8f * s_NodeMaxFontSize); 442 | int height = nbLignes * (int)(1.5f * s_NodeMaxFontSize); 443 | 444 | float factor = Math.Min(nodeSize.x / width, nodeSize.y / height); 445 | 446 | factor = Mathf.Clamp01(factor); 447 | 448 | return Mathf.CeilToInt(s_NodeMaxFontSize * factor); 449 | } 450 | 451 | // Convert vertex position from normalized layout to render rect 452 | private static Vector2 ScaleVertex(Vector2 v, Vector2 offset, Vector2 scaleFactor) 453 | { 454 | return new Vector2((v.x + offset.x) * scaleFactor.x, (v.y + offset.y) * scaleFactor.y); 455 | } 456 | 457 | // Draw a node an return true if it has been clicked 458 | private void DrawNode(Rect nodeRect, Node node, bool selected) 459 | { 460 | string nodeType = node.GetContentTypeName(); 461 | NodeTypeLegend nodeTypeLegend = m_LegendForType[nodeType]; 462 | string formattedLabel = Regex.Replace(nodeTypeLegend.label, "((? GetChildren(Node node) 33 | { 34 | // Children are the Playable Inputs. 35 | if(node is PlayableNode) 36 | return GetInputsFromPlayableNode((Playable)node.content); 37 | if(node is PlayableOutputNode) 38 | return GetInputsFromPlayableOutputNode((PlayableOutput)node.content); 39 | 40 | return new List(); 41 | } 42 | 43 | private List GetInputsFromPlayableNode(Playable h) 44 | { 45 | var inputs = new List(); 46 | if (h.IsValid()) 47 | { 48 | for (int port = 0; port < h.GetInputCount(); ++port) 49 | { 50 | Playable playable = h.GetInput(port); 51 | if (playable.IsValid()) 52 | { 53 | float weight = h.GetInputWeight(port); 54 | Node node = CreateNodeFromPlayable(playable, weight); 55 | inputs.Add(node); 56 | } 57 | } 58 | } 59 | return inputs; 60 | } 61 | 62 | private List GetInputsFromPlayableOutputNode(PlayableOutput h) 63 | { 64 | var inputs = new List(); 65 | if (h.IsOutputValid()) 66 | { 67 | Playable playable = h.GetSourcePlayable(); 68 | if (playable.IsValid()) 69 | { 70 | Node node = CreateNodeFromPlayable(playable, 1); 71 | inputs.Add(node); 72 | } 73 | } 74 | return inputs; 75 | } 76 | 77 | private PlayableNode CreateNodeFromPlayable(Playable h, float weight) 78 | { 79 | var type = h.GetPlayableType(); 80 | if (type == typeof(AnimationClipPlayable)) 81 | return new AnimationClipPlayableNode(h, weight); 82 | if (type == typeof(AnimationLayerMixerPlayable)) 83 | return new AnimationLayerMixerPlayableNode(h, weight); 84 | return new PlayableNode(h, weight); 85 | } 86 | 87 | private PlayableOutputNode CreateNodeFromPlayableOutput(PlayableOutput h) 88 | { 89 | return new PlayableOutputNode(h); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Editor/PlayableGraphVisualizer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6916b006634cd21469ca63e6425c528b 3 | timeCreated: 1478627468 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/PlayableGraphVisualizerWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | using UnityEngine.Playables; 5 | using UnityEditor; 6 | 7 | namespace GraphVisualizer 8 | { 9 | public class PlayableGraphVisualizerWindow : EditorWindow, IHasCustomMenu 10 | { 11 | private IGraphRenderer m_Renderer; 12 | private IGraphLayout m_Layout; 13 | 14 | private List m_Graphs; 15 | private PlayableGraph m_CurrentGraph; 16 | private GraphSettings m_GraphSettings; 17 | 18 | #region Configuration 19 | 20 | private static readonly float s_ToolbarHeight = 17f; 21 | private static readonly float s_DefaultMaximumNormalizedNodeSize = 0.8f; 22 | private static readonly float s_DefaultMaximumNodeSizeInPixels = 100.0f; 23 | private static readonly float s_DefaultAspectRatio = 1.5f; 24 | 25 | #endregion 26 | 27 | private PlayableGraphVisualizerWindow() 28 | { 29 | m_GraphSettings.maximumNormalizedNodeSize = s_DefaultMaximumNormalizedNodeSize; 30 | m_GraphSettings.maximumNodeSizeInPixels = s_DefaultMaximumNodeSizeInPixels; 31 | m_GraphSettings.aspectRatio = s_DefaultAspectRatio; 32 | m_GraphSettings.showInspector = true; 33 | m_GraphSettings.showLegend = true; 34 | } 35 | 36 | [MenuItem("Window/Analysis/PlayableGraph Visualizer")] 37 | public static void ShowWindow() 38 | { 39 | GetWindow("PlayableGraph Visualizer"); 40 | } 41 | 42 | private PlayableGraph GetSelectedGraphInToolBar(List graphs, PlayableGraph currentGraph) 43 | { 44 | EditorGUILayout.BeginHorizontal(EditorStyles.toolbar, GUILayout.Width(position.width)); 45 | 46 | List options = new List(graphs.Count); 47 | foreach (var graph in graphs) 48 | { 49 | string name = graph.GetEditorName(); 50 | options.Add(name.Length != 0 ? name : "[Unnamed]"); 51 | } 52 | 53 | int currentSelection = graphs.IndexOf(currentGraph); 54 | int newSelection = EditorGUILayout.Popup(currentSelection != -1 ? currentSelection : 0, options.ToArray(), GUILayout.Width(200)); 55 | 56 | PlayableGraph selectedGraph = new PlayableGraph(); 57 | if (newSelection != -1) 58 | selectedGraph = graphs[newSelection]; 59 | 60 | GUILayout.FlexibleSpace(); 61 | EditorGUILayout.EndHorizontal(); 62 | 63 | return selectedGraph; 64 | } 65 | 66 | private static void ShowMessage(string msg) 67 | { 68 | GUILayout.BeginVertical(); 69 | GUILayout.FlexibleSpace(); 70 | 71 | GUILayout.BeginHorizontal(); 72 | GUILayout.FlexibleSpace(); 73 | 74 | GUILayout.Label(msg); 75 | 76 | GUILayout.FlexibleSpace(); 77 | GUILayout.EndHorizontal(); 78 | 79 | GUILayout.FlexibleSpace(); 80 | GUILayout.EndVertical(); 81 | } 82 | 83 | void Update() 84 | { 85 | // If in Play mode, refresh the graph each update. 86 | if (EditorApplication.isPlaying) 87 | Repaint(); 88 | } 89 | 90 | void OnInspectorUpdate() 91 | { 92 | // If not in Play mode, refresh the graph less frequently. 93 | if (!EditorApplication.isPlaying) 94 | Repaint(); 95 | } 96 | 97 | void OnEnable() 98 | { 99 | m_Graphs = new List(UnityEditor.Playables.Utility.GetAllGraphs()); 100 | 101 | UnityEditor.Playables.Utility.graphCreated += OnGraphCreated; 102 | UnityEditor.Playables.Utility.destroyingGraph += OnDestroyingGraph; 103 | } 104 | 105 | void OnGraphCreated(PlayableGraph graph) 106 | { 107 | if (!m_Graphs.Contains(graph)) 108 | m_Graphs.Add(graph); 109 | } 110 | 111 | void OnDestroyingGraph(PlayableGraph graph) 112 | { 113 | m_Graphs.Remove(graph); 114 | } 115 | 116 | void OnDisable() 117 | { 118 | UnityEditor.Playables.Utility.graphCreated -= OnGraphCreated; 119 | UnityEditor.Playables.Utility.destroyingGraph -= OnDestroyingGraph; 120 | } 121 | 122 | void OnGUI() 123 | { 124 | // Early out if there is no graphs. 125 | var selectedGraphs = GetGraphList(); 126 | if (selectedGraphs.Count == 0) 127 | { 128 | ShowMessage("No PlayableGraph in the scene"); 129 | return; 130 | } 131 | 132 | GUILayout.BeginVertical(); 133 | m_CurrentGraph = GetSelectedGraphInToolBar(selectedGraphs, m_CurrentGraph); 134 | GUILayout.EndVertical(); 135 | 136 | if (!m_CurrentGraph.IsValid()) 137 | { 138 | ShowMessage("Selected PlayableGraph is invalid"); 139 | return; 140 | } 141 | 142 | var graph = new PlayableGraphVisualizer(m_CurrentGraph); 143 | graph.Refresh(); 144 | 145 | if (graph.IsEmpty()) 146 | { 147 | ShowMessage("Selected PlayableGraph is empty"); 148 | return; 149 | } 150 | 151 | if (m_Layout == null) 152 | m_Layout = new ReingoldTilford(); 153 | 154 | m_Layout.CalculateLayout(graph); 155 | 156 | var graphRect = new Rect(0, s_ToolbarHeight, position.width, position.height - s_ToolbarHeight); 157 | 158 | if (m_Renderer == null) 159 | m_Renderer = new DefaultGraphRenderer(); 160 | 161 | m_Renderer.Draw(m_Layout, graphRect, m_GraphSettings); 162 | } 163 | 164 | private List GetGraphList() 165 | { 166 | var selectedGraphs = new List(); 167 | foreach (var clientGraph in GraphVisualizerClient.GetGraphs()) 168 | { 169 | if (clientGraph.IsValid()) 170 | selectedGraphs.Add(clientGraph); 171 | } 172 | 173 | if (selectedGraphs.Count == 0) 174 | selectedGraphs = m_Graphs.ToList(); 175 | 176 | return selectedGraphs; 177 | } 178 | 179 | #region Custom_Menu 180 | 181 | public virtual void AddItemsToMenu(GenericMenu menu) 182 | { 183 | menu.AddItem(new GUIContent("Inspector"), m_GraphSettings.showInspector, ToggleInspector); 184 | menu.AddItem(new GUIContent("Legend"), m_GraphSettings.showLegend, ToggleLegend); 185 | } 186 | 187 | void ToggleInspector() 188 | { 189 | m_GraphSettings.showInspector = !m_GraphSettings.showInspector; 190 | } 191 | 192 | void ToggleLegend() 193 | { 194 | m_GraphSettings.showLegend = !m_GraphSettings.showLegend; 195 | } 196 | 197 | #endregion 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Editor/PlayableGraphVisualizerWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0d5877bb4ffe3464b96f021ac399d1a9 3 | timeCreated: 1429717867 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/PlayableNodes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6de1d78f98164fba9586bb01c76f0633 3 | timeCreated: 1532111318 -------------------------------------------------------------------------------- /Editor/PlayableNodes/AnimationClipPlayableNode.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using UnityEngine.Animations; 3 | using UnityEngine.Playables; 4 | 5 | namespace GraphVisualizer 6 | { 7 | public class AnimationClipPlayableNode : PlayableNode 8 | { 9 | public AnimationClipPlayableNode(Playable content, float weight = 1.0f) 10 | : base(content, weight) 11 | { 12 | } 13 | 14 | public override string ToString() 15 | { 16 | var sb = new StringBuilder(); 17 | 18 | sb.AppendLine(base.ToString()); 19 | 20 | var p = (Playable) content; 21 | if (p.IsValid()) 22 | { 23 | var acp = (AnimationClipPlayable) p; 24 | var clip = acp.GetAnimationClip(); 25 | sb.AppendLine(InfoString("Clip", clip ? clip.name : "(none)")); 26 | if (clip) 27 | { 28 | sb.AppendLine(InfoString("ClipLength", clip.length)); 29 | } 30 | sb.AppendLine(InfoString("ApplyFootIK", acp.GetApplyFootIK())); 31 | sb.AppendLine(InfoString("ApplyPlayableIK", acp.GetApplyPlayableIK())); 32 | } 33 | 34 | return sb.ToString(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Editor/PlayableNodes/AnimationClipPlayableNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c8a64993dff7471c82e0857c60d771f9 3 | timeCreated: 1532111483 -------------------------------------------------------------------------------- /Editor/PlayableNodes/AnimationLayerMixerPlayableNode.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using UnityEngine.Animations; 3 | using UnityEngine.Playables; 4 | 5 | namespace GraphVisualizer 6 | { 7 | public class AnimationLayerMixerPlayableNode : PlayableNode 8 | { 9 | public AnimationLayerMixerPlayableNode(Playable content, float weight = 1.0f) 10 | : base(content, weight) 11 | { 12 | } 13 | 14 | public override string ToString() 15 | { 16 | var sb = new StringBuilder(); 17 | 18 | sb.AppendLine(base.ToString()); 19 | 20 | var p = (Playable) content; 21 | if (p.IsValid()) 22 | { 23 | var almp = (AnimationLayerMixerPlayable) p; 24 | for (uint i = 0; i < almp.GetInputCount(); ++i) 25 | sb.AppendLine(InfoString(string.Format("IsLayerAdditive #{0}", i + 1), almp.IsLayerAdditive(i))); 26 | } 27 | 28 | return sb.ToString(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Editor/PlayableNodes/AnimationLayerMixerPlayableNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a7134908431664a5f93c280d29638fe6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PlayableNodes/PlayableNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using UnityEngine.Playables; 4 | 5 | namespace GraphVisualizer 6 | { 7 | public class PlayableNode : SharedPlayableNode 8 | { 9 | public PlayableNode(Playable content, float weight = 1.0f) 10 | : base(content, weight, content.GetPlayState() == PlayState.Playing) 11 | { 12 | } 13 | 14 | public override Type GetContentType() 15 | { 16 | Playable p = Playable.Null; 17 | try 18 | { 19 | p = (Playable) content; 20 | } 21 | catch 22 | { 23 | // Ignore. 24 | } 25 | 26 | return p.IsValid() ? p.GetPlayableType() : null; 27 | } 28 | 29 | public override string GetContentTypeShortName() 30 | { 31 | // Remove the extra Playable at the end of the Playable types. 32 | string shortName = base.GetContentTypeShortName(); 33 | string cleanName = RemoveFromEnd(shortName, "Playable"); 34 | return string.IsNullOrEmpty(cleanName) ? shortName : cleanName; 35 | } 36 | 37 | public override string ToString() 38 | { 39 | var sb = new StringBuilder(); 40 | 41 | sb.AppendLine(InfoString("Handle", GetContentTypeShortName())); 42 | 43 | var p = (Playable) content; 44 | sb.AppendLine(InfoString("IsValid", p.IsValid())); 45 | if (p.IsValid()) 46 | { 47 | sb.AppendLine(InfoString("IsDone", p.IsDone())); 48 | sb.AppendLine(InfoString("InputCount", p.GetInputCount())); 49 | sb.AppendLine(InfoString("OutputCount", p.GetOutputCount())); 50 | sb.AppendLine(InfoString("PlayState", p.GetPlayState())); 51 | sb.AppendLine(InfoString("Speed", p.GetSpeed())); 52 | sb.AppendLine(InfoString("Duration", p.GetDuration())); 53 | sb.AppendLine(InfoString("Time", p.GetTime())); 54 | } 55 | 56 | return sb.ToString(); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Editor/PlayableNodes/PlayableNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0e774d676f04404684f86d6345ff0db4 3 | timeCreated: 1532111427 -------------------------------------------------------------------------------- /Editor/PlayableNodes/PlayableOutputNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using UnityEngine.Playables; 4 | using UnityEditor.Playables; 5 | 6 | namespace GraphVisualizer 7 | { 8 | public class PlayableOutputNode : SharedPlayableNode 9 | { 10 | public PlayableOutputNode(PlayableOutput content) 11 | : base(content, content.GetWeight(), true) 12 | { 13 | } 14 | 15 | public override Type GetContentType() 16 | { 17 | PlayableOutput po = PlayableOutput.Null; 18 | try 19 | { 20 | po = (PlayableOutput) content; 21 | } 22 | catch 23 | { 24 | // Ignore. 25 | } 26 | 27 | return po.IsOutputValid() ? po.GetPlayableOutputType() : null; 28 | } 29 | 30 | public override string GetContentTypeShortName() 31 | { 32 | // Remove the extra Playable at the end of the Playable types. 33 | string shortName = base.GetContentTypeShortName(); 34 | string cleanName = RemoveFromEnd(shortName, "PlayableOutput") + "Output"; 35 | return string.IsNullOrEmpty(cleanName) ? shortName : cleanName; 36 | } 37 | 38 | public override string ToString() 39 | { 40 | var sb = new StringBuilder(); 41 | 42 | sb.AppendLine(InfoString("Handle", GetContentTypeShortName())); 43 | 44 | var po = (PlayableOutput) content; 45 | if (po.IsOutputValid()) 46 | { 47 | #if UNITY_2019_1_OR_NEWER 48 | sb.AppendLine(InfoString("Name", po.GetEditorName())); 49 | #endif 50 | sb.AppendLine(InfoString("IsValid", po.IsOutputValid())); 51 | sb.AppendLine(InfoString("Weight", po.GetWeight())); 52 | #if UNITY_2018_2_OR_NEWER 53 | sb.AppendLine(InfoString("SourceOutputPort", po.GetSourceOutputPort())); 54 | #else 55 | sb.AppendLine(InfoString("SourceInputPort", po.GetSourceInputPort())); 56 | #endif 57 | } 58 | 59 | return sb.ToString(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Editor/PlayableNodes/PlayableOutputNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c833ea0f9e2c4e728025d542f53eb505 3 | timeCreated: 1532111540 -------------------------------------------------------------------------------- /Editor/PlayableNodes/SharedPlayableNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GraphVisualizer 4 | { 5 | public class SharedPlayableNode : Node 6 | { 7 | public SharedPlayableNode(object content, float weight = 1.0f, bool active = false) 8 | : base(content, weight, active) 9 | { 10 | } 11 | 12 | protected static string InfoString(string key, double value) 13 | { 14 | if (Math.Abs(value) < 100000.0) 15 | return string.Format("{0}: {1:#.###}", key, value); 16 | if (value == double.MaxValue) 17 | return string.Format("{0}: +Inf", key); 18 | if (value == double.MinValue) 19 | return string.Format("{0}: -Inf", key); 20 | return string.Format("{0}: {1:E4}", key, value); 21 | } 22 | 23 | protected static string InfoString(string key, int value) 24 | { 25 | return string.Format("{0}: {1:D}", key, value); 26 | } 27 | 28 | protected static string InfoString(string key, object value) 29 | { 30 | return "" + key + ": " + (value ?? "(none)"); 31 | } 32 | 33 | protected static string RemoveFromEnd(string str, string suffix) 34 | { 35 | if (str.EndsWith(suffix)) 36 | { 37 | return str.Substring(0, str.Length - suffix.Length); 38 | } 39 | 40 | return str; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Editor/PlayableNodes/SharedPlayableNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6c8170737f3d421585799e0ea80b4549 3 | timeCreated: 1532111367 -------------------------------------------------------------------------------- /Editor/Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45ead97298b414d48a80edc5808f19d8 3 | folderAsset: yes 4 | timeCreated: 1478547578 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/Resources/Node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/graph-visualizer/cfb3a9f75dd2ef7cc4253b0439806646cdd79814/Editor/Resources/Node.png -------------------------------------------------------------------------------- /Editor/Resources/Node.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3fa46d96b130e304983151d9c178b928 3 | timeCreated: 1478701872 4 | licenseType: Pro 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 1 11 | sRGBTexture: 1 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapFadeDistanceStart: 1 16 | mipMapFadeDistanceEnd: 3 17 | bumpmap: 18 | convertToNormalMap: 0 19 | externalNormalMap: 0 20 | heightScale: 0.25 21 | normalMapFilter: 0 22 | isReadable: 0 23 | grayScaleToAlpha: 0 24 | generateCubemap: 6 25 | cubemapConvolution: 0 26 | seamlessCubemap: 0 27 | textureFormat: 1 28 | maxTextureSize: 2048 29 | textureSettings: 30 | filterMode: -1 31 | aniso: -1 32 | mipBias: -1 33 | wrapMode: -1 34 | nPOTScale: 1 35 | lightmap: 0 36 | compressionQuality: 50 37 | spriteMode: 0 38 | spriteExtrude: 1 39 | spriteMeshType: 1 40 | alignment: 0 41 | spritePivot: {x: 0.5, y: 0.5} 42 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 43 | spritePixelsToUnits: 100 44 | alphaUsage: 1 45 | alphaIsTransparency: 0 46 | spriteTessellationDetail: -1 47 | textureType: 0 48 | textureShape: 1 49 | maxTextureSizeSet: 0 50 | compressionQualitySet: 0 51 | textureFormatSet: 0 52 | platformSettings: 53 | - buildTarget: DefaultTexturePlatform 54 | maxTextureSize: 2048 55 | textureFormat: -1 56 | textureCompression: 1 57 | compressionQuality: 50 58 | crunchedCompression: 0 59 | allowsAlphaSplitting: 0 60 | overridden: 0 61 | spriteSheet: 62 | serializedVersion: 2 63 | sprites: [] 64 | outline: [] 65 | spritePackingTag: 66 | userData: 67 | assetBundleName: 68 | assetBundleVariant: 69 | -------------------------------------------------------------------------------- /Editor/Unity.PlayableGraphVisualizer.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PlayableGraphVisualizer.Editor", 3 | "references": [ 4 | "Unity.PlayableGraphVisualizer" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [] 10 | } 11 | -------------------------------------------------------------------------------- /Editor/Unity.PlayableGraphVisualizer.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2702b94ed25b25fdda2226e5c974e9e8 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | PlayableGraph Visualizer copyright © 2019 Unity Technologies ApS 2 | 3 | Licensed under the Unity Companion License for Unity-dependent projects -- see [Unity Companion License](http://www.unity3d.com/legal/licenses/Unity_Companion_License). 4 | 5 | Unless expressly provided otherwise, the Software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details on these and other terms and conditions. 6 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 62812bfcdb70b4eb5a530f88c74eed08 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayableGraph Visualizer 2 | 3 | The PlayableGraph Visualizer is a tool that displays the PlayableGraphs in the scene. 4 | It can be used in both Play and Edit mode and will always reflect the current state of the graph. 5 | Playable nodes are represented by colored nodes, varying according to their type. Connections color intensity indicates its weight. 6 | 7 | See the [Documentation](Documentation~/playablegraph-visualizer.md) for the installation and the usage. 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4d3b312137004957a58901fa628c2cb 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7f28890d30869494fa92713ab81b8a0d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/GraphVisualizerClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine.Playables; 3 | 4 | // Bridge between runtime and editor code: the graph created in runtime code can call GraphVisualizerClient.Show(...) 5 | // and the EditorWindow will register itself with the client to display any available graph. 6 | public class GraphVisualizerClient 7 | { 8 | private static GraphVisualizerClient s_Instance; 9 | private List m_Graphs = new List(); 10 | 11 | public static GraphVisualizerClient instance 12 | { 13 | get 14 | { 15 | if (s_Instance == null) 16 | s_Instance = new GraphVisualizerClient(); 17 | return s_Instance; 18 | } 19 | } 20 | 21 | ~GraphVisualizerClient() 22 | { 23 | m_Graphs.Clear(); 24 | } 25 | 26 | public static void Show(PlayableGraph graph) 27 | { 28 | if (!instance.m_Graphs.Contains(graph)) 29 | { 30 | instance.m_Graphs.Add(graph); 31 | } 32 | } 33 | 34 | public static void Hide(PlayableGraph graph) 35 | { 36 | if (instance.m_Graphs.Contains(graph)) 37 | { 38 | instance.m_Graphs.Remove(graph); 39 | } 40 | } 41 | 42 | public static void ClearGraphs() 43 | { 44 | instance.m_Graphs.Clear(); 45 | } 46 | 47 | public static IEnumerable GetGraphs() 48 | { 49 | return instance.m_Graphs; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Runtime/GraphVisualizerClient.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9114e458dd675a840b81a836551082c4 3 | timeCreated: 1432743245 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Runtime/Unity.PlayableGraphVisualizer.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PlayableGraphVisualizer", 3 | "references": [], 4 | "includePlatforms": [], 5 | "excludePlatforms": [] 6 | } 7 | -------------------------------------------------------------------------------- /Runtime/Unity.PlayableGraphVisualizer.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d8e7b33ba1e4a89fc8a64b6b1e97a6ac 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db91a525414eed845a7f8efa1070a218 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4c186e2d570110b48de95a1c5fecc03 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Runtime/GraphVisualizerClientTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Linq; 3 | using UnityEngine.Playables; 4 | 5 | class GraphVisualizerClientTest 6 | { 7 | [TearDown] 8 | public void TearDown() 9 | { 10 | // Clear graphs between tests, otherwise graphs are still referenced across tests. 11 | GraphVisualizerClient.ClearGraphs(); 12 | } 13 | 14 | private static PlayableGraph CreatePlayableGraph(string name) 15 | { 16 | var graph = PlayableGraph.Create(name); 17 | ScriptPlayableOutput.Create(graph, "output"); 18 | return graph; 19 | } 20 | 21 | [Test] 22 | public void CanShowGraph() 23 | { 24 | var graph1 = CreatePlayableGraph("test1"); 25 | var graph2 = CreatePlayableGraph("test2"); 26 | 27 | GraphVisualizerClient.Show(graph1); 28 | var graphs = GraphVisualizerClient.GetGraphs().ToArray(); 29 | 30 | Assert.That(graphs.Length, Is.EqualTo(1)); 31 | Assert.That(graphs[0].GetEditorName(), Is.EqualTo(graph1.GetEditorName())); 32 | 33 | GraphVisualizerClient.Show(graph2); 34 | graphs = GraphVisualizerClient.GetGraphs().ToArray(); 35 | 36 | Assert.That(graphs.Length, Is.EqualTo(2)); 37 | Assert.That(graphs[0].GetEditorName(), Is.EqualTo(graph1.GetEditorName())); 38 | Assert.That(graphs[1].GetEditorName(), Is.EqualTo(graph2.GetEditorName())); 39 | 40 | graph1.Destroy(); 41 | graph2.Destroy(); 42 | } 43 | 44 | [Test] 45 | public void CannotShowSameGraphTwice() 46 | { 47 | var graph1 = CreatePlayableGraph("test1"); 48 | 49 | GraphVisualizerClient.Show(graph1); 50 | var graphs = GraphVisualizerClient.GetGraphs().ToArray(); 51 | 52 | Assert.That(graphs.Length, Is.EqualTo(1)); 53 | 54 | graph1.Destroy(); 55 | } 56 | 57 | [Test] 58 | public void CanHideGraph() 59 | { 60 | var graph1 = CreatePlayableGraph("test1"); 61 | var graph2 = CreatePlayableGraph("test2"); 62 | 63 | GraphVisualizerClient.Show(graph1); 64 | GraphVisualizerClient.Show(graph2); 65 | var graphs = GraphVisualizerClient.GetGraphs().ToArray(); 66 | 67 | Assert.That(graphs.Length, Is.EqualTo(2)); 68 | Assert.That(graphs[0].GetEditorName(), Is.EqualTo(graph1.GetEditorName())); 69 | Assert.That(graphs[1].GetEditorName(), Is.EqualTo(graph2.GetEditorName())); 70 | 71 | GraphVisualizerClient.Hide(graph1); 72 | graphs = GraphVisualizerClient.GetGraphs().ToArray(); 73 | 74 | Assert.That(graphs.Length, Is.EqualTo(1)); 75 | Assert.That(graphs[0].GetEditorName(), Is.EqualTo(graph2.GetEditorName())); 76 | 77 | graph1.Destroy(); 78 | graph2.Destroy(); 79 | } 80 | 81 | [Test] 82 | public void CanClearGraphs() 83 | { 84 | var graph1 = CreatePlayableGraph("test1"); 85 | var graph2 = CreatePlayableGraph("test2"); 86 | 87 | GraphVisualizerClient.Show(graph1); 88 | GraphVisualizerClient.Show(graph2); 89 | var graphs = GraphVisualizerClient.GetGraphs().ToArray(); 90 | 91 | Assert.That(graphs.Length, Is.EqualTo(2)); 92 | 93 | GraphVisualizerClient.ClearGraphs(); 94 | graphs = GraphVisualizerClient.GetGraphs().ToArray(); 95 | 96 | Assert.That(graphs.Length, Is.EqualTo(0)); 97 | 98 | graph1.Destroy(); 99 | graph2.Destroy(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/Runtime/GraphVisualizerClientTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 76f082e5010cb366e9639d3b6f99834e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Runtime/Unity.PlayableGraphVisualizer.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PlayableGraphVisualizer.Tests", 3 | "references": [ 4 | "Unity.PlayableGraphVisualizer" 5 | ], 6 | "optionalUnityReferences": [ 7 | "TestAssemblies" 8 | ], 9 | "includePlatforms": [], 10 | "excludePlatforms": [] 11 | } 12 | -------------------------------------------------------------------------------- /Tests/Runtime/Unity.PlayableGraphVisualizer.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 24b367a1e8631110f924a62b35bba83d 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.unity.playablegraph-visualizer", 3 | "displayName": "PlayableGraph Visualizer", 4 | "version": "0.2.1-preview.3", 5 | "unity": "2018.1", 6 | "description": "The PlayableGraph Visualizer is a tool that displays the PlayableGraphs in the scene. It can be used in both Play and Edit mode and will always reflect the current state of the graph. Playable nodes are represented by colored nodes, varying according to their type. Connections color intensity indicates its weight.", 7 | "licence": "MIT", 8 | "author": { 9 | "name": "Unity" 10 | }, 11 | "keywords": [ 12 | "playable", 13 | "graph", 14 | "playablegraph", 15 | "animation", 16 | "timeline" 17 | ], 18 | "dependencies": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 739bdc2aae9cc990f962d6c12c1f1558 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------