Add BehaviourGraph for executable behavior trees + refactor Excel export/import to support both graph types

main^2
Vinny 2026-03-30 20:09:08 +08:00
parent 4fefe78bb8
commit 6557e9dfcf
18 changed files with 497 additions and 66 deletions

View File

@ -0,0 +1,24 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b8409462a7f3e4d46a886d68e7626d94, type: 3}
m_Name: ActivityGraph
m_EditorClassIdentifier:
_serializedGraph: '{"type":"GameplayEditor.Core.GameplayGraph","nodes":[{"levelID":1,"nodeID":1,"nodeLayer":1,"eventTypeGroup":"1","priorityWeight":"1","eventMappingID":1,"_position":{"x":660.0},"$type":"GameplayEditor.Core.GameplayNode"}],"connections":[],"canvasGroups":[],"localBlackboard":{"_variables":{}}}'
_objectReferences: []
_graphSource:
_version: 3.33
_category:
_comments:
_translation: {x: -281, y: 182}
_zoomFactor: 1
_haltSerialization: 0
_externalSerializationFile: {fileID: 0}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a0bc0a969202f49498e8d63e4da11d62
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -13,8 +13,8 @@ MonoBehaviour:
m_Name: BT_test
m_EditorClassIdentifier:
_serializedGraph: '{"type":"NodeCanvas.BehaviourTrees.BehaviourTree","nodes":[{"dynamic":true,"_tag":"\u521d\u59cb\u5316","_position":{"x":560.0,"y":280.0},"_comment":"\u8fd9\u662f\u5173\u5361\u521d\u59cb\u5316\uff0c\u5173\u5361\u5f00\u59cb\u5fc5\u6709","$type":"NodeCanvas.BehaviourTrees.Sequencer","$id":"0"},{"dynamic":true,"_position":{"x":420.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.Sequencer","$id":"1"},{"_action":{"actions":[{"dictionary":{"_name":"Dictionary","_targetVariableID":"34aa1bb3-2cc8-4d06-9fb0-9d751d56ba17"},"key":{"_value":"1"},"value":{},"$type":"NodeCanvas.Tasks.Actions.AddElementToDictionary`1[[UnityEngine.Camera,
UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},{"parent":{},"clonePosition":{},"cloneRotation":{},"saveCloneAs":{"_name":""},"$type":"NodeCanvas.Tasks.Actions.InstantiateGameObject"}],"$type":"NodeCanvas.Framework.ActionList"},"_position":{"x":120.0,"y":540.0},"_comment":"\u6dfb\u52a0\u76f8\u673a\u5230\u5217\u8868","$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"2"},{"_action":{"dictionary":{"_name":"Dictionary","_targetVariableID":"34aa1bb3-2cc8-4d06-9fb0-9d751d56ba17"},"key":{"_value":"1"},"saveAs":{"_name":"Main","_targetVariableID":"dc6ba0ab-ee9b-4bac-b4a9-8084a2f509b0"},"$type":"NodeCanvas.Tasks.Actions.GetDictionaryElement`1[[UnityEngine.Camera,
UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},"_position":{"x":320.0,"y":540.0},"_comment":"\u4ece\u5217\u8868\u62ff\u76f8\u673a","$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"3"},{"dynamic":true,"_position":{"x":560.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.Selector","$id":"4"},{"_position":{"x":700.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.Parallel","$id":"5"},{"_position":{"x":700.0,"y":540.0},"$type":"NodeCanvas.BehaviourTrees.ConditionNode"},{"dynamic":true,"desires":[{"considerations":[]},{"considerations":[]},{"considerations":[]}],"_position":{"x":1040.0,"y":280.0},"$type":"NodeCanvas.BehaviourTrees.PrioritySelector","$version":1,"$id":"7"},{"_position":{"x":900.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"8"},{"_position":{"x":1040.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"9"},{"_position":{"x":1180.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"10"},{"_position":{"x":560.0,"y":540.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode"},{"_condition":{"conditions":[{"valueA":{"_name":""},"valueB":{},"$type":"NodeCanvas.Tasks.Conditions.CheckInt"},{"valueA":{"_name":"Dictionary","_targetVariableID":"34aa1bb3-2cc8-4d06-9fb0-9d751d56ba17"},"valueB":{"_value":{"":null}},"$type":"NodeCanvas.Tasks.Conditions.CheckVariable`1[[System.Collections.Generic.Dictionary`2[[System.String,
UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},{"parent":{},"clonePosition":{},"cloneRotation":{},"saveCloneAs":{"_name":""},"$type":"NodeCanvas.Tasks.Actions.InstantiateGameObject"}],"$type":"NodeCanvas.Framework.ActionList"},"_position":{"x":31.53383,"y":683.4225},"_comment":"\u6dfb\u52a0\u76f8\u673a\u5230\u5217\u8868","$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"2"},{"_action":{"dictionary":{"_name":"Dictionary","_targetVariableID":"34aa1bb3-2cc8-4d06-9fb0-9d751d56ba17"},"key":{"_value":"1"},"saveAs":{"_name":"Main","_targetVariableID":"dc6ba0ab-ee9b-4bac-b4a9-8084a2f509b0"},"$type":"NodeCanvas.Tasks.Actions.GetDictionaryElement`1[[UnityEngine.Camera,
UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},"_position":{"x":388.3602,"y":604.3391},"_comment":"\u4ece\u5217\u8868\u62ff\u76f8\u673a","$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"3"},{"dynamic":true,"_position":{"x":560.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.Selector","$id":"4"},{"_position":{"x":700.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.Parallel","$id":"5"},{"_position":{"x":700.0,"y":540.0},"$type":"NodeCanvas.BehaviourTrees.ConditionNode"},{"dynamic":true,"desires":[{"considerations":[]},{"considerations":[]},{"considerations":[]}],"_position":{"x":1040.0,"y":280.0},"$type":"NodeCanvas.BehaviourTrees.PrioritySelector","$version":1,"$id":"7"},{"_position":{"x":900.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"8"},{"_position":{"x":1040.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"9"},{"_position":{"x":1180.0,"y":420.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"10"},{"_position":{"x":560.0,"y":540.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode"},{"_condition":{"conditions":[{"valueA":{"_name":""},"valueB":{},"$type":"NodeCanvas.Tasks.Conditions.CheckInt"},{"valueA":{"_name":"Dictionary","_targetVariableID":"34aa1bb3-2cc8-4d06-9fb0-9d751d56ba17"},"valueB":{"_value":{"":null}},"$type":"NodeCanvas.Tasks.Conditions.CheckVariable`1[[System.Collections.Generic.Dictionary`2[[System.String,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[UnityEngine.Camera,
UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]],
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"}],"$type":"NodeCanvas.Framework.ConditionList"},"_position":{"x":480.0,"y":760.0},"$type":"NodeCanvas.BehaviourTrees.ConditionNode"}],"connections":[{"_sourceNode":{"$ref":"0"},"_targetNode":{"$ref":"1"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"0"},"_targetNode":{"$ref":"4"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"0"},"_targetNode":{"$ref":"5"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"1"},"_targetNode":{"$ref":"2"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"1"},"_targetNode":{"$ref":"3"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"7","$version":1},"_targetNode":{"$ref":"8"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"7","$version":1},"_targetNode":{"$ref":"9"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"7","$version":1},"_targetNode":{"$ref":"10"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"}],"canvasGroups":[],"localBlackboard":{"_variables":{"myBoolean":{"_name":"myBoolean","_id":"597a5d7c-3bd9-4688-839b-d14d91ff6172","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
@ -28,7 +28,7 @@ MonoBehaviour:
_version: 3.33
_category:
_comments:
_translation: {x: -8, y: 216}
_zoomFactor: 1
_translation: {x: 275, y: -62}
_zoomFactor: 0.59610087
_haltSerialization: 0
_externalSerializationFile: {fileID: 0}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d04cbc7dec380c4a80a1cf00bcab601
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e498e185f67a544479d637626a1f98a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f68ed535e8fe19d43ba9ae227e07c76d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
using NodeCanvas.BehaviourTrees;
using ParadoxNotion;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树 Graph - 支持可执行的行为逻辑 + 表格编辑
/// 继承 BehaviourTree额外支持 ActivityNodeBase 节点的导出/导入
/// </summary>
[System.Serializable]
[CreateAssetMenu(menuName = "NodeCanvas/Behaviour Graph")]
public class BehaviourGraph : BehaviourTree
{
public void ExportToExcel(string path)
{
Export.ExcelExporter.Export(this, path);
}
public void ImportFromExcel(string path)
{
Export.ExcelImporter.Import(path, this);
}
public List<T> GetNodesByType<T>() where T : Node
{
return allNodes.OfType<T>().ToList();
}
public List<ActivityNodeBase> GetAllActivityNodes()
{
return allNodes.OfType<ActivityNodeBase>().ToList();
}
public Dictionary<string, List<ActivityNodeBase>> GetNodesByTableName()
{
var result = new Dictionary<string, List<ActivityNodeBase>>();
foreach (var node in GetAllActivityNodes())
{
if (!result.ContainsKey(node.TableName))
result[node.TableName] = new List<ActivityNodeBase>();
result[node.TableName].Add(node);
}
return result;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25cde69a219386b44bccc0c08271bdc2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 758081b0b34f2e942b68f9fa35170ba8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -3,28 +3,41 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Core;
using NodeCanvas.Framework;
using UnityEngine;
namespace GameplayEditor.Export
{
public static class ExcelExporter
{
public static void Export(GameplayGraph graph, string basePath)
public static void Export(Graph graph, string basePath)
{
var nodesByTable = graph.GetNodesByTableName();
var activityNodes = graph.allNodes.OfType<ActivityNodeBase>().ToList();
if (activityNodes.Count == 0)
{
Debug.LogWarning("No ActivityNodeBase nodes found in graph");
return;
}
var nodesByTable = new Dictionary<string, List<ActivityNodeBase>>();
foreach (var node in activityNodes)
{
if (!nodesByTable.ContainsKey(node.TableName))
nodesByTable[node.TableName] = new List<ActivityNodeBase>();
nodesByTable[node.TableName].Add(node);
}
foreach (var kvp in nodesByTable)
{
var tableName = kvp.Key;
var nodes = kvp.Value;
if (nodes.Count == 0) continue;
var excelPath = Path.Combine(Path.GetDirectoryName(basePath), $"{tableName}.csv");
ExportTable(nodes, excelPath);
}
Debug.Log($"Exported {nodesByTable.Count} tables from {basePath}");
Debug.Log($"Exported {nodesByTable.Count} tables from graph");
}
private static void ExportTable(List<ActivityNodeBase> nodes, string filePath)

View File

@ -25,13 +25,13 @@ namespace GameplayEditor.Export
{ "DialogInfo", () => new DialogInfoNode() },
};
public static void Import(string filePath, GameplayGraph graph)
public static void Import(string filePath, Graph graph)
{
var tableName = Path.GetFileNameWithoutExtension(filePath);
ImportTable(filePath, graph, tableName);
}
public static void ImportTable(string filePath, GameplayGraph graph, string tableName)
public static void ImportTable(string filePath, Graph graph, string tableName)
{
if (!File.Exists(filePath))
{

View File

@ -68,15 +68,23 @@ namespace GameplayEditor
if (currentGraph == null)
{
EditorGUILayout.HelpBox("请选择一个 Graph", MessageType.Info);
EditorGUILayout.HelpBox("请选择一个 GraphGameplayGraph 或 BehaviourGraph", MessageType.Info);
return;
}
canvasScroll = EditorGUILayout.BeginScrollView(canvasScroll);
var nodesByTable = (currentGraph as Core.GameplayGraph)?.GetNodesByTableName();
if (nodesByTable != null)
var activityNodes = currentGraph.allNodes.OfType<Core.ActivityNodeBase>().ToList();
if (activityNodes.Count > 0)
{
var nodesByTable = new Dictionary<string, List<Core.ActivityNodeBase>>();
foreach (var node in activityNodes)
{
if (!nodesByTable.ContainsKey(node.TableName))
nodesByTable[node.TableName] = new List<Core.ActivityNodeBase>();
nodesByTable[node.TableName].Add(node);
}
foreach (var kvp in nodesByTable.OrderBy(x => x.Key))
{
EditorGUILayout.LabelField($"{kvp.Key} ({kvp.Value.Count})", EditorStyles.boldLabel);
@ -89,6 +97,10 @@ namespace GameplayEditor
}
}
}
else
{
EditorGUILayout.HelpBox("Canvas 中没有 ActivityNodeBase 节点", MessageType.Info);
}
EditorGUILayout.EndScrollView();
}
@ -125,7 +137,7 @@ namespace GameplayEditor
}
var basePath = "Assets/玩法编辑器.csv";
(currentGraph as Core.GameplayGraph)?.ExportToExcel(basePath);
Export.ExcelExporter.Export(currentGraph, basePath);
EditorUtility.DisplayDialog("成功", $"已导出所有表到 Assets/ 目录", "确定");
}
@ -142,7 +154,7 @@ namespace GameplayEditor
{
var filePath = System.IO.Path.Combine(basePath, $"{tableName}.csv");
if (System.IO.File.Exists(filePath))
Export.ExcelImporter.ImportTable(filePath, currentGraph as Core.GameplayGraph, tableName);
Export.ExcelImporter.ImportTable(filePath, currentGraph, tableName);
}
EditorUtility.DisplayDialog("成功", "已导入所有 CSV 表", "确定");

4
Assets/BehaviourTree.csv Normal file
View File

@ -0,0 +1,4 @@
LevelID,NodeID,NodeLayer,EventTypeGroup,Priority,EventMappingID,InteractVisible,NodeState,ParentNodeID
int,int,int,string,string,int,bool,bool,int
关卡编号ID,节点ID唯一,节点层级,节点事件类型组(|分隔),优先级权重(|分隔),存储事件ID映射,交互是否可见,节点是否可进入,父节点ID-1表示根节点
1,1,1,1,1,1,True,True,-1
1 LevelID NodeID NodeLayer EventTypeGroup Priority EventMappingID InteractVisible NodeState ParentNodeID
2 int int int string string int bool bool int
3 关卡编号ID 节点ID(唯一) 节点层级 节点事件类型组(|分隔) 优先级权重(|分隔) 存储事件ID映射 交互是否可见 节点是否可进入 父节点ID(-1表示根节点)
4 1 1 1 1 1 1 True True -1

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 89e2f58b96cb09245a29a89b64f4239d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,26 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fd283cf39e183a04d814b09c20b2a810, type: 3}
m_Name: New Graph Data Schema
m_EditorClassIdentifier:
sheetName: "\u6280\u80FD"
fields:
- fieldName: ID
excelColumnName: NodeID
fieldType: 1
- fieldName: name
excelColumnName: NodeName
fieldType: 0
- fieldName: comment
excelColumnName: "\u63CF\u8FF0"
fieldType: 0
nodeTypeFilter: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0de0e36ee42ae394a8ac5b18dc6303ed
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -382,6 +382,103 @@ PlayerSettings:
m_Height: 36
m_Kind: 0
m_SubKind:
- m_BuildTarget: iPhone
m_Icons:
- m_Textures: []
m_Width: 180
m_Height: 180
m_Kind: 0
m_SubKind: iPhone
- m_Textures: []
m_Width: 120
m_Height: 120
m_Kind: 0
m_SubKind: iPhone
- m_Textures: []
m_Width: 167
m_Height: 167
m_Kind: 0
m_SubKind: iPad
- m_Textures: []
m_Width: 152
m_Height: 152
m_Kind: 0
m_SubKind: iPad
- m_Textures: []
m_Width: 76
m_Height: 76
m_Kind: 0
m_SubKind: iPad
- m_Textures: []
m_Width: 120
m_Height: 120
m_Kind: 3
m_SubKind: iPhone
- m_Textures: []
m_Width: 80
m_Height: 80
m_Kind: 3
m_SubKind: iPhone
- m_Textures: []
m_Width: 80
m_Height: 80
m_Kind: 3
m_SubKind: iPad
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 3
m_SubKind: iPad
- m_Textures: []
m_Width: 87
m_Height: 87
m_Kind: 1
m_SubKind: iPhone
- m_Textures: []
m_Width: 58
m_Height: 58
m_Kind: 1
m_SubKind: iPhone
- m_Textures: []
m_Width: 29
m_Height: 29
m_Kind: 1
m_SubKind: iPhone
- m_Textures: []
m_Width: 58
m_Height: 58
m_Kind: 1
m_SubKind: iPad
- m_Textures: []
m_Width: 29
m_Height: 29
m_Kind: 1
m_SubKind: iPad
- m_Textures: []
m_Width: 60
m_Height: 60
m_Kind: 2
m_SubKind: iPhone
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 2
m_SubKind: iPhone
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 2
m_SubKind: iPad
- m_Textures: []
m_Width: 20
m_Height: 20
m_Kind: 2
m_SubKind: iPad
- m_Textures: []
m_Width: 1024
m_Height: 1024
m_Kind: 4
m_SubKind: App Store
m_BuildTargetBatching:
- m_BuildTarget: WebGL
m_StaticBatching: 1

View File

@ -177,92 +177,218 @@ UI 资源。填写需要使用的关卡和映射 ID可以填 String。
```
Assets/BP_Scripts/GameplayEditor/
├── Core/
│ ├── GameplayGraph.cs ← 自定义 Graph 类型Asset 本体)
│ ├── GameplayNode.cs ← 节点数据模型含9个业务字段
│ └── GameplayConnection.cs ← 节点连线类型
│ ├── ActivityNodeBase.cs ← 所有节点的抽象基类
│ ├── GameplayNode.cs ← 行为树逻辑节点工作表2
│ ├── ActivityStageNode.cs ← 关卡配置节点工作表1
│ ├── BehaviourDocNodes.cs ← 节点说明 + 组合节点工作表3/4
│ ├── LutNodes.cs ← UI/Camera/FX资源映射3.2/3.3/3.4
│ ├── DataNodes.cs ← 地图/对话配置节点3.5/3.6/3.7
│ ├── GameplayGraph.cs ← 自定义 Graph 类型
│ └── GameplayConnection.cs ← 节点连线类型
├── Export/
│ ├── ExcelExporter.cs ← Canvas → CSV 导出逻辑
│ └── ExcelImporter.cs ← CSV → Canvas 导入逻辑
├── GameplayEditorWindow.cs ← 编辑器窗口 UI
├── GraphDataSchema.cs ← 字段映射配置ScriptableObject
├── GraphExcelSyncManager.cs ← 运行时同步组件
└── ExcelDataProvider.cs ← CSV 读写基础工具
│ ├── ExcelExporter.cs ← Canvas → 多个 CSV 导出
│ └── ExcelImporter.cs 多个 CSV → Canvas 导入
├── GameplayEditorWindow.cs ← 编辑器窗口 UI(支持表格选择)
├── GraphDataSchema.cs ← 字段映射配置ScriptableObject
├── GraphExcelSyncManager.cs ← 运行时同步组件
└── ExcelDataProvider.cs ← CSV 读写基础工具
```
---
### 5.2 快速开始
### 5.2 快速开始(完整流程)
**第一步:创建 GameplayGraph Asset**
在 Project 窗口中右键 → **Create → NodeCanvas → Gameplay Graph**,命名为关卡名,例如 `Level01Graph`。
在 Project 窗口中右键 → **Create → NodeCanvas → Gameplay Graph**,命名为 `ActivityGraph`。
**第二步:创建 GraphDataSchema 配置**
右键 → **Create → GameplayEditor → GraphDataSchema**,命名为 `Level01Schema`,在 Inspector 中可查看字段映射关系(用于编辑器窗口右侧展示)。
**第三步:打开玩法编辑器窗口**
**第二步:打开玩法编辑器窗口**
菜单栏 → **Window → Gameplay Editor**
```
┌──────────────────────────────────────────────────────────────┐
│ [Graph 拖槽] [Schema 拖槽] [导出 Canvas→CSV] [导入 CSV→Canvas] │ ← 工具栏
│ [Graph 拖槽] [Schema 拖槽] [导出所有表→CSV] [导入CSV→Canvas] │ ← 工具栏
├─────────────────────┬────────────────────────────────────────┤
│ Canvas 可视化编辑 │ CSV 字段映射 │
│ 显示所有节点列表 │ 显示 Schema 中配置的字段映射关系 │
│ Canvas 节点列表 │ 表格选择 + 字段映射 │
│ 按表格分组显示所有 │ 下拉菜单选择要查看的表格 │
│ 节点10种类型 │ 显示该表的字段结构 │
└─────────────────────┴────────────────────────────────────────┘
```
`Level01Graph` 和 `Level01Schema` 分别拖入工具栏对应拖槽
`ActivityGraph` 拖入工具栏左侧的 **Graph 拖槽**
**第四步:在 Canvas 中编辑节点**
**第三步:在 Canvas 中添加节点**
双击 `Level01Graph.asset` 打开 NodeCanvas 编辑器,右键画布 → **Add Node → GameplayEditor → GameplayNode** 添加节点。
双击 `ActivityGraph.asset` 打开 NodeCanvas 编辑器,右键画布 → **Add Node → GameplayEditor → [节点类型]**
点击节点在 Inspector 中编辑以下字段
支持的节点类型10种
| 字段名 | 类型 | 说明 |
|-------------------|--------|---------------------------------------------------|
| Level ID | int | 关卡编号 ID对应 ActivityStageID |
| Node ID | int | 节点唯一 ID**同一 Graph 内不能重复** |
| Node Layer | int | 节点层级0 为顶层根节点) |
| Event Type Group | string | 事件类型组,多个用 `\|` 分隔,如 `1\|2\|3` |
| Priority | string | 优先级权重,多个用 `\|` 分隔,如 `10\|20` |
| Event Mapping ID | int | 事件 ID 映射值(对应各 Lut 表中的映射 ID |
| Interact Visible | bool | 交互时是否显示该节点 |
| Node State | bool | 节点是否处于可进入状态 |
| Parent Node ID | int | 父节点的 Node ID**-1 表示根节点(无父节点)** |
| 节点类型 | 对应表格 | 用途 |
|---------|---------|------|
| GameplayNode | BehaviourTree | 行为树逻辑节点 |
| ActivityStageNode | ActivityStage | 关卡配置(场景、地图、相机) |
| BehaviourNodeDocNode | BehaviourNodeDoc | 节点类型说明(注释用) |
| ActivityGroupNode | ActivityGroup | 组合节点说明 |
| UiLutNode | ActivityUiLut | UI资源映射 |
| CameraLutNode | ActivityCameraLut | 相机资源映射 |
| FXLutNode | ActivityFXLut | 特效资源映射 |
| MapInfoNode | ActivityMapInfoLut | 地图打点支持8个点 |
| DialogByStageNode | ActivityDialogInfoByStage | 对话索引 |
| DialogInfoNode | DialogInfo | 对话详细配置 |
拖拽节点端口建立连线,连线关系在导入时会根据 `ParentNodeID` 字段自动重建。
**第四步:编辑节点字段**
**第五步:导出 Canvas → CSV**
点击节点在 Inspector 中填写对应字段。每种节点的字段不同,参考下方字段说明。
工具栏点击 **"导出 Canvas → CSV"**,输出到 `Assets/玩法编辑器.csv`
**第五步:导出所有表 → CSV**
生成格式行1=字段名行2=类型行3=说明行4起=节点数据):
工具栏点击 **"导出所有表 → CSV"**,自动生成 10 个 CSV 文件到 `Assets/` 目录:
```
Assets/
├── BehaviourTree.csv
├── ActivityStage.csv
├── BehaviourNodeDoc.csv
├── ActivityGroup.csv
├── ActivityUiLut.csv
├── ActivityCameraLut.csv
├── ActivityFXLut.csv
├── ActivityMapInfoLut.csv
├── ActivityDialogInfoByStage.csv
└── DialogInfo.csv
```
每个 CSV 格式行1=字段名行2=类型行3=说明行4起=数据):
```
LevelID,NodeID,NodeLayer,EventTypeGroup,Priority,EventMappingID,InteractVisible,NodeState,ParentNodeID
int,int,int,string,string,int,bool,bool,int
关卡编号ID,节点ID唯一,节点层级,节点事件类型组(|分隔),优先级权重(|分隔),存储事件ID映射,交互是否可见,节点是否可进入,父节点ID-1表示无父节点
关卡编号ID,节点ID唯一,节点层级,节点事件类型组(|分隔),优先级权重(|分隔),存储事件ID映射,交互是否可见,节点是否可进入,父节点ID-1表示节点)
1,101,0,1|2,10|20,5001,True,True,-1
1,102,1,3,30,5002,True,True,101
```
> 生成的 `.csv` 可直接用 Excel 打开(打开时选 UTF-8 编码)。
**第六步:用 Excel 批量编辑**
**第六步:批量编辑后导入 CSV → Canvas**
1. 用 Excel 打开任意 CSV打开时选 UTF-8 编码)
2. 修改数据(**不要删除前3行表头**
3. 保存文件
1. 用 Excel 修改 `Assets/玩法编辑器.csv`**不要删除前3行表头**
2. 保存文件
3. 回到 Unity点击 **"导入 CSV → Canvas"**
**第七步:导入 CSV → Canvas**
> **注意:导入会清空当前 Canvas 中的所有节点后重建。** 导入后自动根据 `ParentNodeID` 重建连线,`-1` 表示根节点不连线。
回到 Unity点击 **"导入 CSV → Canvas"**
> **注意:导入会清空当前 Canvas 中的所有节点后重建。** 导入后自动根据 `ParentNodeID` 重建 GameplayNode 连线。
---
### 5.3 嵌套节点示例
### 5.3 各表字段说明
#### 工作表1ActivityStage关卡配置
| 字段 | 类型 | 说明 |
|------|------|------|
| ActivityStageID | int | 行为树ID全局唯一 |
| Doc | string | 关卡备注 |
| SceneName | string | 场景名称 |
| MapInfo | string | 地图打点组ID |
| CloseLoadingDelaySeconds | float | 关闭加载画面延迟(秒) |
| CameraID | int | 默认相机ID |
#### 工作表2BehaviourTree行为树逻辑
| 字段 | 类型 | 说明 |
|------|------|------|
| LevelID | int | 关卡编号ID |
| NodeID | int | 节点唯一ID**同一Graph内不重复** |
| NodeLayer | int | 节点层级0=顶层) |
| EventTypeGroup | string | 事件类型组(`\|`分隔,如 `1\|2\|3` |
| Priority | string | 优先级权重(`\|`分隔,如 `10\|20` |
| EventMappingID | int | 事件ID映射值 |
| InteractVisible | bool | 交互时是否显示 |
| NodeState | bool | 节点是否可进入 |
| ParentNodeID | int | 父节点ID**-1=根节点** |
#### 工作表3BehaviourNodeDoc节点说明
| 字段 | 类型 | 说明 |
|------|------|------|
| NodeType | string | 节点类型(如 DoGetTime |
| Description | string | 节点描述 |
| ChineseMapping | string | 中文节点映射 |
| Param1~5 | string | 参数1~5说明 |
#### 工作表4ActivityGroup组合节点
| 字段 | 类型 | 说明 |
|------|------|------|
| GroupTreeID | string | 组合树ID |
| GroupTreeType | string | 组合树类型/节点名称 |
| Comment | string | 注释 |
#### 3.2ActivityUiLutUI资源映射
| 字段 | 类型 | 说明 |
|------|------|------|
| StageID | int | 关卡ID |
| UIMappingID | int | UI映射ID |
| PrefabPath | string | Prefab路径 |
#### 3.3ActivityCameraLut相机资源映射
| 字段 | 类型 | 说明 |
|------|------|------|
| StageID | int | 关卡ID |
| CamMappingID | int | 相机映射ID |
| PrefabPath | string | Prefab路径 |
#### 3.4ActivityFXLut特效资源映射
| 字段 | 类型 | 说明 |
|------|------|------|
| StageID | int | 关卡ID |
| FXMappingID | int | 特效映射ID |
| PrefabPath | string | Prefab路径 |
#### 3.5ActivityMapInfoLut地图打点
支持最多8个打点每个打点包含 ID 和 Vector3 坐标(格式:`x\|y\|z`
| 字段 | 类型 | 说明 |
|------|------|------|
| InfoID | int | 地图信息ID |
| PointID_1~8 | string | 打点ID |
| Coord_1~8 | Vector3 | 坐标x\|y\|z |
#### 3.6ActivityDialogInfoByStage对话索引
| 字段 | 类型 | 说明 |
|------|------|------|
| StageID | int | 玩法关卡ID |
| DialogConfigTable | string | 对应文本配置表名称 |
#### 3.7DialogInfo对话详细配置
行为树对应节点:`DoShowDialog(DialogInfoID)`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 配置ID |
| isMask | bool | 是否开启蒙层 |
| MaskTarget | string | 蒙层位置(万分比坐标 x,y |
| ClickKey | int | 关闭蒙层键值 |
| DialogType | int | 战中类型 |
| UIType | int | UI样式 |
| PrefabPath | string | UI预制体路径默认空 |
| Name | string | 显示角色名 |
| Text | string | 正文(支持富文本) |
| Duration | float | 显示时间 |
| Params1~5 | string | 特殊参数1~5 |
---
### 5.4 嵌套节点示例(仅 GameplayNode
**CSV 数据:**
@ -287,14 +413,14 @@ int,int,int,...,int
---
### 5.4 运行时自动同步(可选)
### 5.5 运行时自动同步(可选)
如需在运行时自动同步,将 `GraphExcelSyncManager` 组件挂到场景中的 GameObject 上:
1. **Add Component → GraphExcelSyncManager**
2. Inspector 中绑定 `Target Graph``Schema`
之后每 Graph 触发序列化事件时会自动写入 CSV。
之后每<EFBFBD><EFBFBD> Graph 触发序列化事件时会自动写入 CSV。
---
@ -305,6 +431,8 @@ int,int,int,...,int
| 导入后节点全部重叠 | 导入时节点默认位置为 Vector2.zero | 在 Canvas 中手动排布节点位置 |
| CSV 用 Excel 打开乱码 | 编码不一致 | Excel → 数据 → 从文本/CSV 导入 → 选 UTF-8 |
| 导入报错 "CSV file format invalid" | 缺少前3行表头 | 保留 NAME/TYPE/DOC 三行数据从第4行开始 |
| 点击导出提示"请选择 Graph 和 Schema" | 工具栏拖槽为空 | Graph 和 Schema 两个拖槽都需要赋值 |
| Create 菜单找不到 Gameplay Graph | 正常,菜单已注册 | 右键 → Create → NodeCanvas → Gameplay Graph |
| QA 发现流程卡死 | 可能是配置问题也可能是程序问题 | 先策划自检行为树节点报错,确认非配置问题再提程序单 |
| 点击导出提示"请选择 Graph" | Graph 拖槽为空 | Graph 拖槽必须赋值 |
| 导入时提示"Unknown table name" | CSV 文件名不匹配 | 确保 CSV 文件名为标准表名BehaviourTree.csv 等) |
| 某个表导入失败 | 该表的 CSV 格式错误 | 检查前3行表头是否完整数据行是否有缺失 |
| 导出后找不到 CSV 文件 | 导出路径不对 | CSV 文件生成在 Assets/ 目录,刷新 Project 窗口 |
| ParentNodeID 连线没有重建 | 只有 GameplayNode 支持嵌套 | 其他节点类型不支持父子关系 |