Create a simple dialogue authoring system, tied into our items (#10)
- Editor dialogue graph - Asset importer for processing the graph into runtime data - DialogueComponent that steers the dialogue interactions - DialogueCanbas with a scalable speech bubble to display everything - Brief README overview of the system Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #10
This commit is contained in:
20
Assets/Editor/Dialogue/DialogueGraph.cs
Normal file
20
Assets/Editor/Dialogue/DialogueGraph.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Unity.GraphToolkit.Editor;
|
||||
|
||||
namespace Editor.Dialogue
|
||||
{
|
||||
[Serializable]
|
||||
[Graph(AssetExtension)]
|
||||
public class DialogueGraph : Graph
|
||||
{
|
||||
public const string AssetExtension = "dialoguegraph";
|
||||
|
||||
[MenuItem("Assets/Create/Dialogue Graph", false)]
|
||||
private static void CreateAssetFile()
|
||||
{
|
||||
GraphDatabase.PromptInProjectBrowserToCreateNewAsset<DialogueGraph>();
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Editor/Dialogue/DialogueGraph.cs.meta
Normal file
3
Assets/Editor/Dialogue/DialogueGraph.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eff5cdbb7a22496e93560cffa5754029
|
||||
timeCreated: 1758869773
|
||||
262
Assets/Editor/Dialogue/DialogueGraphImporter.cs
Normal file
262
Assets/Editor/Dialogue/DialogueGraphImporter.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor.AssetImporters;
|
||||
using Unity.GraphToolkit.Editor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dialogue;
|
||||
using PuzzleS;
|
||||
|
||||
namespace Editor.Dialogue
|
||||
{
|
||||
[ScriptedImporter(1, DialogueGraph.AssetExtension)]
|
||||
public class DialogueGraphImporter : ScriptedImporter
|
||||
{
|
||||
public override void OnImportAsset(AssetImportContext ctx)
|
||||
{
|
||||
DialogueGraph editorGraph = GraphDatabase.LoadGraphForImporter<DialogueGraph>(ctx.assetPath);
|
||||
RuntimeDialogueGraph runtimeGraph = ScriptableObject.CreateInstance<RuntimeDialogueGraph>();
|
||||
var nodeIDMap = new Dictionary<INode, string>();
|
||||
|
||||
// Generate stable GUIDs for each node
|
||||
foreach (var node in editorGraph.GetNodes())
|
||||
{
|
||||
nodeIDMap[node] = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
// Process start node to get entry point and speaker name
|
||||
var startNode = editorGraph.GetNodes().OfType<StartNode>().FirstOrDefault();
|
||||
if (startNode != null)
|
||||
{
|
||||
var entryPoint = startNode.GetOutputPorts().FirstOrDefault()?.firstConnectedPort;
|
||||
if (entryPoint != null)
|
||||
{
|
||||
runtimeGraph.entryNodeID = nodeIDMap[entryPoint.GetNode()];
|
||||
}
|
||||
|
||||
runtimeGraph.speakerName = GetPortValue<string>(startNode.GetInputPortByName("SpeakerName"));
|
||||
}
|
||||
|
||||
// Process each node in the graph
|
||||
foreach (var iNode in editorGraph.GetNodes())
|
||||
{
|
||||
if (iNode is StartNode || iNode is IVariableNode)
|
||||
continue;
|
||||
|
||||
var runtimeNode = new RuntimeDialogueNode{ nodeID = nodeIDMap[iNode] };
|
||||
|
||||
// Process node based on its type
|
||||
if (iNode is DialogueNode dialogueNode)
|
||||
{
|
||||
// Process base dialogue node properties (for all node types)
|
||||
ProcessDialogueNodeBase(dialogueNode, runtimeNode);
|
||||
|
||||
if (iNode is WaitOnPuzzleStep puzzleNode)
|
||||
{
|
||||
ProcessPuzzleNode(puzzleNode, runtimeNode);
|
||||
}
|
||||
else if (iNode is WaitOnPickup pickupNode)
|
||||
{
|
||||
ProcessPickupNode(pickupNode, runtimeNode);
|
||||
}
|
||||
else if (iNode is WaitOnSlot slotNode)
|
||||
{
|
||||
ProcessSlotNode(slotNode, runtimeNode);
|
||||
}
|
||||
else if (iNode is WaitOnCombination combinationNode)
|
||||
{
|
||||
ProcessCombinationNode(combinationNode, runtimeNode);
|
||||
}
|
||||
}
|
||||
else if (iNode is EndNode)
|
||||
{
|
||||
runtimeNode.nodeType = RuntimeDialogueNodeType.End;
|
||||
// End nodes don't have next nodes, so we don't need to look for "out" ports
|
||||
}
|
||||
|
||||
// Get next node connection (skip for EndNodes as they don't have out ports)
|
||||
if (!(iNode is EndNode))
|
||||
{
|
||||
// Check if the node has output ports before trying to get one by name
|
||||
var outputPorts = iNode.GetOutputPorts();
|
||||
if (outputPorts != null && outputPorts.Any())
|
||||
{
|
||||
var outPort = outputPorts.FirstOrDefault(p => p.name == "out");
|
||||
if (outPort != null && outPort.firstConnectedPort != null)
|
||||
{
|
||||
runtimeNode.nextNodeID = nodeIDMap[outPort.firstConnectedPort.GetNode()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add node to runtime graph
|
||||
runtimeGraph.allNodes.Add(runtimeNode);
|
||||
}
|
||||
|
||||
ctx.AddObjectToAsset("RuntimeData", runtimeGraph);
|
||||
ctx.SetMainObject(runtimeGraph);
|
||||
|
||||
Debug.Log($"Imported DialogueGraph with {runtimeGraph.allNodes.Count} nodes.");
|
||||
}
|
||||
|
||||
private void ProcessDialogueNodeBase(DialogueNode node, RuntimeDialogueNode runtimeNode)
|
||||
{
|
||||
// Set default node type
|
||||
runtimeNode.nodeType = RuntimeDialogueNodeType.Dialogue;
|
||||
|
||||
// Get line type and count options
|
||||
var lineTypeOption = node.GetNodeOptionByName("DialogueLineType");
|
||||
lineTypeOption.TryGetValue<DialogueType>(out var lineType);
|
||||
|
||||
var lineCountOption = node.GetNodeOptionByName("NoLines");
|
||||
lineCountOption.TryGetValue<int>(out var lineCount);
|
||||
|
||||
// Process dialogue lines based on line type
|
||||
if (lineType == DialogueType.SayMultipleLines)
|
||||
{
|
||||
for (var i = 0; i < lineCount; i++)
|
||||
{
|
||||
var lineValue = GetPortValue<string>(node.GetInputPortByName($"DefaultDialogueLine{i + 1}"));
|
||||
if (!string.IsNullOrEmpty(lineValue))
|
||||
{
|
||||
runtimeNode.dialogueLines.Add(lineValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var lineValue = GetPortValue<string>(node.GetInputPortByName("DefaultDialogueLine"));
|
||||
if (!string.IsNullOrEmpty(lineValue))
|
||||
{
|
||||
runtimeNode.dialogueLines.Add(lineValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Get loop through lines option
|
||||
var loopThroughLines = GetPortValue<bool>(node.GetInputPortByName("LoopThroughDefaultLines"));
|
||||
runtimeNode.loopThroughLines = loopThroughLines;
|
||||
}
|
||||
|
||||
private void ProcessPuzzleNode(WaitOnPuzzleStep node, RuntimeDialogueNode runtimeNode)
|
||||
{
|
||||
runtimeNode.nodeType = RuntimeDialogueNodeType.WaitOnPuzzleStep;
|
||||
|
||||
var puzzleStep = GetPortValue<PuzzleStepSO>(node.GetInputPortByName("RequiredPuzzleStep"));
|
||||
if (puzzleStep != null)
|
||||
{
|
||||
runtimeNode.puzzleStepID = puzzleStep.stepId;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessPickupNode(WaitOnPickup node, RuntimeDialogueNode runtimeNode)
|
||||
{
|
||||
runtimeNode.nodeType = RuntimeDialogueNodeType.WaitOnPickup;
|
||||
|
||||
var pickup = GetPortValue<PickupItemData>(node.GetInputPortByName("RequiredPickup"));
|
||||
if (pickup != null)
|
||||
{
|
||||
runtimeNode.pickupItemID = pickup.itemId;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSlotNode(WaitOnSlot node, RuntimeDialogueNode runtimeNode)
|
||||
{
|
||||
runtimeNode.nodeType = RuntimeDialogueNodeType.WaitOnSlot;
|
||||
|
||||
var slot = GetPortValue<PickupItemData>(node.GetInputPortByName("RequiredSlot"));
|
||||
if (slot != null)
|
||||
{
|
||||
runtimeNode.slotItemID = slot.itemId;
|
||||
}
|
||||
|
||||
// Process incorrect item lines
|
||||
var incorrectItemLineTypeOption = node.GetNodeOptionByName("IncorrectItemDialogueLineType");
|
||||
incorrectItemLineTypeOption.TryGetValue<DialogueType>(out var incorrectItemLineType);
|
||||
|
||||
var incorrectItemLineCountOption = node.GetNodeOptionByName("IncorrectItemNoLines");
|
||||
incorrectItemLineCountOption.TryGetValue<int>(out var incorrectItemLineCount);
|
||||
|
||||
if (incorrectItemLineType == DialogueType.SayMultipleLines)
|
||||
{
|
||||
for (var i = 0; i < incorrectItemLineCount; i++)
|
||||
{
|
||||
var lineValue = GetPortValue<string>(node.GetInputPortByName($"IncorrectItemDialogueLine{i + 1}"));
|
||||
if (!string.IsNullOrEmpty(lineValue))
|
||||
{
|
||||
runtimeNode.incorrectItemLines.Add(lineValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var lineValue = GetPortValue<string>(node.GetInputPortByName("IncorrectItemDialogueLine"));
|
||||
if (!string.IsNullOrEmpty(lineValue))
|
||||
{
|
||||
runtimeNode.incorrectItemLines.Add(lineValue);
|
||||
}
|
||||
}
|
||||
|
||||
runtimeNode.loopThroughIncorrectLines =
|
||||
GetPortValue<bool>(node.GetInputPortByName("LoopThroughIncorrectItemLines"));
|
||||
|
||||
// Process forbidden item lines
|
||||
var forbiddenItemLineTypeOption = node.GetNodeOptionByName("ForbiddenItemDialogueLineType");
|
||||
forbiddenItemLineTypeOption.TryGetValue<DialogueType>(out var forbiddenItemLineType);
|
||||
|
||||
var forbiddenItemLineCountOption = node.GetNodeOptionByName("ForbiddenItemNoLines");
|
||||
forbiddenItemLineCountOption.TryGetValue<int>(out var forbiddenItemLineCount);
|
||||
|
||||
if (forbiddenItemLineType == DialogueType.SayMultipleLines)
|
||||
{
|
||||
for (var i = 0; i < forbiddenItemLineCount; i++)
|
||||
{
|
||||
var lineValue = GetPortValue<string>(node.GetInputPortByName($"ForbiddenItemDialogueLine{i + 1}"));
|
||||
if (!string.IsNullOrEmpty(lineValue))
|
||||
{
|
||||
runtimeNode.forbiddenItemLines.Add(lineValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var lineValue = GetPortValue<string>(node.GetInputPortByName("ForbiddenItemDialogueLine"));
|
||||
if (!string.IsNullOrEmpty(lineValue))
|
||||
{
|
||||
runtimeNode.forbiddenItemLines.Add(lineValue);
|
||||
}
|
||||
}
|
||||
|
||||
runtimeNode.loopThroughForbiddenLines =
|
||||
GetPortValue<bool>(node.GetInputPortByName("LoopThroughForbiddenItemLines"));
|
||||
}
|
||||
|
||||
// Add new method to process combination nodes
|
||||
private void ProcessCombinationNode(WaitOnCombination node, RuntimeDialogueNode runtimeNode)
|
||||
{
|
||||
runtimeNode.nodeType = RuntimeDialogueNodeType.WaitOnCombination;
|
||||
|
||||
var resultItem = GetPortValue<PickupItemData>(node.GetInputPortByName("RequiredResultItem"));
|
||||
if (resultItem != null)
|
||||
{
|
||||
runtimeNode.combinationResultItemID = resultItem.itemId;
|
||||
}
|
||||
}
|
||||
|
||||
private T GetPortValue<T>(IPort port)
|
||||
{
|
||||
if (port == null) return default(T);
|
||||
|
||||
if (port.isConnected)
|
||||
{
|
||||
if (port.firstConnectedPort.GetNode() is IVariableNode variableNode)
|
||||
{
|
||||
variableNode.variable.TryGetDefaultValue(out T value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
port.TryGetValue(out T fallbackValue);
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Editor/Dialogue/DialogueGraphImporter.cs.meta
Normal file
3
Assets/Editor/Dialogue/DialogueGraphImporter.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2334d3ee5254a2bbcb316035c681b27
|
||||
timeCreated: 1758871584
|
||||
209
Assets/Editor/Dialogue/DialogueNodes.cs
Normal file
209
Assets/Editor/Dialogue/DialogueNodes.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using UnityEngine;
|
||||
using Unity.GraphToolkit.Editor;
|
||||
using System;
|
||||
|
||||
namespace Editor.Dialogue
|
||||
{
|
||||
[Serializable]
|
||||
public class StartNode : Node
|
||||
{
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddOutputPort("out").Build();
|
||||
|
||||
context.AddInputPort<string>("SpeakerName").Build();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EndNode : Node
|
||||
{
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddInputPort("in").Build();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public enum DialogueType
|
||||
{
|
||||
SayOneLine,
|
||||
SayMultipleLines
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class DialogueNode : Node
|
||||
{
|
||||
const string LineTypeOptionName = "DialogueLineType";
|
||||
const string NoLinesOptionName = "NoLines";
|
||||
const string LoopThroughDefaultLinesOptionName = "LoopThroughDefaultLines";
|
||||
const string DefaultDialogueLineOptionName = "DefaultDialogueLine";
|
||||
|
||||
|
||||
protected override void OnDefineOptions(IOptionDefinitionContext context)
|
||||
{
|
||||
context.AddOption<DialogueType>(LineTypeOptionName)
|
||||
.WithDisplayName("Default Line Type")
|
||||
.WithDefaultValue(DialogueType.SayOneLine)
|
||||
.Delayed();
|
||||
|
||||
|
||||
context.AddOption<int>(NoLinesOptionName)
|
||||
.WithDisplayName("Number of Default Lines")
|
||||
.WithDefaultValue(1)
|
||||
.Delayed();
|
||||
}
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddInputPort("in").Build();
|
||||
context.AddOutputPort("out").Build();
|
||||
|
||||
var lineTypeOption = GetNodeOptionByName(LineTypeOptionName);
|
||||
lineTypeOption.TryGetValue<DialogueType>(out var lineType);
|
||||
var lineCountOption = GetNodeOptionByName(NoLinesOptionName);
|
||||
lineCountOption.TryGetValue<int>(out var lineCount);
|
||||
|
||||
if (lineType == DialogueType.SayMultipleLines)
|
||||
{
|
||||
for (var i = 0; i < lineCount; i++)
|
||||
{
|
||||
context.AddInputPort<string>($"{DefaultDialogueLineOptionName}{i + 1}").WithDisplayName($"Default Dialogue Line {i + 1}").Build();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.AddInputPort<string>($"{DefaultDialogueLineOptionName}").WithDisplayName("Default Dialogue Line").Build();
|
||||
}
|
||||
|
||||
context.AddInputPort<bool>($"{LoopThroughDefaultLinesOptionName}").WithDisplayName("Loop Through Default Lines?").Build();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WaitOnPuzzleStep : DialogueNode
|
||||
{
|
||||
const string RequiredPuzzleStep = "RequiredPuzzleStep";
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddInputPort<PuzzleStepSO>(RequiredPuzzleStep).WithDisplayName("Required Puzzle Step").Build();
|
||||
|
||||
base.OnDefinePorts(context);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WaitOnPickup : DialogueNode
|
||||
{
|
||||
const string RequiredPickupsOptionName = "RequiredPickup";
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddInputPort<PickupItemData>(RequiredPickupsOptionName).WithDisplayName("Required Pickup").Build();
|
||||
|
||||
base.OnDefinePorts(context);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WaitOnSlot : DialogueNode
|
||||
{
|
||||
const string RequiredSlotOptionName = "RequiredSlot";
|
||||
// Incorrect item - i.e. not the correct one but also not forbidden
|
||||
const string IncorrectItemLineTypeOptionName = "IncorrectItemDialogueLineType";
|
||||
const string IncorrectItemNoLinesOptionName = "IncorrectItemNoLines";
|
||||
const string LoopThroughIncorrectItemLinesOptionName = "LoopThroughIncorrectItemLines";
|
||||
const string IncorrectIteDialogueLineOptionName = "IncorrectItemDialogueLine";
|
||||
// Explicitely forbidden item
|
||||
const string ForbiddenItemLineTypeOptionName = "ForbiddenItemDialogueLineType";
|
||||
const string ForbiddenItemNoLinesOptionName = "ForbiddenItemNoLines";
|
||||
const string LoopThroughForbiddenItemLinesOptionName = "LoopThroughForbiddenItemLines";
|
||||
const string ForbiddenIteDialogueLineOptionName = "ForbiddenItemDialogueLine";
|
||||
|
||||
protected override void OnDefineOptions(IOptionDefinitionContext context)
|
||||
{
|
||||
base.OnDefineOptions(context);
|
||||
|
||||
// Incorrect
|
||||
context.AddOption<DialogueType>(IncorrectItemLineTypeOptionName)
|
||||
.WithDisplayName("Incorrect Item Line Type")
|
||||
.WithDefaultValue(DialogueType.SayOneLine)
|
||||
.Delayed();
|
||||
context.AddOption<int>(IncorrectItemNoLinesOptionName)
|
||||
.WithDisplayName("Number of Incorrect Item Lines")
|
||||
.WithDefaultValue(1)
|
||||
.Delayed();
|
||||
|
||||
// Forbidden
|
||||
context.AddOption<DialogueType>(ForbiddenItemLineTypeOptionName)
|
||||
.WithDisplayName("Forbidden Item Line Type")
|
||||
.WithDefaultValue(DialogueType.SayOneLine)
|
||||
.Delayed();
|
||||
context.AddOption<int>(ForbiddenItemNoLinesOptionName)
|
||||
.WithDisplayName("Forbidden of Incorrect Item Lines")
|
||||
.WithDefaultValue(1)
|
||||
.Delayed();
|
||||
}
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddInputPort<PickupItemData>(RequiredSlotOptionName).WithDisplayName("Required Slot").Build();
|
||||
|
||||
base.OnDefinePorts(context);
|
||||
|
||||
// Incorrect
|
||||
var incorrectItemLineTypeOption = GetNodeOptionByName(IncorrectItemLineTypeOptionName);
|
||||
incorrectItemLineTypeOption.TryGetValue<DialogueType>(out var incorrectItemLineType);
|
||||
var incorrectItemLineCountOption = GetNodeOptionByName(IncorrectItemNoLinesOptionName);
|
||||
incorrectItemLineCountOption.TryGetValue<int>(out var incorrectItemLineCount);
|
||||
|
||||
if (incorrectItemLineType == DialogueType.SayMultipleLines)
|
||||
{
|
||||
for (var i = 0; i < incorrectItemLineCount; i++)
|
||||
{
|
||||
context.AddInputPort<string>($"{IncorrectIteDialogueLineOptionName}{i + 1}").WithDisplayName($"Incorrect Item Dialogue Line {i + 1}").Build();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.AddInputPort<string>($"{IncorrectIteDialogueLineOptionName}").WithDisplayName("Incorrect Item Dialogue Line").Build();
|
||||
}
|
||||
|
||||
context.AddInputPort<bool>($"{LoopThroughIncorrectItemLinesOptionName}").WithDisplayName("Loop Through Incorrect Item Lines?").Build();
|
||||
|
||||
// Forbidden
|
||||
var forbiddenItemLineTypeOption = GetNodeOptionByName(ForbiddenItemLineTypeOptionName);
|
||||
forbiddenItemLineTypeOption.TryGetValue<DialogueType>(out var forbiddenItemLineType);
|
||||
var forbiddenItemLineCountOption = GetNodeOptionByName(ForbiddenItemNoLinesOptionName);
|
||||
forbiddenItemLineCountOption.TryGetValue<int>(out var forbiddenItemLineCount);
|
||||
|
||||
if (forbiddenItemLineType == DialogueType.SayMultipleLines)
|
||||
{
|
||||
for (var i = 0; i < forbiddenItemLineCount; i++)
|
||||
{
|
||||
context.AddInputPort<string>($"{ForbiddenIteDialogueLineOptionName}{i + 1}").WithDisplayName($"Forbidden Item Dialogue Line {i + 1}").Build();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.AddInputPort<string>($"{ForbiddenIteDialogueLineOptionName}").WithDisplayName("Forbidden Item Dialogue Line").Build();
|
||||
}
|
||||
|
||||
context.AddInputPort<bool>($"{LoopThroughForbiddenItemLinesOptionName}").WithDisplayName("Loop Through Forbidden Item Lines?").Build();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WaitOnCombination : DialogueNode
|
||||
{
|
||||
const string RequiredResultItemOptionName = "RequiredResultItem";
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
context.AddInputPort<PickupItemData>(RequiredResultItemOptionName).WithDisplayName("Required Result Item").Build();
|
||||
|
||||
base.OnDefinePorts(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Editor/Dialogue/DialogueNodes.cs.meta
Normal file
3
Assets/Editor/Dialogue/DialogueNodes.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8b3213c46b447a9adc5627a273f2b2d
|
||||
timeCreated: 1758870466
|
||||
Reference in New Issue
Block a user