From 4fefe78bb8df95735560b4ca35081438be9117ba Mon Sep 17 00:00:00 2001 From: Vinny Date: Mon, 30 Mar 2026 19:45:49 +0800 Subject: [PATCH] Extend gameplay editor to support all 10 table types with ActivityNodeBase architecture --- .../GameplayEditor/Core/ActivityNodeBase.cs | 46 +++ .../GameplayEditor/Core/ActivityStageNode.cs | 59 ++++ .../GameplayEditor/Core/BehaviourDocNodes.cs | 108 ++++++ .../GameplayEditor/Core/DataNodes.cs | 246 ++++++++++++++ .../GameplayEditor/Core/GameplayGraph.cs | 25 +- .../GameplayEditor/Core/GameplayNode.cs | 101 +++--- .../GameplayEditor/Core/LutNodes.cs | 126 +++++++ .../GameplayEditor/Export/ExcelExporter.cs | 42 ++- .../GameplayEditor/Export/ExcelImporter.cs | 115 +++---- .../GameplayEditor/GameplayEditorWindow.cs | 100 +++--- 设计文档/活动关卡事件构建表.csv | 26 ++ 设计文档/玩法编辑器.csv | 122 +++++++ 设计文档/玩法编辑器使用手册.md | 310 ++++++++++++++++++ 13 files changed, 1254 insertions(+), 172 deletions(-) create mode 100644 Assets/BP_Scripts/GameplayEditor/Core/ActivityNodeBase.cs create mode 100644 Assets/BP_Scripts/GameplayEditor/Core/ActivityStageNode.cs create mode 100644 Assets/BP_Scripts/GameplayEditor/Core/BehaviourDocNodes.cs create mode 100644 Assets/BP_Scripts/GameplayEditor/Core/DataNodes.cs create mode 100644 Assets/BP_Scripts/GameplayEditor/Core/LutNodes.cs create mode 100644 设计文档/活动关卡事件构建表.csv create mode 100644 设计文档/玩法编辑器.csv create mode 100644 设计文档/玩法编辑器使用手册.md 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 发现流程卡死 | 可能是配置问题也可能是程序问题 | 先策划自检行为树节点报错,确认非配置问题再提程序单 |