Extend gameplay editor to support all 10 table types with ActivityNodeBase architecture

main^2
Vinny 2026-03-30 19:45:49 +08:00
parent ec0b40acc9
commit 4fefe78bb8
13 changed files with 1254 additions and 172 deletions

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using NodeCanvas.Framework;
using ParadoxNotion;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 所有活动节点类型的抽象基类
/// 每个子类对应一张配置表,有独立的字段结构和导出逻辑
/// </summary>
[Serializable]
public abstract class ActivityNodeBase : Node
{
/// <summary>表格名称,用于路由到对应 CSV 文件</summary>
public abstract string TableName { get; }
/// <summary>字段名CSV 第1行</summary>
public abstract string[] FieldNames { get; }
/// <summary>字段类型CSV 第2行</summary>
public abstract string[] FieldTypes { get; }
/// <summary>字段说明CSV 第3行</summary>
public abstract string[] FieldDocs { get; }
/// <summary>将节点数据序列化为一行 CSV 数据</summary>
public abstract Dictionary<string, string> ToExcelRow();
/// <summary>从 CSV 一行数据反序列化到节点字段</summary>
public abstract void FromExcelRow(Dictionary<string, string> 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;
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 工作表1玩法关卡配置表ActivityStage
/// 索引玩法场景的逻辑ID、场景资源、地图坐标打点list、默认主相机
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "ActivityStageID", activityStageID.ToString() },
{ "Doc", doc },
{ "SceneName", sceneName },
{ "MapInfo", mapInfo },
{ "CloseLoadingDelaySeconds", closeLoadingDelay.ToString("F2") },
{ "CameraID", cameraID.ToString() },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
}

View File

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 工作表3行为树节点查询与说明
/// 用于注释用途,记录节点类型、描述和参数说明
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "NodeType", nodeType },
{ "Description", description },
{ "ChineseMapping", chineseMapping },
{ "Param1", param1 },
{ "Param2", param2 },
{ "Param3", param3 },
{ "Param4", param4 },
{ "Param5", param5 },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
/// <summary>
/// 工作表4组合节点说明
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "GroupTreeID", groupTreeID },
{ "GroupTreeType", groupTreeType },
{ "Comment", comment },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
}

View File

@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 3.5 ActivityMapInfoLut地图打点存储表
/// 支持最多8个打点点位ID + Vector3坐标Vector3 存储格式x|y|z
/// </summary>
[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<MapPoint> points = new List<MapPoint>();
private static readonly int MaxPoints = 8;
public override string[] FieldNames
{
get
{
var names = new List<string> { "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<string> { "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<string> { "地图信息ID" };
for (int i = 1; i <= MaxPoints; i++) { docs.Add($"点位ID_{i}"); docs.Add($"坐标_{i}x|y|z"); }
return docs.ToArray();
}
}
public override Dictionary<string, string> ToExcelRow()
{
var row = new Dictionary<string, string> { { "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<string, string> 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
}
/// <summary>
/// 3.6 ActivityDialogInfoByStage活动文本UI配置索引表
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "StageID", stageID.ToString() },
{ "DialogConfigTable", dialogConfigTable },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
/// <summary>
/// 3.7 DialogInfo对话详细配置表
/// 行为树对应节点DoShowDialog(DialogInfoID)
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "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<string, string> 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 TypeUI样式", 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
}
}

View File

@ -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<GameplayNode> GetGameplayNodes()
public List<T> GetNodesByType<T>() where T : ActivityNodeBase
{
var result = new List<GameplayNode>();
foreach (var node in allNodes)
return allNodes.OfType<T>().ToList();
}
public List<ActivityNodeBase> GetAllActivityNodes()
{
return allNodes.OfType<ActivityNodeBase>().ToList();
}
public Dictionary<string, List<ActivityNodeBase>> GetNodesByTableName()
{
var result = new Dictionary<string, List<ActivityNodeBase>>();
foreach (var node in GetAllActivityNodes())
{
if (node is GameplayNode gNode)
result.Add(gNode);
if (!result.ContainsKey(node.TableName))
result[node.TableName] = new List<ActivityNodeBase>();
result[node.TableName].Add(node);
}
return result;
}

View File

@ -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]
/// <summary>
/// 工作表2行为树逻辑节点
/// 每行对应行为树中一个节点,通过 ParentNodeID 构成树形结构
/// </summary>
[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<string, string> ToExcelRow()
public override Dictionary<string, string> ToExcelRow() => new Dictionary<string, string>
{
return new Dictionary<string, string>
{
{ "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<string, string> rowData)
public override void FromExcelRow(Dictionary<string, string> 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;
}
}
}

View File

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 3.2 ActivityUiLutUI资源映射表
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "StageID", stageID.ToString() },
{ "UIMappingID", uiMappingID.ToString() },
{ "PrefabPath", prefabPath },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
/// <summary>
/// 3.3 ActivityCameraLut相机资源映射表
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "StageID", stageID.ToString() },
{ "CamMappingID", camMappingID.ToString() },
{ "PrefabPath", prefabPath },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
/// <summary>
/// 3.4 ActivityFXLut特效资源映射表
/// </summary>
[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<string, string> ToExcelRow() => new Dictionary<string, string>
{
{ "StageID", stageID.ToString() },
{ "FXMappingID",fxMappingID.ToString() },
{ "PrefabPath", prefabPath },
};
public override void FromExcelRow(Dictionary<string, string> 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
}
}

View File

@ -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<ActivityNodeBase> nodes, string filePath)
{
if (nodes.Count == 0) return;
var lines = new List<string>();
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)

View File

@ -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<string, Func<ActivityNodeBase>> NodeFactories =
new Dictionary<string, Func<ActivityNodeBase>>
{
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<ActivityNodeBase>()
.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<int, GameplayNode>();
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<string, string>();
for (int j = 0; j < fieldNames.Count && j < values.Count; j++)
rowData[fieldNames[j]] = values[j];
var node = graph.AddNode<GameplayNode>(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<string, string> ParseCsvLine(string line)
{
var result = new Dictionary<string, string>();
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<string> 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);

View File

@ -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 表", "确定");
}
}
}

View File

@ -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,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,
1 初始化配置列表
2 NAME LevelID NodeID NodeLayer EventTypeGroup Priority EventMappingID InteractVisible NodeState
3 TYPE ind int int (array#sep=|),int (array#sep=|),int int bool bool
4 DOC 关卡章节ID 节点ID(唯一) 节点层级 节点池类型组 (1战斗|2抉择|3奖励|4商店|5休息区) 类型权重 万分比 存储事件ID映射 交互点是否可见 节点是否可交互
5 VALUE(例) 1001 10001 1 1|2|3|4|5 2000|2000|2000|2000|2000 100001 默认TRUE 默认TRUE
6 1001 10002 2 1|2|3|4|5 0|0|0|5000|5000 100002
7 1001 10003 3 1|2|3|4|5 0|0|0|7000|3000 100003
8
9
10 事件Action配置列表
11 NAME EventID EventType EventPool Removed
12 TYPE int int int bool
13 DOC 事件id(唯一) 节点类型 (1战斗|2抉择|3奖励|4商店|5休息区) 事件池 (1战斗|2抉择|3奖励|4商店|5休息区) 消耗完后是否移除事件池
14 VALUE(例) 9001 1 1 默认TRUE
15 9002 2 2 默认TRUE
16 9003 4 3 FALSE
17
18
19
20
21
22

View File

@ -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如发现流程卡死或报错需要先向策划负责人提单策划检查判断确定是非配置问题再向其他对应下游提出修改需求。,,,,,,,,,,,,,,,,,,,,,,
1 1.设计目的
2
3 优化版本并行开发效率,缩短特殊玩法需求频繁迭代功能的开发周期。
4
5 2.工作流
6
7 对应负责人开启活动需求统筹表,下游各组资产制作完成后,根据需求统筹表描述放到对应路径提交上传,策划通过下列几个表格进行配置好索引表并上传,在引擎内使用导入工具检查资源。
8
9 3.配置
10
11 活动玩法会涉及到场景资源,镜头资源,UI资源,特效资源,所有资源会配表做静态字典映射,在开启活动模块式根据映射加载对应资源,行为树逻辑编辑器在节点引用时会从对应字典内进行查找引用。
12
13 3.1 行为树配置表(自定义name)
14
15 工作表1:玩法关卡配置表,用来索引玩法场景的逻辑ID,场景资源,地图坐标打点list,默认主相机
16 打点功能编辑器
17 Doc 行为树ID 关卡备注 场景名称 地图打点组 延迟关闭加载图 默认相机ID ...
18 Name ActivityStageID Doc SceneName MapInfo CloseLoadingDelay CameraID
19 Type Int String String ID Float Int
20
21 工作表2:行为树逻辑表,配置行为树头文件和行为树逻辑本身。
22
23 行为树id 说明(注释) 节点 节点 节点 节点 节点 节点 ... 行索引id
24 301502490 行为树头文件 平行 ①【关卡ID+0】为头文件,里面的逻辑只执行一次,用来声明初始数据或执行一些初始化逻辑。
25 组合 方法:通用关卡赋值 ②【关卡ID】后开始是行为树正文,主要写正式逻辑,默认1秒运行60次(根据性能需求可调整到1秒20次)。
26 组合 方法:关卡初始化 ③【行索引id】会在当前行拥有功能节点时打表自动生成,方便报错时定位行数。
27 后面是你自己的逻辑 ④【组合方法】封装函数或子树,类似行为树的子树统一处理一些通用赋值,比如开场是否隐藏UI,是否播放某个入场动画,对多字典进行赋值等等。
28 ⑤ 行为树默认包含四个基础执行节点:
29 30150249 行为树逻辑正文 顺序 ● 平行:执行该节点下的所有子节点并返回true,无论子节点是否执行成功。
30 组合 方法:关卡预准备 ● 顺序:该节点下一旦有子节点返回false则停止执行返回false,子节点全部返回true该节点才返回true。
31 平行 ● 选择:该节点下一旦有子节点返回true则停止执行返回true,子节点全部返回false该节点才返回false。
32 组合 方法:关卡通用集合 ● 乱选:随机执行一个子节点,结果为true则返回true,结果false返回false。
33 后面是你自己的逻辑 ⑥ 需要一个组合方法在头文件里分别在黑板创建1#(当前行为树域)2#(角色域)3#(自定义域)4#(系统全局域),UI,相机,特效,演出配置的映射字典
34 ~ (或是在外部Data管理,功能节点默认找对应字典)。
35
36 待讨论:
37 1.行为树运行帧数是否需要做限制。如果限制,会提升性能,但后续一些需要精准tick的逻辑无法实现,比如一些音游点击类的活动。如果不限制,需要对策划配置进行约束比如禁止使用行为树通写每帧大量物体set坐标的逻辑来控制对象移动
38 2.行为树表管理,每个策划一张自己的行为树表区分文件名,如果需要用就自己配置自己的,表头和注释从模板表里同步到策划表,这样可以防止并行开发时互相占用表格。
39 3.索引ID是否统一用int(查询性能以及命名统一性),需要额外做一个ID段分配器,给策划分配自己的独立ID段。
40
41 工作表3:行为树进行节点查询与说明,主要是注释用
42 例:
43 节点类型 描述 中文节点映射 参数1 参数2 参数3 参数4 参数5
44 DoGetTime 参数1(帧数) = 当前关卡时间(帧数) + 参数2 (秒 转 帧数) int赋值时间 int 帧数存值 fix 时间 单位秒
45 CheckNpcCamp 比较阵营是否相等或包含参数1NPC参数2阵营 ,(包含、相交、相等都是队友) 比较阵营 int 自定义 int 自定义
46 DoSetFightTarget 设置Npc1(参数1)的攻击目标为Npc2(参数2) 参数1:Npc1 参数2:Npc2(攻击目标) 设置战斗目标 int Npc1 int Npc2的Id
47
48
49
50 工作表4:组合节点说明,同上
51 例:
52 组合树ID 组合树类型 注释
53 String 节点名称
54 200009 方法:关卡初始化
55 200010 方法:关卡预准备
56
57
58 3.2 ActivityUiLut
59
60 UI资源,填写需要使用的关卡和映射ID,可以填String
61 关卡ID UI映射ID Prefab路径
62 int int path
63
64
65 3.3 ActivityCameraLut
66
67 相机资源,配置同上
68 关卡ID Cam映射id Prefab路径
69 int int path
70
71
72 3.4 ActivityFXLut
73
74 特效资源,配置同上
75 关卡ID FX映射id Prefab路径
76 int int path
77
78
79 3.5 ActivityMapInfoLut
80
81 地图打点存储表,需要对应的地图点位编辑器
82 infoID 点位ID 坐标 点位ID 坐标 点位ID 坐标 点位ID 坐标
83 int String Vector3 String Vector3 String Vector3 String Vector3
84
85
86 3.6 ActivityDialogInfoByStage
87
88 活动文本UI配置索引表,文本配置ID就是下面DialogInfo配置表的name
89
90 玩法关卡ID 对应文本配置表
91 int String
92
93
94 3.7 DialogInfo+自定义序号
95
96 具体文本配置
97 与行为树一样,每个策划一张自己的dialoginfo,需要先跟本地化管线讨论
98 id isMask MaskTarget ClickKey DialogType UiType PrefabPath Name Text Duration Params1 Params2 Params3 Params4 Params5
99 配置ID 是否开启蒙层 蒙层位置 关闭蒙层键值 战中类型 UI样式,用来改变同类型战中的样式 UI预制体路径 默认不填写 显示角色名 正文 支持富文本 显示时间 特殊参数1 特殊参数2 特殊参数3 特殊参数4 特殊参数5
100 int bool 万分比屏幕坐标 int int int String 对话正文 float
101
102 行为树对应功能节点
103 节点名称 参数1
104 DoShowDialog DialogInfoID
105 int int
106
107 4.QA&Debug
108
109 1.上传对应行为树前需要策划自检,流程是否完可以整跑通,过程中是否有节点报错,根据对应报错ID行进行Debug
110 2.资源检查,需要策划导入时自行检查资源是否与需求对应,是否在导入时出现丢失索引。
111 3.提测QA后,QA如发现流程卡死或报错,需要先向策划负责人提单,策划检查判断确定是非配置问题,再向其他对应下游提出修改需求。

View File

@ -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 发现流程卡死 | 可能是配置问题也可能是程序问题 | 先策划自检行为树节点报错,确认非配置问题再提程序单 |