From fb1825179ae46331067a340782d2d95b8dcc6cab Mon Sep 17 00:00:00 2001 From: Ash Blue Date: Mon, 15 Apr 2024 19:46:58 -0700 Subject: [PATCH] feat: added experimental nested graph playback restoration --- .../DialogueController/DialogueController.cs | 119 ++++++++++++++++-- .../Runtime/DialoguePlayback.cs | 26 ++++ .../Runtime/Graphs/GraphRuntime.cs | 6 + .../Runtime/Graphs/IGraph.cs | 3 + .../com.fluid.dialogue/Runtime/Nodes/INode.cs | 3 + .../Runtime/Nodes/PlayGraph/NodePlayGraph.cs | 2 + .../com.unity.services.core/Settings.json | 0 README.md | 33 +++++ 8 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 ProjectSettings/Packages/com.unity.services.core/Settings.json diff --git a/Assets/com.fluid.dialogue/Runtime/DialogueController/DialogueController.cs b/Assets/com.fluid.dialogue/Runtime/DialogueController/DialogueController.cs index a8f17e3..a08c798 100644 --- a/Assets/com.fluid.dialogue/Runtime/DialogueController/DialogueController.cs +++ b/Assets/com.fluid.dialogue/Runtime/DialogueController/DialogueController.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using CleverCrow.Fluid.Databases; using CleverCrow.Fluid.Dialogues.Choices; using CleverCrow.Fluid.Dialogues.Graphs; using CleverCrow.Fluid.Dialogues.Nodes; +using CleverCrow.Fluid.Dialogues.Nodes.PlayGraph; using UnityEngine; namespace CleverCrow.Fluid.Dialogues { @@ -16,13 +18,29 @@ public interface IDialogueController { public class DialogueController : IDialogueController { private readonly Stack _activeDialogue = new Stack(); + private readonly List _parentHierarchy = new(); [Obsolete("Use LocalDatabaseExtended instead")] public IDatabaseInstance LocalDatabase { get; } public IDatabaseInstanceExtended LocalDatabaseExtended { get; } public IDialogueEvents Events { get; } = new DialogueEvents(); + + /// + /// Playback runtime that corresponds to the current graph definition (may be nested) + /// public IDialoguePlayback ActiveDialogue => _activeDialogue.Count > 0 ? _activeDialogue.Peek() : null; + /// + /// Return the root of the currently playing dialogue + /// + public IGraphData RootGraph => _activeDialogue.Count > 0 ? _activeDialogue.ToArray().Last().Graph.Data : null; + + /// + /// Keeps track of the IDs of all nested graph node containers currently playing in order. Important for restoring nested + /// dialogue graphs that are currently playing from serialized data. + /// + public IReadOnlyList ParentHierarchy => _parentHierarchy; + [Obsolete("Use DatabaseInstanceExtended instead. Old databases do not support GameObjects")] public DialogueController (IDatabaseInstance localDatabase) { LocalDatabase = localDatabase; @@ -37,6 +55,41 @@ public DialogueController (IDatabaseInstanceExtended localDatabase) { } public void Play (IDialoguePlayback playback, IGameObjectOverride[] gameObjectOverrides = null) { + PlaybackSetup(playback, gameObjectOverrides); + playback.Play(); + } + + // @NOTE gameObjectOverrides will be deprecated. It can easily be replaced with a send message system that looks up the target GameObject by string. This is a lot to maintain and messy + public void Play (IGraphData graph, IGameObjectOverride[] gameObjectOverrides = null) { + var runtime = new GraphRuntime(this, graph); + Play(new DialoguePlayback(runtime, this, new DialogueEvents()), gameObjectOverrides); + } + + // @TODO Needs some tests + public void Play (IGraphData graph, IReadOnlyList parentHierarchy, string nodeId) { + var runtime = new GraphRuntime(this, graph); + + // Setup the root graph with all proper hooks + var origin = new DialoguePlayback(runtime, this, new DialogueEvents()); + PlaybackSetup(origin); + + var playback = origin; + foreach (var id in parentHierarchy) { + // Set and get the pointer + playback.SetPointer(id); + if (playback.Pointer is not NodePlayGraph node) { + throw new InvalidOperationException($"Parent hierarchy ID {id} does not contain a nested graph"); + } + + // Add the child playback runtime + AddChild(node.Graph as DialogueGraph); + playback = _activeDialogue.Peek() as DialoguePlayback; + } + + playback.SetPointerAndPlay(nodeId); + } + + void PlaybackSetup (IDialoguePlayback playback, IGameObjectOverride[] gameObjectOverrides = null) { SetupDatabases(gameObjectOverrides); Stop(); @@ -49,12 +102,6 @@ public void Play (IDialoguePlayback playback, IGameObjectOverride[] gameObjectOv playback.Events.End.AddListener(TriggerEnd); _activeDialogue.Push(playback); - playback.Play(); - } - - public void Play (IGraphData graph, IGameObjectOverride[] gameObjectOverrides = null) { - var runtime = new GraphRuntime(this, graph); - Play(new DialoguePlayback(runtime, this, new DialogueEvents()), gameObjectOverrides); } private void SetupDatabases (IGameObjectOverride[] gameObjectOverrides) { @@ -72,6 +119,26 @@ private void SetupDatabases (IGameObjectOverride[] gameObjectOverrides) { } public void PlayChild (IDialoguePlayback playback) { + SetupChild(playback); + playback.Play(); + } + + public void PlayChild (IGraphData graph) { + // @TODO Test this + _parentHierarchy.Add(_activeDialogue.Peek().Pointer.UniqueId); + + var runtime = new GraphRuntime(this, graph); + PlayChild(new DialoguePlayback(runtime, this, new DialogueEvents())); + } + + void AddChild (IGraphData graph) { + _parentHierarchy.Add(_activeDialogue.Peek().Pointer.UniqueId); + + var runtime = new GraphRuntime(this, graph); + SetupChild(new DialoguePlayback(runtime, this, new DialogueEvents())); + } + + void SetupChild (IDialoguePlayback playback) { if (ActiveDialogue == null) { throw new InvalidOperationException("Cannot trigger child dialogue, nothing is playing"); } @@ -87,12 +154,6 @@ public void PlayChild (IDialoguePlayback playback) { playback.Events.NodeEnter.AddListener(TriggerEnterNode); _activeDialogue.Push(playback); - playback.Play(); - } - - public void PlayChild (IGraphData graph) { - var runtime = new GraphRuntime(this, graph); - PlayChild(new DialoguePlayback(runtime, this, new DialogueEvents())); } private void TriggerBegin () { @@ -138,6 +199,40 @@ public void Stop () { } _activeDialogue.Clear(); + // @TODO Test this + _parentHierarchy.Clear(); + } + + // @TODO Write a test for this if possible, might not be easy + /// + /// Verifies a nested graph can be played. Not the most runtime friendly operation so use sparingly + /// + public bool CanPlay (IGraphData graph, List parentHierarchy, string nodeId) { + // Use the parent hierarchy to find the nested graph + var nestedGraph = graph; + foreach (var id in parentHierarchy) { + var node = GetNode(nestedGraph, id); + if (node == null) return false; + + if (node is NodePlayGraphData playGraph) { + nestedGraph = playGraph.dialogueGraph; + } else { + return false; + } + } + + // Check if the node exists in the nested graph + return GetNode(nestedGraph, nodeId) != null; + } + + INodeData GetNode (IGraphData graph, string id) { + foreach (var node in graph.Nodes) { + if (node.UniqueId == id) { + return node; + } + } + + return null; } } } diff --git a/Assets/com.fluid.dialogue/Runtime/DialoguePlayback.cs b/Assets/com.fluid.dialogue/Runtime/DialoguePlayback.cs index 392cc71..9f43977 100644 --- a/Assets/com.fluid.dialogue/Runtime/DialoguePlayback.cs +++ b/Assets/com.fluid.dialogue/Runtime/DialoguePlayback.cs @@ -2,12 +2,20 @@ using CleverCrow.Fluid.Dialogues.Actions; using CleverCrow.Fluid.Dialogues.Graphs; using CleverCrow.Fluid.Dialogues.Nodes; +using UnityEngine; namespace CleverCrow.Fluid.Dialogues { public interface IDialoguePlayback { IDialogueEvents Events { get; } IDialogueController ParentCtrl { get; } + IGraph Graph { get; } + + /// + /// Current node data being used for the runtime + /// + INode Pointer { get; } + void Next (); void Play (); void Tick (); @@ -23,6 +31,7 @@ public class DialoguePlayback : IDialoguePlayback { public IDialogueEvents Events { get;} public IDialogueController ParentCtrl { get; } public INode Pointer { get; private set; } + public IGraph Graph => _graph; public DialoguePlayback (IGraph graph, IDialogueController ctrl, IDialogueEvents events) { _graph = graph; @@ -119,5 +128,22 @@ public void SelectChoice (int index) { Pointer = choice.GetValidChildNode(); Next(current, Pointer); } + + public void SetPointer (string id) { + _playing = true; + Pointer = _graph.GetNodeByDataId(id); + + if (Pointer == null) { + Debug.LogError($"Pointer not found when maually setting pointer: {id}. This graph will instantly end when run"); + } + } + + // Allows playing the pointer from a nested location without crashing + public void SetPointerAndPlay (string id) { + SetPointer(id); + Events.Begin.Invoke(); + + Next(null, Pointer); + } } } diff --git a/Assets/com.fluid.dialogue/Runtime/Graphs/GraphRuntime.cs b/Assets/com.fluid.dialogue/Runtime/Graphs/GraphRuntime.cs index d9d5758..7d09aee 100644 --- a/Assets/com.fluid.dialogue/Runtime/Graphs/GraphRuntime.cs +++ b/Assets/com.fluid.dialogue/Runtime/Graphs/GraphRuntime.cs @@ -7,6 +7,7 @@ public class GraphRuntime : IGraph { private readonly Dictionary _dataToRuntime; public INode Root { get; } + public IGraphData Data { get; } public GraphRuntime (IDialogueController dialogue, IGraphData data) { _dataToRuntime = data.Nodes.ToDictionary( @@ -14,10 +15,15 @@ public GraphRuntime (IDialogueController dialogue, IGraphData data) { v => v.GetRuntime(this, dialogue)); Root = GetCopy(data.Root); + Data = data; } public INode GetCopy (INodeData original) { return _dataToRuntime[original]; } + + public INode GetNodeByDataId (string id) { + return _dataToRuntime.FirstOrDefault(n => n.Key.UniqueId == id).Value; + } } } diff --git a/Assets/com.fluid.dialogue/Runtime/Graphs/IGraph.cs b/Assets/com.fluid.dialogue/Runtime/Graphs/IGraph.cs index 2556179..e3ac4d9 100644 --- a/Assets/com.fluid.dialogue/Runtime/Graphs/IGraph.cs +++ b/Assets/com.fluid.dialogue/Runtime/Graphs/IGraph.cs @@ -3,6 +3,9 @@ namespace CleverCrow.Fluid.Dialogues.Graphs { public interface IGraph { INode Root { get; } + IGraphData Data { get; } + INode GetCopy (INodeData nodeData); + INode GetNodeByDataId (string id); } } diff --git a/Assets/com.fluid.dialogue/Runtime/Nodes/INode.cs b/Assets/com.fluid.dialogue/Runtime/Nodes/INode.cs index 0600007..2974cf3 100644 --- a/Assets/com.fluid.dialogue/Runtime/Nodes/INode.cs +++ b/Assets/com.fluid.dialogue/Runtime/Nodes/INode.cs @@ -9,6 +9,9 @@ public interface INode : IUniqueId { bool IsValid { get; } List HubChoices { get; } + /// + /// Returns the first valid child node + /// INode Next (); void Play (IDialoguePlayback playback); IChoice GetChoice (int index); diff --git a/Assets/com.fluid.dialogue/Runtime/Nodes/PlayGraph/NodePlayGraph.cs b/Assets/com.fluid.dialogue/Runtime/Nodes/PlayGraph/NodePlayGraph.cs index 160bcc4..5ff82af 100644 --- a/Assets/com.fluid.dialogue/Runtime/Nodes/PlayGraph/NodePlayGraph.cs +++ b/Assets/com.fluid.dialogue/Runtime/Nodes/PlayGraph/NodePlayGraph.cs @@ -7,6 +7,8 @@ namespace CleverCrow.Fluid.Dialogues.Nodes.PlayGraph { public class NodePlayGraph : NodeBase { private readonly IGraphData _graph; + public IGraphData Graph => _graph; + public NodePlayGraph ( IGraph runtime, string uniqueId, diff --git a/ProjectSettings/Packages/com.unity.services.core/Settings.json b/ProjectSettings/Packages/com.unity.services.core/Settings.json new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 002ed54..9f4e8a5 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,39 @@ public class CustomCondition : ConditionDataBase { } ``` +### Resuming Graph Playback + +Graph playback can be resumed from anywhere. This includes nested graphs and the exact node you want to resume from. + +```C# +var ctrl = new DialogueController(...); + +// Play your graph to generate runtime data and advance the graph to a specific node by hand +ctrl.Play(...); + +// Record the required serialized properties +var graph = ctrl.RootGraph; // Finds the root graph that is playing +var parentHierarchy = dialogue.Ctrl.ParentHierarchy; // Gets the IDs of all nodes the nested graphs that are playing +var nextNodeId = ctrl.ActiveDialogue.Pointer.Next()?.UniqueId; // You probably want the next node in your graph, not the current one + +// Create a new graph and resume playback +var newCtrl = new DialogueController(...); + +if (newCtrl.CanPlay(graph, parentHierarchy, nextNodeId)) { + // Failsafe that makes sure our graph playback data is valid + newCtrl.Play(graph, parentHierarchy, nextNodeId); +} else { + // If the data is invalid, just play the graph from the beginning or implement your own fallback logic + newCtrl.Play(graph); +} +``` + +## Releases + +Please note that whatever node you're resuming from will play that exact node with enter actions and all previous nodes will not trigger anything. This is a direct reference and does not simulate the graph from the beginning. + +```c# + ## Releases Archives of specific versions and release notes are available on the [releases page](https://github.com/ashblue/fluid-dialogue/releases).