Extend gameplay editor to support all 10 table types with ActivityNodeBase architecture
parent
ec0b40acc9
commit
4fefe78bb8
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameplayEditor.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 3.2 ActivityUiLut:UI资源映射表
|
||||
/// </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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 表", "确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
|
|
@ -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如发现流程卡死或报错,需要先向策划负责人提单,策划检查判断确定是非配置问题,再向其他对应下游提出修改需求。,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
|
|
@ -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 发现流程卡死 | 可能是配置问题也可能是程序问题 | 先策划自检行为树节点报错,确认非配置问题再提程序单 |
|
||||
Loading…
Reference in New Issue