diff --git a/Assets/BP_Scripts/GameplayEditor/Core/ActivityNodeBase.cs b/Assets/BP_Scripts/GameplayEditor/Core/ActivityNodeBase.cs
new file mode 100644
index 0000000..6f06f58
--- /dev/null
+++ b/Assets/BP_Scripts/GameplayEditor/Core/ActivityNodeBase.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using NodeCanvas.Framework;
+using ParadoxNotion;
+using UnityEngine;
+
+namespace GameplayEditor.Core
+{
+ ///
+ /// 所有活动节点类型的抽象基类
+ /// 每个子类对应一张配置表,有独立的字段结构和导出逻辑
+ ///
+ [Serializable]
+ public abstract class ActivityNodeBase : Node
+ {
+ /// 表格名称,用于路由到对应 CSV 文件
+ public abstract string TableName { get; }
+
+ /// 字段名(CSV 第1行)
+ public abstract string[] FieldNames { get; }
+
+ /// 字段类型(CSV 第2行)
+ public abstract string[] FieldTypes { get; }
+
+ /// 字段说明(CSV 第3行)
+ public abstract string[] FieldDocs { get; }
+
+ /// 将节点数据序列化为一行 CSV 数据
+ public abstract Dictionary ToExcelRow();
+
+ /// 从 CSV 一行数据反序列化到节点字段
+ public abstract void FromExcelRow(Dictionary data);
+
+ // 所有子类共用的 Node 抽象属性
+ public override int maxInConnections => -1;
+ public override int maxOutConnections => -1;
+ public override Type outConnectionType => typeof(GameplayConnection);
+ public override bool allowAsPrime => true;
+ public override bool canSelfConnect => false;
+ public override Alignment2x2 commentsAlignment => Alignment2x2.Bottom;
+ public override Alignment2x2 iconAlignment => Alignment2x2.Default;
+
+ protected override Status OnExecute(Component agent, IBlackboard blackboard)
+ => Status.Success;
+ }
+}
diff --git a/Assets/BP_Scripts/GameplayEditor/Core/ActivityStageNode.cs b/Assets/BP_Scripts/GameplayEditor/Core/ActivityStageNode.cs
new file mode 100644
index 0000000..85e8711
--- /dev/null
+++ b/Assets/BP_Scripts/GameplayEditor/Core/ActivityStageNode.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace GameplayEditor.Core
+{
+ ///
+ /// 工作表1:玩法关卡配置表(ActivityStage)
+ /// 索引玩法场景的逻辑ID、场景资源、地图坐标打点list、默认主相机
+ ///
+ [Serializable]
+ public class ActivityStageNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityStage";
+
+ [SerializeField] private int activityStageID;
+ [SerializeField] private string doc = "";
+ [SerializeField] private string sceneName = "";
+ [SerializeField] private string mapInfo = "";
+ [SerializeField] private float closeLoadingDelay;
+ [SerializeField] private int cameraID;
+
+ public override string[] FieldNames => new[] { "ActivityStageID", "Doc", "SceneName", "MapInfo", "CloseLoadingDelaySeconds", "CameraID" };
+ public override string[] FieldTypes => new[] { "int", "string", "string", "ID", "float", "int" };
+ public override string[] FieldDocs => new[] { "行为树ID", "关卡备注", "场景名称", "地图打点组", "是否关闭加载画面(秒)", "默认相机ID" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "ActivityStageID", activityStageID.ToString() },
+ { "Doc", doc },
+ { "SceneName", sceneName },
+ { "MapInfo", mapInfo },
+ { "CloseLoadingDelaySeconds", closeLoadingDelay.ToString("F2") },
+ { "CameraID", cameraID.ToString() },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("ActivityStageID", out var v)) activityStageID = int.Parse(v);
+ if (d.TryGetValue("Doc", out v)) doc = v;
+ if (d.TryGetValue("SceneName", out v)) sceneName = v;
+ if (d.TryGetValue("MapInfo", out v)) mapInfo = v;
+ if (d.TryGetValue("CloseLoadingDelaySeconds", out v)) float.TryParse(v, out closeLoadingDelay);
+ if (d.TryGetValue("CameraID", out v)) int.TryParse(v, out cameraID);
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ activityStageID = UnityEditor.EditorGUILayout.IntField("Activity Stage ID", activityStageID);
+ doc = UnityEditor.EditorGUILayout.TextField("Doc(关卡备注)", doc);
+ sceneName = UnityEditor.EditorGUILayout.TextField("Scene Name", sceneName);
+ mapInfo = UnityEditor.EditorGUILayout.TextField("Map Info(打点组ID)", mapInfo);
+ closeLoadingDelay = UnityEditor.EditorGUILayout.FloatField("Close Loading Delay (s)", closeLoadingDelay);
+ cameraID = UnityEditor.EditorGUILayout.IntField("Camera ID", cameraID);
+ }
+#endif
+ }
+}
diff --git a/Assets/BP_Scripts/GameplayEditor/Core/BehaviourDocNodes.cs b/Assets/BP_Scripts/GameplayEditor/Core/BehaviourDocNodes.cs
new file mode 100644
index 0000000..8aa406e
--- /dev/null
+++ b/Assets/BP_Scripts/GameplayEditor/Core/BehaviourDocNodes.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace GameplayEditor.Core
+{
+ ///
+ /// 工作表3:行为树节点查询与说明
+ /// 用于注释用途,记录节点类型、描述和参数说明
+ ///
+ [Serializable]
+ public class BehaviourNodeDocNode : ActivityNodeBase
+ {
+ public override string TableName => "BehaviourNodeDoc";
+
+ [SerializeField] private string nodeType = "";
+ [SerializeField] private string description = "";
+ [SerializeField] private string chineseMapping = "";
+ [SerializeField] private string param1 = "";
+ [SerializeField] private string param2 = "";
+ [SerializeField] private string param3 = "";
+ [SerializeField] private string param4 = "";
+ [SerializeField] private string param5 = "";
+
+ public override string[] FieldNames => new[] { "NodeType", "Description", "ChineseMapping", "Param1", "Param2", "Param3", "Param4", "Param5" };
+ public override string[] FieldTypes => new[] { "string", "string", "string", "string", "string", "string", "string", "string" };
+ public override string[] FieldDocs => new[] { "节点类型", "节点描述", "中文节点映射", "参数1说明", "参数2说明", "参数3说明", "参数4说明", "参数5说明" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "NodeType", nodeType },
+ { "Description", description },
+ { "ChineseMapping", chineseMapping },
+ { "Param1", param1 },
+ { "Param2", param2 },
+ { "Param3", param3 },
+ { "Param4", param4 },
+ { "Param5", param5 },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("NodeType", out var v)) nodeType = v;
+ if (d.TryGetValue("Description", out v)) description = v;
+ if (d.TryGetValue("ChineseMapping", out v)) chineseMapping = v;
+ if (d.TryGetValue("Param1", out v)) param1 = v;
+ if (d.TryGetValue("Param2", out v)) param2 = v;
+ if (d.TryGetValue("Param3", out v)) param3 = v;
+ if (d.TryGetValue("Param4", out v)) param4 = v;
+ if (d.TryGetValue("Param5", out v)) param5 = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ nodeType = UnityEditor.EditorGUILayout.TextField("Node Type(节点类型)", nodeType);
+ description = UnityEditor.EditorGUILayout.TextField("Description(描述)", description);
+ chineseMapping = UnityEditor.EditorGUILayout.TextField("Chinese Mapping(中文映射)", chineseMapping);
+ UnityEditor.EditorGUILayout.LabelField("参数说明", UnityEditor.EditorStyles.boldLabel);
+ param1 = UnityEditor.EditorGUILayout.TextField("Param1", param1);
+ param2 = UnityEditor.EditorGUILayout.TextField("Param2", param2);
+ param3 = UnityEditor.EditorGUILayout.TextField("Param3", param3);
+ param4 = UnityEditor.EditorGUILayout.TextField("Param4", param4);
+ param5 = UnityEditor.EditorGUILayout.TextField("Param5", param5);
+ }
+#endif
+ }
+
+ ///
+ /// 工作表4:组合节点说明
+ ///
+ [Serializable]
+ public class ActivityGroupNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityGroup";
+
+ [SerializeField] private string groupTreeID = "";
+ [SerializeField] private string groupTreeType = "";
+ [SerializeField] private string comment = "";
+
+ public override string[] FieldNames => new[] { "GroupTreeID", "GroupTreeType", "Comment" };
+ public override string[] FieldTypes => new[] { "string", "string", "string" };
+ public override string[] FieldDocs => new[] { "组合树ID", "组合树类型/节点名称", "注释" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "GroupTreeID", groupTreeID },
+ { "GroupTreeType", groupTreeType },
+ { "Comment", comment },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("GroupTreeID", out var v)) groupTreeID = v;
+ if (d.TryGetValue("GroupTreeType", out v)) groupTreeType = v;
+ if (d.TryGetValue("Comment", out v)) comment = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ groupTreeID = UnityEditor.EditorGUILayout.TextField("Group Tree ID(组合树ID)", groupTreeID);
+ groupTreeType = UnityEditor.EditorGUILayout.TextField("Group Tree Type(类型)", groupTreeType);
+ comment = UnityEditor.EditorGUILayout.TextField("Comment(注释)", comment);
+ }
+#endif
+ }
+}
diff --git a/Assets/BP_Scripts/GameplayEditor/Core/DataNodes.cs b/Assets/BP_Scripts/GameplayEditor/Core/DataNodes.cs
new file mode 100644
index 0000000..8d50bdd
--- /dev/null
+++ b/Assets/BP_Scripts/GameplayEditor/Core/DataNodes.cs
@@ -0,0 +1,246 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace GameplayEditor.Core
+{
+ ///
+ /// 3.5 ActivityMapInfoLut:地图打点存储表
+ /// 支持最多8个打点(点位ID + Vector3坐标),Vector3 存储格式:x|y|z
+ ///
+ [Serializable]
+ public class MapInfoNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityMapInfoLut";
+
+ [Serializable]
+ public struct MapPoint
+ {
+ public string pointID;
+ public Vector3 coord;
+
+ public string CoordToString() => $"{coord.x}|{coord.y}|{coord.z}";
+
+ public static Vector3 ParseCoord(string s)
+ {
+ var parts = s.Split('|');
+ if (parts.Length != 3) return Vector3.zero;
+ return new Vector3(
+ float.TryParse(parts[0], out var x) ? x : 0,
+ float.TryParse(parts[1], out var y) ? y : 0,
+ float.TryParse(parts[2], out var z) ? z : 0);
+ }
+ }
+
+ [SerializeField] private int infoID;
+ [SerializeField] private List points = new List();
+
+ private static readonly int MaxPoints = 8;
+
+ public override string[] FieldNames
+ {
+ get
+ {
+ var names = new List { "InfoID" };
+ for (int i = 1; i <= MaxPoints; i++) { names.Add($"PointID_{i}"); names.Add($"Coord_{i}"); }
+ return names.ToArray();
+ }
+ }
+ public override string[] FieldTypes
+ {
+ get
+ {
+ var types = new List { "int" };
+ for (int i = 0; i < MaxPoints; i++) { types.Add("string"); types.Add("Vector3"); }
+ return types.ToArray();
+ }
+ }
+ public override string[] FieldDocs
+ {
+ get
+ {
+ var docs = new List { "地图信息ID" };
+ for (int i = 1; i <= MaxPoints; i++) { docs.Add($"点位ID_{i}"); docs.Add($"坐标_{i}(x|y|z)"); }
+ return docs.ToArray();
+ }
+ }
+
+ public override Dictionary ToExcelRow()
+ {
+ var row = new Dictionary { { "InfoID", infoID.ToString() } };
+ for (int i = 0; i < MaxPoints; i++)
+ {
+ var p = i < points.Count ? points[i] : new MapPoint();
+ row[$"PointID_{i + 1}"] = p.pointID ?? "";
+ row[$"Coord_{i + 1}"] = i < points.Count ? p.CoordToString() : "";
+ }
+ return row;
+ }
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("InfoID", out var v)) int.TryParse(v, out infoID);
+ points.Clear();
+ for (int i = 1; i <= MaxPoints; i++)
+ {
+ d.TryGetValue($"PointID_{i}", out var pid);
+ d.TryGetValue($"Coord_{i}", out var coord);
+ if (string.IsNullOrEmpty(pid) && string.IsNullOrEmpty(coord)) continue;
+ points.Add(new MapPoint { pointID = pid ?? "", coord = MapPoint.ParseCoord(coord ?? "") });
+ }
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ infoID = UnityEditor.EditorGUILayout.IntField("Info ID", infoID);
+ UnityEditor.EditorGUILayout.LabelField($"打点数量: {points.Count}", UnityEditor.EditorStyles.miniLabel);
+
+ for (int i = 0; i < points.Count; i++)
+ {
+ UnityEditor.EditorGUILayout.BeginVertical(UnityEditor.EditorStyles.helpBox);
+ var p = points[i];
+ p.pointID = UnityEditor.EditorGUILayout.TextField($" Point {i + 1} ID", p.pointID);
+ p.coord = UnityEditor.EditorGUILayout.Vector3Field($" Coord {i + 1}", p.coord);
+ points[i] = p;
+ UnityEditor.EditorGUILayout.EndVertical();
+ }
+
+ UnityEditor.EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("+ 添加打点") && points.Count < MaxPoints)
+ points.Add(new MapPoint { pointID = "", coord = Vector3.zero });
+ if (GUILayout.Button("- 删除末尾") && points.Count > 0)
+ points.RemoveAt(points.Count - 1);
+ UnityEditor.EditorGUILayout.EndHorizontal();
+ }
+#endif
+ }
+
+ ///
+ /// 3.6 ActivityDialogInfoByStage:活动文本UI配置索引表
+ ///
+ [Serializable]
+ public class DialogByStageNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityDialogInfoByStage";
+
+ [SerializeField] private int stageID;
+ [SerializeField] private string dialogConfigTable = "";
+
+ public override string[] FieldNames => new[] { "StageID", "DialogConfigTable" };
+ public override string[] FieldTypes => new[] { "int", "string" };
+ public override string[] FieldDocs => new[] { "玩法关卡ID", "对应文本配置表名称" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "StageID", stageID.ToString() },
+ { "DialogConfigTable", dialogConfigTable },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("StageID", out var v)) int.TryParse(v, out stageID);
+ if (d.TryGetValue("DialogConfigTable", out v)) dialogConfigTable = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ stageID = UnityEditor.EditorGUILayout.IntField("Stage ID(玩法关卡ID)", stageID);
+ dialogConfigTable = UnityEditor.EditorGUILayout.TextField("Dialog Config Table", dialogConfigTable);
+ }
+#endif
+ }
+
+ ///
+ /// 3.7 DialogInfo:对话详细配置表
+ /// 行为树对应节点:DoShowDialog(DialogInfoID)
+ ///
+ [Serializable]
+ public class DialogInfoNode : ActivityNodeBase
+ {
+ public override string TableName => "DialogInfo";
+
+ [SerializeField] private int id;
+ [SerializeField] private bool isMask;
+ [SerializeField] private string maskTarget = ""; // 万分比屏幕坐标 "x,y"
+ [SerializeField] private int clickKey;
+ [SerializeField] private int dialogType;
+ [SerializeField] private int uiType;
+ [SerializeField] private string prefabPath = "";
+ [SerializeField] private string displayName = "";
+ [SerializeField] private string text = "";
+ [SerializeField] private float duration;
+ [SerializeField] private string param1 = "";
+ [SerializeField] private string param2 = "";
+ [SerializeField] private string param3 = "";
+ [SerializeField] private string param4 = "";
+ [SerializeField] private string param5 = "";
+
+ public override string[] FieldNames => new[] { "id", "isMask", "MaskTarget", "ClickKey", "DialogType", "UIType", "PrefabPath", "Name", "Text", "Duration", "Params1", "Params2", "Params3", "Params4", "Params5" };
+ public override string[] FieldTypes => new[] { "int", "bool", "万分比屏幕坐标", "int", "int", "int", "string", "string", "string", "float", "string", "string", "string", "string", "string" };
+ public override string[] FieldDocs => new[] { "配置ID", "是否开启蒙层", "蒙层位置(万分比屏幕坐标)", "关闭蒙层键值", "战中类型", "UI样式", "UI预制体路径(默认不填)", "显示角色名", "正文(支持富文本)", "显示时间", "特殊参数1", "特殊参数2", "特殊参数3", "特殊参数4", "特殊参数5" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "id", id.ToString() },
+ { "isMask", isMask.ToString() },
+ { "MaskTarget", maskTarget },
+ { "ClickKey", clickKey.ToString() },
+ { "DialogType", dialogType.ToString() },
+ { "UIType", uiType.ToString() },
+ { "PrefabPath", prefabPath },
+ { "Name", displayName },
+ { "Text", text },
+ { "Duration", duration.ToString("F2") },
+ { "Params1", param1 },
+ { "Params2", param2 },
+ { "Params3", param3 },
+ { "Params4", param4 },
+ { "Params5", param5 },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("id", out var v)) int.TryParse(v, out id);
+ if (d.TryGetValue("isMask", out v)) bool.TryParse(v, out isMask);
+ if (d.TryGetValue("MaskTarget", out v)) maskTarget = v;
+ if (d.TryGetValue("ClickKey", out v)) int.TryParse(v, out clickKey);
+ if (d.TryGetValue("DialogType", out v)) int.TryParse(v, out dialogType);
+ if (d.TryGetValue("UIType", out v)) int.TryParse(v, out uiType);
+ if (d.TryGetValue("PrefabPath", out v)) prefabPath = v;
+ if (d.TryGetValue("Name", out v)) displayName = v;
+ if (d.TryGetValue("Text", out v)) text = v;
+ if (d.TryGetValue("Duration", out v)) float.TryParse(v, out duration);
+ if (d.TryGetValue("Params1", out v)) param1 = v;
+ if (d.TryGetValue("Params2", out v)) param2 = v;
+ if (d.TryGetValue("Params3", out v)) param3 = v;
+ if (d.TryGetValue("Params4", out v)) param4 = v;
+ if (d.TryGetValue("Params5", out v)) param5 = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ id = UnityEditor.EditorGUILayout.IntField("ID(配置ID)", id);
+ isMask = UnityEditor.EditorGUILayout.Toggle("isMask(开启蒙层)", isMask);
+ if (isMask)
+ maskTarget = UnityEditor.EditorGUILayout.TextField(" Mask Target(万分比坐标 x,y)", maskTarget);
+ clickKey = UnityEditor.EditorGUILayout.IntField("Click Key(关闭蒙层键值)", clickKey);
+ dialogType = UnityEditor.EditorGUILayout.IntField("Dialog Type(战中类型)", dialogType);
+ uiType = UnityEditor.EditorGUILayout.IntField("UI Type(UI样式)", uiType);
+ prefabPath = UnityEditor.EditorGUILayout.TextField("Prefab Path(默认空)", prefabPath);
+ displayName = UnityEditor.EditorGUILayout.TextField("Name(显示角色名)", displayName);
+ UnityEditor.EditorGUILayout.LabelField("Text(正文,支持富文本)");
+ text = UnityEditor.EditorGUILayout.TextArea(text, GUILayout.MinHeight(48));
+ duration = UnityEditor.EditorGUILayout.FloatField("Duration(显示时间)", duration);
+ UnityEditor.EditorGUILayout.LabelField("特殊参数", UnityEditor.EditorStyles.boldLabel);
+ param1 = UnityEditor.EditorGUILayout.TextField("Params1", param1);
+ param2 = UnityEditor.EditorGUILayout.TextField("Params2", param2);
+ param3 = UnityEditor.EditorGUILayout.TextField("Params3", param3);
+ param4 = UnityEditor.EditorGUILayout.TextField("Params4", param4);
+ param5 = UnityEditor.EditorGUILayout.TextField("Params5", param5);
+ }
+#endif
+ }
+}
diff --git a/Assets/BP_Scripts/GameplayEditor/Core/GameplayGraph.cs b/Assets/BP_Scripts/GameplayEditor/Core/GameplayGraph.cs
index eb9ea81..0f8c670 100644
--- a/Assets/BP_Scripts/GameplayEditor/Core/GameplayGraph.cs
+++ b/Assets/BP_Scripts/GameplayEditor/Core/GameplayGraph.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Linq;
using NodeCanvas.Framework;
using ParadoxNotion;
using UnityEngine;
@@ -6,9 +7,10 @@ using UnityEngine;
namespace GameplayEditor.Core
{
[System.Serializable]
+ [UnityEngine.CreateAssetMenu(menuName = "NodeCanvas/Gameplay Graph")]
public class GameplayGraph : Graph
{
- public override System.Type baseNodeType => typeof(GameplayNode);
+ public override System.Type baseNodeType => typeof(ActivityNodeBase);
public override bool requiresAgent => false;
public override bool requiresPrimeNode => false;
public override bool isTree => false;
@@ -26,13 +28,24 @@ namespace GameplayEditor.Core
Export.ExcelImporter.Import(path, this);
}
- public List GetGameplayNodes()
+ public List GetNodesByType() where T : ActivityNodeBase
{
- var result = new List();
- foreach (var node in allNodes)
+ return allNodes.OfType().ToList();
+ }
+
+ public List GetAllActivityNodes()
+ {
+ return allNodes.OfType().ToList();
+ }
+
+ public Dictionary> GetNodesByTableName()
+ {
+ var result = new Dictionary>();
+ foreach (var node in GetAllActivityNodes())
{
- if (node is GameplayNode gNode)
- result.Add(gNode);
+ if (!result.ContainsKey(node.TableName))
+ result[node.TableName] = new List();
+ result[node.TableName].Add(node);
}
return result;
}
diff --git a/Assets/BP_Scripts/GameplayEditor/Core/GameplayNode.cs b/Assets/BP_Scripts/GameplayEditor/Core/GameplayNode.cs
index 479981a..1ccc6ac 100644
--- a/Assets/BP_Scripts/GameplayEditor/Core/GameplayNode.cs
+++ b/Assets/BP_Scripts/GameplayEditor/Core/GameplayNode.cs
@@ -1,17 +1,20 @@
using System;
using System.Collections.Generic;
-using NodeCanvas.Framework;
-using ParadoxNotion;
-using ParadoxNotion.Design;
using ParadoxNotion.Serialization.FullSerializer;
using UnityEngine;
namespace GameplayEditor.Core
{
- [System.Serializable]
+ ///
+ /// 工作表2:行为树逻辑节点
+ /// 每行对应行为树中一个节点,通过 ParentNodeID 构成树形结构
+ ///
+ [Serializable]
[fsSerializeAsReference, fsDeserializeOverwrite]
- public class GameplayNode : Node
+ public class GameplayNode : ActivityNodeBase
{
+ public override string TableName => "BehaviourTree";
+
[SerializeField] private int levelID;
[SerializeField] private int nodeID;
[SerializeField] private int nodeLayer;
@@ -22,71 +25,59 @@ namespace GameplayEditor.Core
[SerializeField] private bool nodeState = true;
[SerializeField] private int parentNodeID = -1;
- public int LevelID { get => levelID; set => levelID = value; }
- public int NodeID { get => nodeID; set => nodeID = value; }
- public int NodeLayer { get => nodeLayer; set => nodeLayer = value; }
+ public int LevelID { get => levelID; set => levelID = value; }
+ public int NodeID { get => nodeID; set => nodeID = value; }
+ public int NodeLayer { get => nodeLayer; set => nodeLayer = value; }
public string EventTypeGroup { get => eventTypeGroup; set => eventTypeGroup = value; }
public string PriorityWeight { get => priorityWeight; set => priorityWeight = value; }
- public int EventMappingID { get => eventMappingID; set => eventMappingID = value; }
+ public int EventMappingID { get => eventMappingID; set => eventMappingID = value; }
public bool InteractVisible { get => interactVisible; set => interactVisible = value; }
- public bool NodeState { get => nodeState; set => nodeState = value; }
- public int ParentNodeID { get => parentNodeID; set => parentNodeID = value; }
+ public bool NodeState { get => nodeState; set => nodeState = value; }
+ public int ParentNodeID { get => parentNodeID; set => parentNodeID = value; }
- public override int maxInConnections => -1;
- public override int maxOutConnections => -1;
- public override Type outConnectionType => typeof(GameplayConnection);
- public override bool allowAsPrime => true;
- public override bool canSelfConnect => false;
- public override Alignment2x2 commentsAlignment => Alignment2x2.Bottom;
- public override Alignment2x2 iconAlignment => Alignment2x2.Default;
+ public override string[] FieldNames => new[] { "LevelID", "NodeID", "NodeLayer", "EventTypeGroup", "Priority", "EventMappingID", "InteractVisible", "NodeState", "ParentNodeID" };
+ public override string[] FieldTypes => new[] { "int", "int", "int", "string", "string", "int", "bool", "bool", "int" };
+ public override string[] FieldDocs => new[] { "关卡编号ID", "节点ID(唯一)", "节点层级", "节点事件类型组(|分隔)", "优先级权重(|分隔)", "存储事件ID映射", "交互是否可见", "节点是否可进入", "父节点ID(-1表示根节点)" };
- public Dictionary ToExcelRow()
+ public override Dictionary ToExcelRow() => new Dictionary
{
- return new Dictionary
- {
- { "LevelID", levelID.ToString() },
- { "NodeID", nodeID.ToString() },
- { "NodeLayer", nodeLayer.ToString() },
- { "EventTypeGroup", eventTypeGroup },
- { "Priority", priorityWeight },
- { "EventMappingID", eventMappingID.ToString() },
- { "InteractVisible", interactVisible.ToString() },
- { "NodeState", nodeState.ToString() },
- { "ParentNodeID", parentNodeID.ToString() }
- };
- }
+ { "LevelID", levelID.ToString() },
+ { "NodeID", nodeID.ToString() },
+ { "NodeLayer", nodeLayer.ToString() },
+ { "EventTypeGroup", eventTypeGroup },
+ { "Priority", priorityWeight },
+ { "EventMappingID", eventMappingID.ToString() },
+ { "InteractVisible",interactVisible.ToString() },
+ { "NodeState", nodeState.ToString() },
+ { "ParentNodeID", parentNodeID.ToString() },
+ };
- public void FromExcelRow(Dictionary rowData)
+ public override void FromExcelRow(Dictionary d)
{
- if (rowData.TryGetValue("LevelID", out var val)) levelID = int.Parse(val);
- if (rowData.TryGetValue("NodeID", out val)) nodeID = int.Parse(val);
- if (rowData.TryGetValue("NodeLayer", out val)) nodeLayer = int.Parse(val);
- if (rowData.TryGetValue("EventTypeGroup", out val)) eventTypeGroup = val;
- if (rowData.TryGetValue("Priority", out val)) priorityWeight = val;
- if (rowData.TryGetValue("EventMappingID", out val)) eventMappingID = int.Parse(val);
- if (rowData.TryGetValue("InteractVisible", out val)) interactVisible = bool.Parse(val);
- if (rowData.TryGetValue("NodeState", out val)) nodeState = bool.Parse(val);
- if (rowData.TryGetValue("ParentNodeID", out val)) parentNodeID = int.Parse(val);
+ if (d.TryGetValue("LevelID", out var v)) levelID = int.Parse(v);
+ if (d.TryGetValue("NodeID", out v)) nodeID = int.Parse(v);
+ if (d.TryGetValue("NodeLayer", out v)) nodeLayer = int.Parse(v);
+ if (d.TryGetValue("EventTypeGroup", out v)) eventTypeGroup = v;
+ if (d.TryGetValue("Priority", out v)) priorityWeight = v;
+ if (d.TryGetValue("EventMappingID", out v)) eventMappingID = int.Parse(v);
+ if (d.TryGetValue("InteractVisible", out v)) interactVisible = bool.Parse(v);
+ if (d.TryGetValue("NodeState", out v)) nodeState = bool.Parse(v);
+ if (d.TryGetValue("ParentNodeID", out v)) parentNodeID = int.Parse(v);
}
#if UNITY_EDITOR
protected override void OnNodeInspectorGUI()
{
- levelID = UnityEditor.EditorGUILayout.IntField("Level ID", levelID);
- nodeID = UnityEditor.EditorGUILayout.IntField("Node ID", nodeID);
- nodeLayer = UnityEditor.EditorGUILayout.IntField("Node Layer", nodeLayer);
- eventTypeGroup = UnityEditor.EditorGUILayout.TextField("Event Type Group", eventTypeGroup);
- priorityWeight = UnityEditor.EditorGUILayout.TextField("Priority", priorityWeight);
- eventMappingID = UnityEditor.EditorGUILayout.IntField("Event Mapping ID", eventMappingID);
+ levelID = UnityEditor.EditorGUILayout.IntField("Level ID", levelID);
+ nodeID = UnityEditor.EditorGUILayout.IntField("Node ID", nodeID);
+ nodeLayer = UnityEditor.EditorGUILayout.IntField("Node Layer", nodeLayer);
+ eventTypeGroup = UnityEditor.EditorGUILayout.TextField("Event Type Group (|分隔)", eventTypeGroup);
+ priorityWeight = UnityEditor.EditorGUILayout.TextField("Priority (|分隔)", priorityWeight);
+ eventMappingID = UnityEditor.EditorGUILayout.IntField("Event Mapping ID", eventMappingID);
interactVisible = UnityEditor.EditorGUILayout.Toggle("Interact Visible", interactVisible);
- nodeState = UnityEditor.EditorGUILayout.Toggle("Node State", nodeState);
- parentNodeID = UnityEditor.EditorGUILayout.IntField("Parent Node ID", parentNodeID);
+ nodeState = UnityEditor.EditorGUILayout.Toggle("Node State", nodeState);
+ parentNodeID = UnityEditor.EditorGUILayout.IntField("Parent Node ID (-1=根节点)", parentNodeID);
}
#endif
-
- protected override Status OnExecute(Component agent, IBlackboard blackboard)
- {
- return Status.Success;
- }
}
}
diff --git a/Assets/BP_Scripts/GameplayEditor/Core/LutNodes.cs b/Assets/BP_Scripts/GameplayEditor/Core/LutNodes.cs
new file mode 100644
index 0000000..5625ae0
--- /dev/null
+++ b/Assets/BP_Scripts/GameplayEditor/Core/LutNodes.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace GameplayEditor.Core
+{
+ ///
+ /// 3.2 ActivityUiLut:UI资源映射表
+ ///
+ [Serializable]
+ public class UiLutNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityUiLut";
+
+ [SerializeField] private int stageID;
+ [SerializeField] private int uiMappingID;
+ [SerializeField] private string prefabPath = "";
+
+ public override string[] FieldNames => new[] { "StageID", "UIMappingID", "PrefabPath" };
+ public override string[] FieldTypes => new[] { "int", "int", "path" };
+ public override string[] FieldDocs => new[] { "关卡ID", "UI映射ID", "Prefab路径" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "StageID", stageID.ToString() },
+ { "UIMappingID", uiMappingID.ToString() },
+ { "PrefabPath", prefabPath },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("StageID", out var v)) int.TryParse(v, out stageID);
+ if (d.TryGetValue("UIMappingID", out v)) int.TryParse(v, out uiMappingID);
+ if (d.TryGetValue("PrefabPath", out v)) prefabPath = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ stageID = UnityEditor.EditorGUILayout.IntField("Stage ID(关卡ID)", stageID);
+ uiMappingID = UnityEditor.EditorGUILayout.IntField("UI Mapping ID", uiMappingID);
+ prefabPath = UnityEditor.EditorGUILayout.TextField("Prefab Path", prefabPath);
+ }
+#endif
+ }
+
+ ///
+ /// 3.3 ActivityCameraLut:相机资源映射表
+ ///
+ [Serializable]
+ public class CameraLutNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityCameraLut";
+
+ [SerializeField] private int stageID;
+ [SerializeField] private int camMappingID;
+ [SerializeField] private string prefabPath = "";
+
+ public override string[] FieldNames => new[] { "StageID", "CamMappingID", "PrefabPath" };
+ public override string[] FieldTypes => new[] { "int", "int", "path" };
+ public override string[] FieldDocs => new[] { "关卡ID", "Cam映射ID", "Prefab路径" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "StageID", stageID.ToString() },
+ { "CamMappingID", camMappingID.ToString() },
+ { "PrefabPath", prefabPath },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("StageID", out var v)) int.TryParse(v, out stageID);
+ if (d.TryGetValue("CamMappingID", out v)) int.TryParse(v, out camMappingID);
+ if (d.TryGetValue("PrefabPath", out v)) prefabPath = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ stageID = UnityEditor.EditorGUILayout.IntField("Stage ID(关卡ID)", stageID);
+ camMappingID = UnityEditor.EditorGUILayout.IntField("Cam Mapping ID", camMappingID);
+ prefabPath = UnityEditor.EditorGUILayout.TextField("Prefab Path", prefabPath);
+ }
+#endif
+ }
+
+ ///
+ /// 3.4 ActivityFXLut:特效资源映射表
+ ///
+ [Serializable]
+ public class FXLutNode : ActivityNodeBase
+ {
+ public override string TableName => "ActivityFXLut";
+
+ [SerializeField] private int stageID;
+ [SerializeField] private int fxMappingID;
+ [SerializeField] private string prefabPath = "";
+
+ public override string[] FieldNames => new[] { "StageID", "FXMappingID", "PrefabPath" };
+ public override string[] FieldTypes => new[] { "int", "int", "path" };
+ public override string[] FieldDocs => new[] { "关卡ID", "FX映射ID", "Prefab路径" };
+
+ public override Dictionary ToExcelRow() => new Dictionary
+ {
+ { "StageID", stageID.ToString() },
+ { "FXMappingID",fxMappingID.ToString() },
+ { "PrefabPath", prefabPath },
+ };
+
+ public override void FromExcelRow(Dictionary d)
+ {
+ if (d.TryGetValue("StageID", out var v)) int.TryParse(v, out stageID);
+ if (d.TryGetValue("FXMappingID", out v)) int.TryParse(v, out fxMappingID);
+ if (d.TryGetValue("PrefabPath", out v)) prefabPath = v;
+ }
+
+#if UNITY_EDITOR
+ protected override void OnNodeInspectorGUI()
+ {
+ stageID = UnityEditor.EditorGUILayout.IntField("Stage ID(关卡ID)", stageID);
+ fxMappingID = UnityEditor.EditorGUILayout.IntField("FX Mapping ID", fxMappingID);
+ prefabPath = UnityEditor.EditorGUILayout.TextField("Prefab Path", prefabPath);
+ }
+#endif
+ }
+}
diff --git a/Assets/BP_Scripts/GameplayEditor/Export/ExcelExporter.cs b/Assets/BP_Scripts/GameplayEditor/Export/ExcelExporter.cs
index 11e6ead..f0bdeda 100644
--- a/Assets/BP_Scripts/GameplayEditor/Export/ExcelExporter.cs
+++ b/Assets/BP_Scripts/GameplayEditor/Export/ExcelExporter.cs
@@ -3,33 +3,51 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Core;
+using UnityEngine;
namespace GameplayEditor.Export
{
public static class ExcelExporter
{
- private static readonly string[] FieldNames = { "LevelID", "NodeID", "NodeLayer", "EventTypeGroup", "Priority", "EventMappingID", "InteractVisible", "NodeState", "ParentNodeID" };
- private static readonly string[] FieldTypes = { "int", "int", "int", "string", "string", "int", "bool", "bool", "int" };
- private static readonly string[] FieldDocs = { "关卡编号ID", "节点ID(唯一)", "节点层级", "节点事件类型组(|分隔)", "优先级权重(|分隔)", "存储事件ID映射", "交互是否可见", "节点是否可进入", "父节点ID(-1表示无父节点)" };
-
- public static void Export(GameplayGraph graph, string excelPath)
+ public static void Export(GameplayGraph graph, string basePath)
{
+ var nodesByTable = graph.GetNodesByTableName();
+
+ 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}");
+ }
+
+ private static void ExportTable(List nodes, string filePath)
+ {
+ if (nodes.Count == 0) return;
+
var lines = new List();
+ var firstNode = nodes[0];
- lines.Add(string.Join(",", FieldNames));
- lines.Add(string.Join(",", FieldTypes));
- lines.Add(string.Join(",", FieldDocs));
+ lines.Add(string.Join(",", firstNode.FieldNames.Select(EscapeCsv)));
+ lines.Add(string.Join(",", firstNode.FieldTypes.Select(EscapeCsv)));
+ lines.Add(string.Join(",", firstNode.FieldDocs.Select(EscapeCsv)));
- var nodes = graph.GetGameplayNodes();
foreach (var node in nodes)
{
var rowData = node.ToExcelRow();
- var values = FieldNames.Select(name => rowData.TryGetValue(name, out var val) ? EscapeCsv(val) : "").ToArray();
+ var values = firstNode.FieldNames.Select(name =>
+ rowData.TryGetValue(name, out var val) ? EscapeCsv(val) : "").ToArray();
lines.Add(string.Join(",", values));
}
- File.WriteAllLines(excelPath, lines);
- UnityEngine.Debug.Log($"Exported {nodes.Count} nodes to {excelPath}");
+ File.WriteAllLines(filePath, lines);
+ Debug.Log($"Exported {nodes.Count} nodes to {filePath}");
}
private static string EscapeCsv(string value)
diff --git a/Assets/BP_Scripts/GameplayEditor/Export/ExcelImporter.cs b/Assets/BP_Scripts/GameplayEditor/Export/ExcelImporter.cs
index c035b12..5b4fffc 100644
--- a/Assets/BP_Scripts/GameplayEditor/Export/ExcelImporter.cs
+++ b/Assets/BP_Scripts/GameplayEditor/Export/ExcelImporter.cs
@@ -10,64 +10,84 @@ namespace GameplayEditor.Export
{
public static class ExcelImporter
{
- private static readonly string[] FieldNames = { "LevelID", "NodeID", "NodeLayer", "EventTypeGroup", "Priority", "EventMappingID", "InteractVisible", "NodeState", "ParentNodeID" };
-
- public static void Import(string excelPath, GameplayGraph graph)
- {
- if (!File.Exists(excelPath))
+ private static readonly Dictionary> NodeFactories =
+ new Dictionary>
{
- Debug.LogError($"Excel file not found: {excelPath}");
+ { "BehaviourTree", () => new GameplayNode() },
+ { "ActivityStage", () => new ActivityStageNode() },
+ { "BehaviourNodeDoc", () => new BehaviourNodeDocNode() },
+ { "ActivityGroup", () => new ActivityGroupNode() },
+ { "ActivityUiLut", () => new UiLutNode() },
+ { "ActivityCameraLut", () => new CameraLutNode() },
+ { "ActivityFXLut", () => new FXLutNode() },
+ { "ActivityMapInfoLut", () => new MapInfoNode() },
+ { "ActivityDialogInfoByStage", () => new DialogByStageNode() },
+ { "DialogInfo", () => new DialogInfoNode() },
+ };
+
+ public static void Import(string filePath, GameplayGraph graph)
+ {
+ var tableName = Path.GetFileNameWithoutExtension(filePath);
+ ImportTable(filePath, graph, tableName);
+ }
+
+ public static void ImportTable(string filePath, GameplayGraph graph, string tableName)
+ {
+ if (!File.Exists(filePath))
+ {
+ Debug.LogError($"CSV not found: {filePath}");
return;
}
- var lines = File.ReadAllLines(excelPath);
+ if (!NodeFactories.TryGetValue(tableName, out var factory))
+ {
+ Debug.LogError($"Unknown table name: {tableName}. Valid tables: {string.Join(", ", NodeFactories.Keys)}");
+ return;
+ }
+
+ var lines = File.ReadAllLines(filePath);
if (lines.Length < 4)
{
- Debug.LogError("CSV file format invalid: need at least NAME, TYPE, DOC, and data rows");
+ Debug.LogError($"CSV format invalid ({filePath}): need at least NAME/TYPE/DOC rows + 1 data row");
return;
}
- var nodesToRemove = graph.allNodes.ToList();
- foreach (var node in nodesToRemove)
- {
+ // 删除当前图中同类型节点
+ var toRemove = graph.allNodes.OfType()
+ .Where(n => n.TableName == tableName).ToList();
+ foreach (var node in toRemove)
graph.RemoveNode(node);
- }
+ var fieldNames = ParseCsv(lines[0]);
+
+ // 按节点 ID 建立父子关系(仅 GameplayNode 使用)
var nodeDict = new Dictionary();
+
for (int i = 3; i < lines.Length; i++)
{
- var rowData = ParseCsvLine(lines[i]);
- if (rowData.Count == 0) continue;
+ if (string.IsNullOrWhiteSpace(lines[i])) continue;
+ var values = ParseCsv(lines[i]);
+ var rowData = new Dictionary();
+ for (int j = 0; j < fieldNames.Count && j < values.Count; j++)
+ rowData[fieldNames[j]] = values[j];
- var node = graph.AddNode(Vector2.zero);
- node.FromExcelRow(rowData);
- nodeDict[node.NodeID] = node;
+ var node = graph.AddNode(factory().GetType(), Vector2.right * i * 220) as ActivityNodeBase;
+ node?.FromExcelRow(rowData);
+
+ if (node is GameplayNode gn)
+ nodeDict[gn.NodeID] = gn;
}
+ // 重建 GameplayNode 父子连线
foreach (var kvp in nodeDict)
{
- var node = kvp.Value;
- if (node.ParentNodeID >= 0 && nodeDict.TryGetValue(node.ParentNodeID, out var parentNode))
- {
- graph.ConnectNodes(parentNode, node);
- }
+ var child = kvp.Value;
+ if (child.ParentNodeID >= 0 && nodeDict.TryGetValue(child.ParentNodeID, out var parent))
+ graph.ConnectNodes(parent, child);
}
graph.Validate();
- Debug.Log($"Imported {nodeDict.Count} nodes from CSV");
- }
-
- private static Dictionary ParseCsvLine(string line)
- {
- var result = new Dictionary();
- var values = ParseCsv(line);
-
- for (int i = 0; i < values.Count && i < FieldNames.Length; i++)
- {
- result[FieldNames[i]] = values[i];
- }
-
- return result;
+ Debug.Log($"Imported {lines.Length - 3} rows from {filePath} as {tableName}");
}
private static List ParseCsv(string line)
@@ -79,28 +99,13 @@ namespace GameplayEditor.Export
for (int i = 0; i < line.Length; i++)
{
var c = line[i];
-
if (c == '"')
{
- if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
- {
- current += '"';
- i++;
- }
- else
- {
- inQuotes = !inQuotes;
- }
- }
- else if (c == ',' && !inQuotes)
- {
- result.Add(current);
- current = "";
- }
- else
- {
- current += c;
+ if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') { current += '"'; i++; }
+ else inQuotes = !inQuotes;
}
+ else if (c == ',' && !inQuotes) { result.Add(current); current = ""; }
+ else current += c;
}
result.Add(current);
diff --git a/Assets/BP_Scripts/GameplayEditor/GameplayEditorWindow.cs b/Assets/BP_Scripts/GameplayEditor/GameplayEditorWindow.cs
index 703c634..d23846a 100644
--- a/Assets/BP_Scripts/GameplayEditor/GameplayEditorWindow.cs
+++ b/Assets/BP_Scripts/GameplayEditor/GameplayEditorWindow.cs
@@ -2,6 +2,7 @@ using UnityEngine;
using UnityEditor;
using NodeCanvas.Framework;
using System.Collections.Generic;
+using System.Linq;
namespace GameplayEditor
{
@@ -9,10 +10,14 @@ namespace GameplayEditor
{
private Graph currentGraph;
private GraphDataSchema currentSchema;
- private ExcelDataProvider excelProvider;
-
private Vector2 canvasScroll;
- private Vector2 tableScroll;
+ private int selectedTableIndex;
+
+ private static readonly string[] TableNames = {
+ "BehaviourTree", "ActivityStage", "BehaviourNodeDoc", "ActivityGroup",
+ "ActivityUiLut", "ActivityCameraLut", "ActivityFXLut",
+ "ActivityMapInfoLut", "ActivityDialogInfoByStage", "DialogInfo"
+ };
[MenuItem("Window/Gameplay Editor")]
public static void ShowWindow()
@@ -33,15 +38,11 @@ namespace GameplayEditor
currentGraph = EditorGUILayout.ObjectField(currentGraph, typeof(Graph), false, GUILayout.Width(200)) as Graph;
currentSchema = EditorGUILayout.ObjectField(currentSchema, typeof(GraphDataSchema), false, GUILayout.Width(200)) as GraphDataSchema;
- if (GUILayout.Button("导出 Canvas → CSV", EditorStyles.toolbarButton, GUILayout.Width(120)))
- {
+ if (GUILayout.Button("导出所有表 → CSV", EditorStyles.toolbarButton, GUILayout.Width(140)))
SyncCanvasToExcel();
- }
- if (GUILayout.Button("导入 CSV → Canvas", EditorStyles.toolbarButton, GUILayout.Width(120)))
- {
+ if (GUILayout.Button("导入 CSV → Canvas", EditorStyles.toolbarButton, GUILayout.Width(140)))
SyncExcelToCanvas();
- }
EditorGUILayout.EndHorizontal();
}
@@ -63,7 +64,7 @@ namespace GameplayEditor
private void DrawCanvasPanel()
{
- EditorGUILayout.LabelField("Canvas 可视化编辑", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField("Canvas 节点列表", EditorStyles.boldLabel);
if (currentGraph == null)
{
@@ -72,14 +73,21 @@ namespace GameplayEditor
}
canvasScroll = EditorGUILayout.BeginScrollView(canvasScroll);
- EditorGUILayout.LabelField($"节点数: {currentGraph.allNodes.Count}", EditorStyles.miniLabel);
- foreach (var node in currentGraph.allNodes)
+ var nodesByTable = (currentGraph as Core.GameplayGraph)?.GetNodesByTableName();
+ if (nodesByTable != null)
{
- EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
- EditorGUILayout.LabelField($"[{node.ID}] {node.name}", GUILayout.Width(200));
- EditorGUILayout.LabelField(node.GetType().Name, EditorStyles.miniLabel);
- EditorGUILayout.EndHorizontal();
+ foreach (var kvp in nodesByTable.OrderBy(x => x.Key))
+ {
+ EditorGUILayout.LabelField($"{kvp.Key} ({kvp.Value.Count})", EditorStyles.boldLabel);
+ foreach (var node in kvp.Value)
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
+ EditorGUILayout.LabelField($" [{node.ID}] {node.name}", GUILayout.Width(200));
+ EditorGUILayout.LabelField(node.GetType().Name, EditorStyles.miniLabel);
+ EditorGUILayout.EndHorizontal();
+ }
+ }
}
EditorGUILayout.EndScrollView();
@@ -87,53 +95,57 @@ namespace GameplayEditor
private void DrawTablePanel()
{
- EditorGUILayout.LabelField("CSV 表格映射", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField("表格选择", EditorStyles.boldLabel);
- if (currentSchema == null)
+ selectedTableIndex = EditorGUILayout.Popup("选择表格", selectedTableIndex, TableNames);
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField($"表格: {TableNames[selectedTableIndex]}", EditorStyles.boldLabel);
+
+ if (currentSchema != null)
{
- EditorGUILayout.HelpBox("请选择一个 Schema", MessageType.Info);
- return;
+ EditorGUILayout.LabelField($"字段映射数: {currentSchema.fields.Count}", EditorStyles.miniLabel);
+ foreach (var field in currentSchema.fields)
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
+ EditorGUILayout.LabelField(field.fieldName, GUILayout.Width(150));
+ EditorGUILayout.LabelField("→", GUILayout.Width(20));
+ EditorGUILayout.LabelField(field.excelColumnName, GUILayout.Width(150));
+ EditorGUILayout.EndHorizontal();
+ }
}
-
- tableScroll = EditorGUILayout.BeginScrollView(tableScroll);
- EditorGUILayout.LabelField($"字段映射数: {currentSchema.fields.Count}", EditorStyles.miniLabel);
-
- foreach (var field in currentSchema.fields)
- {
- EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
- EditorGUILayout.LabelField(field.fieldName, GUILayout.Width(150));
- EditorGUILayout.LabelField("→", GUILayout.Width(20));
- EditorGUILayout.LabelField(field.excelColumnName, GUILayout.Width(150));
- EditorGUILayout.EndHorizontal();
- }
-
- EditorGUILayout.EndScrollView();
}
private void SyncCanvasToExcel()
{
- if (currentGraph == null || currentSchema == null)
+ if (currentGraph == null)
{
- EditorUtility.DisplayDialog("错误", "请选择 Graph 和 Schema", "确定");
+ EditorUtility.DisplayDialog("错误", "请选择 Graph", "确定");
return;
}
- var excelPath = "Assets/玩法编辑器.csv";
- Export.ExcelExporter.Export(currentGraph as Core.GameplayGraph, excelPath);
- EditorUtility.DisplayDialog("成功", $"已导出到 {excelPath}", "确定");
+ var basePath = "Assets/玩法编辑器.csv";
+ (currentGraph as Core.GameplayGraph)?.ExportToExcel(basePath);
+ EditorUtility.DisplayDialog("成功", $"已导出所有表到 Assets/ 目录", "确定");
}
private void SyncExcelToCanvas()
{
- if (currentGraph == null || currentSchema == null)
+ if (currentGraph == null)
{
- EditorUtility.DisplayDialog("错误", "请选择 Graph 和 Schema", "确定");
+ EditorUtility.DisplayDialog("错误", "请选择 Graph", "确定");
return;
}
- var excelPath = "Assets/玩法编辑器.csv";
- Export.ExcelImporter.Import(excelPath, currentGraph as Core.GameplayGraph);
- EditorUtility.DisplayDialog("成功", "已从 CSV 导入", "确定");
+ var basePath = "Assets/";
+ foreach (var tableName in TableNames)
+ {
+ var filePath = System.IO.Path.Combine(basePath, $"{tableName}.csv");
+ if (System.IO.File.Exists(filePath))
+ Export.ExcelImporter.ImportTable(filePath, currentGraph as Core.GameplayGraph, tableName);
+ }
+
+ EditorUtility.DisplayDialog("成功", "已导入所有 CSV 表", "确定");
}
}
}
diff --git a/设计文档/活动关卡事件构建表.csv b/设计文档/活动关卡事件构建表.csv
new file mode 100644
index 0000000..70bbaff
--- /dev/null
+++ b/设计文档/活动关卡事件构建表.csv
@@ -0,0 +1,26 @@
+ʼб,,,,,,,,,,,,,,,,,,,,,,,,,
+NAME,LevelID,NodeID,NodeLayer,EventTypeGroup,Priority,EventMappingID,InteractVisible,NodeState,,,,,,,,,,,,,,,,,
+TYPE,ind,int,int,"(array#sep=|),int","(array#sep=|),int",int,bool,bool,,,,,,,,,,,,,,,,,
+DOC,ؿ½ID,ڵIDΨһ,ڵ㼶,"ڵ
+1ս|2|3|4̵|5Ϣ","Ȩ
+ֱ",洢¼IDӳ,Ƿɼ,ڵǷɽ,,,,,,,,,,,,,,,,,
+VALUE,1001,10001,1,1|2|3|4|5,2000|2000|2000|2000|2000,100001,ĬTRUE,ĬTRUE,,,,,,,,,,,,,,,,,
+,1001,10002,2,1|2|3|4|5,0|0|0|5000|5000,100002,,,,,,,,,,,,,,,,,,,
+,1001,10003,3,1|2|3|4|5,0|0|0|7000|3000,100003,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+¼Actionб,,,,,,,,,,,,,,,,,,,,,,,,,
+NAME,EventID,EventType,EventPool,Removed,,,,,,,,,,,,,,,,,,,,,
+TYPE,int,int,int,bool,,,,,,,,,,,,,,,,,,,,,
+DOC,¼idΨһ,"ڵ
+1ս|2|3|4̵|5Ϣ","¼
+1ս|2|3|4̵|5Ϣ",ǷƳ¼,,,,,,,,,,,,,,,,,,,,,
+VALUE,9001,1,1,ĬTRUE,,,,,,,,,,,,,,,,,,,,,
+,9002,2,2,ĬTRUE,,,,,,,,,,,,,,,,,,,,,
+,9003,4,3,FALSE,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
diff --git a/设计文档/玩法编辑器.csv b/设计文档/玩法编辑器.csv
new file mode 100644
index 0000000..c7068a4
--- /dev/null
+++ b/设计文档/玩法编辑器.csv
@@ -0,0 +1,122 @@
+,1.设计目的,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,优化版本并行开发效率,缩短特殊玩法需求频繁迭代功能的开发周期。,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,2.工作流,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,对应负责人开启活动需求统筹表,下游各组资产制作完成后,根据需求统筹表描述放到对应路径提交上传,策划通过下列几个表格进行配置好索引表并上传,在引擎内使用导入工具检查资源。,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,3.配置,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,活动玩法会涉及到场景资源,镜头资源,UI资源,特效资源,所有资源会配表做静态字典映射,在开启活动模块式根据映射加载对应资源,行为树逻辑编辑器在节点引用时会从对应字典内进行查找引用。,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.1 行为树配置表(自定义name),,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,工作表1:玩法关卡配置表,用来索引玩法场景的逻辑ID,场景资源,地图坐标打点list,默认主相机,,,,,,,,,,,,,,,,,,,,,,
+,,,打点功能编辑器,,,,,,,,,,,,,,,,,,,,,,
+,,,Doc,行为树ID,关卡备注,场景名称,地图打点组,延迟关闭加载图,默认相机ID,...,,,,,,,,,,,,,,,
+,,,Name,ActivityStageID,Doc,SceneName,MapInfo,CloseLoadingDelay,CameraID,,,,,,,,,,,,,,,,
+,,,Type,Int,String,String,ID,Float,Int,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,工作表2:行为树逻辑表,配置行为树头文件和行为树逻辑本身。,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,行为树id,说明(注释),节点,节点,节点,节点,节点,节点,...,行索引id,,,,,,,,,,,,,
+,,,301502490,行为树头文件,平行,,,,,,,,,①【关卡ID+0】为头文件,里面的逻辑只执行一次,用来声明初始数据或执行一些初始化逻辑。,,,,,,,,,,,
+,,,,,,组合 方法:通用关卡赋值,,,,,,,,②【关卡ID】后开始是行为树正文,主要写正式逻辑,默认1秒运行60次(根据性能需求可调整到1秒20次)。,,,,,,,,,,,
+,,,,,,组合 方法:关卡初始化,,,,,,,,③【行索引id】会在当前行拥有功能节点时打表自动生成,方便报错时定位行数。,,,,,,,,,,,
+,,,,,,后面是你自己的逻辑,,,,,,,,④【组合方法】封装函数或子树,类似行为树的子树统一处理一些通用赋值,比如开场是否隐藏UI,是否播放某个入场动画,对多字典进行赋值等等。,,,,,,,,,,,
+,,,,,,,,,,,,,,⑤ 行为树默认包含四个基础执行节点:,,,,,,,,,,,
+,,,30150249,行为树逻辑正文,顺序,,,,,,,,, ● 平行:执行该节点下的所有子节点并返回true,无论子节点是否执行成功。,,,,,,,,,,,
+,,,,,,组合 方法:关卡预准备,,,,,,,, ● 顺序:该节点下一旦有子节点返回false则停止执行返回false,子节点全部返回true该节点才返回true。,,,,,,,,,,,
+,,,,,,平行,,,,,,,, ● 选择:该节点下一旦有子节点返回true则停止执行返回true,子节点全部返回false该节点才返回false。,,,,,,,,,,,
+,,,,,,,组合 方法:关卡通用集合,,,,,,, ● 乱选:随机执行一个子节点,结果为true则返回true,结果false返回false。,,,,,,,,,,,
+,,,,,,,后面是你自己的逻辑,,,,,,,⑥ 需要一个组合方法在头文件里分别在黑板创建1#(当前行为树域)2#(角色域)3#(自定义域)4#(系统全局域),UI,相机,特效,演出配置的映射字典,,,,,,,,,,,
+,,,~,,,,,,,,,,, (或是在外部Data管理,功能节点默认找对应字典)。,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,待讨论:,,,,,,,,,,,,,,,,,,,,,,
+,,,1.行为树运行帧数是否需要做限制。如果限制,会提升性能,但后续一些需要精准tick的逻辑无法实现,比如一些音游点击类的活动。如果不限制,需要对策划配置进行约束比如禁止使用行为树通写每帧大量物体set坐标的逻辑来控制对象移动,,,,,,,,,,,,,,,,,,,,,,
+,,,2.行为树表管理,每个策划一张自己的行为树表区分文件名,如果需要用就自己配置自己的,表头和注释从模板表里同步到策划表,这样可以防止并行开发时互相占用表格。,,,,,,,,,,,,,,,,,,,,,,
+,,,3.索引ID是否统一用int(查询性能以及命名统一性),需要额外做一个ID段分配器,给策划分配自己的独立ID段。,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,工作表3:行为树进行节点查询与说明,主要是注释用,,,,,,,,,,,,,,,,,,,,,,
+,,,例:,,,,,,,,,,,,,,,,,,,,,,
+,,,节点类型,描述,,,中文节点映射,参数1,参数2,参数3,参数4,参数5,,,,,,,,,,,,,
+,,,DoGetTime,"参数1(帧数) = 当前关卡时间(帧数) + 参数2
+(秒 转 帧数)",,,int赋值时间,"int
+帧数存值","fix
+时间 单位秒",,,,,,,,,,,,,,,,
+,,,CheckNpcCamp,比较阵营是否相等或包含参数1NPC参数2阵营 ,(包含、相交、相等都是队友),,,比较阵营,"int
+自定义","int
+自定义",,,,,,,,,,,,,,,,
+,,,DoSetFightTarget,"设置Npc1(参数1)的攻击目标为Npc2(参数2)
+参数1:Npc1
+参数2:Npc2(攻击目标)",,,设置战斗目标,"int
+Npc1","int
+ Npc2的Id",,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,"工作表4:组合节点说明,同上",,,,,,,,,,,,,,,,,,,,,,
+,,,例:,,,,,,,,,,,,,,,,,,,,,,
+,,,组合树ID,组合树类型,注释,,,,,,,,,,,,,,,,,,,,
+,,,String,节点名称,,,,,,,,,,,,,,,,,,,,,
+,,,200009,方法:关卡初始化,,,,,,,,,,,,,,,,,,,,,
+,,,200010,方法:关卡预准备,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.2 ActivityUiLut,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,UI资源,填写需要使用的关卡和映射ID,可以填String,,,,,,,,,,,,,,,,,,,,,,
+,,,关卡ID,UI映射ID,Prefab路径,,,,,,,,,,,,,,,,,,,,
+,,,int,int,path,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.3 ActivityCameraLut,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,相机资源,配置同上,,,,,,,,,,,,,,,,,,,,,,
+,,,关卡ID,Cam映射id,Prefab路径,,,,,,,,,,,,,,,,,,,,
+,,,int,int,path,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.4 ActivityFXLut,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,特效资源,配置同上,,,,,,,,,,,,,,,,,,,,,,
+,,,关卡ID,FX映射id,Prefab路径,,,,,,,,,,,,,,,,,,,,
+,,,int,int,path,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.5 ActivityMapInfoLut,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,地图打点存储表,需要对应的地图点位编辑器,,,,,,,,,,,,,,,,,,,,,,
+,,,infoID,点位ID,坐标,点位ID,坐标,点位ID,坐标,点位ID,坐标,,,,,,,,,,,,,,
+,,,int,String,Vector3,String,Vector3,String,Vector3,String,Vector3,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.6 ActivityDialogInfoByStage,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,活动文本UI配置索引表,文本配置ID就是下面DialogInfo配置表的name,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,玩法关卡ID,对应文本配置表,,,,,,,,,,,,,,,,,,,,,
+,,,int,String,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,3.7 DialogInfo+自定义序号,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,具体文本配置,,,,,,,,,,,,,,,,,,,,,,
+,,,与行为树一样,每个策划一张自己的dialoginfo,需要先跟本地化管线讨论,,,,,,,,,,,,,,,,,,,,,,
+,,,id,isMask,MaskTarget,ClickKey,DialogType,UiType,PrefabPath,Name,Text,Duration,Params1,Params2,Params3,Params4,Params5,,,,,,,,
+,,,配置ID,是否开启蒙层,蒙层位置,关闭蒙层键值,战中类型,UI样式,用来改变同类型战中的样式,"UI预制体路径
+默认不填写",显示角色名,"正文
+支持富文本",显示时间,特殊参数1,特殊参数2,特殊参数3,特殊参数4,特殊参数5,,,,,,,,
+,,,int,bool,万分比屏幕坐标,int,int,int,,String,对话正文,float,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,行为树对应功能节点,,,,,,,,,,,,,,,,,,,,,,
+,,,节点名称,参数1,,,,,,,,,,,,,,,,,,,,,
+,,,DoShowDialog,DialogInfoID,,,,,,,,,,,,,,,,,,,,,
+,,,int,int,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,4.QA&Debug,,,,,,,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,,,,,,,
+,,,1.上传对应行为树前需要策划自检,流程是否完可以整跑通,过程中是否有节点报错,根据对应报错ID行进行Debug,,,,,,,,,,,,,,,,,,,,,,
+,,,2.资源检查,需要策划导入时自行检查资源是否与需求对应,是否在导入时出现丢失索引。,,,,,,,,,,,,,,,,,,,,,,
+,,,3.提测QA后,QA如发现流程卡死或报错,需要先向策划负责人提单,策划检查判断确定是非配置问题,再向其他对应下游提出修改需求。,,,,,,,,,,,,,,,,,,,,,,
diff --git a/设计文档/玩法编辑器使用手册.md b/设计文档/玩法编辑器使用手册.md
new file mode 100644
index 0000000..331275d
--- /dev/null
+++ b/设计文档/玩法编辑器使用手册.md
@@ -0,0 +1,310 @@
+# 玩法编辑器使用手册
+
+## 1. 设计目的
+
+优化版本并行开发效率,缩短特殊玩法需求频繁迭代功能的开发周期。
+
+---
+
+## 2. 工作流
+
+对应负责人开启活动需求统筹表 → 下游各组资产制作完成后 → 根据需求统筹表描述放到对应路径提交上传 → **策划通过玩法编辑器进行配置好索引表并上传** → 在引擎内使用导入工具检查资源。
+
+---
+
+## 3. 配置说明
+
+活动玩法会涉及到场景资源、镜头资源、UI资源、特效资源。所有资源会配表做静态字典映射,在开启活动模块时根据映射加载对应资源。**行为树逻辑编辑器在节点引用时会从对应字典内进行查找引用。**
+
+---
+
+## 3.1 行为树配置表(自定义 name)
+
+### 工作表1:玩法关卡配置表
+
+用来索引玩法场景的逻辑 ID、场景资源、地图坐标打点 list、默认主相机。
+
+| Doc | 行为树ID | 关卡备注 | 场景名称 | 地图打点组 | 是否关闭加载画面 | 默认相机ID |
+|------|------------------|---------|-------------|------------|--------------------------|----------|
+| Name | ActivityStageID | Doc | SceneName | MapInfo | CloseLoadingDelaySeconds | CameraID |
+| Type | Int | String | String | ID | Float | Int |
+
+> 打开:**功能编辑器**
+
+---
+
+### 工作表2:行为树逻辑表
+
+配置行为树头文件和行为树逻辑本身。
+
+| 列名 | 说明 |
+|-----------|-------------------------------------------------------------|
+| 行为树ID | 全局唯一标识(如 301502490) |
+| 说明(注释)| 行为树文件说明 |
+| 节点 | 各级子节点名称(可展开多列) |
+| 行索引ID | 行为节点在行为树中的引用 ID |
+
+**节点层级结构说明(树形):**
+
+```
+行为树ID: 301502490 (行为树文件 平行)
+ ├── 组合 方法:关卡通用关卡赋值
+ ├── 组合 方法:关卡初始化
+ └── 后面是你自己的逻辑
+
+行为树ID: 30150249 (行为树逻辑正文 顺序)
+ ├── 组合 方法:关卡预准备
+ ├── 平行
+ │ └── 组合 方法:关卡通用集合
+ └── 后面是你自己的逻辑
+```
+
+**行为树关键说明:**
+
+1. **【关卡ID+0】** 为头文件,里面的逻辑只执行一次,用来声明初始数据或执行一些初始化逻辑。
+2. **【关卡ID】** 后开始是行为树正文,主要写正式逻辑,默认1秒运行60次(根据性能需求可调整到1秒20次)。
+3. **【行索引】** 会在当前行有子功能节点时打表自动生成,方便报错时定位行数。
+4. **【组合方法】** 封装函数或子树,类似行为树的子树结构一处理一些通用赋值,比如开场是否隐藏UI、是否播放某个入场动画、对多个字典进行赋值等等。
+5. 行为树默认包含四个基础执行节点:
+ - **平行**:执行该节点下的所有子节点并返回true,无论子节点是否执行成功。
+ - **顺序**:该节点下一旦有子节点返回false则停止执行返回false,子节点全部返回true该节点才返回true。
+ - **选择**:该节点下一旦有子节点返回true则停止执行返回true,子节点全部返回false该节点才返回false。
+ - **乱选**:随机执行一个子节点,结果为true则返回true,结果false返回false。
+6. 需要一个组合方法在头文件里分别在黑板里创建1#(当前行为树域)、2#(角色域)、3#(自定义域)、4#(系统全局域)、UI、相机、特效、演出配置的映射字典(**或者是外部Data管理,功能节点默认找对应字典**)。
+
+---
+
+### 工作表3:行为树节点查询与说明(注释用)
+
+| 节点类型 | 描述 | 中文节点映射 | 参数1 | 参数2 | 参数3 | 参数4 | 参数5 |
+|-------------------|----------------------------------------------------------|-------------|-------------|--------------|-------|-------|-------|
+| DoGetTime | 参数1(帧数)= 当前关卡时间(帧数)+ 参数2(秒转帧数) | int赋值时间 | int 帧数存值 | fix 时间单位秒 | | | |
+| CheckNpcCamp | 比较阵营是否相等或包含参数1NPC参数2阵营(包含、相交、相等都是队友) | 比较阵营 | int 自定义 | int 自定义 | | | |
+| DoSetFightTarget | 设置Npc1(参数1)的攻击目标为Npc2(参数2) | 设置战斗目标 | int Npc1 | int Npc2的Id | | | |
+
+---
+
+### 工作表4:组合节点说明(同工作表3)
+
+| 组合树ID | 组合树类型 | 注释 |
+|-----------|-----------|------|
+| String | 节点名称 | |
+| 200009 | 万法:关卡初始化 | |
+| 200010 | 万法:关卡预准备 | |
+
+---
+
+## 3.2 ActivityUiLut
+
+UI 资源。填写需要使用的关卡和映射 ID,可以填 String。
+
+| 关卡ID | UI映射ID | Prefab路径 |
+|-------|---------|-----------|
+| int | int | path |
+
+---
+
+## 3.3 ActivityCameraLut
+
+相机资源,配置同上。
+
+| 关卡ID | Cam映射ID | Prefab路径 |
+|-------|----------|-----------|
+| int | int | path |
+
+---
+
+## 3.4 ActivityFXLut
+
+特效资源,配置同上。
+
+| 关卡ID | FX映射ID | Prefab路径 |
+|-------|---------|-----------|
+| int | int | path |
+
+---
+
+## 3.5 ActivityMapInfoLut
+
+地图打点存储表,需要对应的地图点位编辑器。
+
+| infoID | 点位ID | 坐标 | 点位ID | 坐标 | 点位ID | 坐标 | 点位ID | 坐标 |
+|--------|---------|---------|---------|---------|---------|---------|---------|---------|
+| int | String | Vector3 | String | Vector3 | String | Vector3 | String | Vector3 |
+
+---
+
+## 3.6 ActivityDialogInfoByStage
+
+活动文本 UI 配置索引表。文本配置 ID 就是下面 DialogInfo 配置表的 name。
+
+| 玩法关卡ID | 对应文本配置表 |
+|----------|-------------|
+| int | String |
+
+---
+
+## 3.7 DialogInfo + 自定义序号
+
+具体文本配置(**与行为树一样,每个策划一张自己的 dialoginfo,需要先跟本地化管线讨论**)。
+
+| id | isMask | MaskTarget | ClickKey | DialogType | UIType | PrefabPath | Name | Text | Duration | Params1 | Params2 | Params3 | Params4 | Params5 |
+|--------|--------|---------------|---------|------------|--------|----------------------|---------|-----------|---------|---------|---------|---------|---------|---------|
+| 配置ID | 是否开启蒙层 | 蒙层位置(万分比屏幕坐标) | 关闭蒙层键值 | 战中类型 | UI样式(变同类型战中样式) | UI预制体路径(默认不填写) | 显示角色名 | 正文(支持富文本) | 显示时间 | 特殊参数1 | 特殊参数2 | 特殊参数3 | 特殊参数4 | 特殊参数5 |
+| int | bool | 万分比屏幕坐标 | int | int | int | | String | 对话正文 | float | | | | | |
+
+**行为树对应功能节点:**
+
+| 节点名称 | 参数1 |
+|--------------|-------------|
+| DoShowDialog | DialogInfoID |
+| int | int |
+
+---
+
+## 4. QA & Debug
+
+1. 上传对应行为树前需要策划自检,流程是否可以整跑通,过程中是否有节点报错,根据对应报错 ID 进行 Debug。
+2. 资源检查:需要策划导入时自行检查资源是否与需求对应,是否在导入时出现失索引。
+3. 提测 QA 后,QA 如发现流程卡死或报错,需要先向策划负责人提单,策划检查判断确定是非配置问题,再向其他对应下游提出修改需求。
+
+---
+
+## 5. 玩法编辑器工具操作指南
+
+### 5.1 文件结构
+
+```
+Assets/BP_Scripts/GameplayEditor/
+├── Core/
+│ ├── GameplayGraph.cs ← 自定义 Graph 类型(Asset 本体)
+│ ├── GameplayNode.cs ← 节点数据模型(含9个业务字段)
+│ └── GameplayConnection.cs ← 节点连线类型
+├── Export/
+│ ├── ExcelExporter.cs ← Canvas → CSV 导出逻辑
+│ └── ExcelImporter.cs ← CSV → Canvas 导入逻辑
+├── GameplayEditorWindow.cs ← 编辑器窗口 UI
+├── GraphDataSchema.cs ← 字段映射配置(ScriptableObject)
+├── GraphExcelSyncManager.cs ← 运行时同步组件
+└── ExcelDataProvider.cs ← CSV 读写基础工具
+```
+
+---
+
+### 5.2 快速开始
+
+**第一步:创建 GameplayGraph Asset**
+
+在 Project 窗口中右键 → **Create → NodeCanvas → Gameplay Graph**,命名为关卡名,例如 `Level01Graph`。
+
+**第二步:创建 GraphDataSchema 配置**
+
+右键 → **Create → GameplayEditor → GraphDataSchema**,命名为 `Level01Schema`,在 Inspector 中可查看字段映射关系(用于编辑器窗口右侧展示)。
+
+**第三步:打开玩法编辑器窗口**
+
+菜单栏 → **Window → Gameplay Editor**
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ [Graph 拖槽] [Schema 拖槽] [导出 Canvas→CSV] [导入 CSV→Canvas] │ ← 工具栏
+├─────────────────────┬────────────────────────────────────────┤
+│ Canvas 可视化编辑 │ CSV 字段映射 │
+│ 显示所有节点列表 │ 显示 Schema 中配置的字段映射关系 │
+└─────────────────────┴────────────────────────────────────────┘
+```
+
+将 `Level01Graph` 和 `Level01Schema` 分别拖入工具栏对应拖槽。
+
+**第四步:在 Canvas 中编辑节点**
+
+双击 `Level01Graph.asset` 打开 NodeCanvas 编辑器,右键画布 → **Add Node → GameplayEditor → GameplayNode** 添加节点。
+
+点击节点在 Inspector 中编辑以下字段:
+
+| 字段名 | 类型 | 说明 |
+|-------------------|--------|---------------------------------------------------|
+| 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 表示根节点(无父节点)** |
+
+拖拽节点端口建立连线,连线关系在导入时会根据 `ParentNodeID` 字段自动重建。
+
+**第五步:导出 Canvas → CSV**
+
+工具栏点击 **"导出 Canvas → CSV"**,输出到 `Assets/玩法编辑器.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表示无父节点)
+1,101,0,1|2,10|20,5001,True,True,-1
+1,102,1,3,30,5002,True,True,101
+```
+
+> 生成的 `.csv` 可直接用 Excel 打开(打开时选 UTF-8 编码)。
+
+**第六步:批量编辑后导入 CSV → Canvas**
+
+1. 用 Excel 修改 `Assets/玩法编辑器.csv`(**不要删除前3行表头**)
+2. 保存文件
+3. 回到 Unity,点击 **"导入 CSV → Canvas"**
+
+> **注意:导入会清空当前 Canvas 中的所有节点后重建。** 导入后自动根据 `ParentNodeID` 重建连线,`-1` 表示根节点不连线。
+
+---
+
+### 5.3 嵌套节点示例
+
+**CSV 数据:**
+
+```
+LevelID,NodeID,NodeLayer,...,ParentNodeID
+int,int,int,...,int
+(doc行)
+1,100,0,...,-1
+1,101,1,...,100
+1,102,1,...,100
+1,103,2,...,101
+```
+
+**Canvas 连线结构:**
+
+```
+[NodeID:100](根节点,ParentNodeID=-1)
+ ├── [NodeID:101](ParentNodeID=100)
+ │ └── [NodeID:103](ParentNodeID=101)
+ └── [NodeID:102](ParentNodeID=100)
+```
+
+---
+
+### 5.4 运行时自动同步(可选)
+
+如需在运行时自动同步,将 `GraphExcelSyncManager` 组件挂到场景中的 GameObject 上:
+
+1. **Add Component → GraphExcelSyncManager**
+2. Inspector 中绑定 `Target Graph` 和 `Schema`
+
+之后每次 Graph 触发序列化事件时会自动写入 CSV。
+
+---
+
+## 6. 常见问题
+
+| 问题 | 原因 | 解决方法 |
+|------|------|---------|
+| 导入后节点全部重叠 | 导入时节点默认位置为 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 发现流程卡死 | 可能是配置问题也可能是程序问题 | 先策划自检行为树节点报错,确认非配置问题再提程序单 |