Skip to content

Commit

Permalink
feat: added experimental nested graph playback restoration
Browse files Browse the repository at this point in the history
  • Loading branch information
clevercrowgames committed Apr 16, 2024
1 parent e92a3ca commit fb18251
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,13 +18,29 @@ public interface IDialogueController {

public class DialogueController : IDialogueController {
private readonly Stack<IDialoguePlayback> _activeDialogue = new Stack<IDialoguePlayback>();
private readonly List<string> _parentHierarchy = new();

[Obsolete("Use LocalDatabaseExtended instead")]
public IDatabaseInstance LocalDatabase { get; }
public IDatabaseInstanceExtended LocalDatabaseExtended { get; }
public IDialogueEvents Events { get; } = new DialogueEvents();

/// <summary>
/// Playback runtime that corresponds to the current graph definition (may be nested)
/// </summary>
public IDialoguePlayback ActiveDialogue => _activeDialogue.Count > 0 ? _activeDialogue.Peek() : null;

/// <summary>
/// Return the root of the currently playing dialogue
/// </summary>
public IGraphData RootGraph => _activeDialogue.Count > 0 ? _activeDialogue.ToArray().Last().Graph.Data : null;

/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<string> ParentHierarchy => _parentHierarchy;

[Obsolete("Use DatabaseInstanceExtended instead. Old databases do not support GameObjects")]
public DialogueController (IDatabaseInstance localDatabase) {
LocalDatabase = localDatabase;
Expand All @@ -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<string> 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();
Expand All @@ -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) {
Expand All @@ -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");
}
Expand All @@ -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 () {
Expand Down Expand Up @@ -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
/// <summary>
/// Verifies a nested graph can be played. Not the most runtime friendly operation so use sparingly
/// </summary>
public bool CanPlay (IGraphData graph, List<string> 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;
}
}
}
26 changes: 26 additions & 0 deletions Assets/com.fluid.dialogue/Runtime/DialoguePlayback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

/// <summary>
/// Current node data being used for the runtime
/// </summary>
INode Pointer { get; }

void Next ();
void Play ();
void Tick ();
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
6 changes: 6 additions & 0 deletions Assets/com.fluid.dialogue/Runtime/Graphs/GraphRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ public class GraphRuntime : IGraph {
private readonly Dictionary<INodeData, INode> _dataToRuntime;

public INode Root { get; }
public IGraphData Data { get; }

public GraphRuntime (IDialogueController dialogue, IGraphData data) {
_dataToRuntime = data.Nodes.ToDictionary(
k => k,
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;
}
}
}
3 changes: 3 additions & 0 deletions Assets/com.fluid.dialogue/Runtime/Graphs/IGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
3 changes: 3 additions & 0 deletions Assets/com.fluid.dialogue/Runtime/Nodes/INode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public interface INode : IUniqueId {
bool IsValid { get; }
List<IChoice> HubChoices { get; }

/// <summary>
/// Returns the first valid child node
/// </summary>
INode Next ();
void Play (IDialoguePlayback playback);
IChoice GetChoice (int index);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Empty file.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down

0 comments on commit fb18251

Please sign in to comment.