Liyizhen 2026-04-27 14:29:49 +08:00
commit 905b7b37e2
347 changed files with 40978 additions and 2931 deletions

6
.vsconfig Normal file
View File

@ -0,0 +1,6 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.ManagedGame"
]
}

View File

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

View File

@ -0,0 +1,41 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7a686a47eee2fa44cb0a34b5d86e4d5e, type: 3}
m_Name: BT_Kart_Advanced
m_EditorClassIdentifier:
_serializedGraph: '{"type":"NodeCanvas.BehaviourTrees.BehaviourTree","nodes":[{"_name":"\u8f66\u8f86\u4e3b\u63a7\uff08\u5e76\u884c\uff09","_position":{"x":400.0,"y":60.0},"$type":"NodeCanvas.BehaviourTrees.Parallel","$id":"0"},{"_name":"\u8f93\u5165\u540c\u6b65\u6a21\u5757","_position":{"x":180.0,"y":210.0},"$type":"NodeCanvas.BehaviourTrees.Sequencer","$id":"1"},{"_action":{"accelerate":{"_name":"Accelerate"},"turn":{"_name":"Turn"},"brake":{"_name":"Brake"},"handbrake":{"_name":"Handbrake"},"gear":{"_name":"Gear"},"driveMode":{"_name":"DriveMode"},"headlights":{"_name":"Lights"},"engineOn":{"_name":"EngineOn"},"horn":{"_name":"Horn"},"$type":"NLD.Nodes.SetVehicleInput"},"_name":"\u540c\u6b65\u8f93\u5165\u5230BaseInput","_position":{"x":180.0,"y":360.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"2"},{"_name":"\u72b6\u6001\u76d1\u63a7\u6a21\u5757","_position":{"x":400.0,"y":210.0},"$type":"NodeCanvas.BehaviourTrees.Sequencer","$id":"3"},{"_action":{"outLocalSpeed":{"_name":"LocalSpeed"},"outVelocity":{"_name":"Velocity"},"outGear":{"_name":"CurrentGear"},"outState":{"_name":"CurrentState"},"outFuelPercent":{"_name":"FuelPercent"},"outFuelConsumption":{"_name":"FuelConsumption"},"outEngineOn":{"_name":"EngineStatus"},"outLightsOn":{"_name":"LightsStatus"},"$type":"NLD.Nodes.ReadVehicleStatusTask"},"_name":"\u8bfb\u53d6\u8f66\u8f86\u72b6\u6001\u5230\u9ed1\u677f","_position":{"x":400.0,"y":360.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"4"},{"_name":"\u72b6\u6001\u8bfb\u53d6","_position":{"x":620.0,"y":210.0},"$type":"NodeCanvas.BehaviourTrees.Sequencer","$id":"5"},{"_action":{"outLocalSpeed":{},"outVelocity":{},"outGear":{},"outState":{},"outFuelPercent":{},"outFuelConsumption":{},"outEngineOn":{},"outLightsOn":{},"$type":"NLD.Nodes.ReadVehicleStatusTask"},"_name":"\u8bfb\u53d6\u72b6\u6001","_position":{"x":620.0,"y":360.0},"$type":"NodeCanvas.BehaviourTrees.ActionNode","$id":"6"}],"connections":[{"_sourceNode":{"$ref":"0"},"_targetNode":{"$ref":"1"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"0"},"_targetNode":{"$ref":"3"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"0"},"_targetNode":{"$ref":"5"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"1"},"_targetNode":{"$ref":"2"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"3"},"_targetNode":{"$ref":"4"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"},{"_sourceNode":{"$ref":"5"},"_targetNode":{"$ref":"6"},"$type":"NodeCanvas.BehaviourTrees.BTConnection"}],"canvasGroups":[],"localBlackboard":{"_variables":{"Accelerate":{"_name":"Accelerate","_id":"606f050e-c25c-444b-ae49-da6562991999","$type":"NodeCanvas.Framework.Variable`1[[System.Single,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Turn":{"_name":"Turn","_id":"ef72b931-18bf-4f81-adf3-df1280305ba2","$type":"NodeCanvas.Framework.Variable`1[[System.Single,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Brake":{"_name":"Brake","_id":"c9ae4b2c-ebb0-45f7-a7e7-7a28603d9651","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Handbrake":{"_name":"Handbrake","_id":"d8ddd4c5-47f5-47ad-a67c-a0b73627a6fd","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Gear":{"_name":"Gear","_id":"d6c7207d-ebe9-44a5-a810-46bb55371d2a","$type":"NodeCanvas.Framework.Variable`1[[KartGame.KartSystems.GearMode,
KartGame, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},"DriveMode":{"_value":2,"_name":"DriveMode","_id":"6fd1842d-306f-4c08-8aaa-39d224a695c5","$type":"NodeCanvas.Framework.Variable`1[[KartGame.KartSystems.DriveTrainMode,
KartGame, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},"EngineOn":{"_value":true,"_name":"EngineOn","_id":"40934b08-0dfa-4dca-9f47-5f9daeee7169","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Lights":{"_name":"Lights","_id":"9078b220-6821-4adc-8d74-0a3944d9dfaf","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Horn":{"_name":"Horn","_id":"2842451d-9339-446b-8eee-a5f987099680","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"LocalSpeed":{"_name":"LocalSpeed","_id":"28ddb0c2-3e81-4f2c-a918-de8330092f93","$type":"NodeCanvas.Framework.Variable`1[[System.Single,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"Velocity":{"_name":"Velocity","_id":"b112d672-fe70-4b2f-abc7-79a9298c7ec1","$type":"NodeCanvas.Framework.Variable`1[[System.Single,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"CurrentGear":{"_name":"CurrentGear","_id":"755079af-f39e-4c6a-9a54-75d814f6f89d","$type":"NodeCanvas.Framework.Variable`1[[KartGame.KartSystems.GearMode,
KartGame, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},"CurrentState":{"_name":"CurrentState","_id":"b18d2e52-d381-4afa-9626-446127f37397","$type":"NodeCanvas.Framework.Variable`1[[KartGame.KartSystems.VehicleState,
KartGame, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]"},"FuelPercent":{"_value":1.0,"_name":"FuelPercent","_id":"676d92f5-7aa7-432e-b56b-dd31a1c76a17","$type":"NodeCanvas.Framework.Variable`1[[System.Single,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"FuelConsumption":{"_name":"FuelConsumption","_id":"745142e7-9911-4569-af87-3d2e93ef3982","$type":"NodeCanvas.Framework.Variable`1[[System.Single,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"EngineStatus":{"_value":true,"_name":"EngineStatus","_id":"4b3fd72d-0058-4d92-a07c-e18f6f11a02c","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"},"LightsStatus":{"_name":"LightsStatus","_id":"bef36727-02f3-4c3b-b9df-13a65270ff09","$type":"NodeCanvas.Framework.Variable`1[[System.Boolean,
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"}}},"derivedData":{"repeat":true,"$type":"NodeCanvas.BehaviourTrees.BehaviourTree+DerivedSerializationData"}}'
_objectReferences: []
_graphSource:
_version: 3.33
_category:
_comments:
_translation: {x: 0, y: 0}
_zoomFactor: 1
_haltSerialization: 0
_externalSerializationFile: {fileID: 0}

View File

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

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: efb2368ee8bed9140a42c2805613ed11
guid: 4cfe3e64882de254fba06f94153dab56
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 00ebdf5df6fcdac4690f8271175946b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,66 @@
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 相机资源映射
/// </summary>
[CreateAssetMenu(fileName = "CameraLut", menuName = "Gameplay/Lut/Camera Lut")]
public class ActivityCameraLut : ScriptableObject
{
[Tooltip("相机资源映射列表(新版,包含详细参数)")]
public List<CameraMapping> CameraMappings = new List<CameraMapping>();
[Tooltip("是否使用新版详细配置")]
public bool UseDetailedConfig = true;
public CameraMapping FindMapping(int mappingId)
{
return CameraMappings.Find(m => m.MappingID == mappingId);
}
/// <summary>
/// 添加相机映射(向后兼容)
/// </summary>
public CameraMapping AddMapping(int mappingId, string prefabPath)
{
var mapping = new CameraMapping
{
MappingID = mappingId,
PrefabPath = prefabPath
};
mapping.ApplyDefaults();
CameraMappings.Add(mapping);
return mapping;
}
/// <summary>
/// 验证所有相机配置
/// </summary>
public bool ValidateAll(out List<string> errors)
{
errors = new List<string>();
var idSet = new HashSet<int>();
foreach (var mapping in CameraMappings)
{
if (!mapping.Validate(out var error))
{
errors.Add($"CameraID={mapping.MappingID}: {error}");
}
if (idSet.Contains(mapping.MappingID))
{
errors.Add($"重复的CameraID: {mapping.MappingID}");
}
else
{
idSet.Add(mapping.MappingID);
}
}
return errors.Count == 0;
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c3420f187011c1a43824ca0d4c88d6c6
guid: c8a2de62fbe079740b13718f921aefa1
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,82 @@
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 特效资源映射
/// </summary>
[CreateAssetMenu(fileName = "FXLut", menuName = "Gameplay/Lut/FX Lut")]
public class ActivityFXLut : ScriptableObject
{
[Tooltip("特效资源映射列表(新版,包含生命周期参数)")]
public List<FXMapping> FxMappings = new List<FXMapping>();
[Tooltip("是否使用新版详细配置")]
public bool UseDetailedConfig = true;
public FXMapping FindMapping(int mappingId)
{
return FxMappings.Find(m => m.MappingID == mappingId);
}
/// <summary>
/// 添加特效映射(向后兼容)
/// </summary>
public FXMapping AddMapping(int mappingId, string prefabPath)
{
var mapping = new FXMapping
{
MappingID = mappingId,
PrefabPath = prefabPath
};
mapping.ApplyDefaults();
FxMappings.Add(mapping);
return mapping;
}
/// <summary>
/// 获取特定类型的所有特效
/// </summary>
public List<FXMapping> GetMappingsByDuration(float minDuration, float maxDuration)
{
return FxMappings.FindAll(m => m.Duration >= minDuration && m.Duration <= maxDuration);
}
/// <summary>
/// 获取所有循环特效
/// </summary>
public List<FXMapping> GetLoopingEffects()
{
return FxMappings.FindAll(m => m.Loop);
}
/// <summary>
/// 验证所有特效配置
/// </summary>
public bool ValidateAll(out List<string> errors)
{
errors = new List<string>();
var idSet = new HashSet<int>();
foreach (var mapping in FxMappings)
{
if (!mapping.Validate(out var error))
{
errors.Add($"FXID={mapping.MappingID}: {error}");
}
if (idSet.Contains(mapping.MappingID))
{
errors.Add($"重复的FXID: {mapping.MappingID}");
}
else
{
idSet.Add(mapping.MappingID);
}
}
return errors.Count == 0;
}
}
}

View File

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

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 地图信息映射
/// </summary>
[CreateAssetMenu(fileName = "MapInfoLut", menuName = "Gameplay/Lut/MapInfo Lut")]
public class ActivityMapInfoLut : ScriptableObject
{
[Tooltip("地图信息ID")]
public int InfoID;
[Tooltip("点位列表")]
public List<MapPoint> Points = new List<MapPoint>();
public MapPoint FindPoint(string pointId)
{
return Points.Find(p => p.PointID == pointId);
}
public MapPoint FindPoint(int index)
{
if (index >= 0 && index < Points.Count)
return Points[index];
return null;
}
}
}

View File

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

View File

@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using NodeCanvas.BehaviourTrees;
using GameplayEditor.Core;
namespace GameplayEditor.Config
{
/// <summary>
/// 玩法关卡配置数据
/// 对应Excel: ActivityStageConfig Sheet
/// </summary>
[CreateAssetMenu(fileName = "ActivityStageConfig", menuName = "Gameplay/Activity Stage Config")]
public class ActivityStageConfig : ScriptableObject
{
[Header("基础信息")]
[Tooltip("行为树ID唯一")]
public int ActivityStageID;
[Tooltip("关卡备注")]
[TextArea(2, 4)]
public string Doc;
[Tooltip("场景名称")]
public string SceneName;
[Header("地图配置")]
[Tooltip("地图打点组ID引用ActivityMapInfoLut")]
public int MapInfo;
[Header("加载配置")]
[Tooltip("延迟关闭加载图(秒)")]
[Range(0, 10)]
public float CloseLoadingDelay;
[Header("相机配置")]
[Tooltip("默认相机ID引用ActivityCameraLut")]
public int CameraID;
[Header("运行时配置")]
[Tooltip("Tick率模式")]
public TickRateMode TickMode = TickRateMode.Normal;
[Tooltip("行为树运行帧率tick/秒Normal模式下有效")]
[Range(20, 120)]
public int TickRate = 60;
[Tooltip("无限制模式下是否使用固定DeltaTime")]
public bool UseFixedDeltaTimeInUnlimited = true;
[Tooltip("无限制模式下的固定DeltaTime")]
public float UnlimitedDeltaTime = 0.016f;
[Header("关联资产")]
[Tooltip("头文件行为树(可选)")]
public BehaviourTree HeaderTree;
[Tooltip("正文行为树")]
public BehaviourTree BodyTree;
[Tooltip("运行时数据配置")]
public Core.BehaviourTreeRuntimeData RuntimeData;
/// <summary>
/// 获取基础StageID
/// </summary>
public int GetBaseStageID()
{
return Core.BehaviourTreeExtended.GetBaseStageID(ActivityStageID);
}
/// <summary>
/// 是否为头文件配置
/// </summary>
public bool IsHeaderFile()
{
return Core.BehaviourTreeExtended.IsHeaderFile(ActivityStageID);
}
/// <summary>
/// 验证配置完整性
/// </summary>
public bool Validate(out string errorMessage)
{
if (ActivityStageID <= 0)
{
errorMessage = "ActivityStageID必须大于0";
return false;
}
if (string.IsNullOrWhiteSpace(SceneName))
{
errorMessage = "SceneName不能为空";
return false;
}
if (BodyTree == null)
{
errorMessage = "正文行为树不能为空";
return false;
}
errorMessage = null;
return true;
}
/// <summary>
/// 应用默认值(用于修复缺失配置)
/// </summary>
public void ApplyDefaults()
{
var defaultConfig = GameplayDefaultConfig.Instance;
defaultConfig.ApplyDefaults(this);
}
/// <summary>
/// 验证并尝试自动修复
/// </summary>
public bool ValidateAndFix(out string errorMessage)
{
// 先应用默认值
ApplyDefaults();
// 然后验证
return Validate(out errorMessage);
}
/// <summary>
/// 获取完整验证报告
/// </summary>
public List<string> GetValidationReport()
{
var reports = new List<string>();
if (ActivityStageID <= 0)
reports.Add("[错误] ActivityStageID必须大于0");
if (string.IsNullOrWhiteSpace(SceneName))
reports.Add($"[警告] SceneName为空将使用默认值: {GameplayDefaultConfig.Instance.DefaultSceneName}");
if (BodyTree == null)
reports.Add("[错误] 正文行为树不能为空");
if (TickRate < GameplayDefaultConfig.Instance.MinTickRate ||
TickRate > GameplayDefaultConfig.Instance.MaxTickRate)
{
reports.Add($"[警告] TickRate {TickRate} 超出推荐范围 [{GameplayDefaultConfig.Instance.MinTickRate}, {GameplayDefaultConfig.Instance.MaxTickRate}]");
}
if (CloseLoadingDelay < 0)
reports.Add("[警告] CloseLoadingDelay不能为负数");
if (reports.Count == 0)
reports.Add("[通过] 配置验证通过");
return reports;
}
}
/// <summary>
/// 玩法关卡配置容器
/// 管理所有关卡配置
/// </summary>
[CreateAssetMenu(fileName = "StageConfigDatabase", menuName = "Gameplay/Stage Config Database")]
public class ActivityStageConfigDatabase : ScriptableObject
{
[Tooltip("所有关卡配置")]
public List<ActivityStageConfig> Configs = new List<ActivityStageConfig>();
/// <summary>
/// 根据StageID查找配置
/// </summary>
public ActivityStageConfig FindByStageID(int stageId)
{
return Configs.Find(c => c.ActivityStageID == stageId);
}
/// <summary>
/// 根据场景名称查找配置
/// </summary>
public ActivityStageConfig FindBySceneName(string sceneName)
{
return Configs.Find(c => c.SceneName == sceneName);
}
/// <summary>
/// 添加或更新配置
/// </summary>
public void AddOrUpdate(ActivityStageConfig config)
{
var existing = FindByStageID(config.ActivityStageID);
if (existing != null)
{
Configs.Remove(existing);
}
Configs.Add(config);
}
/// <summary>
/// 移除配置
/// </summary>
public bool Remove(int stageId)
{
var config = FindByStageID(stageId);
if (config != null)
{
return Configs.Remove(config);
}
return false;
}
/// <summary>
/// 获取所有基础StageID去重
/// </summary>
public List<int> GetAllBaseStageIDs()
{
var result = new HashSet<int>();
foreach (var config in Configs)
{
result.Add(config.GetBaseStageID());
}
return new List<int>(result);
}
}
/// <summary>
/// 玩法关卡配置解析器
/// 从Excel解析ActivityStageConfig Sheet
/// </summary>
[System.Serializable]
public class ActivityStageConfigData
{
public int ActivityStageID;
public string Doc;
public string SceneName;
public int MapInfo;
public float CloseLoadingDelay;
public int CameraID;
public TickRateMode TickMode = TickRateMode.Normal;
public int TickRate = 60;
public bool UseFixedDeltaTimeInUnlimited = true;
public float UnlimitedDeltaTime = 0.016f;
/// <summary>
/// 从字典创建
/// </summary>
public static ActivityStageConfigData FromDictionary(Dictionary<string, string> data)
{
var result = new ActivityStageConfigData();
if (data.TryGetValue("ActivityStageID", out var idStr) && int.TryParse(idStr, out var id))
result.ActivityStageID = id;
if (data.TryGetValue("Doc", out var doc))
result.Doc = doc;
if (data.TryGetValue("SceneName", out var sceneName))
result.SceneName = sceneName;
if (data.TryGetValue("MapInfo", out var mapInfoStr) && int.TryParse(mapInfoStr, out var mapInfo))
result.MapInfo = mapInfo;
if (data.TryGetValue("CloseLoadingDelay", out var delayStr) && float.TryParse(delayStr, out var delay))
result.CloseLoadingDelay = delay;
if (data.TryGetValue("CameraID", out var cameraIdStr) && int.TryParse(cameraIdStr, out var cameraId))
result.CameraID = cameraId;
if (data.TryGetValue("TickMode", out var tickModeStr) && System.Enum.TryParse<TickRateMode>(tickModeStr, out var tickMode))
result.TickMode = tickMode;
if (data.TryGetValue("TickRate", out var tickRateStr) && int.TryParse(tickRateStr, out var tickRate))
result.TickRate = tickRate;
if (data.TryGetValue("UseFixedDeltaTimeInUnlimited", out var useFixedDtStr) && bool.TryParse(useFixedDtStr, out var useFixedDt))
result.UseFixedDeltaTimeInUnlimited = useFixedDt;
if (data.TryGetValue("UnlimitedDeltaTime", out var unlimitedDtStr) && float.TryParse(unlimitedDtStr, out var unlimitedDt))
result.UnlimitedDeltaTime = unlimitedDt;
return result;
}
/// <summary>
/// 应用到ScriptableObject
/// </summary>
public void ApplyTo(ActivityStageConfig config)
{
config.ActivityStageID = ActivityStageID;
config.Doc = Doc;
config.SceneName = SceneName;
config.MapInfo = MapInfo;
config.CloseLoadingDelay = CloseLoadingDelay;
config.CameraID = CameraID;
config.TickMode = TickMode;
config.TickRate = TickRate;
config.UseFixedDeltaTimeInUnlimited = UseFixedDeltaTimeInUnlimited;
config.UnlimitedDeltaTime = UnlimitedDeltaTime;
}
}
}

View File

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

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// UI资源映射
/// </summary>
[CreateAssetMenu(fileName = "UILut", menuName = "Gameplay/Lut/UI Lut")]
public class ActivityUiLut : ScriptableObject
{
[Tooltip("UI资源映射列表")]
public List<ResourceMapping> UiMappings = new List<ResourceMapping>();
public ResourceMapping FindMapping(int mappingId)
{
return UiMappings.Find(m => m.MappingID == mappingId);
}
}
}

View File

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

View File

@ -0,0 +1,361 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 关卡对话信息映射条目
/// 建立关卡ID与DialogInfo配置表的关联
/// </summary>
[Serializable]
public class DialogInfoByStageEntry
{
[Tooltip("玩法关卡ID")]
public int StageID;
[Tooltip("对应文本配置表名(如 DialogInfo_策划A")]
public string DialogInfoSheetName;
[Tooltip("备注说明")]
public string Doc;
}
/// <summary>
/// 关卡对话信息映射配置
/// 对应Excel: ActivityDialogInfoByStage Sheet
/// 作为索引表将StageID映射到具体的DialogInfo配置表
/// </summary>
[CreateAssetMenu(fileName = "DialogInfoByStageConfig", menuName = "Gameplay/Dialog Info By Stage Config")]
public class DialogInfoByStageConfig : ScriptableObject
{
[Tooltip("映射条目列表")]
public List<DialogInfoByStageEntry> Entries = new List<DialogInfoByStageEntry>();
[Tooltip("配置来源Excel路径")]
public string SourceExcelPath = "";
/// <summary>
/// StageID到SheetName的映射缓存
/// </summary>
[NonSerialized]
private Dictionary<int, string> _stageToSheetCache;
/// <summary>
/// 根据StageID查找对应的DialogInfo表名
/// </summary>
public string GetSheetNameByStageID(int stageId)
{
if (_stageToSheetCache == null)
BuildCache();
return _stageToSheetCache.TryGetValue(stageId, out var sheetName) ? sheetName : null;
}
/// <summary>
/// 查找条目
/// </summary>
public DialogInfoByStageEntry FindEntry(int stageId)
{
return Entries.Find(e => e.StageID == stageId);
}
/// <summary>
/// 添加或更新映射
/// </summary>
public void AddOrUpdateEntry(DialogInfoByStageEntry entry)
{
var existing = FindEntry(entry.StageID);
if (existing != null)
{
Entries.Remove(existing);
}
Entries.Add(entry);
_stageToSheetCache = null; // 清除缓存
}
/// <summary>
/// 移除映射
/// </summary>
public bool RemoveEntry(int stageId)
{
var entry = FindEntry(stageId);
if (entry != null)
{
Entries.Remove(entry);
_stageToSheetCache = null;
return true;
}
return false;
}
/// <summary>
/// 构建映射缓存
/// </summary>
public void BuildCache()
{
_stageToSheetCache = new Dictionary<int, string>();
foreach (var entry in Entries)
{
if (!_stageToSheetCache.ContainsKey(entry.StageID))
{
_stageToSheetCache[entry.StageID] = entry.DialogInfoSheetName;
}
else
{
Debug.LogWarning($"[DialogInfoByStage] 重复的StageID: {entry.StageID}");
}
}
}
/// <summary>
/// 获取所有StageID列表
/// </summary>
public List<int> GetAllStageIDs()
{
if (_stageToSheetCache == null)
BuildCache();
return new List<int>(_stageToSheetCache.Keys);
}
/// <summary>
/// 获取映射字典供DialogInfoDatabase使用
/// </summary>
public Dictionary<int, string> GetStageToSheetDictionary()
{
if (_stageToSheetCache == null)
BuildCache();
return new Dictionary<int, string>(_stageToSheetCache);
}
/// <summary>
/// 验证配置完整性
/// </summary>
public bool Validate(out List<string> errors)
{
errors = new List<string>();
var stageIdSet = new HashSet<int>();
var sheetNameSet = new HashSet<string>();
foreach (var entry in Entries)
{
// 检查StageID
if (entry.StageID <= 0)
{
errors.Add($"无效的StageID: {entry.StageID}");
}
else if (stageIdSet.Contains(entry.StageID))
{
errors.Add($"重复的StageID: {entry.StageID}");
}
else
{
stageIdSet.Add(entry.StageID);
}
// 检查SheetName
if (string.IsNullOrWhiteSpace(entry.DialogInfoSheetName))
{
errors.Add($"StageID={entry.StageID} 的DialogInfoSheetName不能为空");
}
else
{
sheetNameSet.Add(entry.DialogInfoSheetName);
}
}
return errors.Count == 0;
}
/// <summary>
/// 获取统计信息
/// </summary>
public string GetStatistics()
{
return $"总共 {Entries.Count} 个关卡映射,涉及 {new HashSet<string>(Entries.ConvertAll(e => e.DialogInfoSheetName)).Count} 个DialogInfo表";
}
}
/// <summary>
/// 对话信息运行时管理器
/// 整合DialogInfoByStageConfig和DialogInfoDatabase提供统一的查询接口
/// 同时管理运行时对话状态
/// </summary>
public class DialogInfoManager
{
private static DialogInfoManager _instance;
public static DialogInfoManager Instance => _instance ??= new DialogInfoManager();
private DialogInfoByStageConfig _byStageConfig;
private DialogInfoDatabase _database;
/// <summary>
/// 是否已初始化
/// </summary>
public bool IsInitialized => _database != null && _byStageConfig != null;
// 运行时对话状态
private Dictionary<int, DialogState> _dialogStates = new Dictionary<int, DialogState>();
/// <summary>
/// 对话状态
/// </summary>
private class DialogState
{
public bool IsActive;
public float StartTime;
public float Duration;
}
/// <summary>
/// 初始化管理器
/// </summary>
public void Initialize(DialogInfoByStageConfig byStageConfig, DialogInfoDatabase database)
{
_byStageConfig = byStageConfig;
_database = database;
// 构建数据库缓存
if (_byStageConfig != null && _database != null)
{
var mapping = _byStageConfig.GetStageToSheetDictionary();
_database.BuildStageCache(mapping);
}
}
/// <summary>
/// 获取指定关卡的所有对话
/// </summary>
public List<DialogInfoData> GetDialogsByStage(int stageId)
{
if (_database == null)
{
Debug.LogError("[DialogInfoManager] 未初始化,数据库为空");
return new List<DialogInfoData>();
}
return _database.GetDialogsByStage(stageId);
}
/// <summary>
/// 根据对话ID查找对话
/// </summary>
public DialogInfoData GetDialogByID(int dialogId)
{
if (_database == null)
{
Debug.LogError("[DialogInfoManager] 未初始化,数据库为空");
return null;
}
return _database.FindDialogByID(dialogId);
}
/// <summary>
/// 获取关卡对应的DialogInfo表名
/// </summary>
public string GetSheetNameByStage(int stageId)
{
if (_byStageConfig == null)
{
Debug.LogError("[DialogInfoManager] 未初始化,索引配置为空");
return null;
}
return _byStageConfig.GetSheetNameByStageID(stageId);
}
// ═════════════════════════════════════════════════════════════════
// 运行时对话状态管理
// ═════════════════════════════════════════════════════════════════
/// <summary>
/// 显示对话
/// </summary>
public bool ShowDialog(int dialogId)
{
var dialog = GetDialogByID(dialogId);
if (dialog == null)
{
Debug.LogWarning($"[DialogInfoManager] 未找到对话配置: ID={dialogId}");
return false;
}
// 记录对话状态
_dialogStates[dialogId] = new DialogState
{
IsActive = true,
StartTime = Time.time,
Duration = dialog.Duration
};
// TODO: 调用UI系统显示对话
// UIManager.Instance?.ShowDialog(dialog);
Debug.Log($"[DialogInfoManager] 显示对话: ID={dialogId}, Text={dialog.Text}");
return true;
}
/// <summary>
/// 关闭对话
/// </summary>
public void CloseDialog(int dialogId)
{
if (_dialogStates.ContainsKey(dialogId))
{
_dialogStates[dialogId].IsActive = false;
}
// TODO: 调用UI系统关闭对话
// UIManager.Instance?.CloseDialog(dialogId);
Debug.Log($"[DialogInfoManager] 关闭对话: ID={dialogId}");
}
/// <summary>
/// 关闭所有对话
/// </summary>
public void CloseAllDialogs()
{
foreach (var kvp in _dialogStates)
{
kvp.Value.IsActive = false;
}
// TODO: 调用UI系统关闭所有对话
// UIManager.Instance?.CloseAllDialogs();
Debug.Log("[DialogInfoManager] 关闭所有对话");
}
/// <summary>
/// 检查对话是否激活
/// </summary>
public bool IsDialogActive(int dialogId)
{
if (_dialogStates.TryGetValue(dialogId, out var state))
{
return state.IsActive;
}
return false;
}
/// <summary>
/// 获取对话剩余时间
/// </summary>
public float GetDialogRemainingTime(int dialogId)
{
if (_dialogStates.TryGetValue(dialogId, out var state) && state.IsActive)
{
if (state.Duration <= 0)
return -1; // 永久显示
var elapsed = Time.time - state.StartTime;
return Mathf.Max(0, state.Duration - elapsed);
}
return 0;
}
}
}

View File

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

View File

@ -0,0 +1,415 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 对话信息数据
/// 对应Excel: DialogInfo Sheet
/// </summary>
[Serializable]
public class DialogInfoData
{
[Tooltip("配置ID")]
public int ID;
[Tooltip("是否开启蒙层")]
public bool IsMask = false;
[Tooltip("蒙层位置(万分比屏幕坐标)")]
public Vector2 MaskTarget = Vector2.zero;
[Tooltip("关闭蒙层键值")]
public string ClickKey = "";
[Tooltip("战中类型")]
public int DialogType = 0;
[Tooltip("UI样式用来改变同类型战中的样式")]
public int UiType = 0;
[Tooltip("UI预制体路径")]
public string PrefabPath = "";
[Tooltip("显示角色名")]
public string CharacterName = "";
[Tooltip("对话正文(支持富文本)")]
[TextArea(3, 10)]
public string Text = "";
[Tooltip("显示时间0表示一直显示直到点击")]
public float Duration = 0f;
[Tooltip("特殊参数1")]
public string Params1 = "";
[Tooltip("特殊参数2")]
public string Params2 = "";
[Tooltip("特殊参数3")]
public string Params3 = "";
[Tooltip("特殊参数4")]
public string Params4 = "";
[Tooltip("特殊参数5")]
public string Params5 = "";
[Header("富文本")]
[Tooltip("是否启用富文本")]
public bool EnableRichText = true;
[Tooltip("富文本样式名称(留空使用默认)")]
public string RichTextStyleName = "";
[Tooltip("自定义富文本配置(可选)")]
public RichTextConfig CustomRichTextConfig;
[Tooltip("文本是否需要验证")]
public bool ValidateRichText = true;
[Header("多语言")]
[Tooltip("英文文本")]
public string Text_EN = "";
[Tooltip("日文文本")]
public string Text_JP = "";
[Tooltip("韩文文本")]
public string Text_KR = "";
[Tooltip("法文文本")]
public string Text_FR = "";
[Tooltip("德文文本")]
public string Text_DE = "";
[Tooltip("西班牙文文本")]
public string Text_ES = "";
/// <summary>
/// 获取参数列表
/// </summary>
public string[] GetParams()
{
return new[] { Params1, Params2, Params3, Params4, Params5 };
}
/// <summary>
/// 替换文本中的占位符
/// </summary>
public string FormatText(params string[] args)
{
if (string.IsNullOrEmpty(Text))
return Text;
try
{
return string.Format(Text, args);
}
catch (FormatException)
{
Debug.LogWarning($"[DialogInfo] 文本格式化失败: ID={ID}, Text={Text}");
return Text;
}
}
/// <summary>
/// 验证配置有效性
/// </summary>
public bool Validate(out string errorMessage)
{
if (ID <= 0)
{
errorMessage = $"DialogInfo ID必须大于0";
return false;
}
if (string.IsNullOrWhiteSpace(Text))
{
errorMessage = $"DialogInfo ID={ID} 的Text不能为空";
return false;
}
// 富文本验证
if (ValidateRichText && EnableRichText && !string.IsNullOrEmpty(Text) && Text.Contains("<"))
{
var config = CustomRichTextConfig ?? GetEffectiveRichTextConfig();
if (config != null)
{
var validator = config.CreateValidator();
var result = validator.Validate(Text);
if (!result.IsValid)
{
errorMessage = $"DialogInfo ID={ID} 的富文本验证失败: {string.Join(", ", result.Errors)}";
return false;
}
}
}
errorMessage = null;
return true;
}
/// <summary>
/// 验证富文本(返回详细结果)
/// </summary>
public Utils.RichTextValidator.ValidationResult ValidateRichTextContent()
{
if (!EnableRichText || string.IsNullOrEmpty(Text) || !Text.Contains("<"))
{
return new Utils.RichTextValidator.ValidationResult
{
IsValid = true,
CleanedText = Text
};
}
var config = GetEffectiveRichTextConfig();
if (config == null)
{
return new Utils.RichTextValidator.ValidationResult
{
IsValid = true,
CleanedText = Text
};
}
var validator = config.CreateValidator();
return validator.Validate(Text);
}
/// <summary>
/// 获取有效的富文本配置
/// </summary>
public RichTextConfig GetEffectiveRichTextConfig()
{
// 优先使用自定义配置
if (CustomRichTextConfig != null)
return CustomRichTextConfig;
// 否则使用默认配置
var defaultConfig = GameplayDefaultConfig.Instance;
if (defaultConfig?.DefaultDialogRichTextStyle != null)
{
// 从样式创建配置
var config = ScriptableObject.CreateInstance<RichTextConfig>();
config.InitializeDefaults();
return config;
}
return null;
}
/// <summary>
/// 清理富文本(移除非法标签)
/// </summary>
public string SanitizeRichText()
{
var result = ValidateRichTextContent();
return result.CleanedText ?? Text;
}
/// <summary>
/// 应用默认值
/// </summary>
public void ApplyDefaults()
{
var defaultConfig = GameplayDefaultConfig.Instance;
defaultConfig.ApplyDefaults(this);
}
/// <summary>
/// 验证并尝试自动修复
/// </summary>
public bool ValidateAndFix(out string errorMessage)
{
ApplyDefaults();
return Validate(out errorMessage);
}
}
/// <summary>
/// 对话信息配置容器
/// 单个DialogInfo表的配置
/// </summary>
[CreateAssetMenu(fileName = "DialogInfoConfig", menuName = "Gameplay/Dialog Info Config")]
public class DialogInfoConfig : ScriptableObject
{
[Tooltip("配置来源Sheet名称")]
public string SheetName = "";
[Tooltip("配置来源Excel路径")]
public string SourceExcelPath = "";
[Tooltip("对话配置列表")]
public List<DialogInfoData> Dialogs = new List<DialogInfoData>();
/// <summary>
/// 根据ID查找对话配置
/// </summary>
public DialogInfoData FindByID(int id)
{
return Dialogs.Find(d => d.ID == id);
}
/// <summary>
/// 添加或更新对话配置
/// </summary>
public void AddOrUpdate(DialogInfoData data)
{
var existing = FindByID(data.ID);
if (existing != null)
{
Dialogs.Remove(existing);
}
Dialogs.Add(data);
}
/// <summary>
/// 验证所有配置
/// </summary>
public bool ValidateAll(out List<string> errors)
{
errors = new List<string>();
// 检查ID唯一性
var idSet = new HashSet<int>();
foreach (var dialog in Dialogs)
{
if (idSet.Contains(dialog.ID))
{
errors.Add($"重复的DialogInfo ID: {dialog.ID}");
}
idSet.Add(dialog.ID);
// 验证单个配置
if (!dialog.Validate(out var error))
{
errors.Add(error);
}
}
return errors.Count == 0;
}
}
/// <summary>
/// 对话信息数据库
/// 管理多个DialogInfo配置表
/// 支持按StageID查找对应对话
/// </summary>
[CreateAssetMenu(fileName = "DialogInfoDatabase", menuName = "Gameplay/Dialog Info Database")]
public class DialogInfoDatabase : ScriptableObject
{
[Tooltip("所有对话配置表")]
public List<DialogInfoConfig> Configs = new List<DialogInfoConfig>();
/// <summary>
/// StageID到Config的映射缓存
/// </summary>
[NonSerialized]
private Dictionary<int, DialogInfoConfig> _stageToConfigCache;
/// <summary>
/// 根据对话ID查找配置在所有表中搜索
/// </summary>
public DialogInfoData FindDialogByID(int dialogId)
{
foreach (var config in Configs)
{
var dialog = config.FindByID(dialogId);
if (dialog != null)
return dialog;
}
return null;
}
/// <summary>
/// 获取指定关卡的所有对话
/// </summary>
public List<DialogInfoData> GetDialogsByStage(int stageId)
{
var result = new List<DialogInfoData>();
// 通过映射表查找对应的Config
if (_stageToConfigCache == null)
BuildStageCache();
if (_stageToConfigCache.TryGetValue(stageId, out var config))
{
result.AddRange(config.Dialogs);
}
return result;
}
/// <summary>
/// 添加配置表
/// </summary>
public void AddConfig(DialogInfoConfig config)
{
if (!Configs.Contains(config))
{
Configs.Add(config);
_stageToConfigCache = null; // 清除缓存
}
}
/// <summary>
/// 构建StageID缓存需要配合DialogInfoByStageConfig使用
/// </summary>
public void BuildStageCache(Dictionary<int, string> stageToSheetName = null)
{
_stageToConfigCache = new Dictionary<int, DialogInfoConfig>();
if (stageToSheetName == null)
return;
foreach (var kvp in stageToSheetName)
{
var stageId = kvp.Key;
var sheetName = kvp.Value;
// 查找匹配的Config
var config = Configs.Find(c =>
c.SheetName == sheetName ||
c.name.Contains(sheetName));
if (config != null)
{
_stageToConfigCache[stageId] = config;
}
}
}
/// <summary>
/// 清空缓存
/// </summary>
public void ClearCache()
{
_stageToConfigCache = null;
}
/// <summary>
/// 获取所有对话(跨所有配置表)
/// </summary>
public IEnumerable<DialogInfoData> GetAllDialogs()
{
foreach (var config in Configs)
{
if (config?.Dialogs != null)
{
foreach (var dialog in config.Dialogs)
{
yield return dialog;
}
}
}
}
}
}

View File

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

View File

@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 事件类型枚举
/// </summary>
public enum EventType
{
Battle = 1, // 战斗
Choice = 2, // 抉择
Reward = 3, // 奖励
Shop = 4, // 商店
Rest = 5 // 休息区
}
/// <summary>
/// 事件池类型
/// </summary>
public enum EventPool
{
Battle = 1,
Choice = 2,
Reward = 3,
Shop = 4,
Rest = 5
}
/// <summary>
/// 事件节点配置
/// 对应初始化配置列表
/// </summary>
[Serializable]
public class EventNodeConfig
{
[Tooltip("行索引(自动生成)")]
public int RowIndex;
[Tooltip("关卡章节ID")]
public int LevelID;
[Tooltip("节点ID唯一")]
public int NodeID;
[Tooltip("节点层级")]
public int NodeLayer;
[Tooltip("节点池类型组1战斗|2抉择|3奖励|4商店|5休息区")]
public List<int> EventTypeGroup = new List<int>();
[Tooltip("类型权重万分比如2000|2000|2000|2000|2000")]
public List<int> Priority = new List<int>();
[Tooltip("存储事件ID映射")]
public int EventMappingID;
[Tooltip("交互点是否可见")]
public bool InteractVisible = true;
[Tooltip("节点是否可交互")]
public bool NodeState = true;
}
/// <summary>
/// 事件Action配置
/// 对应事件Action配置列表
/// </summary>
[Serializable]
public class EventActionConfig
{
[Tooltip("事件ID唯一")]
public int EventID;
[Tooltip("节点类型1战斗|2抉择|3奖励|4商店|5休息区")]
public EventType Type;
[Tooltip("事件池")]
public EventPool Pool;
[Tooltip("消耗完后是否移除事件池")]
public bool Removed = true;
}
/// <summary>
/// 活动关卡事件构建配置容器
/// </summary>
[CreateAssetMenu(fileName = "EventBuilderConfig", menuName = "Gameplay/Event Builder Config")]
public class EventBuilderConfig : ScriptableObject
{
[Header("节点配置")]
[Tooltip("初始化配置列表")]
public List<EventNodeConfig> NodeConfigs = new List<EventNodeConfig>();
[Header("事件配置")]
[Tooltip("事件Action配置列表")]
public List<EventActionConfig> ActionConfigs = new List<EventActionConfig>();
[Header("事件映射")]
[Tooltip("EventMappingID 到 EventID 列表的映射配置")]
public EventMappingConfig EventMappingConfig;
[Header("元数据")]
[Tooltip("关联的关卡ID")]
public int LevelID;
[Tooltip("配置来源Excel路径")]
public string SourceExcelPath;
/// <summary>
/// 根据NodeID查找节点配置
/// </summary>
public EventNodeConfig FindNodeByID(int nodeId)
{
return NodeConfigs.Find(n => n.NodeID == nodeId);
}
/// <summary>
/// 根据EventID查找事件配置
/// </summary>
public EventActionConfig FindActionByID(int eventId)
{
return ActionConfigs.Find(a => a.EventID == eventId);
}
/// <summary>
/// 获取指定层级的所有节点
/// </summary>
public List<EventNodeConfig> GetNodesByLayer(int layer)
{
return NodeConfigs.FindAll(n => n.NodeLayer == layer);
}
/// <summary>
/// 根据 EventMappingID 获取对应的事件列表
/// 如果 EventMappingConfig 中未找到,则根据 nodeConfig 的 EventTypeGroup 做 Fallback 匹配
/// </summary>
public List<EventActionConfig> GetEventsByMappingID(int mappingId, EventNodeConfig nodeConfig = null)
{
var result = new List<EventActionConfig>();
// 1. 优先查找显式映射表
if (EventMappingConfig != null)
{
var entry = EventMappingConfig.FindByMappingID(mappingId);
if (entry != null && entry.EventIDs != null)
{
foreach (var eventId in entry.EventIDs)
{
var action = FindActionByID(eventId);
if (action != null) result.Add(action);
}
if (result.Count > 0) return result;
}
}
// 2. Fallback根据 EventTypeGroup 筛选 ActionConfigs
if (nodeConfig != null && nodeConfig.EventTypeGroup != null)
{
foreach (var action in ActionConfigs)
{
if (nodeConfig.EventTypeGroup.Contains((int)action.Type))
{
result.Add(action);
}
}
}
return result;
}
/// <summary>
/// 添加或更新节点配置
/// </summary>
public void AddOrUpdateNode(EventNodeConfig config)
{
var existing = FindNodeByID(config.NodeID);
if (existing != null)
{
NodeConfigs.Remove(existing);
}
NodeConfigs.Add(config);
}
/// <summary>
/// 添加或更新事件配置
/// </summary>
public void AddOrUpdateAction(EventActionConfig config)
{
var existing = FindActionByID(config.EventID);
if (existing != null)
{
ActionConfigs.Remove(existing);
}
ActionConfigs.Add(config);
}
/// <summary>
/// 验证配置完整性
/// </summary>
public bool Validate(out string errorMessage)
{
// 检查节点ID唯一性
var nodeIds = new HashSet<int>();
foreach (var node in NodeConfigs)
{
if (nodeIds.Contains(node.NodeID))
{
errorMessage = $"重复的NodeID: {node.NodeID}";
return false;
}
nodeIds.Add(node.NodeID);
}
// 检查事件ID唯一性
var eventIds = new HashSet<int>();
foreach (var action in ActionConfigs)
{
if (eventIds.Contains(action.EventID))
{
errorMessage = $"重复的EventID: {action.EventID}";
return false;
}
eventIds.Add(action.EventID);
}
errorMessage = null;
return true;
}
/// <summary>
/// 应用默认值到所有节点和事件
/// </summary>
public void ApplyDefaults()
{
var defaultConfig = GameplayDefaultConfig.Instance;
foreach (var node in NodeConfigs)
{
defaultConfig.ApplyDefaults(node);
}
}
/// <summary>
/// 验证并尝试自动修复
/// </summary>
public bool ValidateAndFix(out string errorMessage)
{
ApplyDefaults();
return Validate(out errorMessage);
}
/// <summary>
/// 检查权重总和是否正确
/// </summary>
public bool ValidateWeights(out List<string> errorMessages)
{
errorMessages = new List<string>();
var defaultConfig = GameplayDefaultConfig.Instance;
foreach (var node in NodeConfigs)
{
if (node.Priority == null || node.Priority.Count == 0)
{
errorMessages.Add($"NodeID={node.NodeID} 的Priority为空");
continue;
}
int total = 0;
foreach (var weight in node.Priority)
{
total += weight;
}
if (total != defaultConfig.TotalWeight)
{
errorMessages.Add($"NodeID={node.NodeID} 的权重总和为 {total},应为 {defaultConfig.TotalWeight}");
}
}
return errorMessages.Count == 0;
}
}
/// <summary>
/// 事件构建数据(解析中间结构)
/// </summary>
[Serializable]
public class EventBuilderData
{
public List<EventNodeConfig> NodeConfigs = new List<EventNodeConfig>();
public List<EventActionConfig> ActionConfigs = new List<EventActionConfig>();
public int LevelID;
public EventMappingConfig EventMappingConfig;
/// <summary>
/// 应用到ScriptableObject
/// </summary>
public void ApplyTo(EventBuilderConfig config)
{
config.NodeConfigs = NodeConfigs;
config.ActionConfigs = ActionConfigs;
config.LevelID = LevelID;
config.EventMappingConfig = EventMappingConfig;
}
/// <summary>
/// 基于 EventTypeGroup 自动生成默认的 EventMapping 配置
/// 用于当前 Excel 中没有独立 EventMapping 表时的 Fallback
/// </summary>
public EventMappingConfig BuildDefaultMappings()
{
var mappingConfig = ScriptableObject.CreateInstance<EventMappingConfig>();
foreach (var node in NodeConfigs)
{
if (node.EventMappingID <= 0) continue;
if (node.EventTypeGroup == null || node.EventTypeGroup.Count == 0) continue;
var entry = new EventMappingEntry
{
MappingID = node.EventMappingID,
Doc = $"自动生成的默认映射 (NodeID={node.NodeID})"
};
foreach (var action in ActionConfigs)
{
if (node.EventTypeGroup.Contains((int)action.Type))
{
entry.EventIDs.Add(action.EventID);
}
}
if (entry.EventIDs.Count > 0)
{
mappingConfig.AddOrUpdateEntry(entry);
}
}
return mappingConfig;
}
}
}

View File

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

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 事件映射条目
/// EventMappingID 与具体 EventID 列表的关联
/// </summary>
[Serializable]
public class EventMappingEntry
{
[Tooltip("事件映射ID来自EventNodeConfig.EventMappingID")]
public int MappingID;
[Tooltip("关联的事件ID列表")]
public List<int> EventIDs = new List<int>();
[Tooltip("备注")]
public string Doc;
}
/// <summary>
/// 事件映射配置表
/// 建立 EventMappingID -> EventID List 的映射
/// </summary>
[CreateAssetMenu(fileName = "EventMappingConfig", menuName = "Gameplay/Event Mapping Config")]
public class EventMappingConfig : ScriptableObject
{
[Tooltip("事件映射条目列表")]
public List<EventMappingEntry> Entries = new List<EventMappingEntry>();
/// <summary>
/// 根据 MappingID 查找条目
/// </summary>
public EventMappingEntry FindByMappingID(int mappingId)
{
return Entries.Find(e => e.MappingID == mappingId);
}
/// <summary>
/// 添加或更新映射条目
/// </summary>
public void AddOrUpdateEntry(EventMappingEntry entry)
{
var existing = FindByMappingID(entry.MappingID);
if (existing != null)
{
Entries.Remove(existing);
}
Entries.Add(entry);
}
/// <summary>
/// 验证配置完整性
/// </summary>
public bool Validate(out List<string> errors)
{
errors = new List<string>();
var idSet = new HashSet<int>();
foreach (var entry in Entries)
{
if (entry.MappingID <= 0)
{
errors.Add($"无效的 MappingID: {entry.MappingID}");
continue;
}
if (idSet.Contains(entry.MappingID))
{
errors.Add($"重复的 MappingID: {entry.MappingID}");
}
idSet.Add(entry.MappingID);
if (entry.EventIDs == null || entry.EventIDs.Count == 0)
{
errors.Add($"MappingID={entry.MappingID} 的 EventIDs 为空");
}
}
return errors.Count == 0;
}
}
}

View File

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

View File

@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using GameplayEditor.Core;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 游戏玩法配置默认值中心
/// 集中管理所有配置项的默认值,支持项目级自定义
/// </summary>
[CreateAssetMenu(fileName = "GameplayDefaultConfig", menuName = "Gameplay/Default Config")]
public class GameplayDefaultConfig : ScriptableObject
{
#region 单例访问
private static GameplayDefaultConfig _instance;
/// <summary>
/// 获取默认配置实例
/// </summary>
public static GameplayDefaultConfig Instance
{
get
{
if (_instance == null)
{
_instance = Resources.Load<GameplayDefaultConfig>("GameplayDefaultConfig");
if (_instance == null)
{
// 创建默认配置
_instance = CreateDefaultConfig();
}
}
return _instance;
}
}
/// <summary>
/// 设置自定义实例(用于测试或动态替换)
/// </summary>
public static void SetInstance(GameplayDefaultConfig config)
{
_instance = config;
}
#endregion
#region ActivityStageConfig 默认值
[Header("关卡配置默认值")]
[Tooltip("默认场景名称")]
public string DefaultSceneName = "MainScene";
[Tooltip("默认Tick率模式")]
public TickRateMode DefaultTickMode = TickRateMode.Normal;
[Tooltip("默认Tick率")]
[Range(20, 120)]
public int DefaultTickRate = 60;
[Tooltip("默认延迟关闭加载图时间")]
public float DefaultCloseLoadingDelay = 1f;
[Tooltip("默认相机ID")]
public int DefaultCameraID = 1001;
[Tooltip("默认地图打点组ID")]
public int DefaultMapInfo = 10001;
[Tooltip("无限制模式下是否使用固定DeltaTime")]
public bool DefaultUseFixedDeltaTimeInUnlimited = true;
[Tooltip("无限制模式下的固定DeltaTime")]
public float DefaultUnlimitedDeltaTime = 0.016f;
#endregion
#region DialogInfoConfig 默认值
[Header("对话配置默认值")]
[Tooltip("默认对话显示时间")]
public float DefaultDialogDuration = 3f;
[Tooltip("默认是否开启蒙层")]
public bool DefaultDialogIsMask = false;
[Tooltip("默认蒙层位置")]
public Vector2 DefaultDialogMaskTarget = Vector2.zero;
[Tooltip("默认战中类型")]
public int DefaultDialogType = 0;
[Tooltip("默认UI样式")]
public int DefaultDialogUiType = 0;
[Tooltip("默认角色名")]
public string DefaultDialogCharacterName = "角色";
[Tooltip("默认是否启用富文本")]
public bool DefaultDialogEnableRichText = true;
[Tooltip("默认富文本样式")]
public RichTextStyle DefaultDialogRichTextStyle;
#endregion
#region EventBuilderConfig 默认值
[Header("事件配置默认值")]
[Tooltip("默认节点层级")]
public int DefaultNodeLayer = 1;
[Tooltip("默认事件类型组")]
public List<int> DefaultEventTypeGroup = new List<int> { 1, 2, 3, 4, 5 };
[Tooltip("默认类型权重(万分比)")]
public List<int> DefaultPriority = new List<int> { 2000, 2000, 2000, 2000, 2000 };
[Tooltip("默认交互点可见性")]
public bool DefaultInteractVisible = true;
[Tooltip("默认节点交互状态")]
public bool DefaultNodeState = true;
[Tooltip("默认事件消耗后是否移除")]
public bool DefaultEventRemoved = true;
#endregion
#region CameraConfig 默认值
[Header("相机配置默认值")]
[Tooltip("默认FOV")]
[Range(1, 179)]
public float DefaultCameraFOV = 60f;
[Tooltip("默认优先级")]
public int DefaultCameraPriority = 10;
[Tooltip("默认跟随偏移")]
public Vector3 DefaultCameraFollowOffset = new Vector3(0, 5, -10);
[Tooltip("默认注视偏移")]
public Vector3 DefaultCameraLookAtOffset = Vector3.zero;
[Tooltip("默认过渡时间")]
public float DefaultCameraBlendTime = 0.5f;
[Tooltip("默认过渡样式")]
public CinemachineBlendStyle DefaultCameraBlendStyle = CinemachineBlendStyle.EaseInOut;
#endregion
#region FXConfig 默认值
[Header("特效配置默认值")]
[Tooltip("默认特效持续时间")]
public float DefaultFXDuration = 2f;
[Tooltip("默认是否循环")]
public bool DefaultFXLoop = false;
[Tooltip("默认是否自动销毁")]
public bool DefaultFXAutoDestroy = true;
[Tooltip("默认延迟时间")]
public float DefaultFXDelay = 0f;
[Tooltip("默认播放速度")]
public float DefaultFXSpeed = 1f;
[Tooltip("默认缩放")]
public float DefaultFXScale = 1f;
[Tooltip("默认使用对象池")]
public bool DefaultFXUseObjectPool = true;
#endregion
#region 行为树默认值
[Header("行为树默认值")]
[Tooltip("默认最大递归深度")]
public int DefaultMaxRecursionDepth = 10;
[Tooltip("默认最大节点深度")]
public int DefaultMaxNodeDepth = 100;
[Tooltip("默认节点执行警告阈值")]
public int DefaultFrameExecutionThreshold = 60;
[Tooltip("默认节点执行耗时警告阈值(毫秒)")]
public float DefaultExecutionTimeThresholdMs = 5f;
#endregion
#region ID范围约束
[Header("ID范围约束")]
[Tooltip("启用严格ID范围检查")]
public bool EnableStrictIDRangeCheck = true;
[Tooltip("关卡ID最小值")]
public int MinStageID = 1000;
[Tooltip("关卡ID最大值")]
public int MaxStageID = 1999;
[Tooltip("节点ID最小值")]
public int MinNodeID = 10000;
[Tooltip("节点ID最大值")]
public int MaxNodeID = 19999;
[Tooltip("事件ID最小值")]
public int MinEventID = 9000;
[Tooltip("事件ID最大值")]
public int MaxEventID = 9999;
[Tooltip("组合方法ID最小值")]
public int MinCompositeMethodID = 200000;
[Tooltip("组合方法ID最大值")]
public int MaxCompositeMethodID = 999999;
#endregion
#region 验证阈值
[Header("验证阈值")]
[Tooltip("ID最小值")]
public int MinID = 1;
[Tooltip("ID最大值")]
public int MaxID = 999999;
[Tooltip("最小Tick率")]
public int MinTickRate = 20;
[Tooltip("最大Tick率")]
public int MaxTickRate = 120;
[Tooltip("最小权重")]
public int MinWeight = 0;
[Tooltip("最大权重")]
public int MaxWeight = 10000;
[Tooltip("权重总和")]
public int TotalWeight = 10000;
#endregion
#region 帮助方法
/// <summary>
/// 应用关卡配置默认值
/// </summary>
public void ApplyDefaults(ActivityStageConfig config)
{
if (config == null) return;
if (string.IsNullOrWhiteSpace(config.SceneName))
config.SceneName = DefaultSceneName;
if (config.TickRate < MinTickRate || config.TickRate > MaxTickRate)
config.TickRate = DefaultTickRate;
if (config.CloseLoadingDelay <= 0)
config.CloseLoadingDelay = DefaultCloseLoadingDelay;
if (config.CameraID <= 0)
config.CameraID = DefaultCameraID;
if (config.MapInfo <= 0)
config.MapInfo = DefaultMapInfo;
}
/// <summary>
/// 应用对话配置默认值
/// </summary>
public void ApplyDefaults(DialogInfoData config)
{
if (config == null) return;
if (config.Duration <= 0)
config.Duration = DefaultDialogDuration;
if (string.IsNullOrWhiteSpace(config.CharacterName))
config.CharacterName = DefaultDialogCharacterName;
}
/// <summary>
/// 应用事件配置默认值
/// </summary>
public void ApplyDefaults(EventNodeConfig config)
{
if (config == null) return;
if (config.NodeLayer <= 0)
config.NodeLayer = DefaultNodeLayer;
if (config.EventTypeGroup == null || config.EventTypeGroup.Count == 0)
config.EventTypeGroup = new List<int>(DefaultEventTypeGroup);
if (config.Priority == null || config.Priority.Count == 0)
config.Priority = new List<int>(DefaultPriority);
}
/// <summary>
/// 应用相机配置默认值
/// </summary>
public void ApplyDefaults(CameraMapping config)
{
if (config == null) return;
if (config.FOV <= 0)
config.FOV = DefaultCameraFOV;
if (config.Priority <= 0)
config.Priority = DefaultCameraPriority;
}
/// <summary>
/// 应用特效配置默认值
/// </summary>
public void ApplyDefaults(FXMapping config)
{
if (config == null) return;
if (config.Duration <= 0)
config.Duration = DefaultFXDuration;
if (config.Speed <= 0)
config.Speed = DefaultFXSpeed;
if (config.Scale <= 0)
config.Scale = DefaultFXScale;
}
#endregion
#region 私有方法
/// <summary>
/// 创建默认配置
/// </summary>
private static GameplayDefaultConfig CreateDefaultConfig()
{
var config = CreateInstance<GameplayDefaultConfig>();
config.name = "GameplayDefaultConfig";
return config;
}
#endregion
}
/// <summary>
/// Cinemachine混合样式
/// </summary>
public enum CinemachineBlendStyle
{
Cut,
EaseInOut,
EaseIn,
EaseOut,
HardIn,
HardOut,
Linear
}
/// <summary>
/// 富文本样式
/// </summary>
[Serializable]
public class RichTextStyle
{
[Tooltip("样式名称")]
public string StyleName = "Default";
[Tooltip("字体大小")]
public int FontSize = 24;
[Tooltip("字体颜色")]
public Color FontColor = Color.white;
[Tooltip("是否粗体")]
public bool Bold = false;
[Tooltip("是否斜体")]
public bool Italic = false;
[Tooltip("是否下划线")]
public bool Underline = false;
}
}

View File

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

View File

@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// ID段配置
/// </summary>
[Serializable]
public class IDRange
{
[Tooltip("策划/所有者名称")]
public string OwnerName;
[Tooltip("起始ID")]
public int StartID;
[Tooltip("结束ID")]
public int EndID;
[Tooltip("当前已用ID")]
public int CurrentUsedID;
[Tooltip("描述/备注")]
[TextArea(1, 2)]
public string Description;
/// <summary>
/// 剩余可用ID数量
/// </summary>
public int RemainingCount => EndID - CurrentUsedID;
/// <summary>
/// 使用率0-1
/// </summary>
public float UsageRate => (float)(CurrentUsedID - StartID + 1) / (EndID - StartID + 1);
/// <summary>
/// 是否已耗尽
/// </summary>
public bool IsExhausted => CurrentUsedID >= EndID;
/// <summary>
/// 分配新ID
/// </summary>
public int AllocateID()
{
if (IsExhausted)
return -1;
return ++CurrentUsedID;
}
/// <summary>
/// 检查ID是否在此范围内
/// </summary>
public bool Contains(int id)
{
return id >= StartID && id <= EndID;
}
/// <summary>
/// 验证配置有效性
/// </summary>
public bool Validate(out string error)
{
if (string.IsNullOrWhiteSpace(OwnerName))
{
error = "所有者名称不能为空";
return false;
}
if (StartID <= 0)
{
error = "起始ID必须大于0";
return false;
}
if (EndID < StartID)
{
error = "结束ID必须大于等于起始ID";
return false;
}
if (CurrentUsedID < StartID - 1)
{
error = "当前已用ID不能小于起始ID-1";
return false;
}
error = null;
return true;
}
}
/// <summary>
/// ID段分配配置
/// </summary>
[CreateAssetMenu(fileName = "IDRangeConfig", menuName = "Gameplay/ID Range Config")]
public class IDRangeConfig : ScriptableObject
{
[Tooltip("ID段列表")]
public List<IDRange> Ranges = new List<IDRange>();
/// <summary>
/// 分配新ID
/// </summary>
/// <param name="ownerName">所有者名称</param>
/// <returns>分配的ID-1表示失败</returns>
public int AllocateID(string ownerName)
{
var range = Ranges.Find(r => r.OwnerName == ownerName);
if (range == null)
{
Debug.LogError($"[IDRange] 未找到所有者: {ownerName}");
return -1;
}
var id = range.AllocateID();
if (id < 0)
{
Debug.LogError($"[IDRange] ID段已耗尽: {ownerName} ({range.StartID}-{range.EndID})");
return -1;
}
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
UnityEditor.AssetDatabase.SaveAssets();
#endif
Debug.Log($"[IDRange] 为 {ownerName} 分配ID: {id}");
return id;
}
/// <summary>
/// 添加新ID段
/// </summary>
public bool AddRange(IDRange range, out string error)
{
if (!range.Validate(out error))
return false;
// 检查所有者名称是否已存在
if (Ranges.Exists(r => r.OwnerName == range.OwnerName))
{
error = $"所有者 '{range.OwnerName}' 已存在";
return false;
}
// 检查ID段是否重叠
foreach (var existing in Ranges)
{
if (range.StartID <= existing.EndID && range.EndID >= existing.StartID)
{
error = $"ID段与 '{existing.OwnerName}' 重叠";
return false;
}
}
Ranges.Add(range);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
UnityEditor.AssetDatabase.SaveAssets();
#endif
error = null;
return true;
}
/// <summary>
/// 移除ID段
/// </summary>
public bool RemoveRange(string ownerName)
{
var range = Ranges.Find(r => r.OwnerName == ownerName);
if (range != null)
{
Ranges.Remove(range);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
UnityEditor.AssetDatabase.SaveAssets();
#endif
return true;
}
return false;
}
/// <summary>
/// 根据ID查找所属段
/// </summary>
public IDRange FindRangeByID(int id)
{
return Ranges.Find(r => r.Contains(id));
}
/// <summary>
/// 检查ID是否冲突
/// </summary>
public bool CheckConflict(int id, out string ownerName)
{
var range = FindRangeByID(id);
if (range != null)
{
ownerName = range.OwnerName;
return true;
}
ownerName = null;
return false;
}
/// <summary>
/// 获取预警的ID段使用率>80%
/// </summary>
public List<IDRange> GetWarningRanges(float threshold = 0.8f)
{
return Ranges.Where(r => r.UsageRate >= threshold).ToList();
}
/// <summary>
/// 获取已耗尽的ID段
/// </summary>
public List<IDRange> GetExhaustedRanges()
{
return Ranges.Where(r => r.IsExhausted).ToList();
}
/// <summary>
/// 建议的ID段分配方案
/// </summary>
public static List<IDRange> GetRecommendedRanges()
{
return new List<IDRange>
{
new IDRange { OwnerName = "系统保留", StartID = 100000, EndID = 199999,
Description = "通用组合节点、系统节点" },
new IDRange { OwnerName = "策划A", StartID = 200000, EndID = 299999,
Description = "策划A专用ID段" },
new IDRange { OwnerName = "策划B", StartID = 300000, EndID = 399999,
Description = "策划B专用ID段" },
new IDRange { OwnerName = "策划C", StartID = 400000, EndID = 499999,
Description = "策划C专用ID段" },
};
}
}
}

View File

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

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// LUT配置数据库
/// 统一管理所有LUT配置
/// </summary>
[CreateAssetMenu(fileName = "LutDatabase", menuName = "Gameplay/Lut/Lut Database")]
public class LutConfigDatabase : ScriptableObject
{
[Tooltip("UI LUT配置")]
public List<ActivityUiLut> UiLuts = new List<ActivityUiLut>();
[Tooltip("相机LUT配置")]
public List<ActivityCameraLut> CameraLuts = new List<ActivityCameraLut>();
[Tooltip("特效LUT配置")]
public List<ActivityFXLut> FxLuts = new List<ActivityFXLut>();
[Tooltip("地图信息LUT配置")]
public List<ActivityMapInfoLut> MapInfoLuts = new List<ActivityMapInfoLut>();
}
}

View File

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

View File

@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 通用资源映射基类
/// </summary>
[Serializable]
public class ResourceMapping
{
[Tooltip("关卡ID")]
public int StageID;
[Tooltip("映射ID")]
public int MappingID;
[Tooltip("Prefab路径")]
public string PrefabPath;
[Tooltip("运行时加载的Prefab自动填充")]
public GameObject LoadedPrefab;
/// <summary>
/// 验证路径是否有效
/// </summary>
public bool ValidatePath()
{
if (string.IsNullOrWhiteSpace(PrefabPath))
return false;
#if UNITY_EDITOR
return UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath) != null;
#else
return true;
#endif
}
/// <summary>
/// 应用默认值
/// </summary>
public virtual void ApplyDefaults()
{
// 基类默认值已在创建时设置
}
}
/// <summary>
/// 相机详细映射配置
/// 包含Cinemachine相机参数
/// </summary>
[Serializable]
public class CameraMapping : ResourceMapping
{
[Header("相机参数")]
[Tooltip("视场角(FOV)范围1-179")]
[Range(1, 179)]
public float FOV = 60f;
[Tooltip("相机优先级,高优先级相机会覆盖低优先级")]
public int Priority = 10;
[Header("跟随设置")]
[Tooltip("跟随目标偏移")]
public Vector3 FollowOffset = new Vector3(0, 5, -10);
[Tooltip("跟随目标的最小距离")]
public float FollowMinDistance = 0.1f;
[Tooltip("跟随目标的最大距离")]
public float FollowMaxDistance = 100f;
[Header("注视设置")]
[Tooltip("注视目标偏移")]
public Vector3 LookAtOffset = Vector3.zero;
[Tooltip("注视点的偏移高度")]
public float LookAtHeight = 1.5f;
[Header("过渡设置")]
[Tooltip("过渡时间0表示立即切换")]
public float BlendTime = 0.5f;
[Tooltip("过渡样式")]
public CinemachineBlendStyle BlendStyle = CinemachineBlendStyle.EaseInOut;
[Header("边界设置")]
[Tooltip("是否启用软边界")]
public bool UseSoftZone = true;
[Tooltip("死区宽度")]
[Range(0, 1)]
public float DeadZoneWidth = 0.1f;
[Tooltip("死区高度")]
[Range(0, 1)]
public float DeadZoneHeight = 0.1f;
[Header("震动设置")]
[Tooltip("默认震动幅度")]
public float DefaultShakeAmplitude = 1f;
[Tooltip("默认震动频率")]
public float DefaultShakeFrequency = 1f;
[Tooltip("默认震动持续时间")]
public float DefaultShakeDuration = 0.5f;
[Header("其他")]
[Tooltip("相机备注")]
public string CameraDescription = "";
/// <summary>
/// 应用默认值
/// </summary>
public override void ApplyDefaults()
{
base.ApplyDefaults();
var defaultConfig = GameplayDefaultConfig.Instance;
if (FOV <= 0 || FOV >= 180)
FOV = defaultConfig.DefaultCameraFOV;
if (Priority <= 0)
Priority = defaultConfig.DefaultCameraPriority;
if (BlendTime < 0)
BlendTime = defaultConfig.DefaultCameraBlendTime;
}
/// <summary>
/// 验证相机配置
/// </summary>
public bool Validate(out string errorMessage)
{
if (MappingID <= 0)
{
errorMessage = "Camera MappingID必须大于0";
return false;
}
if (FOV <= 0 || FOV >= 180)
{
errorMessage = $"FOV {FOV} 超出有效范围(0, 180)";
return false;
}
if (Priority < 0)
{
errorMessage = "Priority不能为负数";
return false;
}
errorMessage = null;
return true;
}
}
/// <summary>
/// 特效生命周期映射配置
/// </summary>
[Serializable]
public class FXMapping : ResourceMapping
{
[Header("生命周期")]
[Tooltip("持续时间0表示使用粒子系统默认时长")]
public float Duration = 2f;
[Tooltip("延迟时间(秒),延迟后开始播放")]
public float Delay = 0f;
[Tooltip("是否循环播放")]
public bool Loop = false;
[Tooltip("是否自动销毁")]
public bool AutoDestroy = true;
[Tooltip("销毁前的等待时间(秒)")]
public float DestroyDelay = 0.5f;
[Header("播放控制")]
[Tooltip("播放速度倍率")]
[Range(0.1f, 5f)]
public float Speed = 1f;
[Tooltip("缩放倍率")]
[Range(0.1f, 10f)]
public float Scale = 1f;
[Tooltip("透明度1为不透明")]
[Range(0f, 1f)]
public float Alpha = 1f;
[Header("颜色控制")]
[Tooltip("是否启用颜色覆盖")]
public bool OverrideColor = false;
[Tooltip("覆盖颜色")]
public Color OverrideTint = Color.white;
[Header("对象池")]
[Tooltip("是否使用对象池")]
public bool UseObjectPool = true;
[Tooltip("对象池初始大小")]
public int PoolInitialSize = 5;
[Tooltip("对象池最大大小")]
public int PoolMaxSize = 20;
[Header("空间设置")]
[Tooltip("是否跟随目标")]
public bool AttachToTarget = false;
[Tooltip("跟随目标时的本地位置偏移")]
public Vector3 LocalPositionOffset = Vector3.zero;
[Tooltip("跟随目标时的本地旋转偏移")]
public Vector3 LocalRotationOffset = Vector3.zero;
[Header("层级设置")]
[Tooltip("排序层级")]
public int SortingLayer = 0;
[Tooltip("排序序号")]
public int SortingOrder = 0;
[Header("其他")]
[Tooltip("特效备注")]
public string FXDescription = "";
/// <summary>
/// 应用默认值
/// </summary>
public override void ApplyDefaults()
{
base.ApplyDefaults();
var defaultConfig = GameplayDefaultConfig.Instance;
if (Duration <= 0)
Duration = defaultConfig.DefaultFXDuration;
if (Speed <= 0)
Speed = defaultConfig.DefaultFXSpeed;
if (Scale <= 0)
Scale = defaultConfig.DefaultFXScale;
}
/// <summary>
/// 验证特效配置
/// </summary>
public bool Validate(out string errorMessage)
{
if (MappingID <= 0)
{
errorMessage = "FX MappingID必须大于0";
return false;
}
if (Duration < 0)
{
errorMessage = "Duration不能为负数";
return false;
}
if (Speed <= 0)
{
errorMessage = "Speed必须大于0";
return false;
}
if (Scale <= 0)
{
errorMessage = "Scale必须大于0";
return false;
}
errorMessage = null;
return true;
}
}
/// <summary>
/// 地图点位
/// </summary>
[Serializable]
public class MapPoint
{
[Tooltip("点位ID")]
public string PointID;
[Tooltip("坐标")]
public Vector3 Position;
}
}

View File

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

View File

@ -0,0 +1,426 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameplayEditor.Config
{
/// <summary>
/// 富文本标签类型枚举
/// </summary>
public enum RichTextTag
{
Bold, // 粗体 <b>
Italic, // 斜体 <i>
Color, // 颜色 <color>
Size, // 字号 <size>
Material, // 材质 <material>
Quad, // 图片 <quad>
Gradient, // 渐变 <gradient>
Underline, // 下划线 <u>
Strikethrough, // 删除线 <s>
Superscript, // 上标 <sup>
Subscript, // 下标 <sub>
Mark, // 标记高亮 <mark>
Link, // 超链接 <a>
Font, // 字体 <font>
LineBreak, // 换行 <br>
Space, // 空格 <space>
Align, // 对齐 <align>
Cspace, // 字符间距 <cspace>
LineHeight, // 行高 <line-height>
Pos, // 位置偏移 <pos>
Voffset, // 垂直偏移 <voffset>
NoBreak // 不换行 <nobr>
}
/// <summary>
/// 富文本标签定义
/// </summary>
[Serializable]
public class RichTextTagDefinition
{
[Tooltip("标签类型")]
public RichTextTag TagType;
[Tooltip("标签名称如b、color等")]
public string TagName;
[Tooltip("是否支持属性")]
public bool SupportsAttribute;
[Tooltip("属性名称如color=#ff0000中的color")]
public string AttributeName;
[Tooltip("属性格式示例")]
public string AttributeExample;
[Tooltip("是否允许嵌套")]
public bool AllowNesting;
[Tooltip("最大嵌套深度0表示不允许嵌套")]
public int MaxNestingDepth;
[Tooltip("是否需要闭合标签")]
public bool RequireClosingTag;
[Tooltip("标签描述")]
[TextArea(2, 3)]
public string Description;
[Tooltip("是否启用此标签")]
public bool Enabled = true;
public RichTextTagDefinition()
{
}
public RichTextTagDefinition(RichTextTag tagType, string tagName, bool supportsAttribute = false,
string attributeName = "", string attributeExample = "", bool allowNesting = true,
int maxNestingDepth = 3, bool requireClosingTag = true, string description = "")
{
TagType = tagType;
TagName = tagName;
SupportsAttribute = supportsAttribute;
AttributeName = attributeName;
AttributeExample = attributeExample;
AllowNesting = allowNesting;
MaxNestingDepth = maxNestingDepth;
RequireClosingTag = requireClosingTag;
Description = description;
Enabled = true;
}
}
/// <summary>
/// 颜色预设
/// </summary>
[Serializable]
public class RichTextColorPreset
{
[Tooltip("颜色名称")]
public string ColorName;
[Tooltip("颜色值")]
public Color Color;
[Tooltip("颜色代码(如#FF0000")]
public string ColorCode;
public RichTextColorPreset(string name, Color color, string code)
{
ColorName = name;
Color = color;
ColorCode = code;
}
}
/// <summary>
/// 字号预设
/// </summary>
[Serializable]
public class RichTextSizePreset
{
[Tooltip("尺寸名称")]
public string SizeName;
[Tooltip("尺寸值")]
public int SizeValue;
public RichTextSizePreset(string name, int value)
{
SizeName = name;
SizeValue = value;
}
}
/// <summary>
/// 富文本配置
/// 定义项目支持的富文本标签和规范
/// </summary>
[CreateAssetMenu(fileName = "RichTextConfig", menuName = "Gameplay/Rich Text Config")]
public class RichTextConfig : ScriptableObject
{
[Header("全局设置")]
[Tooltip("是否启用富文本")]
public bool EnableRichText = true;
[Tooltip("最大嵌套深度")]
[Range(1, 10)]
public int MaxNestingDepth = 3;
[Tooltip("最大标签数量(防止性能问题)")]
[Range(10, 500)]
public int MaxTagCount = 100;
[Tooltip("验证时是否区分大小写")]
public bool CaseSensitive = false;
[Header("支持的标签")]
[Tooltip("启用的富文本标签列表")]
public List<RichTextTagDefinition> SupportedTags = new List<RichTextTagDefinition>();
[Header("颜色预设")]
[Tooltip("预定义的颜色列表")]
public List<RichTextColorPreset> ColorPresets = new List<RichTextColorPreset>();
[Header("字号预设")]
[Tooltip("预定义的字号列表")]
public List<RichTextSizePreset> SizePresets = new List<RichTextSizePreset>();
[Header("验证规则")]
[Tooltip("非法标签处理方式")]
public InvalidTagHandling InvalidTagHandling = InvalidTagHandling.Strip;
[Tooltip("是否自动修复常见的标签错误")]
public bool AutoFixCommonErrors = true;
[Tooltip("是否记录验证日志")]
public bool LogValidation = false;
// 缓存字典
[NonSerialized] private Dictionary<string, RichTextTagDefinition> _tagCache;
[NonSerialized] private Dictionary<string, RichTextColorPreset> _colorCache;
[NonSerialized] private Dictionary<string, RichTextSizePreset> _sizeCache;
private void OnEnable()
{
BuildCache();
}
/// <summary>
/// 初始化默认配置
/// </summary>
public void InitializeDefaults()
{
SupportedTags.Clear();
// 添加默认支持的标签
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Bold, "b", false, "", "", true, 5, true, "粗体文本"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Italic, "i", false, "", "", true, 5, true, "斜体文本"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Color, "color", true, "color", "#FF0000 或 red", false, 1, true, "文本颜色"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Size, "size", true, "size", "24 或 +2", false, 1, true, "字号大小"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Underline, "u", false, "", "", true, 3, true, "下划线"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Strikethrough, "s", false, "", "", true, 3, true, "删除线"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Superscript, "sup", false, "", "", false, 1, true, "上标"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Subscript, "sub", false, "", "", false, 1, true, "下标"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Mark, "mark", true, "color", "#FFFF00", false, 1, true, "高亮标记"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.Link, "a", true, "href", "https://example.com", false, 1, true, "超链接"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.LineBreak, "br", false, "", "", false, 0, false, "换行"));
SupportedTags.Add(new RichTextTagDefinition(
RichTextTag.NoBreak, "nobr", false, "", "", true, 1, true, "不换行区域"));
// 初始化颜色预设
ColorPresets.Clear();
ColorPresets.Add(new RichTextColorPreset("red", Color.red, "#FF0000"));
ColorPresets.Add(new RichTextColorPreset("green", Color.green, "#00FF00"));
ColorPresets.Add(new RichTextColorPreset("blue", Color.blue, "#0000FF"));
ColorPresets.Add(new RichTextColorPreset("yellow", Color.yellow, "#FFFF00"));
ColorPresets.Add(new RichTextColorPreset("white", Color.white, "#FFFFFF"));
ColorPresets.Add(new RichTextColorPreset("black", Color.black, "#000000"));
ColorPresets.Add(new RichTextColorPreset("cyan", Color.cyan, "#00FFFF"));
ColorPresets.Add(new RichTextColorPreset("magenta", Color.magenta, "#FF00FF"));
ColorPresets.Add(new RichTextColorPreset("orange", new Color(1f, 0.5f, 0f), "#FF8000"));
ColorPresets.Add(new RichTextColorPreset("gray", Color.gray, "#808080"));
// 初始化字号预设
SizePresets.Clear();
SizePresets.Add(new RichTextSizePreset("small", 12));
SizePresets.Add(new RichTextSizePreset("normal", 16));
SizePresets.Add(new RichTextSizePreset("medium", 20));
SizePresets.Add(new RichTextSizePreset("large", 24));
SizePresets.Add(new RichTextSizePreset("huge", 32));
BuildCache();
}
/// <summary>
/// 构建缓存
/// </summary>
private void BuildCache()
{
_tagCache = new Dictionary<string, RichTextTagDefinition>(StringComparer.OrdinalIgnoreCase);
foreach (var tag in SupportedTags)
{
if (tag.Enabled && !_tagCache.ContainsKey(tag.TagName))
{
_tagCache[tag.TagName] = tag;
}
}
_colorCache = new Dictionary<string, RichTextColorPreset>(StringComparer.OrdinalIgnoreCase);
foreach (var color in ColorPresets)
{
if (!_colorCache.ContainsKey(color.ColorName))
{
_colorCache[color.ColorName] = color;
}
if (!_colorCache.ContainsKey(color.ColorCode))
{
_colorCache[color.ColorCode] = color;
}
}
_sizeCache = new Dictionary<string, RichTextSizePreset>(StringComparer.OrdinalIgnoreCase);
foreach (var size in SizePresets)
{
if (!_sizeCache.ContainsKey(size.SizeName))
{
_sizeCache[size.SizeName] = size;
}
_sizeCache[size.SizeValue.ToString()] = size;
}
}
/// <summary>
/// 检查标签是否支持
/// </summary>
public bool IsTagSupported(string tagName)
{
if (_tagCache == null) BuildCache();
return _tagCache.ContainsKey(tagName);
}
/// <summary>
/// 获取标签定义
/// </summary>
public RichTextTagDefinition GetTagDefinition(string tagName)
{
if (_tagCache == null) BuildCache();
_tagCache.TryGetValue(tagName, out var definition);
return definition;
}
/// <summary>
/// 获取标签定义(通过类型)
/// </summary>
public RichTextTagDefinition GetTagDefinition(RichTextTag tagType)
{
return SupportedTags.Find(t => t.TagType == tagType && t.Enabled);
}
/// <summary>
/// 检查颜色是否有效
/// </summary>
public bool IsValidColor(string colorValue)
{
if (_colorCache == null) BuildCache();
// 检查预设
if (_colorCache.ContainsKey(colorValue))
return true;
// 检查十六进制格式
if (colorValue.StartsWith("#") && (colorValue.Length == 7 || colorValue.Length == 9))
{
return System.Text.RegularExpressions.Regex.IsMatch(colorValue, "^#[0-9A-Fa-f]{6}$") ||
System.Text.RegularExpressions.Regex.IsMatch(colorValue, "^#[0-9A-Fa-f]{8}$");
}
return false;
}
/// <summary>
/// 获取颜色值
/// </summary>
public Color GetColor(string colorValue)
{
if (_colorCache == null) BuildCache();
if (_colorCache.TryGetValue(colorValue, out var preset))
return preset.Color;
// 解析十六进制
if (ColorUtility.TryParseHtmlString(colorValue, out var color))
return color;
return Color.white;
}
/// <summary>
/// 检查字号是否有效
/// </summary>
public bool IsValidSize(string sizeValue)
{
if (_sizeCache == null) BuildCache();
// 检查预设
if (_sizeCache.ContainsKey(sizeValue))
return true;
// 检查数字
if (int.TryParse(sizeValue, out var size))
return size > 0 && size <= 200;
// 检查相对值(如+2、-3
if (sizeValue.StartsWith("+") || sizeValue.StartsWith("-"))
{
return int.TryParse(sizeValue, out _);
}
return false;
}
/// <summary>
/// 获取所有支持的标签名称
/// </summary>
public List<string> GetSupportedTagNames()
{
if (_tagCache == null) BuildCache();
return new List<string>(_tagCache.Keys);
}
/// <summary>
/// 启用/禁用标签
/// </summary>
public void SetTagEnabled(RichTextTag tagType, bool enabled)
{
var tag = SupportedTags.Find(t => t.TagType == tagType);
if (tag != null)
{
tag.Enabled = enabled;
BuildCache();
}
}
/// <summary>
/// 创建验证器
/// </summary>
public Utils.RichTextValidator CreateValidator()
{
return new Utils.RichTextValidator(this);
}
}
/// <summary>
/// 非法标签处理方式
/// </summary>
public enum InvalidTagHandling
{
Strip, // 移除非法标签
Escape, // 转义为普通文本
Warning, // 保留但警告
Error // 报错
}
}

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a1a9b6f3bf5e07344b1a1a522f1c9b5a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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,11 @@
fileFormatVersion: 2
guid: 7d04cbc7dec380c4a80a1cf00bcab601
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,627 @@
using System.Collections.Generic;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树调试器
/// 支持断点、单步执行、变量查看
/// </summary>
public class BTDebugger : MonoBehaviour
{
[Header("调试设置")]
[Tooltip("启用调试")]
public bool EnableDebug = true;
[Tooltip("自动断点(异常时)")]
public bool AutoBreakOnError = true;
// 当前调试的行为树
private BehaviourTree _targetTree;
private BehaviourTreeOwner _treeOwner;
// 断点列表
private HashSet<string> _breakpoints = new HashSet<string>();
private Dictionary<string, string> _conditionBreakpoints = new Dictionary<string, string>();
// 调试状态
private bool _isPaused = false;
// private bool _stepMode = false; // 暂不使用
private Node _currentNode;
private string _lastStatus;
// 执行历史
private Queue<ExecutionRecord> _executionHistory = new Queue<ExecutionRecord>(100);
// 节点状态缓存
private Dictionary<string, NodeDebugInfo> _nodeDebugInfo = new Dictionary<string, NodeDebugInfo>();
// 调试事件
public System.Action<Node> OnBreakpointHit;
public System.Action<Node> OnNodeExecuted;
public System.Action OnDebugPaused;
public System.Action OnDebugResumed;
// 执行记录
public class ExecutionRecord
{
public float Time;
public string NodeName;
public string NodeType;
public Status Result;
public string Message;
}
// 节点调试信息
public class NodeDebugInfo
{
public string NodeId;
public string NodeName;
public int ExecutionCount;
public Status LastStatus;
public float LastExecutionTime;
public bool IsBreakpoint;
public string Condition;
}
private static BTDebugger _instance;
public static BTDebugger Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<BTDebugger>();
}
return _instance;
}
}
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
}
#region 目标设置
/// <summary>
/// 设置调试目标
/// </summary>
public void SetTarget(BehaviourTree tree)
{
_targetTree = tree;
_nodeDebugInfo.Clear();
// 扫描所有节点
if (tree?.primeNode != null)
{
ScanNode(tree.primeNode);
}
Debug.Log($"[BTDebugger] 设置调试目标: {tree?.name}");
}
/// <summary>
/// 递归扫描节点
/// </summary>
private void ScanNode(Node node)
{
if (node == null) return;
string nodeId = GetNodeId(node);
if (!_nodeDebugInfo.ContainsKey(nodeId))
{
_nodeDebugInfo[nodeId] = new NodeDebugInfo
{
NodeId = nodeId,
NodeName = node.name,
IsBreakpoint = _breakpoints.Contains(nodeId)
};
}
// 递归扫描子节点
if (node.outConnections != null)
{
foreach (var conn in node.outConnections)
{
ScanNode(conn.targetNode);
}
}
}
#endregion
#region 断点管理
/// <summary>
/// 添加断点
/// </summary>
public void AddBreakpoint(Node node)
{
string nodeId = GetNodeId(node);
_breakpoints.Add(nodeId);
if (_nodeDebugInfo.TryGetValue(nodeId, out var info))
{
info.IsBreakpoint = true;
}
}
/// <summary>
/// 移除断点
/// </summary>
public void RemoveBreakpoint(Node node)
{
string nodeId = GetNodeId(node);
_breakpoints.Remove(nodeId);
_conditionBreakpoints.Remove(nodeId);
if (_nodeDebugInfo.TryGetValue(nodeId, out var info))
{
info.IsBreakpoint = false;
info.Condition = null;
}
}
/// <summary>
/// 添加条件断点
/// </summary>
public void AddConditionBreakpoint(Node node, string condition)
{
string nodeId = GetNodeId(node);
_breakpoints.Add(nodeId);
_conditionBreakpoints[nodeId] = condition;
if (_nodeDebugInfo.TryGetValue(nodeId, out var info))
{
info.IsBreakpoint = true;
info.Condition = condition;
}
}
/// <summary>
/// 清除所有断点
/// </summary>
public void ClearBreakpoints()
{
_breakpoints.Clear();
_conditionBreakpoints.Clear();
foreach (var info in _nodeDebugInfo.Values)
{
info.IsBreakpoint = false;
info.Condition = null;
}
}
/// <summary>
/// 检查是否是断点
/// </summary>
public bool IsBreakpoint(Node node)
{
return _breakpoints.Contains(GetNodeId(node));
}
#endregion
#region 执行控制
/// <summary>
/// 暂停调试
/// </summary>
public void Pause()
{
_isPaused = true;
OnDebugPaused?.Invoke();
Debug.Log("[BTDebugger] 调试暂停");
}
/// <summary>
/// 继续执行
/// </summary>
public void Resume()
{
_isPaused = false;
// _stepMode = false;
OnDebugResumed?.Invoke();
Debug.Log("[BTDebugger] 调试继续");
}
/// <summary>
/// 单步执行
/// </summary>
public void Step()
{
_isPaused = false;
// _stepMode = true;
// 执行一帧后暂停
StartCoroutine(StepCoroutine());
}
private System.Collections.IEnumerator StepCoroutine()
{
yield return null; // 等待一帧
Pause();
}
/// <summary>
/// 检查是否可以执行
/// </summary>
public bool CanExecute(Node node)
{
if (!EnableDebug)
return true;
// 检查是否暂停
if (_isPaused)
return false;
// 检查断点
string nodeId = GetNodeId(node);
if (_breakpoints.Contains(nodeId))
{
// 检查条件
if (_conditionBreakpoints.TryGetValue(nodeId, out var condition))
{
if (EvaluateCondition(condition, node))
{
HitBreakpoint(node);
return false;
}
}
else
{
HitBreakpoint(node);
return false;
}
}
return true;
}
/// <summary>
/// 触发断点
/// </summary>
private void HitBreakpoint(Node node)
{
_currentNode = node;
_isPaused = true;
Debug.Log($"[BTDebugger] 触发断点: {node.name}");
OnBreakpointHit?.Invoke(node);
}
/// <summary>
/// 评估条件
/// 支持黑板变量、比较运算符、逻辑运算符
/// </summary>
private bool EvaluateCondition(string condition, Node node)
{
if (string.IsNullOrWhiteSpace(condition))
return true;
try
{
var blackboard = GetBlackboard(node);
var evaluator = new ConditionEvaluator(blackboard);
return evaluator.Evaluate(condition);
}
catch (System.Exception ex)
{
Debug.LogWarning($"[BTDebugger] 条件求值错误 '{condition}': {ex.Message}");
return true; // 求值失败时默认触发断点
}
}
/// <summary>
/// 获取节点关联的黑板
/// </summary>
private IBlackboard GetBlackboard(Node node)
{
if (node?.graph is BehaviourTree bt)
return bt.blackboard;
return _targetTree?.blackboard;
}
/// <summary>
/// 轻量级条件表达式求值器
/// </summary>
private class ConditionEvaluator
{
private IBlackboard _blackboard;
public ConditionEvaluator(IBlackboard blackboard)
{
_blackboard = blackboard;
}
public bool Evaluate(string expression)
{
expression = expression.Trim();
// 处理括号(仅支持最外层一对括号)
if (expression.StartsWith("(") && expression.EndsWith(")"))
{
expression = expression.Substring(1, expression.Length - 2).Trim();
}
// 处理 ||
var orParts = SplitTopLevel(expression, "||");
if (orParts.Count > 1)
{
foreach (var part in orParts)
{
if (Evaluate(part.Trim())) return true;
}
return false;
}
// 处理 &&
var andParts = SplitTopLevel(expression, "&&");
if (andParts.Count > 1)
{
foreach (var part in andParts)
{
if (!Evaluate(part.Trim())) return false;
}
return true;
}
// 处理 !
if (expression.StartsWith("!"))
{
return !Evaluate(expression.Substring(1).Trim());
}
// 处理比较
return EvaluateComparison(expression);
}
private bool EvaluateComparison(string expression)
{
var ops = new[] { "==", "!=", "<=", ">=", "<", ">" };
foreach (var op in ops)
{
int idx = FindTopLevelOperator(expression, op);
if (idx >= 0)
{
var left = expression.Substring(0, idx).Trim();
var right = expression.Substring(idx + op.Length).Trim();
return Compare(left, op, right);
}
}
// 没有比较运算符,视为布尔值
return ToBool(ResolveValue(expression));
}
private bool Compare(string leftExpr, string op, string rightExpr)
{
var left = ResolveValue(leftExpr);
var right = ResolveValue(rightExpr);
// 尝试数值比较
if (double.TryParse(left, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var leftNum) &&
double.TryParse(right, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var rightNum))
{
return op switch
{
"==" => Mathf.Approximately((float)leftNum, (float)rightNum),
"!=" => !Mathf.Approximately((float)leftNum, (float)rightNum),
"<" => leftNum < rightNum,
"<=" => leftNum <= rightNum,
">" => leftNum > rightNum,
">=" => leftNum >= rightNum,
_ => false
};
}
// 字符串/其他比较
return op switch
{
"==" => string.Equals(left, right, System.StringComparison.OrdinalIgnoreCase),
"!=" => !string.Equals(left, right, System.StringComparison.OrdinalIgnoreCase),
_ => false
};
}
private string ResolveValue(string expr)
{
expr = expr.Trim();
// 字符串字面量
if ((expr.StartsWith("\"") && expr.EndsWith("\"")) ||
(expr.StartsWith("'") && expr.EndsWith("'")))
{
return expr.Substring(1, expr.Length - 2);
}
// 布尔字面量
if (bool.TryParse(expr, out _)) return expr;
// 数字字面量
if (double.TryParse(expr, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out _)) return expr;
// 尝试从黑板获取变量
if (_blackboard != null && _blackboard.variables != null &&
_blackboard.variables.ContainsKey(expr))
{
var value = _blackboard.GetVariableValue<object>(expr);
return value?.ToString() ?? "";
}
return expr;
}
private bool ToBool(string value)
{
if (bool.TryParse(value, out var b)) return b;
if (int.TryParse(value, out var i)) return i != 0;
if (double.TryParse(value, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var d)) return d != 0;
return !string.IsNullOrEmpty(value);
}
private int FindTopLevelOperator(string expression, string op)
{
int depth = 0;
for (int i = 0; i <= expression.Length - op.Length; i++)
{
if (expression[i] == '(') depth++;
else if (expression[i] == ')') depth--;
else if (depth == 0 && expression.Substring(i, op.Length) == op)
{
// 排除 == 被识别为 =
if (op == "=" && i + 1 < expression.Length && expression[i + 1] == '=')
continue;
return i;
}
}
return -1;
}
private List<string> SplitTopLevel(string expression, string separator)
{
var result = new List<string>();
int depth = 0;
int start = 0;
for (int i = 0; i <= expression.Length - separator.Length; i++)
{
if (expression[i] == '(') depth++;
else if (expression[i] == ')') depth--;
else if (depth == 0 && expression.Substring(i, separator.Length) == separator)
{
result.Add(expression.Substring(start, i - start).Trim());
start = i + separator.Length;
i = start - 1;
}
}
result.Add(expression.Substring(start).Trim());
return result;
}
}
#endregion
#region 节点执行记录
/// <summary>
/// 记录节点执行
/// </summary>
public void RecordNodeExecution(Node node, Status result)
{
if (!EnableDebug) return;
string nodeId = GetNodeId(node);
// 更新节点信息
if (_nodeDebugInfo.TryGetValue(nodeId, out var info))
{
info.ExecutionCount++;
info.LastStatus = result;
info.LastExecutionTime = Time.time;
}
// 添加到历史
var record = new ExecutionRecord
{
Time = Time.time,
NodeName = node.name,
NodeType = node.GetType().Name,
Result = result
};
_executionHistory.Enqueue(record);
if (_executionHistory.Count > 100)
{
_executionHistory.Dequeue();
}
_currentNode = node;
OnNodeExecuted?.Invoke(node);
}
/// <summary>
/// 记录节点状态
/// </summary>
public void SetNodeStatus(Node node, Status status)
{
string nodeId = GetNodeId(node);
if (_nodeDebugInfo.TryGetValue(nodeId, out var info))
{
info.LastStatus = status;
}
}
#endregion
#region 查询方法
/// <summary>
/// 获取节点调试信息
/// </summary>
public NodeDebugInfo GetNodeDebugInfo(Node node)
{
string nodeId = GetNodeId(node);
_nodeDebugInfo.TryGetValue(nodeId, out var info);
return info;
}
/// <summary>
/// 获取所有节点信息
/// </summary>
public IReadOnlyDictionary<string, NodeDebugInfo> GetAllNodeInfo()
{
return _nodeDebugInfo;
}
/// <summary>
/// 获取执行历史
/// </summary>
public IEnumerable<ExecutionRecord> GetExecutionHistory()
{
return _executionHistory;
}
/// <summary>
/// 获取当前执行节点
/// </summary>
public Node GetCurrentNode()
{
return _currentNode;
}
/// <summary>
/// 是否暂停
/// </summary>
public bool IsPaused()
{
return _isPaused;
}
#endregion
#region 私有方法
/// <summary>
/// 获取节点唯一ID
/// </summary>
private string GetNodeId(Node node)
{
return $"{node.GetType().Name}_{node.name}_{node.GetHashCode()}";
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,565 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NodeCanvas.Framework;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树性能监控器
/// 监控节点执行频率、耗时,检测性能问题
/// </summary>
public class BTPerformanceMonitor : MonoBehaviour
{
[Header("监控设置")]
[Tooltip("启用监控")]
public bool EnableMonitoring = true;
[Tooltip("每帧执行警告阈值(同一节点)")]
public int FrameExecutionThreshold = 60;
[Tooltip("节点执行耗时警告阈值(毫秒)")]
public float ExecutionTimeThresholdMs = 5f;
[Tooltip("高频操作警告阈值")]
public int HighFrequencyOperationThreshold = 30;
[Header("检测设置")]
[Tooltip("检测SetPosition/SetRotation等高消耗操作")]
public bool DetectExpensiveOperations = true;
[Tooltip("检测死循环")]
public bool DetectInfiniteLoops = true;
[Tooltip("死循环检测阈值(同一节点连续执行次数)")]
public int InfiniteLoopThreshold = 1000;
// 节点性能数据
private Dictionary<string, NodePerformanceData> _nodePerformanceData =
new Dictionary<string, NodePerformanceData>();
// 当前帧执行计数
private Dictionary<string, int> _currentFrameExecutions = new Dictionary<string, int>();
// 高频操作计数
private Dictionary<string, OperationCounter> _operationCounters =
new Dictionary<string, OperationCounter>();
// 性能报告
private PerformanceReport _currentReport;
// 监控状态
private bool _isRecording = false;
private float _recordingStartTime;
/// <summary>
/// 节点性能数据
/// </summary>
public class NodePerformanceData
{
public string NodeName;
public string NodeType;
public int TotalExecutions;
public int FrameExecutions; // 当前帧执行次数
public float TotalExecutionTimeMs;
public float MaxExecutionTimeMs;
public float AverageExecutionTimeMs => TotalExecutions > 0 ?
TotalExecutionTimeMs / TotalExecutions : 0;
public int WarningCount;
}
/// <summary>
/// 操作计数器
/// </summary>
private class OperationCounter
{
public string OperationName;
public int Count;
public int FrameCount;
public float LastResetTime;
}
/// <summary>
/// 性能报告
/// </summary>
public class PerformanceReport
{
public float Duration;
public int TotalNodeExecutions;
public int WarningCount;
public List<NodePerformanceData> HotNodes = new List<NodePerformanceData>();
public List<string> Warnings = new List<string>();
}
private static BTPerformanceMonitor _instance;
public static BTPerformanceMonitor Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<BTPerformanceMonitor>();
if (_instance == null)
{
var go = new GameObject("BTPerformanceMonitor");
_instance = go.AddComponent<BTPerformanceMonitor>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
// FPS计算
private float _lastFrameTime;
private float _currentFPS;
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
_lastFrameTime = Time.time;
}
/// <summary>
/// 获取当前FPS
/// </summary>
public float GetCurrentFPS()
{
return _currentFPS;
}
private void Update()
{
// 计算FPS
float deltaTime = Time.time - _lastFrameTime;
if (deltaTime > 0)
{
_currentFPS = 1f / Time.unscaledDeltaTime;
}
_lastFrameTime = Time.time;
if (!EnableMonitoring)
return;
// 重置帧计数
ResetFrameCounters();
// 检查高频操作
CheckHighFrequencyOperations();
// 更新报告
UpdateReport();
}
#region 监控接口
/// <summary>
/// 开始记录节点执行
/// </summary>
public void BeginNodeExecution(Node node)
{
if (!EnableMonitoring || node == null)
return;
string nodeId = GetNodeId(node);
if (!_nodePerformanceData.TryGetValue(nodeId, out var data))
{
data = new NodePerformanceData
{
NodeName = node.name,
NodeType = node.GetType().Name
};
_nodePerformanceData[nodeId] = data;
}
data.TotalExecutions++;
data.FrameExecutions++;
// 检查高频执行
if (data.FrameExecutions > FrameExecutionThreshold)
{
LogWarning($"节点高频执行: {node.name} 本帧已执行 {data.FrameExecutions} 次", node);
data.WarningCount++;
}
// 检查死循环
if (DetectInfiniteLoops && data.TotalExecutions > InfiniteLoopThreshold)
{
LogError($"检测到可能的死循环: {node.name} 已执行 {data.TotalExecutions} 次", node);
}
}
/// <summary>
/// 开始记录任务执行(重载)
/// </summary>
public void BeginNodeExecution(Task task)
{
if (!EnableMonitoring || task == null)
return;
string nodeId = $"Task_{task.GetType().Name}_{task.GetHashCode()}";
if (!_nodePerformanceData.TryGetValue(nodeId, out var data))
{
data = new NodePerformanceData
{
NodeName = task.GetType().Name,
NodeType = task.GetType().Name
};
_nodePerformanceData[nodeId] = data;
}
data.TotalExecutions++;
data.FrameExecutions++;
// 检查高频执行
if (data.FrameExecutions > FrameExecutionThreshold)
{
LogWarning($"任务高频执行: {task.GetType().Name} 本帧已执行 {data.FrameExecutions} 次");
data.WarningCount++;
}
// 检查死循环
if (DetectInfiniteLoops && data.TotalExecutions > InfiniteLoopThreshold)
{
LogError($"检测到可能的死循环: {task.GetType().Name} 已执行 {data.TotalExecutions} 次");
}
}
/// <summary>
/// 结束记录节点执行
/// </summary>
public void EndNodeExecution(Node node, float executionTimeMs)
{
if (!EnableMonitoring || node == null)
return;
string nodeId = GetNodeId(node);
if (_nodePerformanceData.TryGetValue(nodeId, out var data))
{
data.TotalExecutionTimeMs += executionTimeMs;
data.MaxExecutionTimeMs = Mathf.Max(data.MaxExecutionTimeMs, executionTimeMs);
// 检查执行耗时
if (executionTimeMs > ExecutionTimeThresholdMs)
{
LogWarning($"节点执行耗时过长: {node.name} 耗时 {executionTimeMs:F2}ms", node);
data.WarningCount++;
}
}
}
/// <summary>
/// 结束记录任务执行(重载)
/// </summary>
public void EndNodeExecution(Task task, float executionTimeMs)
{
if (!EnableMonitoring || task == null)
return;
string nodeId = $"Task_{task.GetType().Name}_{task.GetHashCode()}";
if (_nodePerformanceData.TryGetValue(nodeId, out var data))
{
data.TotalExecutionTimeMs += executionTimeMs;
data.MaxExecutionTimeMs = Mathf.Max(data.MaxExecutionTimeMs, executionTimeMs);
// 检查执行耗时
if (executionTimeMs > ExecutionTimeThresholdMs)
{
LogWarning($"任务执行耗时过长: {task.GetType().Name} 耗时 {executionTimeMs:F2}ms");
data.WarningCount++;
}
}
}
/// <summary>
/// 记录高消耗操作
/// </summary>
public void RecordExpensiveOperation(string operationType, string context = "")
{
if (!EnableMonitoring || !DetectExpensiveOperations)
return;
string key = $"{operationType}_{context}";
if (!_operationCounters.TryGetValue(key, out var counter))
{
counter = new OperationCounter
{
OperationName = operationType,
LastResetTime = Time.time
};
_operationCounters[key] = counter;
}
counter.Count++;
counter.FrameCount++;
// 检查高频操作
if (counter.FrameCount > HighFrequencyOperationThreshold)
{
LogWarning($"高频消耗操作: {operationType} 本帧已执行 {counter.FrameCount} 次 (Context: {context})");
}
}
/// <summary>
/// 记录SetPosition操作
/// </summary>
public void RecordSetPosition(Transform transform)
{
RecordExpensiveOperation("SetPosition", transform?.name ?? "Unknown");
}
/// <summary>
/// 记录SetRotation操作
/// </summary>
public void RecordSetRotation(Transform transform)
{
RecordExpensiveOperation("SetRotation", transform?.name ?? "Unknown");
}
/// <summary>
/// 记录SetActive操作
/// </summary>
public void RecordSetActive(GameObject go)
{
RecordExpensiveOperation("SetActive", go?.name ?? "Unknown");
}
/// <summary>
/// 记录Instantiate操作
/// </summary>
public void RecordInstantiate(string prefabName)
{
RecordExpensiveOperation("Instantiate", prefabName);
}
#endregion
#region 报告功能
/// <summary>
/// 开始记录
/// </summary>
public void StartRecording()
{
_isRecording = true;
_recordingStartTime = Time.time;
_nodePerformanceData.Clear();
_operationCounters.Clear();
Debug.Log("[BTPerformanceMonitor] 开始性能记录");
}
/// <summary>
/// 停止记录并生成报告
/// </summary>
public PerformanceReport StopRecording()
{
_isRecording = false;
_currentReport = GenerateReport();
Debug.Log("[BTPerformanceMonitor] 停止性能记录,生成报告");
return _currentReport;
}
/// <summary>
/// 生成性能报告
/// </summary>
public PerformanceReport GenerateReport()
{
var report = new PerformanceReport
{
Duration = Time.time - _recordingStartTime,
TotalNodeExecutions = _nodePerformanceData.Values.Sum(d => d.TotalExecutions),
WarningCount = _nodePerformanceData.Values.Sum(d => d.WarningCount)
};
// 获取热点节点(执行次数最多或耗时最长)
report.HotNodes = _nodePerformanceData.Values
.OrderByDescending(d => d.TotalExecutions)
.Take(10)
.ToList();
// 生成警告列表
foreach (var data in _nodePerformanceData.Values)
{
if (data.AverageExecutionTimeMs > ExecutionTimeThresholdMs)
{
report.Warnings.Add($"[{data.NodeName}] 平均耗时过高: {data.AverageExecutionTimeMs:F2}ms");
}
if (data.TotalExecutions > 1000)
{
report.Warnings.Add($"[{data.NodeName}] 执行次数过多: {data.TotalExecutions}次");
}
}
return report;
}
/// <summary>
/// 获取当前报告
/// </summary>
public PerformanceReport GetCurrentReport()
{
return _currentReport ?? GenerateReport();
}
/// <summary>
/// 打印报告到控制台
/// </summary>
public void PrintReport()
{
var report = GetCurrentReport();
Debug.Log("========== 行为树性能报告 ==========");
Debug.Log($"记录时长: {report.Duration:F1}秒");
Debug.Log($"总执行次数: {report.TotalNodeExecutions}");
Debug.Log($"警告数: {report.WarningCount}");
Debug.Log("热点节点:");
foreach (var node in report.HotNodes)
{
Debug.Log($" - {node.NodeName}: {node.TotalExecutions}次, " +
$"平均{node.AverageExecutionTimeMs:F2}ms, " +
$"最大{node.MaxExecutionTimeMs:F2}ms");
}
if (report.Warnings.Count > 0)
{
Debug.LogWarning("性能警告:");
foreach (var warning in report.Warnings)
{
Debug.LogWarning($" - {warning}");
}
}
Debug.Log("====================================");
}
#endregion
#region 私有方法
/// <summary>
/// 获取节点唯一ID
/// </summary>
private string GetNodeId(Node node)
{
return $"{node.GetType().Name}_{node.name}_{node.GetHashCode()}";
}
/// <summary>
/// 重置帧计数器
/// </summary>
private void ResetFrameCounters()
{
foreach (var data in _nodePerformanceData.Values)
{
data.FrameExecutions = 0;
}
foreach (var counter in _operationCounters.Values)
{
counter.FrameCount = 0;
}
}
/// <summary>
/// 检查高频操作
/// </summary>
private void CheckHighFrequencyOperations()
{
foreach (var counter in _operationCounters.Values)
{
if (counter.FrameCount > HighFrequencyOperationThreshold)
{
LogWarning($"检测到高频操作: {counter.OperationName} 本帧{counter.FrameCount}次");
}
}
}
/// <summary>
/// 更新报告
/// </summary>
private void UpdateReport()
{
if (_isRecording && Time.time - _recordingStartTime > 10f)
{
// 每10秒自动更新报告
_currentReport = GenerateReport();
}
}
/// <summary>
/// 记录警告
/// </summary>
private void LogWarning(string message, Node node = null)
{
if (node != null)
{
Debug.LogWarning($"[BTPerformance] {message} (Node: {node.name})");
}
else
{
Debug.LogWarning($"[BTPerformance] {message}");
}
}
/// <summary>
/// 记录错误
/// </summary>
private void LogError(string message, Node node = null)
{
if (node != null)
{
Debug.LogError($"[BTPerformance] {message} (Node: {node.name})");
}
else
{
Debug.LogError($"[BTPerformance] {message}");
}
}
#endregion
#region 静态快捷方法
/// <summary>
/// 快速记录节点执行
/// </summary>
public static void RecordNodeExecution(Node node, System.Action action)
{
if (Instance == null || !Instance.EnableMonitoring)
{
action();
return;
}
var stopwatch = new Stopwatch();
stopwatch.Start();
Instance.BeginNodeExecution(node);
action();
stopwatch.Stop();
Instance.EndNodeExecution(node, stopwatch.ElapsedMilliseconds);
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,379 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树运行时可视化器
/// 记录和展示行为树的实时执行状态
/// </summary>
public class BTRuntimeVisualizer : MonoBehaviour
{
[Header("调试设置")]
[Tooltip("启用运行时可视化")]
public bool EnableVisualization = true;
[Tooltip("最大历史记录数")]
public int MaxHistorySize = 100;
[Tooltip("节点状态过期时间(秒)")]
public float NodeStatusExpireTime = 2f;
[Tooltip("是否记录变量变化")]
public bool RecordVariableChanges = true;
// 节点状态数据
private Dictionary<string, NodeStatusData> _nodeStatusMap = new Dictionary<string, NodeStatusData>();
// 执行历史
private Queue<ExecutionRecord> _executionHistory = new Queue<ExecutionRecord>();
// 变量历史
private Dictionary<string, List<VariableChangeRecord>> _variableHistory = new Dictionary<string, List<VariableChangeRecord>>();
// 当前活跃的行为树
private List<BehaviourTree> _activeTrees = new List<BehaviourTree>();
// 单例
private static BTRuntimeVisualizer _instance;
public static BTRuntimeVisualizer Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<BTRuntimeVisualizer>();
if (_instance == null)
{
var go = new GameObject("BTRuntimeVisualizer");
_instance = go.AddComponent<BTRuntimeVisualizer>();
if (Application.isPlaying)
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
// 事件
public event Action<string, NodeStatus> OnNodeStatusChanged;
public event Action<ExecutionRecord> OnExecutionRecorded;
public event Action<string, object> OnVariableChanged;
void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
if (Application.isPlaying)
DontDestroyOnLoad(gameObject);
}
void Update()
{
if (!EnableVisualization)
return;
// 清理过期状态
CleanupExpiredStatuses();
}
#region 节点状态记录
/// <summary>
/// 记录节点状态变化
/// </summary>
public void RecordNodeStatus(Node node, Status status, BehaviourTree tree)
{
if (!EnableVisualization || node == null)
return;
string nodeId = GetNodeUniqueId(node, tree);
string nodeName = GetNodeDisplayName(node);
var statusData = new NodeStatusData
{
NodeId = nodeId,
NodeName = nodeName,
Status = ConvertStatus(status),
Timestamp = Time.time,
TreeName = tree?.name ?? "Unknown",
RowIndex = GetRowIndex(node, tree)
};
_nodeStatusMap[nodeId] = statusData;
// 添加到执行历史
var record = new ExecutionRecord
{
Timestamp = Time.time,
NodeName = nodeName,
NodeId = nodeId,
Result = statusData.Status,
TreeName = tree?.name,
RowIndex = statusData.RowIndex
};
AddToHistory(record);
// 触发事件
OnNodeStatusChanged?.Invoke(nodeId, statusData.Status);
OnExecutionRecorded?.Invoke(record);
}
/// <summary>
/// 获取节点当前状态
/// </summary>
public NodeStatusData GetNodeStatus(string nodeId)
{
if (_nodeStatusMap.TryGetValue(nodeId, out var status))
return status;
return null;
}
/// <summary>
/// 获取所有节点状态
/// </summary>
public List<NodeStatusData> GetAllNodeStatuses()
{
return _nodeStatusMap.Values.ToList();
}
/// <summary>
/// 获取指定行为树的所有节点状态
/// </summary>
public List<NodeStatusData> GetTreeNodeStatuses(string treeName)
{
return _nodeStatusMap.Values
.Where(s => s.TreeName == treeName)
.ToList();
}
#endregion
#region 变量监控
/// <summary>
/// 记录变量变化
/// </summary>
public void RecordVariableChange(string variableName, object value, string treeName)
{
if (!EnableVisualization || !RecordVariableChanges)
return;
if (!_variableHistory.ContainsKey(variableName))
{
_variableHistory[variableName] = new List<VariableChangeRecord>();
}
var record = new VariableChangeRecord
{
Timestamp = Time.time,
VariableName = variableName,
Value = value?.ToString() ?? "null",
TreeName = treeName
};
_variableHistory[variableName].Add(record);
// 限制历史数量
if (_variableHistory[variableName].Count > MaxHistorySize)
{
_variableHistory[variableName].RemoveAt(0);
}
OnVariableChanged?.Invoke(variableName, value);
}
/// <summary>
/// 获取变量历史
/// </summary>
public List<VariableChangeRecord> GetVariableHistory(string variableName)
{
if (_variableHistory.TryGetValue(variableName, out var history))
return history;
return new List<VariableChangeRecord>();
}
/// <summary>
/// 获取所有监控的变量名
/// </summary>
public List<string> GetMonitoredVariables()
{
return _variableHistory.Keys.ToList();
}
#endregion
#region 执行历史
/// <summary>
/// 获取执行历史
/// </summary>
public List<ExecutionRecord> GetExecutionHistory()
{
return _executionHistory.ToList();
}
/// <summary>
/// 获取指定行为树的执行历史
/// </summary>
public List<ExecutionRecord> GetTreeExecutionHistory(string treeName)
{
return _executionHistory.Where(r => r.TreeName == treeName).ToList();
}
/// <summary>
/// 清除历史
/// </summary>
public void ClearHistory()
{
_executionHistory.Clear();
_nodeStatusMap.Clear();
_variableHistory.Clear();
}
private void AddToHistory(ExecutionRecord record)
{
_executionHistory.Enqueue(record);
while (_executionHistory.Count > MaxHistorySize)
{
_executionHistory.Dequeue();
}
}
#endregion
#region 辅助方法
private string GetNodeUniqueId(Node node, BehaviourTree tree)
{
return $"{tree?.name ?? "Unknown"}_{node.GetType().Name}_{node.GetHashCode()}";
}
private string GetNodeDisplayName(Node node)
{
if (!string.IsNullOrEmpty(node.name))
return node.name;
return node.GetType().Name;
}
private NodeStatus ConvertStatus(Status status)
{
return status switch
{
Status.Success => NodeStatus.Success,
Status.Failure => NodeStatus.Failure,
Status.Running => NodeStatus.Running,
Status.Resting => NodeStatus.Resting,
Status.Optional => NodeStatus.Optional,
_ => NodeStatus.Unknown
};
}
private int GetRowIndex(Node node, BehaviourTree tree)
{
if (tree == null) return -1;
var container = BehaviourTreeNodeInfoManager.GetContainer(tree.name);
if (container == null) return -1;
// 尝试通过节点名称匹配
var info = container.NodeInfos.Find(n => n.NodeTypeName == node.name);
return info?.RowIndex ?? -1;
}
private void CleanupExpiredStatuses()
{
float currentTime = Time.time;
var expiredKeys = _nodeStatusMap
.Where(kv => currentTime - kv.Value.Timestamp > NodeStatusExpireTime)
.Select(kv => kv.Key)
.ToList();
foreach (var key in expiredKeys)
{
_nodeStatusMap.Remove(key);
}
}
#endregion
#region 数据类
public enum NodeStatus
{
Unknown,
Success,
Failure,
Running,
Resting,
Optional
}
public class NodeStatusData
{
public string NodeId;
public string NodeName;
public NodeStatus Status;
public float Timestamp;
public string TreeName;
public int RowIndex;
public string GetFormattedTime()
{
return $"{Timestamp:F2}s";
}
public Color GetStatusColor()
{
return Status switch
{
NodeStatus.Success => new Color(0.2f, 0.8f, 0.2f), // 绿色
NodeStatus.Failure => new Color(0.9f, 0.2f, 0.2f), // 红色
NodeStatus.Running => new Color(0.2f, 0.6f, 1f), // 蓝色
NodeStatus.Resting => new Color(0.8f, 0.8f, 0.8f), // 灰色
_ => Color.white
};
}
}
public class ExecutionRecord
{
public float Timestamp;
public string NodeName;
public string NodeId;
public NodeStatus Result;
public string TreeName;
public int RowIndex;
public string GetFormattedTime()
{
return $"{Timestamp:F2}s";
}
}
public class VariableChangeRecord
{
public float Timestamp;
public string VariableName;
public string Value;
public string TreeName;
public string GetFormattedTime()
{
return $"{Timestamp:F2}s";
}
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,212 @@
using System.Collections;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树控制器增强功能
/// 自动降频保护、无限制帧率模式、性能自适应
/// </summary>
public class BehaviourTreeControllerEnhanced : MonoBehaviour
{
[Header("性能保护")]
[Tooltip("启用自动降频保护")]
public bool EnableAdaptiveTickRate = true;
[Tooltip("目标帧率当EnableAdaptiveTickRate时这是最高帧率")]
[Range(20, 120)]
public int TargetTickRate = 60;
[Tooltip("最低帧率限制")]
[Range(10, 60)]
public int MinTickRate = 20;
[Tooltip("帧率调整间隔(秒)")]
public float AdjustmentInterval = 2f;
[Tooltip("性能阈值低于此FPS开始降频")]
public float PerformanceThreshold = 45f;
[Header("特殊模式")]
[Tooltip("启用无限制帧率模式(音游等特殊玩法)")]
public bool UnlimitedTickMode = false;
[Tooltip("无限制模式下的固定DeltaTime")]
public float UnlimitedModeDeltaTime = 0.016f;
[Header("性能监控集成")]
[Tooltip("与性能监控器联动")]
public bool LinkWithPerformanceMonitor = true;
[Tooltip("检测到性能问题时自动降频")]
public bool AutoReduceOnWarning = true;
private float _currentTickInterval;
private float _lastAdjustmentTime;
private float[] _frameTimeBuffer = new float[30];
private int _frameTimeIndex = 0;
private float _currentFPS;
private BehaviourTreeController _baseController;
private bool _isReducedPerformance = false;
private int _originalTickRate;
void Awake()
{
// 延迟到 Start 初始化,避免组件顺序问题
}
void Start()
{
_baseController = GetComponent<BehaviourTreeController>();
if (_baseController == null)
{
Debug.LogError("[BTControllerEnhanced] Need BehaviourTreeController component");
enabled = false;
return;
}
_originalTickRate = TargetTickRate;
_currentTickInterval = 1f / TargetTickRate;
for (int i = 0; i < _frameTimeBuffer.Length; i++)
{
_frameTimeBuffer[i] = 0.016f;
}
}
void Update()
{
if (!enabled || _baseController == null)
return;
RecordFrameTime();
_currentFPS = CalculateAverageFPS();
if (EnableAdaptiveTickRate && !UnlimitedTickMode)
{
UpdateAdaptiveTickRate();
}
if (LinkWithPerformanceMonitor && AutoReduceOnWarning)
{
CheckPerformanceMonitorWarnings();
}
ApplyConfiguration();
}
private void RecordFrameTime()
{
_frameTimeBuffer[_frameTimeIndex] = Time.unscaledDeltaTime;
_frameTimeIndex = (_frameTimeIndex + 1) % _frameTimeBuffer.Length;
}
private float CalculateAverageFPS()
{
float totalTime = 0f;
for (int i = 0; i < _frameTimeBuffer.Length; i++)
{
totalTime += _frameTimeBuffer[i];
}
float avgDeltaTime = totalTime / _frameTimeBuffer.Length;
return avgDeltaTime > 0 ? 1f / avgDeltaTime : 0;
}
private void UpdateAdaptiveTickRate()
{
if (Time.time - _lastAdjustmentTime < AdjustmentInterval)
return;
_lastAdjustmentTime = Time.time;
if (_currentFPS < PerformanceThreshold)
{
if (_baseController.TickRate > MinTickRate)
{
int newRate = Mathf.Max(MinTickRate, _baseController.TickRate - 10);
SetTickRate(newRate);
_isReducedPerformance = true;
Debug.LogWarning("[BTControllerEnhanced] FPS low: " + _currentFPS.ToString("F1") +
", reducing tick rate to: " + newRate);
}
}
else if (_currentFPS > PerformanceThreshold + 15 && _isReducedPerformance)
{
if (_baseController.TickRate < _originalTickRate)
{
int newRate = Mathf.Min(_originalTickRate, _baseController.TickRate + 5);
SetTickRate(newRate);
if (newRate >= _originalTickRate)
{
_isReducedPerformance = false;
Debug.Log("[BTControllerEnhanced] Performance recovered, tick rate restored");
}
}
}
}
private void CheckPerformanceMonitorWarnings()
{
if (BTPerformanceMonitor.Instance == null)
return;
var report = BTPerformanceMonitor.Instance.GetCurrentReport();
if (report.WarningCount > 5 && !_isReducedPerformance)
{
int newRate = Mathf.Max(MinTickRate, _baseController.TickRate - 10);
SetTickRate(newRate);
_isReducedPerformance = true;
Debug.LogWarning("[BTControllerEnhanced] Performance warnings detected, reducing tick rate to: " + newRate);
}
}
private void SetTickRate(int rate)
{
if (_baseController != null)
{
_baseController.TickRate = rate;
_currentTickInterval = 1f / rate;
}
}
private void ApplyConfiguration()
{
if (UnlimitedTickMode && _baseController != null)
{
// Unlimited mode: use fixed delta time
_baseController.TickRate = 0;
}
}
public void SetUnlimitedMode(bool enable)
{
UnlimitedTickMode = enable;
if (enable)
{
Debug.Log("[BTControllerEnhanced] Unlimited tick mode enabled (for rhythm games)");
}
else
{
SetTickRate(TargetTickRate);
Debug.Log("[BTControllerEnhanced] Normal tick mode restored");
}
}
public float GetCurrentFPS()
{
return _currentFPS;
}
public bool IsReducedPerformance()
{
return _isReducedPerformance;
}
}
}

View File

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

View File

@ -0,0 +1,532 @@
using System.Collections.Generic;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using GameplayEditor.Config;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树扩展属性
/// 存储行为树的额外元数据不修改原有BehaviourTree类
/// 与BehaviourTreeNodeInfoContainer一起使用
/// </summary>
public static class BehaviourTreeExtended
{
// 使用EditorPrefs或ScriptableObject存储扩展属性
// 这里使用静态字典运行时存储编辑器中使用ScriptableObject
// 黑板4域变量名对应设计文档 1#~4#
public const string TREE_DICT_KEY = "ActivityTreeDict"; // 1# 当前行为树域
public const string ROLE_DICT_KEY = "ActivityRoleDict"; // 2# 角色域
public const string CUSTOM_DICT_KEY = "ActivityCustomDict"; // 3# 自定义域
public const string GLOBAL_DICT_KEY = "ActivityGlobalDict"; // 4# 系统全局域
/// <summary>
/// 初始化黑板4个字典域
/// </summary>
public static void InitializeBlackboardDomains(IBlackboard blackboard)
{
if (blackboard == null) return;
EnsureDictVariable(blackboard, TREE_DICT_KEY);
EnsureDictVariable(blackboard, ROLE_DICT_KEY);
EnsureDictVariable(blackboard, CUSTOM_DICT_KEY);
EnsureDictVariable(blackboard, GLOBAL_DICT_KEY);
Debug.Log("[BehaviourTreeExtended] 黑板4域初始化完成");
}
private static void EnsureDictVariable(IBlackboard blackboard, string key)
{
if (blackboard.variables == null) return;
if (!blackboard.variables.ContainsKey(key))
{
blackboard.AddVariable<Dictionary<string, object>>(key, new Dictionary<string, object>());
}
}
/// <summary>
/// 检查行为树ID是否为头文件
/// 规则: StageID*10 为头文件 (如 301502490)
/// StageID 为正文 (如 30150249)
/// </summary>
/// <param name="treeId">行为树ID</param>
/// <returns>是否为头文件</returns>
public static bool IsHeaderFile(int treeId)
{
// 头文件特征: 以0结尾且除以10后仍是有效ID
return treeId > 0 && treeId % 10 == 0;
}
/// <summary>
/// 获取基础关卡ID
/// 头文件 301502490 → 30150249
/// 正文 30150249 → 30150249
/// </summary>
/// <param name="treeId">行为树ID</param>
/// <returns>基础关卡ID</returns>
public static int GetBaseStageID(int treeId)
{
if (IsHeaderFile(treeId))
{
return treeId / 10;
}
return treeId;
}
/// <summary>
/// 获取头文件ID
/// 正文 30150249 → 301502490
/// </summary>
/// <param name="stageId">关卡ID</param>
/// <returns>头文件ID</returns>
public static int GetHeaderFileID(int stageId)
{
return stageId * 10;
}
}
/// <summary>
/// 行为树运行时控制器
/// 控制头文件和正文的执行逻辑
/// </summary>
public enum TickRateMode
{
Normal, // 普通模式20-120fps
Unlimited // 无限制模式:每帧都执行(音游等特殊玩法)
}
public class BehaviourTreeController : MonoBehaviour
{
[Header("行为树配置")]
[Tooltip("关卡ID")]
public int StageID;
[Tooltip("头文件行为树(初始化,只执行一次)")]
public BehaviourTree HeaderTree;
[Tooltip("正文行为树(主逻辑,每帧执行)")]
public BehaviourTree BodyTree;
[Header("运行配置")]
[Tooltip("Tick率模式")]
public TickRateMode TickMode = TickRateMode.Normal;
[Range(20, 120)]
[Tooltip("运行帧率tick/秒Normal模式下有效")]
public int TickRate = 60;
[Tooltip("无限制模式下是否使用固定DeltaTime")]
public bool UseFixedDeltaTimeInUnlimited = true;
[Tooltip("无限制模式下的固定DeltaTime")]
public float UnlimitedDeltaTime = 0.016f;
[Tooltip("是否在启动时自动运行")]
public bool AutoStart = true;
// 运行时状态
private bool _headerExecuted = false;
private float _tickInterval;
private float _lastTickTime;
private bool _isRunning = false;
// NodeCanvas 行为树执行代理
private BehaviourTreeOwner _headerOwner;
private BehaviourTreeOwner _bodyOwner;
/// <summary>
/// 是否已执行过头文件
/// </summary>
public bool HeaderExecuted => _headerExecuted;
/// <summary>
/// 是否正在运行
/// </summary>
public bool IsRunning => _isRunning;
void Start()
{
_tickInterval = 1f / TickRate;
if (AutoStart)
{
StartExecution();
}
}
void Update()
{
if (!_isRunning) return;
// 无限制模式:每帧都执行,不检查时间间隔
if (TickMode == TickRateMode.Unlimited)
{
ExecuteTick();
return;
}
// 普通模式检查是否到达tick时间
if (Time.time - _lastTickTime < _tickInterval)
return;
_lastTickTime = Time.time;
ExecuteTick();
}
/// <summary>
/// 执行一次Tick头文件或正文
/// </summary>
private void ExecuteTick()
{
// 执行头文件(直到其运行完成)
if (!_headerExecuted && HeaderTree != null)
{
ExecuteHeaderTree();
return; // 头文件执行期间不执行正文
}
// 执行正文每tick
if (BodyTree != null)
{
ExecuteBodyTree();
}
}
/// <summary>
/// 开始执行行为树
/// </summary>
public void StartExecution()
{
_isRunning = true;
_headerExecuted = false;
_lastTickTime = Time.time;
// 确保管理器已初始化
EnsureManagersInitialized();
// 确保 Owner 和黑板就绪
EnsureOwners();
var bb = GetSharedBlackboard();
_headerOwner.blackboard = bb;
_bodyOwner.blackboard = bb;
// 初始化黑板4域在头文件执行前完成
BehaviourTreeExtended.InitializeBlackboardDomains(bb);
Debug.Log($"[BTController] 开始执行关卡 {StageID} 的行为树,帧率: {TickRate}");
}
/// <summary>
/// 获取共享黑板
/// </summary>
private IBlackboard GetSharedBlackboard()
{
var bb = GetComponent<IBlackboard>();
if (bb == null)
{
bb = gameObject.AddComponent<Blackboard>();
}
return bb;
}
/// <summary>
/// 确保存在两个 BehaviourTreeOwner 分别用于头文件和正文
/// </summary>
private void EnsureOwners()
{
if (_headerOwner == null || _bodyOwner == null)
{
var owners = GetComponents<BehaviourTreeOwner>();
if (owners.Length >= 2)
{
_headerOwner = owners[0];
_bodyOwner = owners[1];
}
else if (owners.Length == 1)
{
_headerOwner = owners[0];
_bodyOwner = gameObject.AddComponent<BehaviourTreeOwner>();
}
else
{
_headerOwner = gameObject.AddComponent<BehaviourTreeOwner>();
_bodyOwner = gameObject.AddComponent<BehaviourTreeOwner>();
}
}
}
/// <summary>
/// 确保关键管理器已初始化(防止因执行顺序导致未初始化)
/// </summary>
private static void EnsureManagersInitialized()
{
// 确保 DialogInfoManager 已初始化
if (!DialogInfoManager.Instance.IsInitialized)
{
#if UNITY_EDITOR
var byStageConfig = AssetDatabase.LoadAssetAtPath<DialogInfoByStageConfig>(
"Assets/Verification/Configs/DialogInfoByStageConfig.asset");
var database = AssetDatabase.LoadAssetAtPath<DialogInfoDatabase>(
"Assets/Verification/Configs/DialogInfoDatabase.asset");
if (byStageConfig != null && database != null)
{
DialogInfoManager.Instance.Initialize(byStageConfig, database);
Debug.Log("[BTController] 自动初始化 DialogInfoManager");
}
#endif
}
// 确保 GameplayManagerHub 存在
var _ = GameplayManagerHub.Instance;
}
/// <summary>
/// 停止执行
/// </summary>
public void StopExecution()
{
_isRunning = false;
_headerOwner?.StopBehaviour();
_bodyOwner?.StopBehaviour();
Debug.Log($"[BTController] 停止执行关卡 {StageID} 的行为树");
}
/// <summary>
/// 重置执行状态(允许再次执行头文件)
/// </summary>
public void ResetExecution()
{
_headerExecuted = false;
_isRunning = false;
_headerOwner?.StopBehaviour();
_bodyOwner?.StopBehaviour();
Debug.Log($"[BTController] 重置关卡 {StageID} 的执行状态");
}
/// <summary>
/// 执行头文件行为树(手动 tick直到其返回非 Running 状态)
/// </summary>
private void ExecuteHeaderTree()
{
if (HeaderTree == null || _headerOwner == null) return;
try
{
// 切换并启动头文件Manual 模式)
if (_headerOwner.behaviour != HeaderTree)
{
_headerOwner.SwitchBehaviour(HeaderTree, Graph.UpdateMode.Manual, null);
}
if (!_headerOwner.isRunning)
{
_headerOwner.StartBehaviour(Graph.UpdateMode.Manual);
}
// Tick 一次
var status = _headerOwner.Tick();
Debug.Log($"[BTController] 头文件 tick: {status}");
if (status != Status.Running)
{
_headerExecuted = true;
Debug.Log($"[BTController] 头文件执行完成: {status}");
// 头文件完成后启动正文
if (BodyTree != null && _bodyOwner != null)
{
if (_bodyOwner.behaviour != BodyTree)
{
_bodyOwner.SwitchBehaviour(BodyTree, Graph.UpdateMode.Manual, null);
}
if (!_bodyOwner.isRunning)
{
_bodyOwner.StartBehaviour(Graph.UpdateMode.Manual);
}
}
}
}
catch (System.Exception ex)
{
Debug.LogError($"[BTController] 头文件执行错误: {ex.Message}");
_headerExecuted = true; // 标记为已执行,避免卡死
}
}
/// <summary>
/// 执行正文行为树(手动 tick
/// </summary>
private void ExecuteBodyTree()
{
if (BodyTree == null || _bodyOwner == null) return;
try
{
// 切换并启动正文Manual 模式)
if (_bodyOwner.behaviour != BodyTree)
{
_bodyOwner.SwitchBehaviour(BodyTree, Graph.UpdateMode.Manual, null);
}
if (!_bodyOwner.isRunning)
{
_bodyOwner.StartBehaviour(Graph.UpdateMode.Manual);
}
// 手动 tick 正文
_bodyOwner.UpdateBehaviour();
}
catch (System.Exception ex)
{
// 使用行索引信息定位错误
var treeName = BodyTree.name;
var errorMsg = BehaviourTreeNodeInfoManager.FormatErrorMessage(
treeName, 0, ex.Message);
Debug.LogError($"[BTController] {errorMsg}");
}
}
/// <summary>
/// 从容器加载行为树
/// </summary>
public void LoadTrees(BehaviourTree headerTree, BehaviourTree bodyTree)
{
HeaderTree = headerTree;
BodyTree = bodyTree;
// 从行为树名称提取StageID
if (BodyTree != null && BodyTree.name.StartsWith("BT_"))
{
if (int.TryParse(BodyTree.name.Substring(3), out var treeId))
{
StageID = treeId;
}
}
}
#region Tick模式控制
/// <summary>
/// 设置Tick率模式
/// </summary>
/// <param name="mode">模式</param>
public void SetTickMode(TickRateMode mode)
{
if (TickMode == mode) return;
TickMode = mode;
if (mode == TickRateMode.Unlimited)
{
Debug.Log($"[BTController] 切换到无限制帧率模式(适用于音游等精准玩法)");
}
else
{
_lastTickTime = Time.time;
Debug.Log($"[BTController] 切换到普通模式,帧率: {TickRate}fps");
}
}
/// <summary>
/// 启用无限制帧率模式(快捷方法)
/// </summary>
public void EnableUnlimitedMode()
{
SetTickMode(TickRateMode.Unlimited);
}
/// <summary>
/// 禁用无限制帧率模式,恢复普通模式
/// </summary>
public void DisableUnlimitedMode()
{
SetTickMode(TickRateMode.Normal);
}
/// <summary>
/// 设置无限制模式下的固定DeltaTime
/// </summary>
public void SetUnlimitedDeltaTime(float deltaTime)
{
UnlimitedDeltaTime = Mathf.Max(0.001f, deltaTime);
}
/// <summary>
/// 当前是否处于无限制模式
/// </summary>
public bool IsUnlimitedMode => TickMode == TickRateMode.Unlimited;
/// <summary>
/// 获取当前实际Tick率无限制模式返回-1
/// </summary>
public int GetCurrentTickRate()
{
return TickMode == TickRateMode.Unlimited ? -1 : TickRate;
}
#endregion
}
/// <summary>
/// 行为树运行时数据
/// 存储在ScriptableObject中配置运行参数
/// </summary>
[CreateAssetMenu(fileName = "BTRuntimeData", menuName = "Gameplay/Behaviour Tree Runtime Data")]
public class BehaviourTreeRuntimeData : ScriptableObject
{
[Header("关卡配置")]
[Tooltip("关卡ID")]
public int StageID;
[Tooltip("头文件行为树引用")]
public BehaviourTree HeaderTree;
[Tooltip("正文行为树引用")]
public BehaviourTree BodyTree;
[Header("运行配置")]
[Tooltip("Tick率模式")]
public TickRateMode TickMode = TickRateMode.Normal;
[Range(20, 120)]
[Tooltip("运行帧率tick/秒默认60Normal模式下有效")]
public int TickRate = 60;
[Tooltip("无限制模式下是否使用固定DeltaTime")]
public bool UseFixedDeltaTimeInUnlimited = true;
[Tooltip("无限制模式下的固定DeltaTime")]
public float UnlimitedDeltaTime = 0.016f;
[Tooltip("是否在场景加载时自动启动")]
public bool AutoStartOnLoad = true;
/// <summary>
/// 创建运行时控制器
/// </summary>
public BehaviourTreeController CreateController(GameObject owner)
{
var controller = owner.AddComponent<BehaviourTreeController>();
controller.StageID = StageID;
controller.HeaderTree = HeaderTree;
controller.BodyTree = BodyTree;
controller.TickMode = TickMode;
controller.TickRate = TickRate;
controller.UseFixedDeltaTimeInUnlimited = UseFixedDeltaTimeInUnlimited;
controller.UnlimitedDeltaTime = UnlimitedDeltaTime;
controller.AutoStart = AutoStartOnLoad;
return controller;
}
/// <summary>
/// 切换到无限制模式(用于音游等特殊玩法)
/// </summary>
public void SetUnlimitedMode(bool enable)
{
TickMode = enable ? TickRateMode.Unlimited : TickRateMode.Normal;
}
}
}

View File

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

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using NodeCanvas.BehaviourTrees;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 行为树节点附加信息
/// 存储行索引、节点ID等元数据用于报错定位和调试
/// </summary>
[Serializable]
public class BehaviourTreeNodeInfo
{
/// <summary>行索引ID自动生成用于报错定位</summary>
public int RowIndex;
/// <summary>节点ID策划配置</summary>
public int NodeID;
/// <summary>节点层级(深度)</summary>
public int NodeLayer;
/// <summary>节点类型名称</summary>
public string NodeTypeName;
/// <summary>原始行数据(调试用)</summary>
public string RawData;
/// <summary>源Excel行号从1开始</summary>
public int ExcelRowNumber;
}
/// <summary>
/// 行为树扩展信息容器
/// 与BehaviourTree资产关联存储额外的元数据
/// </summary>
[CreateAssetMenu(fileName = "BTNodeInfo", menuName = "Gameplay/Behaviour Tree Node Info")]
public class BehaviourTreeNodeInfoContainer : ScriptableObject
{
/// <summary>关联的行为树名称</summary>
public string BehaviourTreeName;
/// <summary>行为树ID</summary>
public int TreeID;
/// <summary>是否为头文件</summary>
public bool IsHeaderFile;
/// <summary>节点信息列表与BehaviourTree节点顺序对应</summary>
public List<BehaviourTreeNodeInfo> NodeInfos = new List<BehaviourTreeNodeInfo>();
/// <summary>
/// 根据节点索引获取节点信息
/// </summary>
/// <param name="nodeIndex">节点索引</param>
/// <returns>节点信息未找到返回null</returns>
public BehaviourTreeNodeInfo GetNodeInfo(int nodeIndex)
{
if (nodeIndex >= 0 && nodeIndex < NodeInfos.Count)
return NodeInfos[nodeIndex];
return null;
}
/// <summary>
/// 根据行索引查找节点信息
/// </summary>
/// <param name="rowIndex">行索引</param>
/// <returns>节点信息未找到返回null</returns>
public BehaviourTreeNodeInfo FindByRowIndex(int rowIndex)
{
return NodeInfos.Find(info => info.RowIndex == rowIndex);
}
/// <summary>
/// 根据节点ID查找节点信息
/// </summary>
/// <param name="nodeId">节点ID</param>
/// <returns>节点信息未找到返回null</returns>
public BehaviourTreeNodeInfo FindByNodeID(int nodeId)
{
return NodeInfos.Find(info => info.NodeID == nodeId);
}
/// <summary>
/// 添加节点信息
/// </summary>
public void AddNodeInfo(BehaviourTreeNodeInfo info)
{
NodeInfos.Add(info);
}
/// <summary>
/// 清空所有节点信息
/// </summary>
public void Clear()
{
NodeInfos.Clear();
}
}
/// <summary>
/// 行为树节点信息管理器
/// 运行时提供节点信息查询服务
/// </summary>
public static class BehaviourTreeNodeInfoManager
{
private static readonly Dictionary<string, BehaviourTreeNodeInfoContainer> _containers =
new Dictionary<string, BehaviourTreeNodeInfoContainer>();
/// <summary>
/// 注册节点信息容器
/// </summary>
/// <param name="treeName">行为树名称</param>
/// <param name="container">节点信息容器</param>
public static void Register(string treeName, BehaviourTreeNodeInfoContainer container)
{
_containers[treeName] = container;
}
/// <summary>
/// 获取节点信息容器
/// </summary>
/// <param name="treeName">行为树名称</param>
/// <returns>节点信息容器未找到返回null</returns>
public static BehaviourTreeNodeInfoContainer GetContainer(string treeName)
{
return _containers.TryGetValue(treeName, out var container) ? container : null;
}
/// <summary>
/// 获取节点的行索引(用于报错定位)
/// </summary>
/// <param name="treeName">行为树名称</param>
/// <param name="nodeIndex">节点索引</param>
/// <returns>行索引,未找到返回-1</returns>
public static int GetRowIndex(string treeName, int nodeIndex)
{
var container = GetContainer(treeName);
if (container == null) return -1;
var info = container.GetNodeInfo(nodeIndex);
return info?.RowIndex ?? -1;
}
/// <summary>
/// 生成错误定位信息
/// </summary>
/// <param name="treeName">行为树名称</param>
/// <param name="nodeIndex">节点索引</param>
/// <param name="errorMessage">错误信息</param>
/// <returns>格式化的错误定位信息</returns>
public static string FormatErrorMessage(string treeName, int nodeIndex, string errorMessage)
{
var rowIndex = GetRowIndex(treeName, nodeIndex);
var container = GetContainer(treeName);
var treeID = container?.TreeID ?? 0;
if (rowIndex > 0)
{
return $"[BT:{treeID} Row:{rowIndex}] {errorMessage}";
}
else
{
return $"[BT:{treeID} Node:{nodeIndex}] {errorMessage}";
}
}
/// <summary>
/// 清空所有注册信息
/// </summary>
public static void Clear()
{
_containers.Clear();
}
}
}

View File

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

View File

@ -0,0 +1,201 @@
using System.Collections.Generic;
using GameplayEditor.Nodes;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 组合方法加载器
/// 自动加载和管理组合方法(子树)
/// </summary>
public class CompositeMethodLoader : MonoBehaviour
{
private static CompositeMethodLoader _instance;
public static CompositeMethodLoader Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<CompositeMethodLoader>();
if (_instance == null)
{
var go = new GameObject("CompositeMethodLoader");
_instance = go.AddComponent<CompositeMethodLoader>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
[Header("组合方法配置")]
[Tooltip("组合方法文件夹路径")]
public string MethodsFolderPath = "Assets/CompositeMethods";
[Tooltip("自动加载所有组合方法")]
public bool AutoLoadOnStart = true;
// 已加载的方法
private Dictionary<int, CompositeMethod> _loadedMethods = new Dictionary<int, CompositeMethod>();
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
if (AutoLoadOnStart)
{
LoadAllMethods();
}
}
/// <summary>
/// 加载所有组合方法
/// </summary>
public void LoadAllMethods()
{
#if UNITY_EDITOR
// 从资源文件夹加载
var guids = UnityEditor.AssetDatabase.FindAssets("t:CompositeMethod",
new[] { MethodsFolderPath });
foreach (var guid in guids)
{
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
var method = UnityEditor.AssetDatabase.LoadAssetAtPath<CompositeMethod>(path);
if (method != null)
{
RegisterMethod(method);
}
}
Debug.Log($"[CompositeMethodLoader] 加载了 {_loadedMethods.Count} 个组合方法");
#endif
}
/// <summary>
/// 注册组合方法
/// </summary>
public void RegisterMethod(CompositeMethod method)
{
if (method == null || method.MethodID <= 0)
{
Debug.LogWarning("[CompositeMethodLoader] 无效的组合方法");
return;
}
_loadedMethods[method.MethodID] = method;
CompositeMethodRegistry.Register(method);
Debug.Log($"[CompositeMethodLoader] 注册方法: {method.MethodName} (ID:{method.MethodID})");
}
/// <summary>
/// 获取组合方法
/// </summary>
public CompositeMethod GetMethod(int methodId)
{
_loadedMethods.TryGetValue(methodId, out var method);
return method;
}
/// <summary>
/// 根据名称获取方法
/// </summary>
public CompositeMethod GetMethodByName(string methodName)
{
foreach (var method in _loadedMethods.Values)
{
if (method.MethodName == methodName || method.DisplayName == methodName)
{
return method;
}
}
return null;
}
/// <summary>
/// 获取所有方法
/// </summary>
public IReadOnlyCollection<CompositeMethod> GetAllMethods()
{
return _loadedMethods.Values;
}
/// <summary>
/// 创建运行时子树实例
/// </summary>
public BehaviourTree CreateRuntimeTree(int methodId)
{
var method = GetMethod(methodId);
if (method == null || method.Tree == null)
{
Debug.LogWarning($"[CompositeMethodLoader] 未找到方法或子树: ID={methodId}");
return null;
}
// 创建运行时实例
var runtimeTree = Instantiate(method.Tree);
runtimeTree.name = $"Runtime_{method.MethodName}";
Debug.Log($"[CompositeMethodLoader] 创建运行时子树: {runtimeTree.name}");
return runtimeTree;
}
/// <summary>
/// 创建运行时子树实例(带参数)
/// </summary>
public BehaviourTree CreateRuntimeTree(int methodId, Dictionary<string, object> parameters)
{
var runtimeTree = CreateRuntimeTree(methodId);
if (runtimeTree == null)
return null;
// 设置参数到子树黑板
if (parameters != null && runtimeTree.blackboard != null)
{
foreach (var kvp in parameters)
{
// 使用AddVariable添加或更新变量
if (runtimeTree.blackboard.variables.ContainsKey(kvp.Key))
{
runtimeTree.blackboard.SetVariableValue(kvp.Key, kvp.Value);
}
else
{
runtimeTree.blackboard.AddVariable(kvp.Key, kvp.Value);
}
Debug.Log($"[CompositeMethodLoader] 设置参数: {kvp.Key} = {kvp.Value}");
}
}
return runtimeTree;
}
/// <summary>
/// 卸载所有方法
/// </summary>
public void UnloadAll()
{
_loadedMethods.Clear();
CompositeMethodRegistry.Clear();
Debug.Log("[CompositeMethodLoader] 卸载所有方法");
}
/// <summary>
/// 检查方法是否存在
/// </summary>
public bool HasMethod(int methodId)
{
return _loadedMethods.ContainsKey(methodId);
}
}
}

View File

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

View File

@ -0,0 +1,9 @@
using NodeCanvas.Framework;
namespace GameplayEditor.Core
{
[System.Serializable]
public class GameplayConnection : Connection
{
}
}

View File

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

View File

@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace GameplayEditor.Core
{
/// <summary>
/// 游戏管理器接口
/// 所有业务系统管理器需要实现的接口
/// </summary>
public interface IGameplayManager
{
string ManagerName { get; }
bool IsInitialized { get; }
void Initialize();
void Shutdown();
}
/// <summary>
/// 阵营管理器接口
/// </summary>
public interface ICampManager : IGameplayManager
{
List<int> GetNpcCamps(string npcId);
bool IsSameCamp(string npcId1, string npcId2);
bool IsEnemyCamp(string npcId1, string npcId2);
}
/// <summary>
/// 战斗管理器接口
/// </summary>
public interface IBattleManager : IGameplayManager
{
void SetFightTarget(string attackerId, string targetId);
float GetNPCHealthPercent(string npcId);
float GetNPCDistance(string npcId1, string npcId2);
bool NPCExists(string npcId);
}
/// <summary>
/// NPC管理器接口
/// </summary>
public interface INPCManager : IGameplayManager
{
GameObject SpawnNPC(string npcId, Vector3 position, Quaternion rotation);
void DespawnNPC(string npcId);
GameObject GetNPC(string npcId);
Vector3 GetNPCPosition(string npcId);
void PlayAnimation(string npcId, string animName);
}
/// <summary>
/// 相机管理器接口
/// </summary>
public interface ICameraManager : IGameplayManager
{
void SwitchToCamera(int cameraId, float transitionTime = 0.5f);
void ResetToDefault();
}
/// <summary>
/// 特效管理器接口
/// </summary>
public interface IFXManager : IGameplayManager
{
void PlayEffect(int effectId, Vector3 position, float duration = 0);
void PlayEffectOnNPC(int effectId, string npcId, float duration = 0);
}
/// <summary>
/// UI管理器接口
/// </summary>
public interface IUIManager : IGameplayManager
{
void ShowDialog(int dialogId);
void CloseDialog(int dialogId);
void CloseAllDialogs();
bool IsDialogActive(int dialogId);
void ShowLoading();
void HideLoading();
}
/// <summary>
/// 游戏管理器中心
/// 统一管理所有业务系统
/// </summary>
public class GameplayManagerHub : MonoBehaviour
{
private static GameplayManagerHub _instance;
public static GameplayManagerHub Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<GameplayManagerHub>();
if (_instance == null)
{
var go = new GameObject("GameplayManagerHub");
_instance = go.AddComponent<GameplayManagerHub>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
// 各业务系统管理器
public ICampManager CampManager { get; private set; }
public IBattleManager BattleManager { get; private set; }
public INPCManager NPCManager { get; private set; }
public ICameraManager CameraManager { get; private set; }
public IFXManager FXManager { get; private set; }
public IUIManager UIManager { get; private set; }
// 管理器注册表
private Dictionary<Type, IGameplayManager> _managers = new Dictionary<Type, IGameplayManager>();
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
/// <summary>
/// 注册管理器
/// </summary>
public void RegisterManager<T>(T manager) where T : IGameplayManager
{
_managers[typeof(T)] = manager;
// 设置特定接口引用
if (manager is ICampManager camp) CampManager = camp;
if (manager is IBattleManager battle) BattleManager = battle;
if (manager is INPCManager npc) NPCManager = npc;
if (manager is ICameraManager cam) CameraManager = cam;
if (manager is IFXManager fx) FXManager = fx;
if (manager is IUIManager ui) UIManager = ui;
Debug.Log($"[GameplayManagerHub] 注册管理器: {manager.ManagerName}");
}
/// <summary>
/// 获取管理器
/// </summary>
public T GetManager<T>() where T : class, IGameplayManager
{
if (_managers.TryGetValue(typeof(T), out var manager))
{
return manager as T;
}
return null;
}
/// <summary>
/// 检查管理器是否已注册
/// </summary>
public bool HasManager<T>() where T : class, IGameplayManager
{
return _managers.ContainsKey(typeof(T));
}
/// <summary>
/// 初始化所有管理器
/// </summary>
public void InitializeAll()
{
foreach (var manager in _managers.Values)
{
if (!manager.IsInitialized)
{
manager.Initialize();
}
}
}
/// <summary>
/// 关闭所有管理器
/// </summary>
public void ShutdownAll()
{
foreach (var manager in _managers.Values)
{
if (manager.IsInitialized)
{
manager.Shutdown();
}
}
}
}
/// <summary>
/// 模拟阵营管理器(用于测试)
/// </summary>
public class MockCampManager : ICampManager
{
public string ManagerName => "MockCampManager";
public bool IsInitialized { get; private set; }
private Dictionary<string, List<int>> _npcCamps = new Dictionary<string, List<int>>();
public void Initialize()
{
IsInitialized = true;
Debug.Log("[MockCampManager] 初始化完成");
}
public void Shutdown()
{
IsInitialized = false;
}
public List<int> GetNpcCamps(string npcId)
{
if (_npcCamps.TryGetValue(npcId, out var camps))
{
return camps;
}
// 根据NPC ID返回模拟阵营
if (int.TryParse(npcId, out var id))
{
if (id >= 2000 && id < 3000)
return new List<int> { 1 }; // 玩家阵营
else if (id >= 3000 && id < 4000)
return new List<int> { 2 }; // 敌方阵营
else if (id >= 4000 && id < 5000)
return new List<int> { 1, 4 }; // 玩家+友方阵营
}
return new List<int> { 0 };
}
public void SetNpcCamp(string npcId, List<int> camps)
{
_npcCamps[npcId] = camps;
}
public bool IsSameCamp(string npcId1, string npcId2)
{
var camps1 = GetNpcCamps(npcId1);
var camps2 = GetNpcCamps(npcId2);
return camps1.Any(c1 => camps2.Any(c2 => c1 == c2));
}
public bool IsEnemyCamp(string npcId1, string npcId2)
{
return !IsSameCamp(npcId1, npcId2);
}
}
/// <summary>
/// 模拟NPC管理器用于测试
/// </summary>
public class MockNPCManager : INPCManager
{
public string ManagerName => "MockNPCManager";
public bool IsInitialized { get; private set; }
private Dictionary<string, GameObject> _npcs = new Dictionary<string, GameObject>();
public void Initialize()
{
IsInitialized = true;
Debug.Log("[MockNPCManager] 初始化完成");
}
public void Shutdown()
{
IsInitialized = false;
}
public GameObject SpawnNPC(string npcId, Vector3 position, Quaternion rotation)
{
var go = new GameObject($"NPC_{npcId}");
go.transform.position = position;
go.transform.rotation = rotation;
_npcs[npcId] = go;
Debug.Log($"[MockNPCManager] 生成NPC: {npcId} 在 {position}");
return go;
}
public void DespawnNPC(string npcId)
{
if (_npcs.TryGetValue(npcId, out var go))
{
if (go != null)
{
UnityEngine.Object.Destroy(go);
}
_npcs.Remove(npcId);
}
}
public GameObject GetNPC(string npcId)
{
_npcs.TryGetValue(npcId, out var go);
return go;
}
public Vector3 GetNPCPosition(string npcId)
{
if (_npcs.TryGetValue(npcId, out var go))
{
return go.transform.position;
}
return Vector3.zero;
}
public void PlayAnimation(string npcId, string animName)
{
Debug.Log($"[MockNPCManager] NPC {npcId} 播放动画: {animName}");
}
}
}

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23ad35a149da760419d8fab64e645ef2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,500 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Excel;
using GameplayEditor.Core;
using GameplayEditor.Config;
using NodeCanvas.BehaviourTrees;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
public class ActivityEditorWindow : EditorWindow
{
private string _xlsmPath = "";
private string _dialogXlsmPath = "";
private string _exportFolderAssetPath = "Assets/BT";
private Vector2 _scrollPos;
private int _selectedTab = 0;
private string[] _tabNames = { "行为树", "对话配置", "LUT配置" };
[MenuItem("Window/Activity Editor/Activity Editor")]
public static void ShowWindow()
{
GetWindow<ActivityEditorWindow>("玩法编辑器");
}
private void OnGUI()
{
EditorGUILayout.LabelField("玩法编辑器 - Excel ↔ Graph 同步", EditorStyles.boldLabel);
EditorGUILayout.Space();
_selectedTab = GUILayout.Toolbar(_selectedTab, _tabNames);
EditorGUILayout.Space();
switch (_selectedTab)
{
case 0:
DrawBehaviourTreeTab();
break;
case 1:
DrawDialogTab();
break;
case 2:
DrawLutTab();
break;
}
EditorGUILayout.Space();
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.Height(120));
EditorGUILayout.HelpBox("导入/导出操作的日志会显示在Console中。使用 Window/Activity Editor/Config Validator 进行配置检查。", MessageType.Info);
EditorGUILayout.EndScrollView();
}
#region 行为树标签页
private void DrawBehaviourTreeTab()
{
// ── 导入 ──────────────────────────────────────────────────────
EditorGUILayout.LabelField("导入行为树", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Excel文件:", GUILayout.Width(80));
_xlsmPath = EditorGUILayout.TextField(_xlsmPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
_xlsmPath = EditorUtility.OpenFilePanel("选择xlsm文件", "表", "xlsm,xlsx");
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox(
"提示:'玩法编辑器.xlsx' 是设计规范文档,实际导入请使用策划填写的数据表(.xlsm 或 .xlsx。",
MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("导入 Excel → BehaviourTree", GUILayout.Height(36)))
ImportFromExcel();
if (GUILayout.Button("导入并验证", GUILayout.Height(36), GUILayout.Width(120)))
ImportFromExcelWithValidation();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// ── 导出 ──────────────────────────────────────────────────────
EditorGUILayout.LabelField("导出行为树(将文件夹内所有树合并到一张表)", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("BT文件夹:", GUILayout.Width(80));
_exportFolderAssetPath = EditorGUILayout.TextField(_exportFolderAssetPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
var abs = EditorUtility.OpenFolderPanel("选择BT文件夹", "Assets", "");
if (!string.IsNullOrEmpty(abs))
{
var rel = "Assets" + abs.Replace(Application.dataPath.Replace("\\", "/"), "").Replace("\\", "/");
_exportFolderAssetPath = rel;
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("导出文件夹内所有树 → Excel", GUILayout.Height(36)))
ExportFolderToExcel();
}
private void ImportFromExcel()
{
if (string.IsNullOrEmpty(_xlsmPath) || !File.Exists(_xlsmPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的xlsm文件", "确定");
return;
}
try
{
var nameResolver = new NodeTypeNameResolver();
nameResolver.LoadFromExcel(_xlsmPath);
var parser = new BtSheetParser(nameResolver);
var results = parser.ParseFromExcelWithInfo(_xlsmPath);
// 保存为资产(必须在 Assets 目录内)
var outputDir = EditorUtility.SaveFolderPanel("选择保存目录", "Assets", "");
if (string.IsNullOrEmpty(outputDir)) return;
// 转换为 Assets/... 相对路径
var normalizedOutput = outputDir.Replace("\\", "/");
var normalizedDataPath = Application.dataPath.Replace("\\", "/");
if (!normalizedOutput.StartsWith(normalizedDataPath))
{
EditorUtility.DisplayDialog("错误", "请选择 Assets 文件夹内的目录", "确定");
return;
}
var relativeDir = "Assets" + normalizedOutput.Substring(normalizedDataPath.Length);
int totalNodes = 0;
foreach (var result in results)
{
// 保存行为树
var treeAssetPath = $"{relativeDir}/{result.Tree.name}.asset";
AssetDatabase.CreateAsset(result.Tree, treeAssetPath);
// 保存节点信息容器
if (result.NodeInfoContainer != null)
{
var infoAssetPath = $"{relativeDir}/{result.Tree.name}_Info.asset";
result.NodeInfoContainer.name = $"{result.Tree.name}_Info";
AssetDatabase.CreateAsset(result.NodeInfoContainer, infoAssetPath);
totalNodes += result.NodeInfoContainer.NodeInfos.Count;
}
}
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("成功", $"导入了 {results.Count} 棵行为树,共 {totalNodes} 个节点", "确定");
Debug.Log($"[ActivityEditor] 导入完成,共 {results.Count} 棵树,{totalNodes} 个节点");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导入失败:{ex.Message}", "确定");
Debug.LogError($"[ActivityEditor] 导入错误:{ex}");
}
}
private void ImportFromExcelWithValidation()
{
if (string.IsNullOrEmpty(_xlsmPath) || !File.Exists(_xlsmPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的xlsm文件", "确定");
return;
}
try
{
var nameResolver = new NodeTypeNameResolver();
nameResolver.LoadFromExcel(_xlsmPath);
var parser = new BtSheetParser(nameResolver);
var results = parser.ParseFromExcelWithInfo(_xlsmPath);
// 验证结果
var validationReport = ConfigValidator.ValidateImportResults(results);
if (validationReport.HasErrors)
{
var errorMsg = string.Join("\n", validationReport.Results
.Where(r => r.IsError)
.Take(5)
.Select(r => $"- {r.Message}"));
if (!EditorUtility.DisplayDialog("发现错误",
$"导入结果包含 {validationReport.ErrorCount} 个错误:\n{errorMsg}\n\n是否继续保存",
"继续", "取消"))
{
return;
}
}
// 保存为资产
var outputDir = EditorUtility.SaveFolderPanel("选择保存目录", "Assets", "");
if (string.IsNullOrEmpty(outputDir)) return;
var normalizedOutput = outputDir.Replace("\\", "/");
var normalizedDataPath = Application.dataPath.Replace("\\", "/");
var relativeDir = "Assets" + normalizedOutput.Substring(normalizedDataPath.Length);
int totalNodes = 0;
foreach (var result in results)
{
var treeAssetPath = $"{relativeDir}/{result.Tree.name}.asset";
AssetDatabase.CreateAsset(result.Tree, treeAssetPath);
if (result.NodeInfoContainer != null)
{
var infoAssetPath = $"{relativeDir}/{result.Tree.name}_Info.asset";
AssetDatabase.CreateAsset(result.NodeInfoContainer, infoAssetPath);
totalNodes += result.NodeInfoContainer.NodeInfos.Count;
}
}
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("成功",
$"导入了 {results.Count} 棵行为树,共 {totalNodes} 个节点\n\n错误: {validationReport.ErrorCount}, 警告: {validationReport.WarningCount}",
"确定");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导入失败:{ex.Message}", "确定");
Debug.LogError($"[ActivityEditor] 导入错误:{ex}");
}
}
private void ExportFolderToExcel()
{
if (string.IsNullOrEmpty(_exportFolderAssetPath) || !AssetDatabase.IsValidFolder(_exportFolderAssetPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的Assets文件夹", "确定");
return;
}
var guids = AssetDatabase.FindAssets("t:BehaviourTree", new[] { _exportFolderAssetPath });
if (guids.Length == 0)
{
EditorUtility.DisplayDialog("错误", $"文件夹内没有找到BehaviourTree资产{_exportFolderAssetPath}", "确定");
return;
}
var trees = guids
.Select(g => AssetDatabase.LoadAssetAtPath<BehaviourTree>(AssetDatabase.GUIDToAssetPath(g)))
.Where(t => t != null)
.OrderBy(t => t.name)
.ToList();
var outputPath = EditorUtility.SaveFilePanel("导出为xlsx", "表", "BehaviourTrees", "xlsx");
if (string.IsNullOrEmpty(outputPath)) return;
try
{
var nameResolver = new NodeTypeNameResolver();
var writer = new BtSheetWriter(nameResolver);
writer.ExportToExcel(trees, outputPath);
EditorUtility.DisplayDialog("成功", $"导出完成:{trees.Count} 棵树 → {outputPath}", "确定");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导出失败:{ex.Message}", "确定");
Debug.LogError($"[ActivityEditor] 导出错误:{ex}");
}
}
#endregion
#region 对话配置标签页
private void DrawDialogTab()
{
EditorGUILayout.LabelField("导入对话配置", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Excel文件:", GUILayout.Width(80));
_dialogXlsmPath = EditorGUILayout.TextField(_dialogXlsmPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
_dialogXlsmPath = EditorUtility.OpenFilePanel("选择xlsm文件", "表", "xlsm,xlsx");
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("导入 DialogInfo", GUILayout.Height(36)))
ImportDialogInfo(false);
if (GUILayout.Button("导入并验证", GUILayout.Height(36), GUILayout.Width(120)))
ImportDialogInfo(true);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("说明", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"系统将自动查找Excel中所有包含'Dialog'的Sheet并导入。\n" +
"同时会解析ActivityDialogInfoByStage索引表。\n" +
"提示:'玩法编辑器.xlsx' 是设计规范文档,实际导入请使用策划填写的数据表(.xlsm 或 .xlsx。",
MessageType.Info);
}
private void ImportDialogInfo(bool withValidation)
{
if (string.IsNullOrEmpty(_dialogXlsmPath) || !File.Exists(_dialogXlsmPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的xlsm文件", "确定");
return;
}
try
{
var parser = new DialogInfoParser();
// 解析DialogInfo
var dialogResults = parser.ParseFromExcel(_dialogXlsmPath);
// 解析索引表
var byStageConfig = parser.ParseByStageConfig(_dialogXlsmPath);
if (dialogResults.Count == 0 && byStageConfig.Entries.Count == 0)
{
EditorUtility.DisplayDialog("警告", "未找到任何对话配置或索引表", "确定");
return;
}
// 验证
if (withValidation)
{
var hasError = false;
foreach (var result in dialogResults)
{
if (result.Errors.Count > 0)
{
hasError = true;
Debug.LogError($"[DialogInfo] {result.SheetName} 有 {result.Errors.Count} 个错误");
}
}
if (hasError)
{
if (!EditorUtility.DisplayDialog("发现错误",
"导入结果包含错误,是否继续保存?", "继续", "取消"))
{
return;
}
}
}
// 选择保存目录
var outputDir = EditorUtility.SaveFolderPanel("选择保存目录", "Assets/Configs", "");
if (string.IsNullOrEmpty(outputDir)) return;
var normalizedOutput = outputDir.Replace("\\", "/");
var normalizedDataPath = Application.dataPath.Replace("\\", "/");
if (!normalizedOutput.StartsWith(normalizedDataPath))
{
EditorUtility.DisplayDialog("错误", "请选择 Assets 文件夹内的目录", "确定");
return;
}
var relativeDir = "Assets" + normalizedOutput.Substring(normalizedDataPath.Length);
// 保存DialogInfo配置
int totalDialogs = 0;
foreach (var result in dialogResults)
{
if (result.Dialogs.Count == 0) continue;
var configName = $"DialogInfo_{result.SheetName}.asset";
var configPath = $"{relativeDir}/{configName}";
var config = ScriptableObject.CreateInstance<DialogInfoConfig>();
config.SheetName = result.SheetName;
config.SourceExcelPath = _dialogXlsmPath;
config.Dialogs = result.Dialogs;
AssetDatabase.CreateAsset(config, configPath);
totalDialogs += result.Dialogs.Count;
}
// 保存索引表
if (byStageConfig.Entries.Count > 0)
{
var indexPath = $"{relativeDir}/DialogInfoByStageConfig.asset";
AssetDatabase.CreateAsset(byStageConfig, indexPath);
}
AssetDatabase.SaveAssets();
var msg = $"导入完成:\n" +
$"- {dialogResults.Count} 个DialogInfo表\n" +
$"- {totalDialogs} 条对话配置\n" +
$"- {byStageConfig.Entries.Count} 条索引映射";
EditorUtility.DisplayDialog("成功", msg, "确定");
Debug.Log($"[ActivityEditor] {msg}");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导入失败:{ex.Message}", "确定");
Debug.LogError($"[ActivityEditor] 导入错误:{ex}");
}
}
#endregion
#region LUT配置标签页
private void DrawLutTab()
{
EditorGUILayout.LabelField("LUT配置导入", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"LUT配置UI、Camera、FX、MapInfo通常在关卡配置导入时一并处理。\n" +
"如需单独导入请选择包含LUT表的Excel文件。\n" +
"提示:'玩法编辑器.xlsx' 是设计规范文档,实际导入请使用策划填写的数据表(.xlsm 或 .xlsx。",
MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("导入LUT配置", GUILayout.Height(36)))
ImportLutConfig();
if (GUILayout.Button("验证现有LUT", GUILayout.Height(36), GUILayout.Width(120)))
ValidateExistingLuts();
EditorGUILayout.EndHorizontal();
}
private void ImportLutConfig()
{
var path = EditorUtility.OpenFilePanel("选择LUT配置Excel", "表", "xlsm,xlsx");
if (string.IsNullOrEmpty(path)) return;
try
{
var parser = new LutConfigParser();
var database = parser.ParseAllLuts(path);
if (database.UiLuts.Count == 0 && database.CameraLuts.Count == 0 &&
database.FxLuts.Count == 0 && database.MapInfoLuts.Count == 0)
{
EditorUtility.DisplayDialog("警告", "未找到任何LUT配置", "确定");
return;
}
var outputDir = EditorUtility.SaveFolderPanel("选择保存目录", "Assets/Configs", "");
if (string.IsNullOrEmpty(outputDir)) return;
var normalizedOutput = outputDir.Replace("\\", "/");
var normalizedDataPath = Application.dataPath.Replace("\\", "/");
var relativeDir = "Assets" + normalizedOutput.Substring(normalizedDataPath.Length);
// 保存数据库
var dbPath = $"{relativeDir}/LutDatabase.asset";
AssetDatabase.CreateAsset(database, dbPath);
AssetDatabase.SaveAssets();
var msg = $"LUT导入完成:\n" +
$"- UI LUT: {database.UiLuts.Count}\n" +
$"- Camera LUT: {database.CameraLuts.Count}\n" +
$"- FX LUT: {database.FxLuts.Count}\n" +
$"- MapInfo LUT: {database.MapInfoLuts.Count}";
EditorUtility.DisplayDialog("成功", msg, "确定");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导入失败:{ex.Message}", "确定");
Debug.LogError($"[ActivityEditor] LUT导入错误{ex}");
}
}
private void ValidateExistingLuts()
{
var report = ConfigValidator.ValidateAll();
// 只显示LUT相关结果
var lutResults = report.Results.Where(r =>
r.Category.Contains("LUT") ||
r.Category.Contains("UI") ||
r.Category.Contains("Camera") ||
r.Category.Contains("FX")).ToList();
if (lutResults.Count == 0)
{
EditorUtility.DisplayDialog("验证完成", "未发现LUT配置或所有LUT配置正常", "确定");
}
else
{
var errors = lutResults.Count(r => r.IsError);
var warnings = lutResults.Count(r => r.IsWarning);
EditorUtility.DisplayDialog("验证完成",
$"LUT验证结果:\n错误: {errors}\n警告: {warnings}\n\n详细结果请查看Console",
"确定");
foreach (var result in lutResults)
{
if (result.IsError)
Debug.LogError($"[LUT] {result.Message}");
else if (result.IsWarning)
Debug.LogWarning($"[LUT] {result.Message}");
}
}
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,336 @@
using System.Collections.Generic;
using System.Linq;
using GameplayEditor.Config;
using GameplayEditor.Core;
using GameplayEditor.Excel;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 玩法关卡配置编辑器窗口
/// 用于导入、查看和编辑ActivityStageConfig
/// </summary>
public class ActivityStageConfigEditor : EditorWindow
{
private string _xlsmPath = "";
private string _outputFolder = "Assets/Configs/StageConfigs";
private Vector2 _scrollPos;
private List<ActivityStageConfigData> _parsedData = new List<ActivityStageConfigData>();
private ActivityStageConfigDatabase _database;
private bool _showParsedData = false;
private bool _showDatabase = true;
[MenuItem("Window/Activity Editor/Stage Config")]
public static void ShowWindow()
{
GetWindow<ActivityStageConfigEditor>("关卡配置");
}
private void OnEnable()
{
// 尝试加载现有数据库
LoadDatabase();
}
private void OnGUI()
{
EditorGUILayout.LabelField("玩法关卡配置表编辑器", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 导入区域
EditorGUILayout.LabelField("导入配置", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Excel文件:", GUILayout.Width(80));
_xlsmPath = EditorGUILayout.TextField(_xlsmPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
_xlsmPath = EditorUtility.OpenFilePanel("选择Excel文件", "设计文档", "xlsx,xlsm");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("输出文件夹:", GUILayout.Width(80));
_outputFolder = EditorGUILayout.TextField(_outputFolder);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
var abs = EditorUtility.OpenFolderPanel("选择输出文件夹", "Assets", "");
if (!string.IsNullOrEmpty(abs))
{
var normalizedDataPath = Application.dataPath.Replace("\\", "/");
if (abs.Replace("\\", "/").StartsWith(normalizedDataPath))
{
_outputFolder = "Assets" + abs.Replace("\\", "/").Substring(normalizedDataPath.Length);
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("解析预览", GUILayout.Height(30)))
{
ParsePreview();
}
if (GUILayout.Button("导入并创建资产", GUILayout.Height(30)))
{
ImportAndCreateAssets();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 解析数据预览
_showParsedData = EditorGUILayout.Foldout(_showParsedData, $"解析预览 ({_parsedData.Count} 条)");
if (_showParsedData && _parsedData.Count > 0)
{
DrawParsedDataPreview();
}
EditorGUILayout.Space();
// 数据库区域
_showDatabase = EditorGUILayout.Foldout(_showDatabase, "配置数据库");
if (_showDatabase)
{
DrawDatabase();
}
EditorGUILayout.Space();
// 快捷操作
EditorGUILayout.LabelField("快捷操作", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("创建新数据库"))
{
CreateNewDatabase();
}
if (GUILayout.Button("加载数据库"))
{
LoadDatabaseFromSelection();
}
EditorGUILayout.EndHorizontal();
}
private void DrawParsedDataPreview()
{
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.Height(200));
foreach (var data in _parsedData)
{
EditorGUILayout.BeginVertical(GUI.skin.box);
EditorGUILayout.LabelField($"StageID: {data.ActivityStageID}", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" 场景: {data.SceneName}");
EditorGUILayout.LabelField($" 备注: {data.Doc}");
EditorGUILayout.LabelField($" MapInfo: {data.MapInfo}, CameraID: {data.CameraID}");
EditorGUILayout.LabelField($" 延迟: {data.CloseLoadingDelay}s, 模式: {data.TickMode}");
if (data.TickMode == TickRateMode.Normal)
EditorGUILayout.LabelField($" 帧率: {data.TickRate}fps");
else
EditorGUILayout.LabelField($" 无限制模式 (DeltaTime: {data.UnlimitedDeltaTime}s)");
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
EditorGUILayout.EndScrollView();
}
private void DrawDatabase()
{
_database = EditorGUILayout.ObjectField("配置数据库", _database,
typeof(ActivityStageConfigDatabase), false) as ActivityStageConfigDatabase;
if (_database != null)
{
EditorGUILayout.LabelField($"配置数量: {_database.Configs.Count}");
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("添加配置"))
{
AddNewConfig();
}
if (GUILayout.Button("清理无效配置"))
{
CleanupInvalidConfigs();
}
EditorGUILayout.EndHorizontal();
// 显示配置列表
var configsToRemove = new List<ActivityStageConfig>();
foreach (var config in _database.Configs)
{
if (config == null) continue;
EditorGUILayout.BeginVertical(GUI.skin.box);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"Stage {config.ActivityStageID}: {config.SceneName}",
EditorStyles.boldLabel);
if (GUILayout.Button("删除", GUILayout.Width(50)))
{
configsToRemove.Add(config);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.ObjectField("配置资产", config, typeof(ActivityStageConfig), false);
EditorGUILayout.EndVertical();
}
// 执行删除
foreach (var config in configsToRemove)
{
_database.Configs.Remove(config);
}
if (configsToRemove.Count > 0)
{
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
}
}
else
{
EditorGUILayout.HelpBox("请创建或加载配置数据库", MessageType.Info);
}
}
private void ParsePreview()
{
if (string.IsNullOrEmpty(_xlsmPath))
{
EditorUtility.DisplayDialog("错误", "请选择Excel文件", "确定");
return;
}
var parser = new ActivityStageConfigParser();
_parsedData = parser.ParseFromExcel(_xlsmPath);
EditorUtility.DisplayDialog("完成", $"解析到 {_parsedData.Count} 条配置", "确定");
}
private void ImportAndCreateAssets()
{
if (_parsedData.Count == 0)
{
EditorUtility.DisplayDialog("错误", "请先解析数据", "确定");
return;
}
// 确保输出文件夹存在
if (!AssetDatabase.IsValidFolder(_outputFolder))
{
var parentFolder = System.IO.Path.GetDirectoryName(_outputFolder).Replace('\\', '/');
var folderName = System.IO.Path.GetFileName(_outputFolder);
AssetDatabase.CreateFolder(parentFolder, folderName);
}
// 创建资产
var parser = new ActivityStageConfigParser();
var configs = parser.CreateAssets(_parsedData, _outputFolder);
// 添加到数据库
if (_database == null)
{
CreateNewDatabase();
}
foreach (var config in configs)
{
_database.AddOrUpdate(config);
}
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"成功创建 {configs.Count} 个配置资产", "确定");
}
private void CreateNewDatabase()
{
var path = EditorUtility.SaveFilePanelInProject(
"保存配置数据库",
"StageConfigDatabase",
"asset",
"",
"Assets/Configs"
);
if (!string.IsNullOrEmpty(path))
{
_database = ScriptableObject.CreateInstance<ActivityStageConfigDatabase>();
AssetDatabase.CreateAsset(_database, path);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"数据库已创建: {path}", "确定");
}
}
private void LoadDatabase()
{
// 尝试在Assets中查找现有数据库
var guids = AssetDatabase.FindAssets("t:ActivityStageConfigDatabase");
if (guids.Length > 0)
{
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
_database = AssetDatabase.LoadAssetAtPath<ActivityStageConfigDatabase>(path);
}
}
private void LoadDatabaseFromSelection()
{
var selected = Selection.GetFiltered<ActivityStageConfigDatabase>(SelectionMode.Assets);
if (selected.Length > 0)
{
_database = selected[0];
}
else
{
EditorUtility.DisplayDialog("错误", "请在Project窗口中选择一个配置数据库", "确定");
}
}
private void AddNewConfig()
{
var config = ScriptableObject.CreateInstance<ActivityStageConfig>();
config.ActivityStageID = GetNextAvailableStageID();
var path = $"{_outputFolder}/StageConfig_{config.ActivityStageID}.asset";
AssetDatabase.CreateAsset(config, path);
_database.Configs.Add(config);
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
}
private int GetNextAvailableStageID()
{
if (_database.Configs.Count == 0) return 1001;
var maxId = _database.Configs
.Where(c => c != null)
.Max(c => c.ActivityStageID);
return maxId + 1;
}
private void CleanupInvalidConfigs()
{
var invalidConfigs = _database.Configs.Where(c => c == null).ToList();
foreach (var config in invalidConfigs)
{
_database.Configs.Remove(config);
}
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"清理了 {invalidConfigs.Count} 个无效配置", "确定");
}
}
}

View File

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

View File

@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NodeCanvas.BehaviourTrees;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 行为树资产清理工具
/// 用于修复或清理损坏的行为树资产
/// </summary>
public class BTAssetCleaner : EditorWindow
{
private string _targetFolder = "Assets/BT";
private bool _showDetails = true;
private List<string> _issues = new List<string>();
[MenuItem("Window/Activity Editor/BT Asset Cleaner")]
public static void ShowWindow()
{
GetWindow<BTAssetCleaner>("BT资产清理");
}
private void OnGUI()
{
EditorGUILayout.LabelField("行为树资产清理工具", EditorStyles.boldLabel);
EditorGUILayout.Space();
EditorGUILayout.LabelField("目标文件夹:", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_targetFolder = EditorGUILayout.TextField(_targetFolder);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
var folder = EditorUtility.OpenFolderPanel("选择BT文件夹", "Assets", "");
if (!string.IsNullOrEmpty(folder))
{
_targetFolder = "Assets" + folder.Replace(Application.dataPath, "").Replace("\\", "/");
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button("扫描问题资产", GUILayout.Height(30)))
{
ScanAssets();
}
if (GUILayout.Button("删除所有BT资产", GUILayout.Height(30)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"这将删除 {_targetFolder} 下的所有BehaviourTree资产是否继续", "删除", "取消"))
{
DeleteAllBTAssets();
}
}
EditorGUILayout.Space();
_showDetails = EditorGUILayout.ToggleLeft("显示详细信息", _showDetails);
if (_showDetails && _issues.Count > 0)
{
EditorGUILayout.LabelField("扫描结果:", EditorStyles.boldLabel);
foreach (var issue in _issues)
{
EditorGUILayout.LabelField($"• {issue}", EditorStyles.miniLabel);
}
}
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"如果遇到序列化错误Cannot create an instance of an interface or abstract type\n" +
"通常是因为旧的行为树资产引用了已不存在的Task类型。\n" +
"建议删除旧资产后重新从Excel导入。",
MessageType.Info);
}
private void ScanAssets()
{
_issues.Clear();
if (!AssetDatabase.IsValidFolder(_targetFolder))
{
_issues.Add($"文件夹不存在: {_targetFolder}");
return;
}
var guids = AssetDatabase.FindAssets("t:BehaviourTree", new[] { _targetFolder });
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var bt = AssetDatabase.LoadAssetAtPath<BehaviourTree>(path);
if (bt == null)
{
_issues.Add($"无法加载: {path}");
}
else if (bt.primeNode == null)
{
_issues.Add($"根节点为空: {path}");
}
}
if (_issues.Count == 0)
{
_issues.Add($"扫描完成,共 {guids.Length} 个行为树资产,未发现明显问题");
}
}
private void DeleteAllBTAssets()
{
if (!AssetDatabase.IsValidFolder(_targetFolder))
{
EditorUtility.DisplayDialog("错误", "文件夹不存在", "确定");
return;
}
var guids = AssetDatabase.FindAssets("t:BehaviourTree", new[] { _targetFolder });
int deleted = 0;
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (AssetDatabase.DeleteAsset(path))
{
deleted++;
}
}
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("完成", $"已删除 {deleted} 个行为树资产", "确定");
_issues.Clear();
_issues.Add($"已删除 {deleted} 个资产");
}
}
}

View File

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

View File

@ -0,0 +1,586 @@
using GameplayEditor.Core;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 行为树调试窗口
/// 提供断点管理、单步执行、变量查看、执行历史等功能
/// </summary>
public class BTDebugWindow : EditorWindow
{
private Vector2 _nodeListScrollPos;
private Vector2 _historyScrollPos;
private Vector2 _variablesScrollPos;
private Vector2 _breakpointScrollPos;
// 调试目标
private BehaviourTree _targetTree;
private BehaviourTreeController _targetController;
private BTDebugger _debugger;
// 搜索过滤
private string _nodeSearchFilter = "";
private string _variableSearchFilter = "";
// 折叠状态
private bool _showNodeList = true;
private bool _showBreakpoints = true;
private bool _showVariables = true;
private bool _showHistory = true;
private bool _showControls = true;
// 选中的节点
private Node _selectedNode;
// 自动刷新
private bool _autoRefresh = true;
private double _lastRefreshTime;
private const float REFRESH_INTERVAL = 0.1f;
// 样式缓存
private GUIStyle _breakpointStyle;
private GUIStyle _currentNodeStyle;
private GUIStyle _executedNodeStyle;
private GUIStyle _toolbarButtonStyle;
private bool _stylesInitialized = false;
[MenuItem("Window/Activity Editor/Behaviour Tree Debugger")]
public static void ShowWindow()
{
var window = GetWindow<BTDebugWindow>("BT Debugger");
window.minSize = new Vector2(400, 600);
window.Show();
}
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
private void InitializeStyles()
{
if (_stylesInitialized) return;
_breakpointStyle = new GUIStyle(EditorStyles.label);
_breakpointStyle.normal.textColor = Color.red;
_breakpointStyle.fontStyle = FontStyle.Bold;
_currentNodeStyle = new GUIStyle(EditorStyles.label);
_currentNodeStyle.normal.textColor = Color.yellow;
_currentNodeStyle.fontStyle = FontStyle.Bold;
_executedNodeStyle = new GUIStyle(EditorStyles.label);
_executedNodeStyle.normal.textColor = Color.green;
_toolbarButtonStyle = new GUIStyle(EditorStyles.toolbarButton);
_toolbarButtonStyle.fontSize = 12;
_stylesInitialized = true;
}
private void OnEditorUpdate()
{
if (!_autoRefresh) return;
if (EditorApplication.timeSinceStartup - _lastRefreshTime < REFRESH_INTERVAL) return;
_lastRefreshTime = EditorApplication.timeSinceStartup;
// 自动查找调试器
if (_debugger == null)
{
_debugger = BTDebugger.Instance;
}
// 自动查找控制器
if (_targetController == null && Application.isPlaying)
{
_targetController = FindObjectOfType<BehaviourTreeController>();
if (_targetController != null)
{
_targetTree = _targetController.BodyTree;
_debugger?.SetTarget(_targetTree);
}
}
Repaint();
}
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingPlayMode)
{
_targetController = null;
_targetTree = null;
}
}
private void OnGUI()
{
InitializeStyles();
DrawToolbar();
if (!Application.isPlaying)
{
DrawNotPlayingMessage();
return;
}
if (_debugger == null)
{
DrawNoDebuggerMessage();
return;
}
DrawDebugControls();
DrawBreakpointPanel();
DrawNodeList();
DrawVariablesPanel();
DrawHistoryPanel();
}
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
RefreshDebugInfo();
}
_autoRefresh = GUILayout.Toggle(_autoRefresh, "自动刷新", EditorStyles.toolbarButton, GUILayout.Width(70));
GUILayout.FlexibleSpace();
// 调试状态指示
if (_debugger != null)
{
var statusColor = _debugger.IsPaused() ? Color.red : Color.green;
var statusText = _debugger.IsPaused() ? "[已暂停]" : "[运行中]";
var prevColor = GUI.color;
GUI.color = statusColor;
GUILayout.Label(statusText, EditorStyles.toolbarButton, GUILayout.Width(60));
GUI.color = prevColor;
}
EditorGUILayout.EndHorizontal();
}
#endregion
#region 调试控制
private void DrawDebugControls()
{
_showControls = EditorGUILayout.Foldout(_showControls, "▶ 调试控制", EditorStyles.foldoutHeader);
if (!_showControls) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 调试目标选择
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("调试目标:", GUILayout.Width(70));
var newTree = EditorGUILayout.ObjectField(_targetTree, typeof(BehaviourTree), true) as BehaviourTree;
if (newTree != _targetTree)
{
_targetTree = newTree;
_debugger?.SetTarget(_targetTree);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 控制按钮
EditorGUILayout.BeginHorizontal();
// 继续/暂停按钮
if (_debugger.IsPaused())
{
if (GUILayout.Button("▶ 继续", GUILayout.Height(30)))
{
_debugger.Resume();
}
}
else
{
if (GUILayout.Button("⏸ 暂停", GUILayout.Height(30)))
{
_debugger.Pause();
}
}
// 单步执行
GUI.enabled = _debugger.IsPaused();
if (GUILayout.Button("⏯ 单步执行", GUILayout.Height(30)))
{
_debugger.Step();
}
GUI.enabled = true;
// 清除断点
if (GUILayout.Button("清除断点", GUILayout.Height(30)))
{
_debugger.ClearBreakpoints();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
#endregion
#region 断点面板
private void DrawBreakpointPanel()
{
_showBreakpoints = EditorGUILayout.Foldout(_showBreakpoints, "🔴 断点列表", EditorStyles.foldoutHeader);
if (!_showBreakpoints) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
var breakpoints = _debugger.GetAllNodeInfo()
.Where(kvp => kvp.Value.IsBreakpoint)
.Select(kvp => kvp.Value)
.ToList();
if (breakpoints.Count == 0)
{
EditorGUILayout.LabelField("暂无断点", EditorStyles.centeredGreyMiniLabel);
}
else
{
_breakpointScrollPos = EditorGUILayout.BeginScrollView(_breakpointScrollPos, GUILayout.MaxHeight(150));
foreach (var bp in breakpoints)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label("🔴", GUILayout.Width(20));
EditorGUILayout.LabelField(bp.NodeName, GUILayout.Width(150));
if (!string.IsNullOrEmpty(bp.Condition))
{
EditorGUILayout.LabelField($"当: {bp.Condition}", EditorStyles.miniLabel);
}
if (GUILayout.Button("删除", GUILayout.Width(50)))
{
// 需要节点引用才能删除,这里只是视觉反馈
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
#endregion
#region 节点列表
private void DrawNodeList()
{
_showNodeList = EditorGUILayout.Foldout(_showNodeList, "📋 节点列表", EditorStyles.foldoutHeader);
if (!_showNodeList) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 搜索框
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("搜索:", GUILayout.Width(40));
_nodeSearchFilter = EditorGUILayout.TextField(_nodeSearchFilter);
if (GUILayout.Button("清除", GUILayout.Width(40)))
{
_nodeSearchFilter = "";
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 节点列表
_nodeListScrollPos = EditorGUILayout.BeginScrollView(_nodeListScrollPos, GUILayout.MinHeight(200));
var nodeInfos = _debugger.GetAllNodeInfo();
var currentNode = _debugger.GetCurrentNode();
foreach (var kvp in nodeInfos)
{
var info = kvp.Value;
// 搜索过滤
if (!string.IsNullOrEmpty(_nodeSearchFilter) &&
!info.NodeName.ToLower().Contains(_nodeSearchFilter.ToLower()))
{
continue;
}
DrawNodeItem(info, currentNode);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
private void DrawNodeItem(BTDebugger.NodeDebugInfo info, Node currentNode)
{
EditorGUILayout.BeginHorizontal();
// 断点复选框
bool wasBreakpoint = info.IsBreakpoint;
bool isBreakpoint = GUILayout.Toggle(wasBreakpoint, "", GUILayout.Width(20));
// 断点状态改变
if (isBreakpoint != wasBreakpoint)
{
// 需要找到对应的Node对象才能设置断点
// 这里简化处理,仅视觉反馈
info.IsBreakpoint = isBreakpoint;
}
// 节点名称(根据状态使用不同颜色)
GUIStyle nameStyle = EditorStyles.label;
if (info.NodeId == $"{currentNode?.GetType().Name}_{currentNode?.name}_{currentNode?.GetHashCode()}")
{
nameStyle = _currentNodeStyle;
GUILayout.Label("▶", GUILayout.Width(15));
}
else if (info.ExecutionCount > 0)
{
nameStyle = _executedNodeStyle;
GUILayout.Label(" ", GUILayout.Width(15));
}
else
{
GUILayout.Label(" ", GUILayout.Width(15));
}
if (info.IsBreakpoint)
{
nameStyle = _breakpointStyle;
}
EditorGUILayout.LabelField(info.NodeName, nameStyle, GUILayout.Width(150));
// 执行次数
if (info.ExecutionCount > 0)
{
EditorGUILayout.LabelField($"x{info.ExecutionCount}", EditorStyles.miniLabel, GUILayout.Width(40));
}
else
{
GUILayout.Space(40);
}
// 上次结果
if (info.LastStatus != Status.Resting)
{
var statusColor = info.LastStatus == Status.Success ? Color.green :
info.LastStatus == Status.Failure ? Color.red : Color.yellow;
var prevColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField(info.LastStatus.ToString(), EditorStyles.miniLabel, GUILayout.Width(60));
GUI.color = prevColor;
}
EditorGUILayout.EndHorizontal();
}
#endregion
#region 变量面板
private void DrawVariablesPanel()
{
_showVariables = EditorGUILayout.Foldout(_showVariables, "🔍 黑板变量", EditorStyles.foldoutHeader);
if (!_showVariables) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 获取黑板
var blackboard = _targetTree?.blackboard;
if (blackboard == null)
{
EditorGUILayout.LabelField("无黑板数据", EditorStyles.centeredGreyMiniLabel);
}
else
{
// 搜索框
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("搜索:", GUILayout.Width(40));
_variableSearchFilter = EditorGUILayout.TextField(_variableSearchFilter);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 变量列表
_variablesScrollPos = EditorGUILayout.BeginScrollView(_variablesScrollPos, GUILayout.MaxHeight(200));
var variables = blackboard.variables;
if (variables != null)
{
foreach (var kvp in variables)
{
var variableName = kvp.Key;
var variable = kvp.Value;
if (variable == null) continue;
// 搜索过滤
if (!string.IsNullOrEmpty(_variableSearchFilter) &&
!variableName.ToLower().Contains(_variableSearchFilter.ToLower()))
{
continue;
}
DrawVariableItem(variableName, variable);
}
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
private void DrawVariableItem(string variableName, Variable variable)
{
EditorGUILayout.BeginHorizontal();
// 变量名
EditorGUILayout.LabelField(variableName, GUILayout.Width(150));
// 类型
EditorGUILayout.LabelField(variable.varType.Name, EditorStyles.miniLabel, GUILayout.Width(100));
// 值
string valueStr = variable.value?.ToString() ?? "null";
if (valueStr.Length > 30)
{
valueStr = valueStr.Substring(0, 30) + "...";
}
EditorGUILayout.LabelField(valueStr, EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
#endregion
#region 历史面板
private void DrawHistoryPanel()
{
_showHistory = EditorGUILayout.Foldout(_showHistory, "📜 执行历史", EditorStyles.foldoutHeader);
if (!_showHistory) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
var history = _debugger.GetExecutionHistory().ToList();
if (history.Count == 0)
{
EditorGUILayout.LabelField("暂无执行记录", EditorStyles.centeredGreyMiniLabel);
}
else
{
_historyScrollPos = EditorGUILayout.BeginScrollView(_historyScrollPos, GUILayout.MaxHeight(150));
// 倒序显示,最新的在前
for (int i = history.Count - 1; i >= 0; i--)
{
var record = history[i];
DrawHistoryItem(record);
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.EndVertical();
}
private void DrawHistoryItem(BTDebugger.ExecutionRecord record)
{
EditorGUILayout.BeginHorizontal();
// 时间
EditorGUILayout.LabelField($"{record.Time:F2}s", EditorStyles.miniLabel, GUILayout.Width(60));
// 节点名
EditorGUILayout.LabelField(record.NodeName, GUILayout.Width(120));
// 结果
var statusColor = record.Result == Status.Success ? Color.green :
record.Result == Status.Failure ? Color.red : Color.yellow;
var prevColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField(record.Result.ToString(), EditorStyles.miniLabel, GUILayout.Width(60));
GUI.color = prevColor;
EditorGUILayout.EndHorizontal();
}
#endregion
#region 消息面板
private void DrawNotPlayingMessage()
{
EditorGUILayout.Space(50);
EditorGUILayout.HelpBox("请在Play模式下使用调试器", MessageType.Info);
if (GUILayout.Button("进入Play模式", GUILayout.Height(40)))
{
EditorApplication.isPlaying = true;
}
}
private void DrawNoDebuggerMessage()
{
EditorGUILayout.Space(50);
EditorGUILayout.HelpBox("未找到BTDebugger组件\n请确保场景中有挂载BTDebugger的对象", MessageType.Warning);
if (GUILayout.Button("自动添加调试器", GUILayout.Height(40)))
{
var go = new GameObject("BTDebugger");
go.AddComponent<BTDebugger>();
}
}
#endregion
#region 私有方法
private void RefreshDebugInfo()
{
if (_debugger != null && _targetTree != null)
{
_debugger.SetTarget(_targetTree);
}
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,300 @@
using System.Collections.Generic;
using System.Linq;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 行为树错误高亮系统
/// 在NodeCanvas编辑器中高亮显示报错的节点
/// </summary>
public class BTErrorHighlighter : EditorWindow
{
// 错误节点记录
private static readonly Dictionary<string, ErrorInfo> _errorNodes = new Dictionary<string, ErrorInfo>();
// 高亮设置
private static readonly Color ERROR_COLOR = new Color(1f, 0.2f, 0.2f, 0.8f);
private static readonly Color WARNING_COLOR = new Color(1f, 0.8f, 0.2f, 0.8f);
// 是否启用高亮
private bool _enableHighlighting = true;
// 滚动位置
private Vector2 _scrollPosition;
[MenuItem("Window/Activity Editor/Error Highlighter")]
public static void ShowWindow()
{
var window = GetWindow<BTErrorHighlighter>("BT错误高亮");
window.minSize = new Vector2(400, 300);
window.Show();
}
private void OnEnable()
{
// 监听错误日志
Application.logMessageReceived += OnLogMessageReceived;
}
private void OnDisable()
{
Application.logMessageReceived -= OnLogMessageReceived;
}
private void OnGUI()
{
EditorGUILayout.LabelField("行为树错误高亮", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
// 启用开关
_enableHighlighting = EditorGUILayout.Toggle("启用高亮", _enableHighlighting);
EditorGUILayout.Space(10);
// 工具栏
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("清除所有", EditorStyles.toolbarButton))
{
ClearAllErrors();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
// 错误列表
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
foreach (var error in _errorNodes.Values.ToList())
{
DrawErrorItem(error);
}
EditorGUILayout.EndScrollView();
// 图例
EditorGUILayout.Space(10);
DrawLegend();
}
private void DrawErrorItem(ErrorInfo error)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
// 状态图标
GUI.color = error.IsWarning ? WARNING_COLOR : ERROR_COLOR;
GUILayout.Label(error.IsWarning ? "⚠" : "✖", GUILayout.Width(20));
GUI.color = Color.white;
// 树名和节点索引
EditorGUILayout.LabelField($"{error.TreeName}[{error.NodeIndex}]", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
// 删除按钮
if (GUILayout.Button("×", GUILayout.Width(20)))
{
RemoveError(error.TreeName, error.NodeIndex);
}
EditorGUILayout.EndHorizontal();
// 错误信息
EditorGUILayout.LabelField(error.Message, EditorStyles.wordWrappedLabel);
// 时间
EditorGUILayout.LabelField($"Time: {error.Timestamp:F2}", EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(3);
}
private void DrawLegend()
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
GUI.color = ERROR_COLOR;
GUILayout.Label("● 错误", GUILayout.Width(60));
GUI.color = WARNING_COLOR;
GUILayout.Label("● 警告", GUILayout.Width(60));
GUI.color = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 注册错误节点
/// </summary>
public static void RegisterError(string treeName, int nodeIndex, string errorMessage, bool isWarning = false)
{
string key = $"{treeName}_{nodeIndex}";
_errorNodes[key] = new ErrorInfo
{
TreeName = treeName,
NodeIndex = nodeIndex,
Message = errorMessage,
IsWarning = isWarning,
Timestamp = Time.time
};
Debug.Log($"[BTErrorHighlighter] Registered {(isWarning ? "warning" : "error")}: {treeName}[{nodeIndex}] - {errorMessage}");
// 刷新窗口
var window = GetWindow<BTErrorHighlighter>();
window.Repaint();
}
/// <summary>
/// 移除错误
/// </summary>
public static void RemoveError(string treeName, int nodeIndex)
{
string key = $"{treeName}_{nodeIndex}";
_errorNodes.Remove(key);
}
/// <summary>
/// 清除指定行为树的错误
/// </summary>
public static void ClearErrors(string treeName)
{
var keysToRemove = _errorNodes.Keys
.Where(k => k.StartsWith(treeName + "_"))
.ToList();
foreach (var key in keysToRemove)
{
_errorNodes.Remove(key);
}
}
/// <summary>
/// 清除所有错误
/// </summary>
public static void ClearAllErrors()
{
_errorNodes.Clear();
}
/// <summary>
/// 获取错误信息
/// </summary>
public static ErrorInfo GetErrorInfo(string treeName, int nodeIndex)
{
string key = $"{treeName}_{nodeIndex}";
return _errorNodes.TryGetValue(key, out var info) ? info : null;
}
/// <summary>
/// 检查节点是否有错误
/// </summary>
public static bool HasError(string treeName, int nodeIndex)
{
string key = $"{treeName}_{nodeIndex}";
return _errorNodes.ContainsKey(key);
}
#region 日志监听
private void OnLogMessageReceived(string condition, string stackTrace, LogType type)
{
if (type != LogType.Error && type != LogType.Warning)
return;
if (!condition.Contains("[BT:") && !condition.Contains("BehaviourTree"))
return;
ParseErrorFromLog(condition, type == LogType.Warning);
}
private void ParseErrorFromLog(string message, bool isWarning)
{
if (!message.StartsWith("[BT:"))
return;
try
{
int treeIdStart = message.IndexOf("[BT:") + 4;
int treeIdEnd = message.IndexOf(" ", treeIdStart);
if (treeIdEnd < 0) treeIdEnd = message.IndexOf("]", treeIdStart);
string treeIdStr = message.Substring(treeIdStart, treeIdEnd - treeIdStart);
int nodeIndex = -1;
int rowIndex = -1;
int rowIdx = message.IndexOf("Row:");
if (rowIdx > 0)
{
int rowNumStart = rowIdx + 4;
int rowNumEnd = message.IndexOf("]", rowNumStart);
if (int.TryParse(message.Substring(rowNumStart, rowNumEnd - rowNumStart), out rowIndex))
{
nodeIndex = FindNodeIndexByRow(treeIdStr, rowIndex);
}
}
else
{
int nodeIdx = message.IndexOf("Node:");
if (nodeIdx > 0)
{
int nodeNumStart = nodeIdx + 5;
int nodeNumEnd = message.IndexOf("]", nodeNumStart);
int.TryParse(message.Substring(nodeNumStart, nodeNumEnd - nodeNumStart), out nodeIndex);
}
}
if (nodeIndex >= 0)
{
int msgStart = message.IndexOf("] ") + 2;
string errorMsg = msgStart < message.Length ? message.Substring(msgStart) : message;
RegisterError($"BT_{treeIdStr}", nodeIndex, errorMsg, isWarning);
}
}
catch
{
// 解析失败,忽略
}
}
private int FindNodeIndexByRow(string treeId, int rowIndex)
{
var container = GameplayEditor.Core.BehaviourTreeNodeInfoManager.GetContainer($"BT_{treeId}");
if (container == null) return -1;
var info = container.NodeInfos.Find(n => n.RowIndex == rowIndex);
if (info != null)
{
return container.NodeInfos.IndexOf(info);
}
return -1;
}
#endregion
#region 数据类
public class ErrorInfo
{
public string TreeName;
public int NodeIndex;
public string Message;
public bool IsWarning;
public float Timestamp;
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,318 @@
using System.Collections.Generic;
using System.Linq;
using GameplayEditor.Core;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 行为树性能监控窗口
/// 显示实时性能数据、热点节点、优化建议
/// </summary>
public class BTPerformanceWindow : EditorWindow
{
private BTPerformanceMonitor _monitor;
private Vector2 _scrollPosition;
private bool _autoRefresh = true;
private float _lastRefreshTime;
private const float REFRESH_INTERVAL = 0.5f;
// 图表数据
private Queue<float> _fpsHistory = new Queue<float>(100);
private Queue<float> _executionTimeHistory = new Queue<float>(100);
private Vector2 _graphScrollPos;
[MenuItem("Window/Activity Editor/Performance Monitor")]
public static void ShowWindow()
{
var window = GetWindow<BTPerformanceWindow>("BT性能监控");
window.minSize = new Vector2(600, 500);
window.Show();
}
private void OnEnable()
{
_monitor = FindObjectOfType<BTPerformanceMonitor>();
}
private void Update()
{
if (_autoRefresh && Time.time - _lastRefreshTime > REFRESH_INTERVAL)
{
_lastRefreshTime = Time.time;
// 记录FPS历史
if (_monitor != null)
{
_fpsHistory.Enqueue(_monitor.GetCurrentFPS());
if (_fpsHistory.Count > 100)
_fpsHistory.Dequeue();
}
Repaint();
}
}
private void OnGUI()
{
EditorGUILayout.LabelField("行为树性能监控", EditorStyles.boldLabel);
if (_monitor == null)
{
_monitor = FindObjectOfType<BTPerformanceMonitor>();
}
if (_monitor == null)
{
EditorGUILayout.HelpBox("未找到BTPerformanceMonitor", MessageType.Warning);
if (GUILayout.Button("创建Monitor"))
{
var go = new GameObject("BTPerformanceMonitor");
go.AddComponent<BTPerformanceMonitor>();
}
return;
}
// 工具栏
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
_monitor.EnableMonitoring = GUILayout.Toggle(_monitor.EnableMonitoring, "启用监控", EditorStyles.toolbarButton);
_autoRefresh = GUILayout.Toggle(_autoRefresh, "自动刷新", EditorStyles.toolbarButton);
if (GUILayout.Button("刷新", EditorStyles.toolbarButton)) Repaint();
if (GUILayout.Button("开始记录", EditorStyles.toolbarButton)) _monitor.StartRecording();
if (GUILayout.Button("停止并生成报告", EditorStyles.toolbarButton)) _monitor.StopRecording();
if (GUILayout.Button("清除", EditorStyles.toolbarButton)) _monitor.StartRecording();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 概览统计
DrawOverview();
EditorGUILayout.Space();
// 性能图表
DrawPerformanceGraph();
EditorGUILayout.Space();
// 热点节点
DrawHotNodes();
EditorGUILayout.Space();
// 优化建议
DrawOptimizationSuggestions();
}
private void DrawOverview()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("性能概览", EditorStyles.boldLabel);
var report = _monitor.GetCurrentReport();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"总节点执行: {report.TotalNodeExecutions}", GUILayout.Width(150));
EditorGUILayout.LabelField($"警告数: {report.WarningCount}", GUILayout.Width(100));
EditorGUILayout.LabelField($"当前FPS: {_monitor.GetCurrentFPS():F1}", GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
if (report.Duration > 0)
{
EditorGUILayout.LabelField($"记录时长: {report.Duration:F1}秒", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
}
private void DrawPerformanceGraph()
{
EditorGUILayout.LabelField("性能趋势", EditorStyles.boldLabel);
var rect = EditorGUILayout.GetControlRect(false, 100);
EditorGUI.DrawRect(rect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
if (_fpsHistory.Count > 1)
{
// 绘制FPS曲线
var fpsArray = _fpsHistory.ToArray();
float maxFPS = Mathf.Max(fpsArray.Max(), 60);
Handles.color = Color.green;
for (int i = 1; i < fpsArray.Length; i++)
{
float x1 = rect.x + (i - 1) * (rect.width / 100);
float x2 = rect.x + i * (rect.width / 100);
float y1 = rect.y + rect.height - (fpsArray[i - 1] / maxFPS) * rect.height;
float y2 = rect.y + rect.height - (fpsArray[i] / maxFPS) * rect.height;
Handles.DrawLine(new Vector3(x1, y1), new Vector3(x2, y2));
}
// 绘制参考线 (30fps, 60fps)
Handles.color = new Color(1, 1, 0, 0.3f);
float y60 = rect.y + rect.height - (60 / maxFPS) * rect.height;
Handles.DrawLine(new Vector3(rect.x, y60), new Vector3(rect.x + rect.width, y60));
Handles.color = new Color(1, 0, 0, 0.3f);
float y30 = rect.y + rect.height - (30 / maxFPS) * rect.height;
Handles.DrawLine(new Vector3(rect.x, y30), new Vector3(rect.x + rect.width, y30));
}
EditorGUILayout.LabelField("绿色=FPS, 黄线=60fps, 红线=30fps", EditorStyles.miniLabel);
}
private void DrawHotNodes()
{
EditorGUILayout.LabelField("热点节点", EditorStyles.boldLabel);
var report = _monitor.GetCurrentReport();
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.Height(200));
// 表头
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("节点名称", GUILayout.Width(200));
EditorGUILayout.LabelField("执行次数", GUILayout.Width(80));
EditorGUILayout.LabelField("平均耗时", GUILayout.Width(80));
EditorGUILayout.LabelField("最大耗时", GUILayout.Width(80));
EditorGUILayout.LabelField("警告", GUILayout.Width(50));
EditorGUILayout.EndHorizontal();
foreach (var node in report.HotNodes)
{
var style = node.WarningCount > 0 ? new GUIStyle(GUI.skin.box) { normal = { background = MakeTex(2, 2, new Color(1, 0.8f, 0.8f)) } } : GUI.skin.box;
EditorGUILayout.BeginHorizontal(style);
EditorGUILayout.LabelField(node.NodeName, GUILayout.Width(200));
EditorGUILayout.LabelField($"{node.TotalExecutions}", GUILayout.Width(80));
EditorGUILayout.LabelField($"{node.AverageExecutionTimeMs:F1}ms", GUILayout.Width(80));
EditorGUILayout.LabelField($"{node.MaxExecutionTimeMs:F1}ms", GUILayout.Width(80));
if (node.WarningCount > 0)
{
GUI.color = Color.red;
EditorGUILayout.LabelField($"⚠ {node.WarningCount}", GUILayout.Width(50));
GUI.color = Color.white;
}
else
{
EditorGUILayout.LabelField("-", GUILayout.Width(50));
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
private void DrawOptimizationSuggestions()
{
var report = _monitor.GetCurrentReport();
var suggestions = GenerateOptimizationSuggestions(report);
if (suggestions.Count > 0)
{
EditorGUILayout.LabelField("优化建议", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
foreach (var suggestion in suggestions)
{
var color = suggestion.Severity == Severity.High ? Color.red :
suggestion.Severity == Severity.Medium ? Color.yellow : Color.white;
GUI.color = color;
EditorGUILayout.LabelField($"• {suggestion.Message}");
GUI.color = Color.white;
if (!string.IsNullOrEmpty(suggestion.Details))
{
EditorGUILayout.LabelField($" {suggestion.Details}", EditorStyles.miniLabel);
}
}
EditorGUILayout.EndVertical();
}
}
private List<OptimizationSuggestion> GenerateOptimizationSuggestions(BTPerformanceMonitor.PerformanceReport report)
{
var suggestions = new List<OptimizationSuggestion>();
// 检查高频执行节点
foreach (var node in report.HotNodes)
{
if (node.TotalExecutions > 1000)
{
suggestions.Add(new OptimizationSuggestion
{
Message = $"节点 '{node.NodeName}' 执行次数过多 ({node.TotalExecutions}次)",
Details = "建议检查是否有死循环或不必要的重复执行",
Severity = Severity.High
});
}
if (node.AverageExecutionTimeMs > 5)
{
suggestions.Add(new OptimizationSuggestion
{
Message = $"节点 '{node.NodeName}' 平均执行时间过长 ({node.AverageExecutionTimeMs:F1}ms)",
Details = "建议优化节点逻辑或降低执行频率",
Severity = Severity.Medium
});
}
}
// 检查FPS
var avgFPS = _fpsHistory.Count > 0 ? _fpsHistory.Average() : 60;
if (avgFPS < 30)
{
suggestions.Add(new OptimizationSuggestion
{
Message = "FPS过低建议降低行为树Tick率",
Details = "可以在BehaviourTreeController中调整TickRate",
Severity = Severity.High
});
}
// 检查警告
if (report.WarningCount > 10)
{
suggestions.Add(new OptimizationSuggestion
{
Message = "性能警告过多,建议检查高消耗操作",
Details = "避免在行为树中频繁调用SetPosition/SetActive等",
Severity = Severity.Medium
});
}
return suggestions;
}
private Texture2D MakeTex(int width, int height, Color col)
{
Color[] pix = new Color[width * height];
for (int i = 0; i < pix.Length; i++)
pix[i] = col;
Texture2D result = new Texture2D(width, height);
result.SetPixels(pix);
result.Apply();
return result;
}
private class OptimizationSuggestion
{
public string Message;
public string Details;
public Severity Severity;
}
private enum Severity
{
Low,
Medium,
High
}
}
}

View File

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

View File

@ -0,0 +1,391 @@
using System.Collections.Generic;
using System.Linq;
using GameplayEditor.Core;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 行为树运行时可视化窗口
/// 实时显示行为树执行状态、节点状态、变量变化
/// </summary>
public class BTRuntimeVisualizerWindow : EditorWindow
{
private Vector2 _nodeStatusScrollPosition;
private Vector2 _historyScrollPosition;
private Vector2 _variableScrollPosition;
private int _selectedTab = 0;
private readonly string[] _tabNames = { "节点状态", "执行历史", "变量监控" };
private string _filterTreeName = "";
private string _filterNodeName = "";
private bool _autoRefresh = true;
private float _lastRefreshTime;
private const float REFRESH_INTERVAL = 0.1f;
private BTRuntimeVisualizer _visualizer;
private Dictionary<string, bool> _expandedTrees = new Dictionary<string, bool>();
[MenuItem("Window/Activity Editor/运行时可视化 #F12")]
public static void ShowWindow()
{
var window = GetWindow<BTRuntimeVisualizerWindow>("BT运行时可视化");
window.minSize = new Vector2(500, 400);
window.Show();
}
private void OnEnable()
{
if (Application.isPlaying)
FindVisualizer();
}
private void FindVisualizer()
{
_visualizer = BTRuntimeVisualizer.Instance;
}
private void Update()
{
if (_autoRefresh && Time.time - _lastRefreshTime > REFRESH_INTERVAL)
{
_lastRefreshTime = Time.time;
Repaint();
}
}
private void OnGUI()
{
DrawToolbar();
EditorGUILayout.Space(5);
_selectedTab = GUILayout.Toolbar(_selectedTab, _tabNames);
EditorGUILayout.Space(5);
switch (_selectedTab)
{
case 0:
DrawNodeStatusTab();
break;
case 1:
DrawExecutionHistoryTab();
break;
case 2:
DrawVariableMonitorTab();
break;
}
}
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
// 自动刷新开关
_autoRefresh = GUILayout.Toggle(_autoRefresh, "自动刷新", EditorStyles.toolbarButton, GUILayout.Width(70));
GUILayout.Space(10);
// 树筛选
EditorGUILayout.LabelField("树:", GUILayout.Width(30));
_filterTreeName = EditorGUILayout.TextField(_filterTreeName, GUILayout.Width(120));
GUILayout.Space(10);
// 节点筛选
EditorGUILayout.LabelField("节点:", GUILayout.Width(35));
_filterNodeName = EditorGUILayout.TextField(_filterNodeName, GUILayout.Width(120));
GUILayout.FlexibleSpace();
// 手动刷新
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
Repaint();
}
// 清除
if (GUILayout.Button("清除", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
_visualizer?.ClearHistory();
}
EditorGUILayout.EndHorizontal();
}
#endregion
#region 节点状态标签页
private void DrawNodeStatusTab()
{
if (_visualizer == null)
{
EditorGUILayout.HelpBox("未找到BTRuntimeVisualizer", MessageType.Warning);
return;
}
var statuses = _visualizer.GetAllNodeStatuses();
// 按树分组
var grouped = statuses.GroupBy(s => s.TreeName).ToList();
_nodeStatusScrollPosition = EditorGUILayout.BeginScrollView(_nodeStatusScrollPosition);
foreach (var group in grouped)
{
string treeName = group.Key;
// 树名称筛选
if (!string.IsNullOrEmpty(_filterTreeName) &&
!treeName.ToLower().Contains(_filterTreeName.ToLower()))
continue;
// 展开/折叠
if (!_expandedTrees.ContainsKey(treeName))
_expandedTrees[treeName] = true;
EditorGUILayout.BeginHorizontal(EditorStyles.foldoutHeader);
_expandedTrees[treeName] = EditorGUILayout.Foldout(_expandedTrees[treeName],
$"{treeName} ({group.Count()} 节点)", true);
// 运行状态指示
var runningCount = group.Count(s => s.Status == BTRuntimeVisualizer.NodeStatus.Running);
if (runningCount > 0)
{
GUI.color = Color.cyan;
GUILayout.Label($"● 运行中({runningCount})", GUILayout.Width(80));
GUI.color = Color.white;
}
EditorGUILayout.EndHorizontal();
if (_expandedTrees[treeName])
{
EditorGUI.indentLevel++;
foreach (var status in group)
{
// 节点名称筛选
if (!string.IsNullOrEmpty(_filterNodeName) &&
!status.NodeName.ToLower().Contains(_filterNodeName.ToLower()))
continue;
DrawNodeStatusItem(status);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(5);
}
EditorGUILayout.EndScrollView();
// 图例
DrawStatusLegend();
}
private void DrawNodeStatusItem(BTRuntimeVisualizer.NodeStatusData status)
{
EditorGUILayout.BeginHorizontal();
// 状态指示器
GUI.color = status.GetStatusColor();
GUILayout.Label("●", GUILayout.Width(15));
GUI.color = Color.white;
// 节点名称
EditorGUILayout.LabelField(status.NodeName, GUILayout.Width(150));
// 状态文字
EditorGUILayout.LabelField(status.Status.ToString(), GUILayout.Width(60));
// 行索引
if (status.RowIndex > 0)
{
GUI.color = Color.yellow;
EditorGUILayout.LabelField($"Row:{status.RowIndex}", GUILayout.Width(60));
GUI.color = Color.white;
}
// 时间
EditorGUILayout.LabelField($"{Time.time - status.Timestamp:F1}s前", GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
}
private void DrawStatusLegend()
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
GUI.color = new Color(0.2f, 0.8f, 0.2f);
GUILayout.Label("● 成功", GUILayout.Width(60));
GUI.color = new Color(0.9f, 0.2f, 0.2f);
GUILayout.Label("● 失败", GUILayout.Width(60));
GUI.color = new Color(0.2f, 0.6f, 1f);
GUILayout.Label("● 运行中", GUILayout.Width(60));
GUI.color = new Color(0.8f, 0.8f, 0.8f);
GUILayout.Label("● 休眠", GUILayout.Width(60));
GUI.color = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
#endregion
#region 执行历史标签页
private void DrawExecutionHistoryTab()
{
if (_visualizer == null)
{
EditorGUILayout.HelpBox("未找到BTRuntimeVisualizer", MessageType.Warning);
return;
}
var history = _visualizer.GetExecutionHistory();
_historyScrollPosition = EditorGUILayout.BeginScrollView(_historyScrollPosition);
// 表头
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("时间", GUILayout.Width(60));
EditorGUILayout.LabelField("行为树", GUILayout.Width(120));
EditorGUILayout.LabelField("节点", GUILayout.Width(150));
EditorGUILayout.LabelField("结果", GUILayout.Width(60));
EditorGUILayout.LabelField("行号", GUILayout.Width(50));
EditorGUILayout.EndHorizontal();
// 倒序显示(最新的在前)
var reversed = history.AsEnumerable().Reverse();
foreach (var record in reversed)
{
// 筛选
if (!string.IsNullOrEmpty(_filterTreeName) &&
!record.TreeName?.ToLower().Contains(_filterTreeName.ToLower()) == true)
continue;
if (!string.IsNullOrEmpty(_filterNodeName) &&
!record.NodeName.ToLower().Contains(_filterNodeName.ToLower()))
continue;
DrawExecutionRecord(record);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.LabelField($"共 {history.Count} 条记录", EditorStyles.miniLabel);
}
private void DrawExecutionRecord(BTRuntimeVisualizer.ExecutionRecord record)
{
EditorGUILayout.BeginHorizontal();
// 时间
EditorGUILayout.LabelField(record.GetFormattedTime(), GUILayout.Width(60));
// 行为树名
EditorGUILayout.LabelField(record.TreeName ?? "Unknown", GUILayout.Width(120));
// 节点名
EditorGUILayout.LabelField(record.NodeName, GUILayout.Width(150));
// 结果(带颜色)
GUI.color = GetResultColor(record.Result);
EditorGUILayout.LabelField(record.Result.ToString(), GUILayout.Width(60));
GUI.color = Color.white;
// 行号
if (record.RowIndex > 0)
{
EditorGUILayout.LabelField(record.RowIndex.ToString(), GUILayout.Width(50));
}
else
{
EditorGUILayout.LabelField("-", GUILayout.Width(50));
}
EditorGUILayout.EndHorizontal();
}
private Color GetResultColor(BTRuntimeVisualizer.NodeStatus status)
{
return status switch
{
BTRuntimeVisualizer.NodeStatus.Success => new Color(0.2f, 0.8f, 0.2f),
BTRuntimeVisualizer.NodeStatus.Failure => new Color(0.9f, 0.2f, 0.2f),
BTRuntimeVisualizer.NodeStatus.Running => new Color(0.2f, 0.6f, 1f),
_ => Color.gray
};
}
#endregion
#region 变量监控标签页
private void DrawVariableMonitorTab()
{
if (_visualizer == null)
{
EditorGUILayout.HelpBox("未找到BTRuntimeVisualizer", MessageType.Warning);
return;
}
var variables = _visualizer.GetMonitoredVariables();
_variableScrollPosition = EditorGUILayout.BeginScrollView(_variableScrollPosition);
foreach (var varName in variables)
{
var history = _visualizer.GetVariableHistory(varName);
if (history.Count == 0) continue;
var latest = history[history.Count - 1];
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(varName, EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField($"{history.Count} 次变化", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
EditorGUILayout.LabelField($"当前值: {latest.Value}", GUILayout.MinWidth(200));
EditorGUILayout.LabelField($"时间: {latest.GetFormattedTime()}", EditorStyles.miniLabel);
// 变化趋势
if (history.Count > 1)
{
var prev = history[history.Count - 2];
if (prev.Value != latest.Value)
{
GUI.color = Color.yellow;
EditorGUILayout.LabelField($"从 {prev.Value} 变更", EditorStyles.miniLabel);
GUI.color = Color.white;
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(3);
}
EditorGUILayout.EndScrollView();
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,113 @@
using GameplayEditor.Core;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 行为树可视化调试器
/// 在SceneView中显示节点执行状态
/// </summary>
[InitializeOnLoad]
public static class BTVisualDebugger
{
private static BehaviourTree _targetTree;
private static Node _currentNode;
private static float _lastUpdateTime;
private static float _highlightDuration = 0.5f;
private static System.Collections.Generic.Dictionary<string, float> _nodeHighlightTimes =
new System.Collections.Generic.Dictionary<string, float>();
static BTVisualDebugger()
{
SceneView.duringSceneGui += OnSceneGUI;
EditorApplication.update += OnEditorUpdate;
}
/// <summary>
/// 设置调试目标
/// </summary>
public static void SetTarget(BehaviourTree tree)
{
_targetTree = tree;
}
private static void OnEditorUpdate()
{
if (Time.time - _lastUpdateTime > 0.1f)
{
_lastUpdateTime = Time.time;
// 获取当前执行节点
if (BTDebugger.Instance != null)
{
_currentNode = BTDebugger.Instance.GetCurrentNode();
}
// 更新高亮时间
var keys = new System.Collections.Generic.List<string>(_nodeHighlightTimes.Keys);
foreach (var key in keys)
{
_nodeHighlightTimes[key] -= 0.1f;
if (_nodeHighlightTimes[key] <= 0)
_nodeHighlightTimes.Remove(key);
}
SceneView.RepaintAll();
}
}
private static void OnSceneGUI(SceneView sceneView)
{
if (_targetTree == null) return;
Handles.BeginGUI();
// 绘制调试信息面板
DrawDebugPanel();
Handles.EndGUI();
}
private static void DrawDebugPanel()
{
GUILayout.BeginArea(new Rect(10, Screen.height - 200, 300, 180), EditorStyles.helpBox);
GUILayout.Label("行为树调试", EditorStyles.boldLabel);
if (_targetTree != null)
{
GUILayout.Label($"目标: {_targetTree.name}", EditorStyles.miniLabel);
GUILayout.Label($"当前节点: {_currentNode?.name ?? "None"}", EditorStyles.miniLabel);
GUILayout.Space(5);
// 显示执行历史
GUILayout.Label("最近执行:", EditorStyles.miniBoldLabel);
int count = 0;
foreach (var kvp in _nodeHighlightTimes)
{
if (count++ >= 5) break;
GUILayout.Label($" {kvp.Key} ({kvp.Value:F1}s)", EditorStyles.miniLabel);
}
}
else
{
GUILayout.Label("未选择目标行为树", EditorStyles.miniLabel);
}
GUILayout.EndArea();
}
/// <summary>
/// 高亮节点由BTDebugger调用
/// </summary>
public static void HighlightNode(Node node)
{
if (node == null) return;
_nodeHighlightTimes[$"{node.name}_{node.GetHashCode()}"] = _highlightDuration;
}
}
}

View File

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

View File

@ -0,0 +1,361 @@
using System.Collections.Generic;
using System.Linq;
using GameplayEditor.Nodes;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 组合方法验证器窗口
/// 用于检测循环依赖、递归深度等问题
/// </summary>
public class CompositeMethodValidatorWindow : EditorWindow
{
private Vector2 _scrollPos;
private List<ValidationResult> _validationResults = new List<ValidationResult>();
private bool _showValidMethods = true;
private CompositeMethod _selectedMethod;
[MenuItem("Window/Activity Editor/Composite Method Validator")]
public static void ShowWindow()
{
var window = GetWindow<CompositeMethodValidatorWindow>("组合方法验证器");
window.minSize = new Vector2(500, 400);
window.Show();
}
private void OnEnable()
{
ValidateAllMethods();
}
private void OnGUI()
{
EditorGUILayout.LabelField("组合方法验证器", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 工具栏
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("重新验证", EditorStyles.toolbarButton))
{
ValidateAllMethods();
}
if (GUILayout.Button("重置递归计数器", EditorStyles.toolbarButton))
{
CompositeMethodNode.ResetAllRecursionDepth();
Debug.Log("[CompositeMethodValidator] 递归计数器已重置");
}
_showValidMethods = GUILayout.Toggle(_showValidMethods, "显示有效方法", EditorStyles.toolbarButton);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 统计信息
DrawStatistics();
EditorGUILayout.Space();
// 验证结果列表
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
foreach (var result in _validationResults)
{
if (!result.HasIssues && !_showValidMethods)
continue;
DrawValidationResult(result);
}
EditorGUILayout.EndScrollView();
}
private void DrawStatistics()
{
var validCount = _validationResults.Count(r => !r.HasIssues);
var warningCount = _validationResults.Count(r => r.Warnings.Count > 0);
var errorCount = _validationResults.Count(r => r.Errors.Count > 0);
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
EditorGUILayout.LabelField($"总方法数: {_validationResults.Count}", GUILayout.Width(100));
EditorGUILayout.LabelField($"✓ 有效: {validCount}", GUILayout.Width(80));
EditorGUILayout.LabelField($"⚠ 警告: {warningCount}", GUILayout.Width(80));
EditorGUILayout.LabelField($"✗ 错误: {errorCount}", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
private void DrawValidationResult(ValidationResult result)
{
var bgColor = result.HasErrors ? new Color(1, 0.8f, 0.8f) :
result.HasWarnings ? new Color(1, 0.9f, 0.7f) :
new Color(0.8f, 1, 0.8f);
EditorGUILayout.BeginVertical(GUI.skin.box);
// 标题行
EditorGUILayout.BeginHorizontal();
var icon = result.HasErrors ? "✗" : result.HasWarnings ? "⚠" : "✓";
EditorGUILayout.LabelField($"{icon} {result.Method.MethodName} (ID:{result.Method.MethodID})",
EditorStyles.boldLabel);
if (GUILayout.Button("查看", GUILayout.Width(50)))
{
Selection.activeObject = result.Method;
EditorGUIUtility.PingObject(result.Method);
}
EditorGUILayout.EndHorizontal();
// 详细信息
if (result.HasIssues)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
foreach (var error in result.Errors)
{
EditorGUILayout.LabelField($" ✗ {error}", new GUIStyle(EditorStyles.label) { normal = { textColor = Color.red } });
}
foreach (var warning in result.Warnings)
{
EditorGUILayout.LabelField($" ⚠ {warning}", new GUIStyle(EditorStyles.label) { normal = { textColor = Color.yellow } });
}
EditorGUILayout.EndVertical();
}
else
{
EditorGUILayout.LabelField(" ✓ 所有检查通过", EditorStyles.miniLabel);
}
// 参数信息
if (result.Method.Parameters.Count > 0)
{
EditorGUILayout.LabelField($" 参数: {string.Join(", ", result.Method.Parameters.Select(p => $"{p.Name}({p.Type}{(p.Required ? "*" : "")})"))}",
EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(2);
}
private void ValidateAllMethods()
{
_validationResults.Clear();
// 加载所有组合方法
var methods = LoadAllCompositeMethods();
foreach (var method in methods)
{
var result = new ValidationResult { Method = method };
// 基本检查
if (method.MethodID <= 0)
{
result.Errors.Add("方法ID必须大于0");
}
if (string.IsNullOrWhiteSpace(method.MethodName))
{
result.Errors.Add("方法名称不能为空");
}
if (method.Tree == null)
{
result.Errors.Add("未关联行为树");
}
else
{
// 检查循环依赖
var cycle = DetectCycle(method, methods);
if (cycle != null)
{
result.Errors.Add($"检测到循环依赖: {string.Join(" -> ", cycle)}");
}
// 检查递归风险
var selfCallCount = CountSelfReferences(method.Tree, method.MethodID);
if (selfCallCount > 0)
{
result.Warnings.Add($"方法内部包含{selfCallCount}个对自身的引用,可能导致递归");
}
}
// 检查参数定义
foreach (var param in method.Parameters)
{
if (string.IsNullOrWhiteSpace(param.Name))
{
result.Errors.Add("存在未命名的参数定义");
}
else if (!IsValidParameterName(param.Name))
{
result.Errors.Add($"参数名'{param.Name}'无效,只能包含字母、数字和下划线");
}
}
_validationResults.Add(result);
}
}
private List<CompositeMethod> LoadAllCompositeMethods()
{
var methods = new List<CompositeMethod>();
var guids = AssetDatabase.FindAssets("t:CompositeMethod");
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var method = AssetDatabase.LoadAssetAtPath<CompositeMethod>(path);
if (method != null)
{
methods.Add(method);
}
}
return methods;
}
/// <summary>
/// 检测循环依赖
/// </summary>
private List<string> DetectCycle(CompositeMethod startMethod, List<CompositeMethod> allMethods)
{
var visited = new HashSet<int>();
var path = new List<string>();
return DetectCycleRecursive(startMethod, visited, path, allMethods);
}
private List<string> DetectCycleRecursive(CompositeMethod current, HashSet<int> visited,
List<string> path, List<CompositeMethod> allMethods)
{
if (visited.Contains(current.MethodID))
{
// 找到循环
path.Add(current.MethodName);
return path;
}
visited.Add(current.MethodID);
path.Add(current.MethodName);
if (current.Tree != null)
{
// 查找树中引用的其他组合方法
var referencedMethods = FindReferencedMethods(current.Tree, allMethods);
foreach (var referenced in referencedMethods)
{
var cycle = DetectCycleRecursive(referenced, new HashSet<int>(visited),
new List<string>(path), allMethods);
if (cycle != null)
return cycle;
}
}
path.RemoveAt(path.Count - 1);
return null;
}
/// <summary>
/// 查找行为树中引用的组合方法
/// </summary>
private List<CompositeMethod> FindReferencedMethods(BehaviourTree tree, List<CompositeMethod> allMethods)
{
var referenced = new List<CompositeMethod>();
if (tree?.primeNode == null)
return referenced;
// 递归查找所有CompositeMethodNode
FindMethodNodesRecursive(tree.primeNode, referenced, allMethods);
return referenced;
}
private void FindMethodNodesRecursive(Node node, List<CompositeMethod> referenced,
List<CompositeMethod> allMethods)
{
if (node == null) return;
// 检查是否是CompositeMethodNode
if (node is CompositeMethodNode methodNode)
{
var method = allMethods.Find(m => m.MethodID == methodNode.MethodID);
if (method != null && !referenced.Contains(method))
{
referenced.Add(method);
}
}
// 递归检查子节点
if (node.outConnections != null)
{
foreach (var conn in node.outConnections)
{
FindMethodNodesRecursive(conn.targetNode, referenced, allMethods);
}
}
}
/// <summary>
/// 计算方法树中对自身的引用次数
/// </summary>
private int CountSelfReferences(BehaviourTree tree, int methodId)
{
int count = 0;
if (tree?.primeNode == null)
return 0;
CountSelfReferencesRecursive(tree.primeNode, methodId, ref count);
return count;
}
private void CountSelfReferencesRecursive(Node node, int methodId, ref int count)
{
if (node == null) return;
if (node is CompositeMethodNode methodNode && methodNode.MethodID == methodId)
{
count++;
}
if (node.outConnections != null)
{
foreach (var conn in node.outConnections)
{
CountSelfReferencesRecursive(conn.targetNode, methodId, ref count);
}
}
}
private bool IsValidParameterName(string name)
{
return System.Text.RegularExpressions.Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$");
}
private class ValidationResult
{
public CompositeMethod Method;
public List<string> Errors = new List<string>();
public List<string> Warnings = new List<string>();
public bool HasErrors => Errors.Count > 0;
public bool HasWarnings => Warnings.Count > 0;
public bool HasIssues => HasErrors || HasWarnings;
}
}
}

View File

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

View File

@ -0,0 +1,975 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Config;
using GameplayEditor.Core;
using GameplayEditor.Excel;
using GameplayEditor.Nodes;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 配置验证器
/// 提供资源检查、配置验证、错误定位等功能
/// </summary>
public static class ConfigValidator
{
#region 验证结果
/// <summary>
/// 验证结果项
/// </summary>
public class ValidationResult
{
public enum ResultType { Info, Warning, Error }
public ResultType Type;
public string Category;
public string Message;
public string Detail; // 详细信息
public int RowIndex; // Excel行索引用于定位
public int ExcelRowNumber; // Excel实际行号
public string SourceFile; // 源文件路径
public Object UnityObject; // 关联的Unity对象双击可跳转
public bool IsError => Type == ResultType.Error;
public bool IsWarning => Type == ResultType.Warning;
}
/// <summary>
/// 验证结果集合
/// </summary>
public class ValidationReport
{
public List<ValidationResult> Results = new List<ValidationResult>();
public string Summary => $"错误: {ErrorCount}, 警告: {WarningCount}, 信息: {InfoCount}";
public int ErrorCount => Results.Count(r => r.IsError);
public int WarningCount => Results.Count(r => r.IsWarning);
public int InfoCount => Results.Count(r => r.Type == ValidationResult.ResultType.Info);
public bool HasErrors => ErrorCount > 0;
public bool HasWarnings => WarningCount > 0;
public void Add(ValidationResult result)
{
Results.Add(result);
}
public void AddError(string category, string message, string detail = "",
int rowIndex = 0, int excelRowNumber = 0, string sourceFile = "")
{
Add(new ValidationResult
{
Type = ValidationResult.ResultType.Error,
Category = category,
Message = message,
Detail = detail,
RowIndex = rowIndex,
ExcelRowNumber = excelRowNumber,
SourceFile = sourceFile
});
}
public void AddWarning(string category, string message, string detail = "",
int rowIndex = 0, int excelRowNumber = 0, string sourceFile = "")
{
Add(new ValidationResult
{
Type = ValidationResult.ResultType.Warning,
Category = category,
Message = message,
Detail = detail,
RowIndex = rowIndex,
ExcelRowNumber = excelRowNumber,
SourceFile = sourceFile
});
}
public void AddInfo(string category, string message, string detail = "",
int rowIndex = 0, int excelRowNumber = 0, string sourceFile = "")
{
Add(new ValidationResult
{
Type = ValidationResult.ResultType.Info,
Category = category,
Message = message,
Detail = detail,
RowIndex = rowIndex,
ExcelRowNumber = excelRowNumber,
SourceFile = sourceFile
});
}
}
#endregion
#region 关卡配置验证
/// <summary>
/// 验证关卡配置
/// </summary>
public static ValidationReport ValidateActivityStageConfig(ActivityStageConfig config)
{
var report = new ValidationReport();
if (config == null)
{
report.AddError("关卡配置", "配置对象为空");
return report;
}
// 验证ID
if (config.ActivityStageID <= 0)
{
report.AddError("关卡配置", $"StageID无效: {config.ActivityStageID}",
"StageID必须大于0");
}
else if (GameplayDefaultConfig.Instance.EnableStrictIDRangeCheck &&
(config.ActivityStageID < GameplayDefaultConfig.Instance.MinStageID ||
config.ActivityStageID > GameplayDefaultConfig.Instance.MaxStageID))
{
report.AddWarning("关卡配置",
$"StageID {config.ActivityStageID} 超出推荐范围 [{GameplayDefaultConfig.Instance.MinStageID}, {GameplayDefaultConfig.Instance.MaxStageID}]",
"如需放宽限制,请在 GameplayDefaultConfig 中关闭 EnableStrictIDRangeCheck 或调整范围");
}
// 验证场景名称
if (string.IsNullOrWhiteSpace(config.SceneName))
{
report.AddError("关卡配置", "场景名称为空",
$"StageID={config.ActivityStageID} 的场景名称不能为空");
}
else
{
// 检查场景是否存在
#if UNITY_EDITOR
var scenePath = $"Assets/Scenes/{config.SceneName}.unity";
if (!File.Exists(scenePath))
{
report.AddWarning("关卡配置", $"场景文件可能不存在: {config.SceneName}",
$"未找到场景文件: {scenePath}");
}
#endif
}
// 验证MapInfo
if (config.MapInfo <= 0)
{
report.AddWarning("关卡配置", "MapInfo未设置",
$"StageID={config.ActivityStageID} 的地图打点组未配置");
}
// 验证CameraID
if (config.CameraID <= 0)
{
report.AddWarning("关卡配置", "CameraID未设置",
$"StageID={config.ActivityStageID} 的默认相机未配置");
}
// 验证行为树
if (config.BodyTree == null)
{
report.AddError("关卡配置", "正文行为树为空",
$"StageID={config.ActivityStageID} 的正文行为树必须配置");
}
else
{
// 递归验证行为树
var btReport = ValidateBehaviourTree(config.BodyTree);
report.Results.AddRange(btReport.Results);
}
// 头文件可选
if (config.HeaderTree != null)
{
var headerReport = ValidateBehaviourTree(config.HeaderTree);
report.Results.AddRange(headerReport.Results);
}
// 验证TickRate
if (config.TickRate < 20 || config.TickRate > 120)
{
report.AddWarning("关卡配置", $"TickRate设置异常: {config.TickRate}",
"建议TickRate范围: 20-120");
}
return report;
}
#endregion
#region 行为树验证
/// <summary>
/// 验证行为树
/// </summary>
public static ValidationReport ValidateBehaviourTree(BehaviourTree tree)
{
var report = new ValidationReport();
if (tree == null)
{
report.AddError("行为树", "行为树对象为空");
return report;
}
if (tree.primeNode == null)
{
report.AddError("行为树", $"行为树 '{tree.name}' 没有根节点");
return report;
}
// 获取节点信息容器
var container = BehaviourTreeNodeInfoManager.GetContainer(tree.name);
// 递归验证节点
ValidateNodeRecursive(tree.primeNode, 0, report, container);
// 检查节点数量
int nodeCount = CountNodes(tree.primeNode);
report.AddInfo("行为树", $"行为树 '{tree.name}' 包含 {nodeCount} 个节点");
return report;
}
/// <summary>
/// 递归验证节点
/// </summary>
private static void ValidateNodeRecursive(Node node, int depth, ValidationReport report,
BehaviourTreeNodeInfoContainer container)
{
if (node == null) return;
// 获取节点信息(用于错误定位)
var nodeInfo = container?.GetNodeInfo(depth);
var rowIndex = nodeInfo?.RowIndex ?? 0;
var excelRow = nodeInfo?.ExcelRowNumber ?? 0;
// 验证节点名称
if (string.IsNullOrEmpty(node.name))
{
report.AddWarning("行为树节点", $"第{depth}个节点名称为空",
"", rowIndex, excelRow);
}
// 验证ActionNode
if (node is ActionNode actionNode)
{
if (actionNode.action == null)
{
report.AddError("行为树节点", $"节点 '{node.name}' 的动作未设置",
"", rowIndex, excelRow);
}
else
{
// 检查是否为DynamicBtActionTask未注册类型
if (actionNode.action is DynamicBtActionTask dynTask)
{
report.AddWarning("行为树节点",
$"节点 '{node.name}' 使用动态类型: {dynTask.NodeTypeName}",
"该类型未在NodeTypeRegistry中注册运行时可能无法正确执行",
rowIndex, excelRow);
}
}
}
// 验证CompositeMethodNode
if (node is CompositeMethodNode compositeNode)
{
if (compositeNode.MethodID <= 0)
{
report.AddError("行为树节点",
$"组合方法节点 '{node.name}' 的方法ID无效",
"", rowIndex, excelRow);
}
else
{
// 严格ID范围检查
if (GameplayDefaultConfig.Instance.EnableStrictIDRangeCheck &&
(compositeNode.MethodID < GameplayDefaultConfig.Instance.MinCompositeMethodID ||
compositeNode.MethodID > GameplayDefaultConfig.Instance.MaxCompositeMethodID))
{
report.AddWarning("行为树节点",
$"组合方法节点 '{node.name}' 的MethodID {compositeNode.MethodID} 超出推荐范围 [{GameplayDefaultConfig.Instance.MinCompositeMethodID}, {GameplayDefaultConfig.Instance.MaxCompositeMethodID}]",
"", rowIndex, excelRow);
}
// 检查方法是否存在
var method = CompositeMethodRegistry.GetMethod(compositeNode.MethodID);
if (method == null)
{
report.AddError("行为树节点",
$"组合方法节点 '{node.name}' 引用的方法不存在: ID={compositeNode.MethodID}",
"", rowIndex, excelRow);
}
else if (method.Tree == null)
{
report.AddWarning("行为树节点",
$"组合方法 '{method.MethodName}' 未关联行为树",
"", rowIndex, excelRow);
}
}
}
// 验证循环引用(简单检查)
if (depth > 100)
{
report.AddError("行为树节点", "节点深度超过100可能存在循环引用",
"", rowIndex, excelRow);
return;
}
// 递归验证子节点
foreach (var conn in node.outConnections)
{
ValidateNodeRecursive(conn.targetNode, depth + 1, report, container);
}
}
/// <summary>
/// 统计节点数量
/// </summary>
private static int CountNodes(Node node)
{
if (node == null) return 0;
int count = 1;
foreach (var conn in node.outConnections)
{
count += CountNodes(conn.targetNode);
}
return count;
}
#endregion
#region DialogInfo验证
/// <summary>
/// 验证DialogInfo配置
/// </summary>
public static ValidationReport ValidateDialogInfo(DialogInfoConfig config)
{
var report = new ValidationReport();
if (config == null)
{
report.AddError("对话配置", "配置对象为空");
return report;
}
var idSet = new HashSet<int>();
foreach (var dialog in config.Dialogs)
{
// ID有效性
if (dialog.ID <= 0)
{
report.AddError("对话配置", $"对话ID无效: {dialog.ID}");
}
// ID唯一性
else if (idSet.Contains(dialog.ID))
{
report.AddError("对话配置", $"对话ID重复: {dialog.ID}");
}
else
{
idSet.Add(dialog.ID);
}
// 文本检查
if (string.IsNullOrWhiteSpace(dialog.Text))
{
report.AddError("对话配置", $"对话ID={dialog.ID} 的文本为空");
}
// Prefab路径检查
if (!string.IsNullOrEmpty(dialog.PrefabPath))
{
#if UNITY_EDITOR
var fullPath = $"Assets/{dialog.PrefabPath}";
if (!File.Exists(fullPath))
{
report.AddWarning("对话配置",
$"对话ID={dialog.ID} 的Prefab可能不存在: {dialog.PrefabPath}");
}
#endif
}
// 持续时间检查
if (dialog.Duration < 0)
{
report.AddWarning("对话配置",
$"对话ID={dialog.ID} 的持续时间不能为负数: {dialog.Duration}");
}
}
report.AddInfo("对话配置", $"共验证 {config.Dialogs.Count} 条对话配置");
return report;
}
/// <summary>
/// 验证DialogInfoByStage索引
/// </summary>
public static ValidationReport ValidateDialogInfoByStage(DialogInfoByStageConfig config)
{
var report = new ValidationReport();
if (config == null)
{
report.AddError("对话索引", "配置对象为空");
return report;
}
var stageIdSet = new HashSet<int>();
foreach (var entry in config.Entries)
{
// StageID有效性
if (entry.StageID <= 0)
{
report.AddError("对话索引", $"StageID无效: {entry.StageID}");
}
// StageID唯一性
else if (stageIdSet.Contains(entry.StageID))
{
report.AddError("对话索引", $"StageID重复: {entry.StageID}");
}
else
{
stageIdSet.Add(entry.StageID);
}
// SheetName检查
if (string.IsNullOrWhiteSpace(entry.DialogInfoSheetName))
{
report.AddError("对话索引", $"StageID={entry.StageID} 的SheetName为空");
}
}
report.AddInfo("对话索引", $"共验证 {config.Entries.Count} 条索引");
return report;
}
#endregion
#region LUT配置验证
/// <summary>
/// 验证LUT资源配置
/// </summary>
public static ValidationReport ValidateLutConfigs(LutConfigDatabase database)
{
var report = new ValidationReport();
if (database == null)
{
report.AddError("LUT配置", "数据库为空");
return report;
}
// 验证UI LUT
foreach (var lut in database.UiLuts)
{
if (lut == null) continue;
foreach (var mapping in lut.UiMappings)
{
ValidateResourceMapping(report, "UI LUT", mapping);
}
}
// 验证Camera LUT
foreach (var lut in database.CameraLuts)
{
if (lut == null) continue;
foreach (var mapping in lut.CameraMappings)
{
ValidateResourceMapping(report, "Camera LUT", mapping);
}
}
// 验证FX LUT
foreach (var lut in database.FxLuts)
{
if (lut == null) continue;
foreach (var mapping in lut.FxMappings)
{
ValidateResourceMapping(report, "FX LUT", mapping);
}
}
return report;
}
/// <summary>
/// 验证资源映射
/// </summary>
private static void ValidateResourceMapping(ValidationReport report, string category, ResourceMapping mapping)
{
if (mapping.MappingID <= 0)
{
report.AddError(category, $"映射ID无效: {mapping.MappingID}");
}
if (string.IsNullOrWhiteSpace(mapping.PrefabPath))
{
report.AddWarning(category, $"映射ID={mapping.MappingID} 的路径为空");
}
else
{
#if UNITY_EDITOR
var fullPath = mapping.PrefabPath.StartsWith("Assets/")
? mapping.PrefabPath
: $"Assets/{mapping.PrefabPath}";
if (!File.Exists(fullPath))
{
report.AddWarning(category,
$"映射ID={mapping.MappingID} 的资源可能不存在: {mapping.PrefabPath}");
}
#endif
}
}
#endregion
#region 批量验证
/// <summary>
/// 验证所有配置
/// </summary>
public static ValidationReport ValidateAll()
{
var report = new ValidationReport();
// 查找所有配置资产
var stageConfigs = LoadAllAssets<ActivityStageConfig>();
var dialogConfigs = LoadAllAssets<DialogInfoConfig>();
var byStageConfigs = LoadAllAssets<DialogInfoByStageConfig>();
var lutDatabases = LoadAllAssets<LutConfigDatabase>();
var behaviourTrees = LoadAllAssets<BehaviourTree>();
// 验证关卡配置
foreach (var config in stageConfigs)
{
var r = ValidateActivityStageConfig(config);
report.Results.AddRange(r.Results);
}
// 验证对话配置
foreach (var config in dialogConfigs)
{
var r = ValidateDialogInfo(config);
report.Results.AddRange(r.Results);
}
// 验证索引配置
foreach (var config in byStageConfigs)
{
var r = ValidateDialogInfoByStage(config);
report.Results.AddRange(r.Results);
}
// 验证LUT配置
foreach (var db in lutDatabases)
{
var r = ValidateLutConfigs(db);
report.Results.AddRange(r.Results);
}
// 验证行为树
foreach (var tree in behaviourTrees)
{
var r = ValidateBehaviourTree(tree);
report.Results.AddRange(r.Results);
}
report.AddInfo("批量验证",
$"完成验证: {stageConfigs.Count} 关卡, {dialogConfigs.Count} 对话表, " +
$"{behaviourTrees.Count} 行为树");
return report;
}
/// <summary>
/// 加载所有指定类型的资产
/// </summary>
private static List<T> LoadAllAssets<T>() where T : Object
{
var result = new List<T>();
#if UNITY_EDITOR
var guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}");
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset != null)
{
result.Add(asset);
}
}
#endif
return result;
}
#endregion
#region 导入时验证
/// <summary>
/// 验证Excel导入结果
/// </summary>
public static ValidationReport ValidateImportResults(List<BehaviourTreeParseResult> results)
{
var report = new ValidationReport();
if (results == null || results.Count == 0)
{
report.AddError("导入", "没有解析到任何行为树");
return report;
}
foreach (var result in results)
{
if (result.Tree == null)
{
report.AddError("导入", "行为树对象为空");
continue;
}
// 验证树结构
var treeReport = ValidateBehaviourTree(result.Tree);
report.Results.AddRange(treeReport.Results);
// 检查节点信息
if (result.NodeInfoContainer == null)
{
report.AddWarning("导入",
$"行为树 '{result.Tree.name}' 没有节点信息容器",
"将无法进行行索引报错定位");
}
else if (result.NodeInfoContainer.NodeInfos.Count == 0)
{
report.AddWarning("导入",
$"行为树 '{result.Tree.name}' 的节点信息为空");
}
}
report.AddInfo("导入", $"成功验证 {results.Count} 棵行为树");
return report;
}
#endregion
#region 默认值验证与修复
/// <summary>
/// 验证并尝试自动修复配置
/// </summary>
public static ValidationReport ValidateAndFix(object config, bool autoFix = false)
{
var report = new ValidationReport();
if (config == null)
{
report.AddError("验证", "配置对象为空");
return report;
}
// 先进行常规验证
var validationReport = ValidateConfig(config);
report.Results.AddRange(validationReport.Results);
// 如果需要自动修复
if (autoFix && report.HasErrors)
{
var recovery = new Utils.ConfigRecovery();
var recoveryResult = recovery.FixConfig(config);
if (recoveryResult.FixedIssues > 0)
{
report.AddInfo("自动修复", $"成功修复 {recoveryResult.FixedIssues} 个问题",
string.Join("\n", recoveryResult.FixDetails));
}
if (recoveryResult.UnfixableErrors > 0)
{
foreach (var error in recoveryResult.ErrorMessages)
{
report.AddError("无法修复", error);
}
}
}
return report;
}
/// <summary>
/// 验证配置(通用入口)
/// </summary>
private static ValidationReport ValidateConfig(object config)
{
return config switch
{
ActivityStageConfig stageConfig => ValidateActivityStageConfig(stageConfig),
DialogInfoConfig dialogConfig => ValidateDialogInfo(dialogConfig),
DialogInfoByStageConfig byStageConfig => ValidateDialogInfoByStage(byStageConfig),
EventBuilderConfig eventConfig => ValidateEventBuilderConfig(eventConfig),
LutConfigDatabase lutDb => ValidateLutConfigs(lutDb),
BehaviourTree tree => ValidateBehaviourTree(tree),
_ => new ValidationReport()
};
}
/// <summary>
/// 验证事件构建配置
/// </summary>
public static ValidationReport ValidateEventBuilderConfig(EventBuilderConfig config)
{
var report = new ValidationReport();
var defaultConfig = GameplayDefaultConfig.Instance;
if (config == null)
{
report.AddError("事件配置", "配置对象为空");
return report;
}
// 验证节点配置
var nodeIds = new HashSet<int>();
for (int i = 0; i < config.NodeConfigs.Count; i++)
{
var node = config.NodeConfigs[i];
// ID有效性
if (node.NodeID <= 0)
{
report.AddError("事件节点", $"第{i}个节点ID无效: {node.NodeID}");
}
else if (nodeIds.Contains(node.NodeID))
{
report.AddError("事件节点", $"重复的NodeID: {node.NodeID}");
}
else
{
nodeIds.Add(node.NodeID);
}
// 严格ID范围检查
if (defaultConfig.EnableStrictIDRangeCheck && node.NodeID > 0 &&
(node.NodeID < defaultConfig.MinNodeID || node.NodeID > defaultConfig.MaxNodeID))
{
report.AddWarning("事件节点",
$"NodeID={node.NodeID} 超出推荐范围 [{defaultConfig.MinNodeID}, {defaultConfig.MaxNodeID}]");
}
// 验证权重总和
if (node.Priority != null && node.Priority.Count > 0)
{
int totalWeight = 0;
foreach (var weight in node.Priority)
{
if (weight < defaultConfig.MinWeight || weight > defaultConfig.MaxWeight)
{
report.AddWarning("事件节点",
$"NodeID={node.NodeID} 的权重 {weight} 超出范围 [{defaultConfig.MinWeight}, {defaultConfig.MaxWeight}]");
}
totalWeight += weight;
}
if (totalWeight != defaultConfig.TotalWeight)
{
report.AddError("事件节点",
$"NodeID={node.NodeID} 的权重总和为 {totalWeight},应为 {defaultConfig.TotalWeight}");
}
}
// 验证EventMappingID
if (node.EventMappingID <= 0)
{
report.AddWarning("事件节点", $"NodeID={node.NodeID} 的EventMappingID未设置");
}
}
// 验证事件Action配置
var eventIds = new HashSet<int>();
for (int i = 0; i < config.ActionConfigs.Count; i++)
{
var action = config.ActionConfigs[i];
if (action.EventID <= 0)
{
report.AddError("事件Action", $"第{i}个EventID无效: {action.EventID}");
}
else if (eventIds.Contains(action.EventID))
{
report.AddError("事件Action", $"重复的EventID: {action.EventID}");
}
else
{
eventIds.Add(action.EventID);
}
// 严格ID范围检查
if (defaultConfig.EnableStrictIDRangeCheck && action.EventID > 0 &&
(action.EventID < defaultConfig.MinEventID || action.EventID > defaultConfig.MaxEventID))
{
report.AddWarning("事件Action",
$"EventID={action.EventID} 超出推荐范围 [{defaultConfig.MinEventID}, {defaultConfig.MaxEventID}]");
}
// 验证类型有效性
if (!System.Enum.IsDefined(typeof(GameplayEditor.Config.EventType), action.Type))
{
report.AddWarning("事件Action", $"EventID={action.EventID} 的EventType无效: {action.Type}");
}
}
report.AddInfo("事件配置",
$"验证完成: {config.NodeConfigs.Count} 个节点, {config.ActionConfigs.Count} 个事件");
return report;
}
/// <summary>
/// 验证默认值配置本身
/// </summary>
public static ValidationReport ValidateDefaultConfig(GameplayDefaultConfig config)
{
var report = new ValidationReport();
if (config == null)
{
report.AddError("默认配置", "配置对象为空");
return report;
}
// 验证TickRate范围
if (config.DefaultTickRate < config.MinTickRate || config.DefaultTickRate > config.MaxTickRate)
{
report.AddError("默认配置",
$"DefaultTickRate {config.DefaultTickRate} 超出范围 [{config.MinTickRate}, {config.MaxTickRate}]");
}
// 验证权重范围
if (config.MinWeight < 0)
{
report.AddError("默认配置", "MinWeight不能为负数");
}
if (config.MaxWeight <= config.MinWeight)
{
report.AddError("默认配置", "MaxWeight必须大于MinWeight");
}
// 验证持续时间
if (config.DefaultDialogDuration <= 0)
{
report.AddWarning("默认配置", "DefaultDialogDuration应大于0");
}
// 验证FOV范围
if (config.DefaultCameraFOV <= 0 || config.DefaultCameraFOV >= 180)
{
report.AddError("默认配置", "DefaultCameraFOV应在(0, 180)范围内");
}
// 验证ID范围配置合理性
if (config.EnableStrictIDRangeCheck)
{
if (config.MinStageID > config.MaxStageID)
report.AddError("默认配置", "MinStageID不能大于MaxStageID");
if (config.MinNodeID > config.MaxNodeID)
report.AddError("默认配置", "MinNodeID不能大于MaxNodeID");
if (config.MinEventID > config.MaxEventID)
report.AddError("默认配置", "MinEventID不能大于MaxEventID");
if (config.MinCompositeMethodID > config.MaxCompositeMethodID)
report.AddError("默认配置", "MinCompositeMethodID不能大于MaxCompositeMethodID");
}
report.AddInfo("默认配置", "默认值配置验证完成");
return report;
}
/// <summary>
/// 批量验证并生成报告
/// </summary>
public static string GenerateValidationReport(List<object> configs, bool autoFix = false)
{
var report = new System.Text.StringBuilder();
report.AppendLine("========== 配置验证报告 ==========");
report.AppendLine($"验证时间: {System.DateTime.Now:yyyy-MM-dd HH:mm:ss}");
report.AppendLine($"配置数量: {configs?.Count ?? 0}");
report.AppendLine($"自动修复: {(autoFix ? "" : "")}");
report.AppendLine();
if (configs == null || configs.Count == 0)
{
report.AppendLine("没有需要验证的配置");
return report.ToString();
}
int totalErrors = 0;
int totalWarnings = 0;
int totalFixed = 0;
for (int i = 0; i < configs.Count; i++)
{
var config = configs[i];
var validationReport = ValidateAndFix(config, autoFix);
totalErrors += validationReport.ErrorCount;
totalWarnings += validationReport.WarningCount;
// 统计修复数量
if (autoFix)
{
var recovery = new Utils.ConfigRecovery();
var recoveryResult = recovery.FixConfig(config);
totalFixed += recoveryResult.FixedIssues;
}
if (validationReport.ErrorCount > 0 || validationReport.WarningCount > 0)
{
report.AppendLine($"配置 {i + 1} ({config.GetType().Name}):");
foreach (var result in validationReport.Results)
{
var prefix = result.Type switch
{
ValidationResult.ResultType.Error => "[错误]",
ValidationResult.ResultType.Warning => "[警告]",
_ => "[信息]"
};
report.AppendLine($" {prefix} [{result.Category}] {result.Message}");
if (!string.IsNullOrEmpty(result.Detail))
{
report.AppendLine($" {result.Detail}");
}
}
report.AppendLine();
}
}
report.AppendLine("========== 统计 ==========");
report.AppendLine($"总错误: {totalErrors}");
report.AppendLine($"总警告: {totalWarnings}");
if (autoFix)
{
report.AppendLine($"自动修复: {totalFixed}");
}
report.AppendLine("==========================");
return report.ToString();
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,358 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using static GameplayEditor.Editor.ConfigValidator;
namespace GameplayEditor.Editor
{
/// <summary>
/// 配置验证器窗口
/// 提供可视化的配置检查和错误定位功能
/// </summary>
public class ConfigValidatorWindow : EditorWindow
{
private Vector2 _scrollPos;
private ValidationReport _currentReport;
private bool _showErrors = true;
private bool _showWarnings = true;
private bool _showInfo = false;
private string _filterText = "";
private List<ValidationResult> _filteredResults = new List<ValidationResult>();
[MenuItem("Window/Activity Editor/Config Validator")]
public static void ShowWindow()
{
GetWindow<ConfigValidatorWindow>("配置验证器");
}
private void OnGUI()
{
DrawToolbar();
EditorGUILayout.Space();
if (_currentReport == null)
{
EditorGUILayout.HelpBox("点击'验证全部'开始检查配置", MessageType.Info);
}
else
{
DrawStatistics();
DrawFilter();
DrawResults();
}
}
/// <summary>
/// 绘制工具栏
/// </summary>
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("验证全部", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ValidateAll();
}
if (GUILayout.Button("验证行为树", EditorStyles.toolbarButton, GUILayout.Width(100)))
{
ValidateBehaviourTrees();
}
if (GUILayout.Button("验证LUT", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ValidateLuts();
}
if (GUILayout.Button("清除", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
_currentReport = null;
_filteredResults.Clear();
}
GUILayout.FlexibleSpace();
if (_currentReport != null && _currentReport.HasErrors)
{
EditorGUILayout.LabelField("发现错误!", new GUIStyle(EditorStyles.toolbarButton)
{
normal = { textColor = Color.red }
}, GUILayout.Width(80));
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 绘制统计信息
/// </summary>
private void DrawStatistics()
{
EditorGUILayout.BeginHorizontal();
// 错误数(红色)
var errorStyle = new GUIStyle(EditorStyles.boldLabel);
errorStyle.normal.textColor = Color.red;
EditorGUILayout.LabelField($"错误: {_currentReport.ErrorCount}", errorStyle, GUILayout.Width(80));
// 警告数(橙色)
var warningStyle = new GUIStyle(EditorStyles.boldLabel);
warningStyle.normal.textColor = new Color(1f, 0.5f, 0f);
EditorGUILayout.LabelField($"警告: {_currentReport.WarningCount}", warningStyle, GUILayout.Width(80));
// 信息数(灰色)
EditorGUILayout.LabelField($"信息: {_currentReport.InfoCount}", GUILayout.Width(80));
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 绘制筛选器
/// </summary>
private void DrawFilter()
{
EditorGUILayout.BeginHorizontal();
_showErrors = EditorGUILayout.ToggleLeft("错误", _showErrors, GUILayout.Width(60));
_showWarnings = EditorGUILayout.ToggleLeft("警告", _showWarnings, GUILayout.Width(60));
_showInfo = EditorGUILayout.ToggleLeft("信息", _showInfo, GUILayout.Width(60));
GUILayout.Space(20);
EditorGUILayout.LabelField("筛选:", GUILayout.Width(40));
var newFilter = EditorGUILayout.TextField(_filterText);
if (newFilter != _filterText)
{
_filterText = newFilter;
ApplyFilter();
}
EditorGUILayout.EndHorizontal();
ApplyFilter();
}
/// <summary>
/// 应用筛选
/// </summary>
private void ApplyFilter()
{
if (_currentReport == null) return;
_filteredResults = _currentReport.Results.Where(r =>
{
// 类型筛选
if (r.IsError && !_showErrors) return false;
if (r.IsWarning && !_showWarnings) return false;
if (r.Type == ValidationResult.ResultType.Info && !_showInfo) return false;
// 文本筛选
if (!string.IsNullOrEmpty(_filterText))
{
var lowerFilter = _filterText.ToLower();
return (r.Message?.ToLower().Contains(lowerFilter) ?? false) ||
(r.Category?.ToLower().Contains(lowerFilter) ?? false) ||
(r.Detail?.ToLower().Contains(lowerFilter) ?? false);
}
return true;
}).ToList();
}
/// <summary>
/// 绘制结果列表
/// </summary>
private void DrawResults()
{
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
foreach (var result in _filteredResults)
{
DrawResultItem(result);
}
if (_filteredResults.Count == 0)
{
EditorGUILayout.HelpBox("没有匹配的结果", MessageType.Info);
}
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 绘制单个结果项
/// </summary>
private void DrawResultItem(ValidationResult result)
{
// 根据类型选择颜色
Color bgColor = result.Type switch
{
ValidationResult.ResultType.Error => new Color(1f, 0.8f, 0.8f),
ValidationResult.ResultType.Warning => new Color(1f, 0.9f, 0.7f),
_ => new Color(0.9f, 0.9f, 0.9f)
};
var oldBg = GUI.backgroundColor;
GUI.backgroundColor = bgColor;
EditorGUILayout.BeginVertical(GUI.skin.box);
GUI.backgroundColor = oldBg;
// 头部:类型 + 分类
EditorGUILayout.BeginHorizontal();
var typeLabel = result.Type switch
{
ValidationResult.ResultType.Error => "[错误]",
ValidationResult.ResultType.Warning => "[警告]",
_ => "[信息]"
};
EditorGUILayout.LabelField(typeLabel, GUILayout.Width(50));
EditorGUILayout.LabelField(result.Category, EditorStyles.boldLabel, GUILayout.Width(100));
GUILayout.FlexibleSpace();
// 行索引信息
if (result.RowIndex > 0)
{
EditorGUILayout.LabelField($"Row:{result.RowIndex}", GUILayout.Width(70));
}
if (result.ExcelRowNumber > 0)
{
EditorGUILayout.LabelField($"Line:{result.ExcelRowNumber}", GUILayout.Width(70));
}
EditorGUILayout.EndHorizontal();
// 消息内容
EditorGUILayout.LabelField(result.Message, EditorStyles.wordWrappedLabel);
// 详细信息
if (!string.IsNullOrEmpty(result.Detail))
{
EditorGUI.indentLevel++;
EditorGUILayout.LabelField(result.Detail, EditorStyles.miniLabel);
EditorGUI.indentLevel--;
}
// 源文件
if (!string.IsNullOrEmpty(result.SourceFile))
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"源: {result.SourceFile}", EditorStyles.miniLabel);
if (GUILayout.Button("打开", GUILayout.Width(60)))
{
OpenSourceFile(result.SourceFile);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
GUILayout.Space(2);
}
/// <summary>
/// 打开源文件
/// </summary>
private void OpenSourceFile(string path)
{
#if UNITY_EDITOR
if (System.IO.File.Exists(path))
{
Application.OpenURL("file://" + path);
}
else
{
Debug.LogWarning($"文件不存在: {path}");
}
#endif
}
/// <summary>
/// 验证全部配置
/// </summary>
private void ValidateAll()
{
_currentReport = ConfigValidator.ValidateAll();
ApplyFilter();
if (_currentReport.HasErrors)
{
Debug.LogError($"[ConfigValidator] 发现 {_currentReport.ErrorCount} 个错误!");
}
else if (_currentReport.HasWarnings)
{
Debug.LogWarning($"[ConfigValidator] 发现 {_currentReport.WarningCount} 个警告");
}
else
{
Debug.Log("[ConfigValidator] 所有配置验证通过!");
}
}
/// <summary>
/// 只验证行为树
/// </summary>
private void ValidateBehaviourTrees()
{
_currentReport = new ValidationReport();
var trees = LoadAllAssets<NodeCanvas.BehaviourTrees.BehaviourTree>();
foreach (var tree in trees)
{
var r = ConfigValidator.ValidateBehaviourTree(tree);
_currentReport.Results.AddRange(r.Results);
}
_currentReport.AddInfo("验证", $"完成 {trees.Count} 棵行为树的验证");
ApplyFilter();
}
/// <summary>
/// 只验证LUT配置
/// </summary>
private void ValidateLuts()
{
_currentReport = new ValidationReport();
var databases = LoadAllAssets<GameplayEditor.Config.LutConfigDatabase>();
foreach (var db in databases)
{
var r = ConfigValidator.ValidateLutConfigs(db);
_currentReport.Results.AddRange(r.Results);
}
_currentReport.AddInfo("验证", $"完成 {databases.Count} 个LUT数据库的验证");
ApplyFilter();
}
/// <summary>
/// 加载所有指定类型的资产
/// </summary>
private List<T> LoadAllAssets<T>() where T : Object
{
var result = new List<T>();
#if UNITY_EDITOR
var guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}");
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset != null)
{
result.Add(asset);
}
}
#endif
return result;
}
}
}

View File

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

View File

@ -0,0 +1,722 @@
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// Excel Diff 工具窗口
/// 对比两个Excel文件的差异
/// </summary>
public class ExcelDiffWindow : EditorWindow
{
private Vector2 _diffScrollPos;
private Vector2 _sheetScrollPos;
// 文件路径
private string _baseFilePath = "";
private string _compareFilePath = "";
// 选项
private bool _ignoreWhitespace = true;
private bool _ignoreCase = false;
private bool _showOnlyDiff = false;
private int _contextLines = 2;
// 差异结果
private List<DiffItem> _diffs = new List<DiffItem>();
private string _selectedSheet = "";
private List<string> _availableSheets = new List<string>();
private DiffResult _diffResult;
// 折叠状态
private bool _showSettings = true;
private bool _showSummary = true;
private bool _showDiffDetails = true;
// 样式
private GUIStyle _addedStyle;
private GUIStyle _removedStyle;
private GUIStyle _modifiedStyle;
private bool _stylesInitialized;
[MenuItem("Window/Activity Editor/Excel Diff Tool")]
public static void ShowWindow()
{
var window = GetWindow<ExcelDiffWindow>("Excel Diff");
window.minSize = new Vector2(800, 600);
window.Show();
}
private void InitializeStyles()
{
if (_stylesInitialized) return;
_addedStyle = new GUIStyle(EditorStyles.label);
_addedStyle.normal.background = MakeTexture(2, 2, new Color(0.2f, 0.8f, 0.2f, 0.3f));
_removedStyle = new GUIStyle(EditorStyles.label);
_removedStyle.normal.background = MakeTexture(2, 2, new Color(0.8f, 0.2f, 0.2f, 0.3f));
_modifiedStyle = new GUIStyle(EditorStyles.label);
_modifiedStyle.normal.background = MakeTexture(2, 2, new Color(0.8f, 0.8f, 0.2f, 0.3f));
_stylesInitialized = true;
}
private Texture2D MakeTexture(int width, int height, Color color)
{
Color[] pixels = new Color[width * height];
for (int i = 0; i < pixels.Length; i++)
pixels[i] = color;
Texture2D result = new Texture2D(width, height);
result.SetPixels(pixels);
result.Apply();
return result;
}
private void OnGUI()
{
InitializeStyles();
DrawToolbar();
_showSettings = EditorGUILayout.Foldout(_showSettings, "⚙️ 设置", EditorStyles.foldoutHeader);
if (_showSettings)
{
DrawSettingsSection();
}
EditorGUILayout.Space(10);
if (_diffResult != null)
{
_showSummary = EditorGUILayout.Foldout(_showSummary, "📊 差异概览", EditorStyles.foldoutHeader);
if (_showSummary)
{
DrawSummarySection();
}
EditorGUILayout.Space(10);
_showDiffDetails = EditorGUILayout.Foldout(_showDiffDetails, "🔍 详细差异", EditorStyles.foldoutHeader);
if (_showDiffDetails)
{
DrawDiffDetailsSection();
}
}
}
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("选择基准文件", EditorStyles.toolbarButton, GUILayout.Width(100)))
{
var path = EditorUtility.OpenFilePanel("选择基准Excel", "", "xlsx");
if (!string.IsNullOrEmpty(path))
{
_baseFilePath = path;
ScanSheets();
}
}
if (GUILayout.Button("选择对比文件", EditorStyles.toolbarButton, GUILayout.Width(100)))
{
var path = EditorUtility.OpenFilePanel("选择对比Excel", "", "xlsx");
if (!string.IsNullOrEmpty(path))
{
_compareFilePath = path;
}
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("执行对比", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ExecuteDiff();
}
if (GUILayout.Button("导出报告", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ExportReport();
}
EditorGUILayout.EndHorizontal();
// 显示当前选择的文件
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("基准:", GUILayout.Width(40));
EditorGUILayout.LabelField(Path.GetFileName(_baseFilePath) ?? "未选择", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("对比:", GUILayout.Width(40));
EditorGUILayout.LabelField(Path.GetFileName(_compareFilePath) ?? "未选择", EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
#endregion
#region 设置区域
private void DrawSettingsSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// Sheet选择
if (_availableSheets.Count > 0)
{
int selectedIndex = _availableSheets.IndexOf(_selectedSheet);
if (selectedIndex < 0) selectedIndex = 0;
int newIndex = EditorGUILayout.Popup("Sheet:", selectedIndex, _availableSheets.ToArray());
if (newIndex != selectedIndex)
{
_selectedSheet = _availableSheets[newIndex];
}
}
else
{
EditorGUILayout.LabelField("Sheet: (请先选择基准文件)", EditorStyles.miniLabel);
}
EditorGUILayout.Space(5);
// 对比选项
_ignoreWhitespace = EditorGUILayout.Toggle("忽略空白字符", _ignoreWhitespace);
_ignoreCase = EditorGUILayout.Toggle("忽略大小写", _ignoreCase);
_showOnlyDiff = EditorGUILayout.Toggle("只显示差异行", _showOnlyDiff);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("上下文行数:", GUILayout.Width(80));
_contextLines = EditorGUILayout.IntSlider(_contextLines, 0, 5);
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
#endregion
#region 概览区域
private void DrawSummarySection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
if (_diffResult == null)
{
EditorGUILayout.LabelField("尚未执行对比", EditorStyles.centeredGreyMiniLabel);
EditorGUILayout.EndVertical();
return;
}
// 统计信息
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("新增行", EditorStyles.boldLabel);
EditorGUILayout.LabelField(_diffResult.AddedRows.Count.ToString(), new GUIStyle(EditorStyles.largeLabel) { normal = { textColor = Color.green } });
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("删除行", EditorStyles.boldLabel);
EditorGUILayout.LabelField(_diffResult.RemovedRows.Count.ToString(), new GUIStyle(EditorStyles.largeLabel) { normal = { textColor = Color.red } });
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("修改单元格", EditorStyles.boldLabel);
EditorGUILayout.LabelField(_diffResult.ModifiedCells.Count.ToString(), new GUIStyle(EditorStyles.largeLabel) { normal = { textColor = Color.yellow } });
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
// Sheet级别统计
if (_diffResult.SheetResults.Count > 0)
{
EditorGUILayout.LabelField("各Sheet差异:", EditorStyles.boldLabel);
foreach (var sheetResult in _diffResult.SheetResults)
{
var color = sheetResult.HasChanges ? Color.yellow : Color.gray;
var prevColor = GUI.color;
GUI.color = color;
EditorGUILayout.LabelField($" {sheetResult.SheetName}: +{sheetResult.AddedCount} -{sheetResult.RemovedCount} ~{sheetResult.ModifiedCount}");
GUI.color = prevColor;
}
}
EditorGUILayout.EndVertical();
}
#endregion
#region 详细差异
private void DrawDiffDetailsSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
if (_diffs.Count == 0)
{
EditorGUILayout.LabelField("无差异或尚未执行对比", EditorStyles.centeredGreyMiniLabel);
EditorGUILayout.EndVertical();
return;
}
// 过滤差异
var filteredDiffs = _showOnlyDiff
? _diffs.Where(d => d.Type != DiffType.Unchanged).ToList()
: _diffs;
_diffScrollPos = EditorGUILayout.BeginScrollView(_diffScrollPos);
// 表头
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("行", GUILayout.Width(50));
EditorGUILayout.LabelField("列", GUILayout.Width(80));
EditorGUILayout.LabelField("基准值", GUILayout.Width(200));
EditorGUILayout.LabelField("对比值", GUILayout.Width(200));
EditorGUILayout.EndHorizontal();
// 差异列表
foreach (var diff in filteredDiffs)
{
DrawDiffItem(diff);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void DrawDiffItem(DiffItem diff)
{
GUIStyle style = diff.Type switch
{
DiffType.Added => _addedStyle,
DiffType.Removed => _removedStyle,
DiffType.Modified => _modifiedStyle,
_ => EditorStyles.label
};
EditorGUILayout.BeginHorizontal(style);
// 行号
string rowText = diff.Type switch
{
DiffType.Added => $"+{diff.Row}",
DiffType.Removed => $"-{diff.Row}",
_ => diff.Row.ToString()
};
EditorGUILayout.LabelField(rowText, GUILayout.Width(50));
// 列名
EditorGUILayout.LabelField(diff.ColumnName ?? "", GUILayout.Width(80));
// 基准值
if (diff.Type == DiffType.Added)
{
EditorGUILayout.LabelField("", GUILayout.Width(200));
}
else
{
EditorGUILayout.LabelField(Truncate(diff.BaseValue, 30), GUILayout.Width(200));
}
// 对比值
if (diff.Type == DiffType.Removed)
{
EditorGUILayout.LabelField("", GUILayout.Width(200));
}
else
{
EditorGUILayout.LabelField(Truncate(diff.CompareValue, 30), GUILayout.Width(200));
}
EditorGUILayout.EndHorizontal();
}
private string Truncate(string value, int maxLength)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Length <= maxLength) return value;
return value.Substring(0, maxLength) + "...";
}
#endregion
#region Diff 逻辑
private void ScanSheets()
{
_availableSheets.Clear();
if (string.IsNullOrEmpty(_baseFilePath) || !File.Exists(_baseFilePath))
return;
try
{
using (var file = new FileStream(_baseFilePath, FileMode.Open, FileAccess.Read))
{
var workbook = new XSSFWorkbook(file);
for (int i = 0; i < workbook.NumberOfSheets; i++)
{
_availableSheets.Add(workbook.GetSheetName(i));
}
if (_availableSheets.Count > 0 && string.IsNullOrEmpty(_selectedSheet))
{
_selectedSheet = _availableSheets[0];
}
workbook.Close();
}
}
catch (Exception ex)
{
Debug.LogError($"[ExcelDiffWindow] 扫描Sheet失败: {ex.Message}");
}
}
private void ExecuteDiff()
{
if (string.IsNullOrEmpty(_baseFilePath) || !File.Exists(_baseFilePath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的基准文件", "确定");
return;
}
if (string.IsNullOrEmpty(_compareFilePath) || !File.Exists(_compareFilePath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的对比文件", "确定");
return;
}
_diffResult = new DiffResult();
_diffs.Clear();
try
{
using (var baseFile = new FileStream(_baseFilePath, FileMode.Open, FileAccess.Read))
using (var compareFile = new FileStream(_compareFilePath, FileMode.Open, FileAccess.Read))
{
var baseWorkbook = new XSSFWorkbook(baseFile);
var compareWorkbook = new XSSFWorkbook(compareFile);
// 对比所有Sheet或指定Sheet
var sheetsToCompare = string.IsNullOrEmpty(_selectedSheet)
? Enumerable.Range(0, baseWorkbook.NumberOfSheets).Select(i => baseWorkbook.GetSheetName(i))
: new[] { _selectedSheet };
foreach (var sheetName in sheetsToCompare)
{
var baseSheet = baseWorkbook.GetSheet(sheetName);
var compareSheet = compareWorkbook.GetSheet(sheetName);
var sheetResult = CompareSheet(baseSheet, compareSheet, sheetName);
_diffResult.SheetResults.Add(sheetResult);
_diffs.AddRange(sheetResult.Diffs);
}
baseWorkbook.Close();
compareWorkbook.Close();
}
Debug.Log($"[ExcelDiffWindow] 对比完成,发现 {_diffs.Count} 处差异");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("错误", $"对比失败: {ex.Message}", "确定");
Debug.LogError($"[ExcelDiffWindow] 对比失败: {ex}");
}
}
private SheetDiffResult CompareSheet(ISheet baseSheet, ISheet compareSheet, string sheetName)
{
var result = new SheetDiffResult { SheetName = sheetName };
if (baseSheet == null && compareSheet == null)
return result;
if (baseSheet == null)
{
// 新增Sheet
result.AddedCount = compareSheet.LastRowNum + 1;
return result;
}
if (compareSheet == null)
{
// 删除Sheet
result.RemovedCount = baseSheet.LastRowNum + 1;
return result;
}
// 读取所有行数据
var baseRows = ReadAllRows(baseSheet);
var compareRows = ReadAllRows(compareSheet);
// 获取ID列索引第0行是NAME行
int idColumnIndex = 0;
var nameRow = baseSheet.GetRow(0);
if (nameRow != null)
{
for (int i = 0; i < nameRow.LastCellNum; i++)
{
if (GetCellValue(nameRow.GetCell(i))?.ToLower().Contains("id") == true)
{
idColumnIndex = i;
break;
}
}
}
// 建立ID到行的映射
var baseRowMap = baseRows.Where(r => r.RowNum >= 3).ToDictionary(
r => GetCellValue(r.GetCell(idColumnIndex)) ?? $"row_{r.RowNum}",
r => r);
var compareRowMap = compareRows.Where(r => r.RowNum >= 3).ToDictionary(
r => GetCellValue(r.GetCell(idColumnIndex)) ?? $"row_{r.RowNum}",
r => r);
var baseIds = new HashSet<string>(baseRowMap.Keys);
var compareIds = new HashSet<string>(compareRowMap.Keys);
// 查找新增和删除
var addedIds = compareIds.Except(baseIds).ToList();
var removedIds = baseIds.Except(compareIds).ToList();
var commonIds = baseIds.Intersect(compareIds).ToList();
// 记录新增行
foreach (var id in addedIds)
{
var row = compareRowMap[id];
result.AddedCount++;
result.Diffs.Add(new DiffItem
{
Type = DiffType.Added,
Row = row.RowNum + 1,
ColumnName = "整行",
CompareValue = "新增行"
});
}
// 记录删除行
foreach (var id in removedIds)
{
var row = baseRowMap[id];
result.RemovedCount++;
result.Diffs.Add(new DiffItem
{
Type = DiffType.Removed,
Row = row.RowNum + 1,
ColumnName = "整行",
BaseValue = "删除行"
});
}
// 对比相同ID的行
foreach (var id in commonIds)
{
var baseRow = baseRowMap[id];
var compareRow = compareRowMap[id];
int maxCol = Math.Max(baseRow.LastCellNum, compareRow.LastCellNum);
var nameRowData = baseSheet.GetRow(0);
for (int col = 0; col < maxCol; col++)
{
var baseValue = GetCellValue(baseRow.GetCell(col));
var compareValue = GetCellValue(compareRow.GetCell(col));
// 标准化比较
if (_ignoreWhitespace)
{
baseValue = baseValue?.Trim();
compareValue = compareValue?.Trim();
}
var comparison = _ignoreCase
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
if (!string.Equals(baseValue, compareValue, comparison))
{
result.ModifiedCount++;
var colName = nameRowData?.GetCell(col)?.StringCellValue ?? $"Col{col}";
result.Diffs.Add(new DiffItem
{
Type = DiffType.Modified,
Row = baseRow.RowNum + 1,
ColumnName = colName,
BaseValue = baseValue ?? "",
CompareValue = compareValue ?? ""
});
}
}
}
return result;
}
private List<IRow> ReadAllRows(ISheet sheet)
{
var rows = new List<IRow>();
for (int i = 0; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (row != null)
{
rows.Add(row);
}
}
return rows;
}
private string GetCellValue(ICell cell)
{
if (cell == null) return "";
switch (cell.CellType)
{
case CellType.String:
return cell.StringCellValue;
case CellType.Numeric:
return cell.NumericCellValue.ToString();
case CellType.Boolean:
return cell.BooleanCellValue.ToString();
default:
return "";
}
}
private void ExportReport()
{
if (_diffResult == null || _diffs.Count == 0)
{
EditorUtility.DisplayDialog("提示", "没有差异可导出", "确定");
return;
}
var path = EditorUtility.SaveFilePanel("导出差异报告", "", "ExcelDiffReport", "txt");
if (string.IsNullOrEmpty(path)) return;
try
{
var sb = new StringBuilder();
sb.AppendLine("╔════════════════════════════════════════════════════════════════╗");
sb.AppendLine("║ Excel 差异对比报告 ║");
sb.AppendLine("╚════════════════════════════════════════════════════════════════╝");
sb.AppendLine();
sb.AppendLine($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"基准文件: {_baseFilePath}");
sb.AppendLine($"对比文件: {_compareFilePath}");
sb.AppendLine();
sb.AppendLine("══════════════════════════════════════════════════════════════════");
sb.AppendLine("统计摘要");
sb.AppendLine("══════════════════════════════════════════════════════════════════");
sb.AppendLine($"新增行: {_diffResult.AddedRows.Count}");
sb.AppendLine($"删除行: {_diffResult.RemovedRows.Count}");
sb.AppendLine($"修改单元格: {_diffResult.ModifiedCells.Count}");
sb.AppendLine();
sb.AppendLine("══════════════════════════════════════════════════════════════════");
sb.AppendLine("详细差异");
sb.AppendLine("══════════════════════════════════════════════════════════════════");
foreach (var diff in _diffs.Where(d => d.Type != DiffType.Unchanged))
{
var typeStr = diff.Type switch
{
DiffType.Added => "[新增]",
DiffType.Removed => "[删除]",
DiffType.Modified => "[修改]",
_ => "[未知]"
};
sb.AppendLine($"{typeStr} 行{diff.Row} {diff.ColumnName}");
if (diff.Type == DiffType.Modified)
{
sb.AppendLine($" 基准: {diff.BaseValue}");
sb.AppendLine($" 对比: {diff.CompareValue}");
}
else if (diff.Type == DiffType.Added)
{
sb.AppendLine($" 值: {diff.CompareValue}");
}
else if (diff.Type == DiffType.Removed)
{
sb.AppendLine($" 值: {diff.BaseValue}");
}
sb.AppendLine();
}
File.WriteAllText(path, sb.ToString());
EditorUtility.DisplayDialog("成功", "报告已导出", "确定");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导出失败: {ex.Message}", "确定");
}
}
#endregion
#region 数据类
private enum DiffType
{
Unchanged,
Added,
Removed,
Modified
}
private class DiffItem
{
public DiffType Type;
public int Row;
public string ColumnName;
public string BaseValue;
public string CompareValue;
}
private class DiffResult
{
public List<IRow> AddedRows = new List<IRow>();
public List<IRow> RemovedRows = new List<IRow>();
public List<(IRow Row, int Column, string OldValue, string NewValue)> ModifiedCells = new List<(IRow, int, string, string)>();
public List<SheetDiffResult> SheetResults = new List<SheetDiffResult>();
}
private class SheetDiffResult
{
public string SheetName;
public int AddedCount;
public int RemovedCount;
public int ModifiedCount;
public bool HasChanges => AddedCount > 0 || RemovedCount > 0 || ModifiedCount > 0;
public List<DiffItem> Diffs = new List<DiffItem>();
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,501 @@
using GameplayEditor.Excel;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// Excel合并工具窗口
/// 提供可视化界面进行多表合并
/// </summary>
public class ExcelMergeWindow : EditorWindow
{
private Vector2 _sourceListScrollPos;
private Vector2 _conflictScrollPos;
// 配置
private string _templatePath = "";
private string _outputPath = "";
private List<string> _sourcePaths = new List<string>();
private string _sheetName = "ActivityStageConfig";
private string _idColumnName = "ActivityStageID";
private bool _generateConflictReport = true;
// 合并结果
private ExcelMergeTool.MergeResult _lastResult;
private bool _isMerging = false;
// 折叠状态
private bool _showConfig = true;
private bool _showSources = true;
private bool _showResult = false;
private bool _showConflicts = false;
// 临时变量
private string _newSourcePath = "";
[MenuItem("Window/Activity Editor/Excel Merge Tool")]
public static void ShowWindow()
{
var window = GetWindow<ExcelMergeWindow>("Excel合并工具");
window.minSize = new Vector2(600, 500);
window.Show();
}
private void OnGUI()
{
DrawToolbar();
_showConfig = EditorGUILayout.Foldout(_showConfig, "⚙️ 合并配置", EditorStyles.foldoutHeader);
if (_showConfig)
{
DrawConfigSection();
}
EditorGUILayout.Space(10);
_showSources = EditorGUILayout.Foldout(_showSources, "📁 源表列表", EditorStyles.foldoutHeader);
if (_showSources)
{
DrawSourcesSection();
}
EditorGUILayout.Space(10);
if (_lastResult != null)
{
_showResult = EditorGUILayout.Foldout(_showResult, "📊 合并结果", EditorStyles.foldoutHeader);
if (_showResult)
{
DrawResultSection();
}
if (_lastResult.Conflicts.Count > 0)
{
_showConflicts = EditorGUILayout.Foldout(_showConflicts, $"⚠️ 冲突列表 ({_lastResult.Conflicts.Count})", EditorStyles.foldoutHeader);
if (_showConflicts)
{
DrawConflictsSection();
}
}
}
}
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("执行合并", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
ExecuteMerge();
}
if (GUILayout.Button("加载配置", EditorStyles.toolbarButton, GUILayout.Width(70)))
{
LoadConfig();
}
if (GUILayout.Button("保存配置", EditorStyles.toolbarButton, GUILayout.Width(70)))
{
SaveConfig();
}
GUILayout.FlexibleSpace();
if (_isMerging)
{
EditorGUILayout.LabelField("合并中...", EditorStyles.toolbarButton);
}
EditorGUILayout.EndHorizontal();
}
#endregion
#region 配置区域
private void DrawConfigSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 模板表
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("模板表:", GUILayout.Width(80));
_templatePath = EditorGUILayout.TextField(_templatePath);
if (GUILayout.Button("浏览", GUILayout.Width(50)))
{
var path = EditorUtility.OpenFilePanel("选择模板表", "", "xlsx");
if (!string.IsNullOrEmpty(path))
{
_templatePath = path;
}
}
EditorGUILayout.EndHorizontal();
// 输出路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("输出路径:", GUILayout.Width(80));
_outputPath = EditorGUILayout.TextField(_outputPath);
if (GUILayout.Button("浏览", GUILayout.Width(50)))
{
var path = EditorUtility.SaveFilePanel("保存合并表", "", "Merged", "xlsx");
if (!string.IsNullOrEmpty(path))
{
_outputPath = path;
}
}
EditorGUILayout.EndHorizontal();
// Sheet名
_sheetName = EditorGUILayout.TextField("Sheet名称:", _sheetName);
// ID列名
_idColumnName = EditorGUILayout.TextField("主键列名:", _idColumnName);
// 选项
_generateConflictReport = EditorGUILayout.Toggle("生成冲突报告", _generateConflictReport);
EditorGUILayout.EndVertical();
}
#endregion
#region 源表列表
private void DrawSourcesSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 添加新源表
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("添加源表:", GUILayout.Width(70));
_newSourcePath = EditorGUILayout.TextField(_newSourcePath);
if (GUILayout.Button("浏览", GUILayout.Width(50)))
{
var path = EditorUtility.OpenFilePanel("选择源表", "", "xlsx");
if (!string.IsNullOrEmpty(path))
{
_newSourcePath = path;
}
}
if (GUILayout.Button("+", GUILayout.Width(30)))
{
if (!string.IsNullOrEmpty(_newSourcePath) && !_sourcePaths.Contains(_newSourcePath))
{
_sourcePaths.Add(_newSourcePath);
_newSourcePath = "";
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 源表列表
EditorGUILayout.LabelField($"已添加 {_sourcePaths.Count} 个源表:", EditorStyles.boldLabel);
_sourceListScrollPos = EditorGUILayout.BeginScrollView(_sourceListScrollPos, GUILayout.MaxHeight(200));
for (int i = 0; i < _sourcePaths.Count; i++)
{
EditorGUILayout.BeginHorizontal();
// 文件名
var fileName = Path.GetFileName(_sourcePaths[i]);
EditorGUILayout.LabelField($"{i + 1}. {fileName}", EditorStyles.label);
GUILayout.FlexibleSpace();
// 删除按钮
if (GUILayout.Button("×", GUILayout.Width(25)))
{
_sourcePaths.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
// 显示完整路径(小字)
EditorGUI.indentLevel++;
EditorGUILayout.LabelField(_sourcePaths[i], EditorStyles.miniLabel);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndScrollView();
// 快速添加目录下所有Excel
EditorGUILayout.Space(5);
if (GUILayout.Button("添加目录下所有Excel"))
{
var folder = EditorUtility.OpenFolderPanel("选择包含Excel的目录", "", "");
if (!string.IsNullOrEmpty(folder))
{
var files = Directory.GetFiles(folder, "*.xlsx")
.Where(f => !f.Contains("~$")) // 排除临时文件
.ToList();
foreach (var file in files)
{
if (!_sourcePaths.Contains(file))
{
_sourcePaths.Add(file);
}
}
}
}
EditorGUILayout.EndVertical();
}
#endregion
#region 结果区域
private void DrawResultSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
if (_lastResult.Success)
{
EditorGUILayout.HelpBox("合并成功!", MessageType.Info);
EditorGUILayout.LabelField($"总数据行数: {_lastResult.TotalRows}", EditorStyles.label);
EditorGUILayout.LabelField($"合并后行数: {_lastResult.MergedRows}", EditorStyles.label);
EditorGUILayout.LabelField($"冲突数: {_lastResult.ConflictCount}", EditorStyles.label);
EditorGUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("打开输出文件"))
{
EditorUtility.RevealInFinder(_lastResult.OutputPath);
}
if (!string.IsNullOrEmpty(_lastResult.ReportPath) && File.Exists(_lastResult.ReportPath))
{
if (GUILayout.Button("打开冲突报告"))
{
EditorUtility.OpenWithDefaultApp(_lastResult.ReportPath);
}
}
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.HelpBox("合并失败!", MessageType.Error);
EditorGUILayout.LabelField("错误信息:", EditorStyles.boldLabel);
foreach (var error in _lastResult.Errors)
{
EditorGUILayout.LabelField($"• {error}", EditorStyles.label);
}
}
EditorGUILayout.EndVertical();
}
#endregion
#region 冲突列表
private void DrawConflictsSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
var pendingConflicts = _lastResult.Conflicts
.Where(c => c.Resolution == ExcelMergeTool.ConflictResolution.Pending)
.ToList();
if (pendingConflicts.Count == 0)
{
EditorGUILayout.HelpBox("所有冲突已自动解决", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox($"有 {pendingConflicts.Count} 个冲突需要手动处理", MessageType.Warning);
_conflictScrollPos = EditorGUILayout.BeginScrollView(_conflictScrollPos, GUILayout.MaxHeight(300));
foreach (var conflict in pendingConflicts)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField($"ID: {conflict.IdValue} | 列: {conflict.ConflictColumn}", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"文件1: {conflict.SourceFile1}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($"值: {conflict.Value1}", EditorStyles.label);
EditorGUILayout.EndVertical();
EditorGUILayout.LabelField("VS", GUILayout.Width(30));
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"文件2: {conflict.SourceFile2}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($"值: {conflict.Value2}", EditorStyles.label);
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 解决选项
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("解决:", GUILayout.Width(40));
if (GUILayout.Button("使用1", GUILayout.Width(60)))
{
conflict.Resolution = ExcelMergeTool.ConflictResolution.UseFirst;
}
if (GUILayout.Button("使用2", GUILayout.Width(60)))
{
conflict.Resolution = ExcelMergeTool.ConflictResolution.UseSecond;
}
if (GUILayout.Button("忽略", GUILayout.Width(50)))
{
conflict.Resolution = ExcelMergeTool.ConflictResolution.UseNewest;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
EditorGUILayout.EndScrollView();
// 重新合并按钮
if (GUILayout.Button("应用解决方式并重新合并", GUILayout.Height(30)))
{
ExecuteMergeWithResolvedConflicts();
}
}
EditorGUILayout.EndVertical();
}
#endregion
#region 操作
private void ExecuteMerge()
{
if (_sourcePaths.Count == 0)
{
EditorUtility.DisplayDialog("错误", "请至少添加一个源表", "确定");
return;
}
if (string.IsNullOrEmpty(_outputPath))
{
EditorUtility.DisplayDialog("错误", "请设置输出路径", "确定");
return;
}
_isMerging = true;
var config = new ExcelMergeTool.MergeConfig
{
TemplatePath = _templatePath,
OutputPath = _outputPath,
SourcePaths = new List<string>(_sourcePaths),
SheetName = _sheetName,
IdColumnName = _idColumnName,
GenerateConflictReport = _generateConflictReport
};
var mergeTool = new ExcelMergeTool();
_lastResult = mergeTool.Merge(config);
_isMerging = false;
_showResult = true;
_showConflicts = _lastResult.Conflicts.Count > 0;
Repaint();
}
private void ExecuteMergeWithResolvedConflicts()
{
// 使用已解决的冲突重新合并
var config = new ExcelMergeTool.MergeConfig
{
TemplatePath = _templatePath,
OutputPath = _outputPath,
SourcePaths = new List<string>(_sourcePaths),
SheetName = _sheetName,
IdColumnName = _idColumnName,
GenerateConflictReport = _generateConflictReport
};
// TODO: 将冲突解决方式传递给合并工具
ExecuteMerge();
}
private void SaveConfig()
{
var config = new MergeWindowConfig
{
TemplatePath = _templatePath,
OutputPath = _outputPath,
SourcePaths = _sourcePaths,
SheetName = _sheetName,
IdColumnName = _idColumnName,
GenerateConflictReport = _generateConflictReport
};
var json = JsonUtility.ToJson(config, true);
var path = EditorUtility.SaveFilePanel("保存配置", "", "ExcelMergeConfig", "json");
if (!string.IsNullOrEmpty(path))
{
File.WriteAllText(path, json);
EditorUtility.DisplayDialog("成功", "配置已保存", "确定");
}
}
private void LoadConfig()
{
var path = EditorUtility.OpenFilePanel("加载配置", "", "json");
if (string.IsNullOrEmpty(path)) return;
try
{
var json = File.ReadAllText(path);
var config = JsonUtility.FromJson<MergeWindowConfig>(json);
_templatePath = config.TemplatePath;
_outputPath = config.OutputPath;
_sourcePaths = config.SourcePaths;
_sheetName = config.SheetName;
_idColumnName = config.IdColumnName;
_generateConflictReport = config.GenerateConflictReport;
EditorUtility.DisplayDialog("成功", "配置已加载", "确定");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"加载失败: {ex.Message}", "确定");
}
}
#endregion
[System.Serializable]
private class MergeWindowConfig
{
public string TemplatePath;
public string OutputPath;
public List<string> SourcePaths;
public string SheetName;
public string IdColumnName;
public bool GenerateConflictReport;
}
}
}

View File

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

View File

@ -0,0 +1,378 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Excel;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// Excel模板同步窗口
/// 多策划表管理工具,用于同步表头结构和注释
/// </summary>
public class ExcelTemplateSyncWindow : EditorWindow
{
private ExcelTemplateSync _syncTool;
// 模板文件
private string _templatePath = "";
private string _selectedTemplateSheet = "";
private List<string> _templateSheets = new List<string>();
private SheetStructure _templateStructure;
// 目标文件
private List<string> _targetPaths = new List<string>();
private Vector2 _targetScrollPos;
// 同步选项
private bool _syncDescriptionOnly = false;
private bool _showStructurePreview = true;
// 对比结果
private Dictionary<string, List<string>> _compareResults;
private Vector2 _compareScrollPos;
// 状态
private string _statusMessage = "";
private MessageType _statusType = MessageType.None;
[MenuItem("Window/Activity Editor/Excel Template Sync")]
public static void ShowWindow()
{
GetWindow<ExcelTemplateSyncWindow>("Excel表同步");
}
private void OnEnable()
{
_syncTool = new ExcelTemplateSync();
}
private void OnGUI()
{
EditorGUILayout.LabelField("Excel模板同步工具", EditorStyles.boldLabel);
EditorGUILayout.Space();
DrawTemplateSection();
EditorGUILayout.Space();
DrawTargetSection();
EditorGUILayout.Space();
DrawOptionsSection();
EditorGUILayout.Space();
DrawActionButtons();
EditorGUILayout.Space();
DrawCompareResults();
EditorGUILayout.Space();
DrawStructurePreview();
EditorGUILayout.Space();
DrawStatusMessage();
}
/// <summary>
/// 模板文件区域
/// </summary>
private void DrawTemplateSection()
{
EditorGUILayout.LabelField("模板文件", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("模板Excel:", GUILayout.Width(80));
EditorGUILayout.TextField(_templatePath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
var path = EditorUtility.OpenFilePanel("选择模板Excel", "表", "xlsx,xlsm");
if (!string.IsNullOrEmpty(path))
{
LoadTemplate(path);
}
}
EditorGUILayout.EndHorizontal();
if (_templateSheets.Count > 0)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("选择Sheet:", GUILayout.Width(80));
var selectedIndex = _templateSheets.IndexOf(_selectedTemplateSheet);
var newIndex = EditorGUILayout.Popup(selectedIndex, _templateSheets.ToArray());
if (newIndex != selectedIndex)
{
_selectedTemplateSheet = _templateSheets[newIndex];
LoadTemplateStructure();
}
EditorGUILayout.EndHorizontal();
}
if (_templateStructure != null)
{
EditorGUILayout.LabelField($"列数: {_templateStructure.Columns.Count}, 最后同步: {_templateStructure.LastSyncTime:yyyy-MM-dd HH:mm:ss}");
}
}
/// <summary>
/// 目标文件区域
/// </summary>
private void DrawTargetSection()
{
EditorGUILayout.LabelField("目标文件", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("添加文件"))
{
var path = EditorUtility.OpenFilePanel("选择目标Excel", "表", "xlsx,xlsm");
if (!string.IsNullOrEmpty(path) && !_targetPaths.Contains(path))
{
_targetPaths.Add(path);
}
}
if (GUILayout.Button("添加文件夹"))
{
var folder = EditorUtility.OpenFolderPanel("选择包含Excel的文件夹", "表", "");
if (!string.IsNullOrEmpty(folder))
{
var files = Directory.GetFiles(folder, "*.xlsx")
.Concat(Directory.GetFiles(folder, "*.xlsm"))
.Where(f => f != _templatePath)
.ToList();
foreach (var file in files)
{
if (!_targetPaths.Contains(file))
{
_targetPaths.Add(file);
}
}
}
}
if (GUILayout.Button("清空"))
{
_targetPaths.Clear();
}
EditorGUILayout.EndHorizontal();
// 目标文件列表
_targetScrollPos = EditorGUILayout.BeginScrollView(_targetScrollPos, GUILayout.Height(100));
var toRemove = new List<string>();
foreach (var path in _targetPaths)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(Path.GetFileName(path), GUILayout.Width(150));
EditorGUILayout.LabelField(path, EditorStyles.miniLabel);
if (GUILayout.Button("X", GUILayout.Width(20)))
{
toRemove.Add(path);
}
EditorGUILayout.EndHorizontal();
}
foreach (var path in toRemove)
{
_targetPaths.Remove(path);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.LabelField($"共 {_targetPaths.Count} 个目标文件");
}
/// <summary>
/// 选项区域
/// </summary>
private void DrawOptionsSection()
{
EditorGUILayout.LabelField("同步选项", EditorStyles.boldLabel);
_syncDescriptionOnly = EditorGUILayout.ToggleLeft("仅同步注释(不修改列名和类型)", _syncDescriptionOnly);
_showStructurePreview = EditorGUILayout.ToggleLeft("显示结构预览", _showStructurePreview);
}
/// <summary>
/// 操作按钮
/// </summary>
private void DrawActionButtons()
{
EditorGUILayout.BeginHorizontal();
GUI.enabled = _templateStructure != null && _targetPaths.Count > 0;
if (GUILayout.Button("同步表头", GUILayout.Height(30)))
{
DoSync();
}
GUI.enabled = true;
GUI.enabled = _templatePath != "" && _targetPaths.Count > 0;
if (GUILayout.Button("对比结构", GUILayout.Height(30)))
{
DoCompare();
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 对比结果区域
/// </summary>
private void DrawCompareResults()
{
if (_compareResults == null || _compareResults.Count == 0)
return;
EditorGUILayout.LabelField("结构差异", EditorStyles.boldLabel);
_compareScrollPos = EditorGUILayout.BeginScrollView(_compareScrollPos, GUILayout.Height(150));
foreach (var kvp in _compareResults)
{
EditorGUILayout.BeginVertical(GUI.skin.box);
EditorGUILayout.LabelField(kvp.Key, EditorStyles.boldLabel);
foreach (var diff in kvp.Value)
{
EditorGUILayout.LabelField($" • {diff}", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
}
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 结构预览区域
/// </summary>
private void DrawStructurePreview()
{
if (!_showStructurePreview || _templateStructure == null)
return;
EditorGUILayout.LabelField("表结构预览", EditorStyles.boldLabel);
var previewScrollPos = EditorGUILayout.BeginScrollView(Vector2.zero, GUILayout.Height(150));
// 表头
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("列名", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("类型", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("注释", EditorStyles.boldLabel);
EditorGUILayout.EndHorizontal();
// 数据行
foreach (var col in _templateStructure.Columns)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(col.ColumnName, GUILayout.Width(150));
EditorGUILayout.LabelField(col.ColumnType, GUILayout.Width(100));
EditorGUILayout.LabelField(col.Description, EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 状态消息
/// </summary>
private void DrawStatusMessage()
{
if (!string.IsNullOrEmpty(_statusMessage))
{
EditorGUILayout.HelpBox(_statusMessage, _statusType);
}
}
/// <summary>
/// 加载模板文件
/// </summary>
private void LoadTemplate(string path)
{
_templatePath = path;
_templateSheets = _syncTool.GetSheetNames(path);
if (_templateSheets.Count > 0)
{
_selectedTemplateSheet = _templateSheets[0];
LoadTemplateStructure();
}
SetStatus($"已加载模板: {Path.GetFileName(path)}", MessageType.Info);
}
/// <summary>
/// 加载模板结构
/// </summary>
private void LoadTemplateStructure()
{
if (string.IsNullOrEmpty(_templatePath) || string.IsNullOrEmpty(_selectedTemplateSheet))
return;
_templateStructure = _syncTool.ReadSheetStructure(_templatePath, _selectedTemplateSheet);
}
/// <summary>
/// 执行同步
/// </summary>
private void DoSync()
{
if (_templateStructure == null)
{
SetStatus("模板结构为空,请先选择模板", MessageType.Error);
return;
}
if (_targetPaths.Count == 0)
{
SetStatus("没有目标文件", MessageType.Error);
return;
}
var result = _syncTool.BatchSync(_templateStructure, _targetPaths, _selectedTemplateSheet, _syncDescriptionOnly);
SetStatus($"同步完成: 成功{result.SuccessCount}个, 失败{result.FailCount}个",
result.FailCount == 0 ? MessageType.Info : MessageType.Warning);
}
/// <summary>
/// 执行对比
/// </summary>
private void DoCompare()
{
if (string.IsNullOrEmpty(_templatePath))
{
SetStatus("请先选择模板文件", MessageType.Error);
return;
}
if (_targetPaths.Count == 0)
{
SetStatus("没有目标文件", MessageType.Error);
return;
}
// 对比第一个目标文件
_compareResults = _syncTool.CompareExcelStructure(_templatePath, _targetPaths[0]);
if (_compareResults.Count == 0)
{
SetStatus("结构完全一致", MessageType.Info);
}
else
{
SetStatus($"发现{_compareResults.Count}个差异", MessageType.Warning);
}
}
/// <summary>
/// 设置状态消息
/// </summary>
private void SetStatus(string message, MessageType type)
{
_statusMessage = message;
_statusType = type;
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,116 @@
using UnityEditor;
using UnityEngine;
using GameplayEditor.Core;
using NodeCanvas.BehaviourTrees;
namespace GameplayEditor.Editor
{
/// <summary>
/// 头文件与正文分离功能测试窗口
/// 用于验证功能2: 头文件与正文分离
/// </summary>
public class HeaderBodyTestWindow : EditorWindow
{
private int _testTreeId = 30150249;
private BehaviourTreeRuntimeData _runtimeData;
private Vector2 _scrollPos;
private string _testLog = "";
[MenuItem("Window/Activity Editor/Header Body Test")]
public static void ShowWindow()
{
GetWindow<HeaderBodyTestWindow>("头文件正文测试");
}
private void OnGUI()
{
EditorGUILayout.LabelField("功能2: 头文件与正文分离 - 测试", EditorStyles.boldLabel);
EditorGUILayout.Space();
// ID测试区域
EditorGUILayout.LabelField("ID转换测试:", EditorStyles.boldLabel);
_testTreeId = EditorGUILayout.IntField("测试ID", _testTreeId);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("检测是否为头文件"))
{
bool isHeader = BehaviourTreeExtended.IsHeaderFile(_testTreeId);
int baseId = BehaviourTreeExtended.GetBaseStageID(_testTreeId);
int headerId = BehaviourTreeExtended.GetHeaderFileID(baseId);
_testLog += $"[检测] ID: {_testTreeId}\n";
_testLog += $" 是否为头文件: {isHeader}\n";
_testLog += $" 基础StageID: {baseId}\n";
_testLog += $" 对应头文件ID: {headerId}\n\n";
}
if (GUILayout.Button("生成头文件ID"))
{
int headerId = BehaviourTreeExtended.GetHeaderFileID(_testTreeId);
_testLog += $"[生成] StageID {_testTreeId} → 头文件ID {headerId}\n\n";
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 运行时数据测试
EditorGUILayout.LabelField("运行时数据测试:", EditorStyles.boldLabel);
_runtimeData = EditorGUILayout.ObjectField("Runtime Data", _runtimeData,
typeof(BehaviourTreeRuntimeData), false) as BehaviourTreeRuntimeData;
if (_runtimeData != null)
{
EditorGUILayout.LabelField($" StageID: {_runtimeData.StageID}");
EditorGUILayout.LabelField($" TickRate: {_runtimeData.TickRate}");
EditorGUILayout.LabelField($" AutoStart: {_runtimeData.AutoStartOnLoad}");
EditorGUILayout.ObjectField(" Header Tree", _runtimeData.HeaderTree,
typeof(BehaviourTree), false);
EditorGUILayout.ObjectField(" Body Tree", _runtimeData.BodyTree,
typeof(BehaviourTree), false);
}
EditorGUILayout.Space();
// 示例按钮
EditorGUILayout.LabelField("示例操作:", EditorStyles.boldLabel);
if (GUILayout.Button("创建示例 Runtime Data"))
{
CreateExampleRuntimeData();
}
EditorGUILayout.Space();
// 日志显示
EditorGUILayout.LabelField("测试日志:", EditorStyles.boldLabel);
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.Height(200));
EditorGUILayout.TextArea(_testLog, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
if (GUILayout.Button("清空日志"))
{
_testLog = "";
}
}
private void CreateExampleRuntimeData()
{
// 创建示例运行时数据
var data = CreateInstance<BehaviourTreeRuntimeData>();
data.StageID = 30150249;
data.TickRate = 60;
data.AutoStartOnLoad = true;
// 保存到Assets
var path = "Assets/BT/TestRuntimeData.asset";
AssetDatabase.CreateAsset(data, path);
AssetDatabase.SaveAssets();
_runtimeData = data;
_testLog += $"[创建] 示例RuntimeData已保存到: {path}\n";
_testLog += $" StageID: {data.StageID}\n";
_testLog += $" 期望头文件: BT_301502490\n";
_testLog += $" 期望正文: BT_30150249\n\n";
}
}
}

View File

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

View File

@ -0,0 +1,411 @@
using System.Collections.Generic;
using System.Linq;
using GameplayEditor.Config;
using GameplayEditor.Excel;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// ID段分配器窗口
/// 管理策划独立ID段
/// </summary>
public class IDAllocatorWindow : EditorWindow
{
private IDRangeConfig _config;
private Vector2 _scrollPos;
private string _newOwnerName = "";
private int _newStartID = 200000;
private int _newEndID = 299999;
private string _testID = "";
private string _allocateOwner = "";
[MenuItem("Window/Activity Editor/ID Allocator")]
public static void ShowWindow()
{
GetWindow<IDAllocatorWindow>("ID分配器");
}
private void OnEnable()
{
LoadConfig();
}
private void OnGUI()
{
EditorGUILayout.LabelField("ID段分配器", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 配置选择
_config = EditorGUILayout.ObjectField("ID配置", _config, typeof(IDRangeConfig), false) as IDRangeConfig;
if (_config == null)
{
EditorGUILayout.HelpBox("请创建或加载ID配置", MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("创建新配置"))
{
CreateNewConfig();
}
if (GUILayout.Button("加载配置"))
{
LoadConfigFromSelection();
}
EditorGUILayout.EndHorizontal();
return;
}
// 统计信息
DrawStatistics();
EditorGUILayout.Space();
// ID段列表
DrawRangesList();
EditorGUILayout.Space();
// 添加新段
DrawAddRange();
EditorGUILayout.Space();
// 分配ID
DrawAllocateID();
EditorGUILayout.Space();
// ID查询
DrawIDQuery();
EditorGUILayout.Space();
// Excel导入导出
DrawExcelOperations();
}
private void DrawStatistics()
{
EditorGUILayout.LabelField("统计信息", EditorStyles.boldLabel);
var warningRanges = _config.GetWarningRanges(0.8f);
var exhaustedRanges = _config.GetExhaustedRanges();
EditorGUILayout.LabelField($"总段数: {_config.Ranges.Count}");
EditorGUILayout.LabelField($"预警段数: {warningRanges.Count}", warningRanges.Count > 0 ? ColorText("red") : GUIStyle.none);
EditorGUILayout.LabelField($"已耗尽段数: {exhaustedRanges.Count}", exhaustedRanges.Count > 0 ? ColorText("red") : GUIStyle.none);
if (warningRanges.Count > 0)
{
EditorGUILayout.HelpBox($"警告: {string.Join(", ", warningRanges.Select(r => r.OwnerName))} 的ID段使用率超过80%", MessageType.Warning);
}
if (exhaustedRanges.Count > 0)
{
EditorGUILayout.HelpBox($"错误: {string.Join(", ", exhaustedRanges.Select(r => r.OwnerName))} 的ID段已耗尽", MessageType.Error);
}
}
private void DrawRangesList()
{
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.Height(200));
var rangesToRemove = new List<string>();
foreach (var range in _config.Ranges)
{
EditorGUILayout.BeginVertical(GUI.skin.box);
// 头部信息
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(range.OwnerName, EditorStyles.boldLabel);
var usageColor = range.UsageRate >= 0.9f ? "red" : (range.UsageRate >= 0.8f ? "orange" : "green");
EditorGUILayout.LabelField($"使用率: {range.UsageRate:P0}", ColorText(usageColor));
if (GUILayout.Button("删除", GUILayout.Width(50)))
{
rangesToRemove.Add(range.OwnerName);
}
EditorGUILayout.EndHorizontal();
// 详细信息
EditorGUILayout.LabelField($"范围: {range.StartID} - {range.EndID}");
EditorGUILayout.LabelField($"已用: {range.CurrentUsedID} / {range.EndID - range.StartID + 1} (剩余: {range.RemainingCount})");
if (!string.IsNullOrEmpty(range.Description))
{
EditorGUILayout.LabelField($"备注: {range.Description}", EditorStyles.wordWrappedLabel);
}
// 进度条
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.DrawRect(rect, new Color(0.2f, 0.2f, 0.2f));
var fillWidth = rect.width * range.UsageRate;
var fillColor = range.UsageRate >= 0.9f ? Color.red : (range.UsageRate >= 0.8f ? new Color(1f, 0.5f, 0f) : Color.green);
EditorGUI.DrawRect(new Rect(rect.x, rect.y, fillWidth, rect.height), fillColor);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
}
EditorGUILayout.EndScrollView();
// 执行删除
foreach (var ownerName in rangesToRemove)
{
_config.RemoveRange(ownerName);
}
}
private void DrawAddRange()
{
EditorGUILayout.LabelField("添加新ID段", EditorStyles.boldLabel);
_newOwnerName = EditorGUILayout.TextField("所有者名称", _newOwnerName);
_newStartID = EditorGUILayout.IntField("起始ID", _newStartID);
_newEndID = EditorGUILayout.IntField("结束ID", _newEndID);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("添加"))
{
AddRange();
}
if (GUILayout.Button("使用推荐配置"))
{
ApplyRecommendedRanges();
}
EditorGUILayout.EndHorizontal();
}
private void DrawAllocateID()
{
EditorGUILayout.LabelField("分配新ID", EditorStyles.boldLabel);
_allocateOwner = EditorGUILayout.TextField("所有者", _allocateOwner);
if (GUILayout.Button("分配ID"))
{
var id = _config.AllocateID(_allocateOwner);
if (id > 0)
{
EditorUtility.DisplayDialog("成功", $"为 {_allocateOwner} 分配ID: {id}", "确定");
}
else
{
EditorUtility.DisplayDialog("失败", $"无法为 {_allocateOwner} 分配ID", "确定");
}
}
}
private void DrawIDQuery()
{
EditorGUILayout.LabelField("ID查询", EditorStyles.boldLabel);
_testID = EditorGUILayout.TextField("查询ID", _testID);
if (GUILayout.Button("查询") && int.TryParse(_testID, out var id))
{
var range = _config.FindRangeByID(id);
if (range != null)
{
EditorUtility.DisplayDialog("查询结果",
$"ID {id} 属于: {range.OwnerName}\n范围: {range.StartID}-{range.EndID}", "确定");
}
else
{
EditorUtility.DisplayDialog("查询结果", $"ID {id} 未在任何段中", "确定");
}
}
}
private void AddRange()
{
var range = new IDRange
{
OwnerName = _newOwnerName,
StartID = _newStartID,
EndID = _newEndID,
CurrentUsedID = _newStartID - 1
};
if (_config.AddRange(range, out var error))
{
EditorUtility.DisplayDialog("成功", $"已添加ID段: {_newOwnerName}", "确定");
_newOwnerName = "";
}
else
{
EditorUtility.DisplayDialog("错误", error, "确定");
}
}
private void ApplyRecommendedRanges()
{
if (EditorUtility.DisplayDialog("确认", "这将添加推荐的ID段配置是否继续", "是", "否"))
{
var recommended = IDRangeConfig.GetRecommendedRanges();
int added = 0;
foreach (var range in recommended)
{
range.CurrentUsedID = range.StartID - 1;
if (_config.AddRange(range, out _))
{
added++;
}
}
EditorUtility.DisplayDialog("完成", $"添加了 {added} 个推荐ID段", "确定");
}
}
private void CreateNewConfig()
{
var path = EditorUtility.SaveFilePanelInProject(
"保存ID配置", "IDRangeConfig", "asset", "", "Assets/Configs");
if (!string.IsNullOrEmpty(path))
{
_config = ScriptableObject.CreateInstance<IDRangeConfig>();
AssetDatabase.CreateAsset(_config, path);
AssetDatabase.SaveAssets();
}
}
private void LoadConfig()
{
var guids = AssetDatabase.FindAssets("t:IDRangeConfig");
if (guids.Length > 0)
{
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
_config = AssetDatabase.LoadAssetAtPath<IDRangeConfig>(path);
}
}
private void LoadConfigFromSelection()
{
var selected = Selection.GetFiltered<IDRangeConfig>(SelectionMode.Assets);
if (selected.Length > 0)
{
_config = selected[0];
}
}
private GUIStyle ColorText(string color)
{
var style = new GUIStyle(EditorStyles.label);
style.normal.textColor = color == "red" ? Color.red : (color == "orange" ? new Color(1f, 0.5f, 0f) : Color.green);
return style;
}
/// <summary>
/// Excel操作区域
/// </summary>
private void DrawExcelOperations()
{
EditorGUILayout.LabelField("Excel操作", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("从Excel导入", GUILayout.Height(30)))
{
ImportFromExcel();
}
if (GUILayout.Button("导出到Excel", GUILayout.Height(30)))
{
ExportToExcel();
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 从Excel导入
/// </summary>
private void ImportFromExcel()
{
var path = EditorUtility.OpenFilePanel("选择ID段配置Excel", "表", "xlsx,xlsm");
if (string.IsNullOrEmpty(path)) return;
var parser = new IDRangeExcelParser();
var ranges = parser.ParseFromExcel(path);
if (ranges.Count == 0)
{
EditorUtility.DisplayDialog("导入失败", "未找到有效的ID段配置", "确定");
return;
}
// 验证配置
var errors = parser.ValidateRanges(ranges);
if (errors.Count > 0)
{
var errorMsg = string.Join("\n", errors.Take(5));
if (!EditorUtility.DisplayDialog("发现警告",
$"配置存在以下问题:\n{errorMsg}\n\n是否继续导入", "继续", "取消"))
{
return;
}
}
// 合并到现有配置
int added = 0;
int updated = 0;
foreach (var range in ranges)
{
var existing = _config.Ranges.Find(r => r.OwnerName == range.OwnerName);
if (existing != null)
{
// 更新现有配置
existing.StartID = range.StartID;
existing.EndID = range.EndID;
existing.CurrentUsedID = range.CurrentUsedID;
existing.Description = range.Description;
updated++;
}
else
{
// 添加新配置
if (_config.AddRange(range, out _))
{
added++;
}
}
}
EditorUtility.SetDirty(_config);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("导入完成",
$"成功导入{ranges.Count}个ID段\n新增: {added}个\n更新: {updated}个", "确定");
}
/// <summary>
/// 导出到Excel
/// </summary>
private void ExportToExcel()
{
if (_config.Ranges.Count == 0)
{
EditorUtility.DisplayDialog("导出失败", "没有ID段配置可导出", "确定");
return;
}
var path = EditorUtility.SaveFilePanel("导出ID段配置", "表", "IDRangeConfig", "xlsx");
if (string.IsNullOrEmpty(path)) return;
var parser = new IDRangeExcelParser();
if (parser.ExportToExcel(_config, path))
{
EditorUtility.DisplayDialog("导出完成", $"已导出{_config.Ranges.Count}个ID段到:\n{path}", "确定");
}
else
{
EditorUtility.DisplayDialog("导出失败", "导出过程中发生错误", "确定");
}
}
}
}

View File

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

View File

@ -0,0 +1,446 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using NodeCanvas.BehaviourTrees;
using NodeCanvas.Framework;
using NodeCanvas.Tasks.Actions;
using NodeCanvas.Tasks.Conditions;
using KartGame.KartSystems;
using NLD.Nodes;
namespace GameplayEditor.Editor
{
public class KartAdvancedSetupWindow : EditorWindow
{
const string BT_PATH = "Assets/BP_Scripts";
const string ANIM_PATH = "Assets/Karting/Animations/Controllers";
const string SCENE_PATH = "Assets/Karting/Scenes/GameplayGyms/PhysicsPlayground.unity";
Vector2 m_Scroll;
string m_Log = "";
[MenuItem("Window/Activity Editor/Kart Advanced Setup")]
static void Open() => GetWindow<KartAdvancedSetupWindow>("Kart Advanced Setup");
void OnGUI()
{
m_Scroll = GUILayout.BeginScrollView(m_Scroll);
GUILayout.Label("Karting 高级载具系统配置", EditorStyles.boldLabel);
GUILayout.Space(10);
if (GUILayout.Button("1. 生成/刷新行为树资产", GUILayout.Height(32)))
GenerateBehaviourTrees();
if (GUILayout.Button("2. 生成 AnimatorController + 动画Clip", GUILayout.Height(32)))
GenerateAnimatorAssets();
if (GUILayout.Button("3. 配置 PhysicsPlayground 场景中的 Kart", GUILayout.Height(32)))
SetupKartInScene();
GUILayout.Space(10);
if (GUILayout.Button("【一键执行全部】", GUILayout.Height(40)))
{
GenerateBehaviourTrees();
GenerateAnimatorAssets();
SetupKartInScene();
}
GUILayout.Space(10);
GUILayout.Label("日志:", EditorStyles.boldLabel);
GUILayout.TextArea(m_Log, GUILayout.ExpandHeight(true));
GUILayout.EndScrollView();
}
void Log(string msg)
{
m_Log += $"[{System.DateTime.Now:HH:mm:ss}] {msg}\n";
Debug.Log($"[KartSetup] {msg}");
}
#region 行为树生成
void GenerateBehaviourTrees()
{
AssetDatabase.Refresh();
// --- 主行为树BT_Kart_Advanced ---
var treePath = BT_PATH + "/BT_Kart_Advanced.asset";
var tree = AssetDatabase.LoadAssetAtPath<BehaviourTree>(treePath);
if (tree != null) AssetDatabase.DeleteAsset(treePath);
tree = ScriptableObject.CreateInstance<BehaviourTree>();
tree.name = "BT_Kart_Advanced";
AssetDatabase.CreateAsset(tree, treePath);
// 确保 Blackboard 变量存在
EnsureBlackboardVariables(tree);
// 根节点Parallel并行执行所有模块
var root = (Parallel)tree.AddNode(typeof(Parallel));
root.name = "车辆主控(并行)";
tree.primeNode = root;
// 模块1输入同步SetVehicleInput 读取 Blackboard 同步到 BTInput
var inputSeq = CreateSequencer(tree, "输入同步模块");
tree.ConnectNodes(root, inputSeq);
var setInput = (ActionNode)tree.AddNode(typeof(ActionNode));
var svc = new SetVehicleInput();
svc.accelerate = CreateBBParam<float>("Accelerate");
svc.turn = CreateBBParam<float>("Turn");
svc.brake = CreateBBParam<bool>("Brake");
svc.handbrake = CreateBBParam<bool>("Handbrake");
svc.gear = CreateBBParam<GearMode>("Gear");
svc.driveMode = CreateBBParam<DriveTrainMode>("DriveMode");
svc.engineOn = CreateBBParam<bool>("EngineOn");
svc.headlights = CreateBBParam<bool>("Lights");
svc.horn = CreateBBParam<bool>("Horn");
setInput.action = svc;
setInput.name = "同步输入到BaseInput";
tree.ConnectNodes(inputSeq, setInput);
// 模块2状态监控读取车辆状态到黑板
var stateSeq = CreateSequencer(tree, "状态监控模块");
tree.ConnectNodes(root, stateSeq);
var readState = (ActionNode)tree.AddNode(typeof(ActionNode));
var rvt = new ReadVehicleStatusTask();
rvt.outLocalSpeed = CreateBBParam<float>("LocalSpeed");
rvt.outVelocity = CreateBBParam<float>("Velocity");
rvt.outGear = CreateBBParam<GearMode>("CurrentGear");
rvt.outState = CreateBBParam<VehicleState>("CurrentState");
rvt.outFuelPercent = CreateBBParam<float>("FuelPercent");
rvt.outFuelConsumption = CreateBBParam<float>("FuelConsumption");
rvt.outEngineOn = CreateBBParam<bool>("EngineStatus");
rvt.outLightsOn = CreateBBParam<bool>("LightsStatus");
readState.action = rvt;
readState.name = "读取车辆状态到黑板";
tree.ConnectNodes(stateSeq, readState);
// 模块3状态读取用于其他系统观察
var stateReadSeq = CreateSequencer(tree, "状态读取");
tree.ConnectNodes(root, stateReadSeq);
var readState2 = (ActionNode)tree.AddNode(typeof(ActionNode));
readState2.action = new ReadVehicleStatusTask();
readState2.name = "读取状态";
tree.ConnectNodes(stateReadSeq, readState2);
ApplyTreeLayout(tree);
tree.SelfSerialize();
EditorUtility.SetDirty(tree);
AssetDatabase.SaveAssets();
Log("BT_Kart_Advanced 生成完成含Blackboard变量绑定");
}
void EnsureBlackboardVariables(BehaviourTree tree)
{
// 通过 blackboard 添加变量
var bb = tree.blackboard;
bb.AddVariable<float>("Accelerate", 0f);
bb.AddVariable<float>("Turn", 0f);
bb.AddVariable<bool>("Brake", false);
bb.AddVariable<bool>("Handbrake", false);
bb.AddVariable<GearMode>("Gear", GearMode.Neutral);
bb.AddVariable<DriveTrainMode>("DriveMode", DriveTrainMode.AllWheelDrive);
bb.AddVariable<bool>("EngineOn", true);
bb.AddVariable<bool>("Lights", false);
bb.AddVariable<bool>("Horn", false);
bb.AddVariable<float>("LocalSpeed", 0f);
bb.AddVariable<float>("Velocity", 0f);
bb.AddVariable<GearMode>("CurrentGear", GearMode.Neutral);
bb.AddVariable<VehicleState>("CurrentState", VehicleState.Off);
bb.AddVariable<float>("FuelPercent", 1f);
bb.AddVariable<float>("FuelConsumption", 0f);
bb.AddVariable<bool>("EngineStatus", true);
bb.AddVariable<bool>("LightsStatus", false);
}
BBParameter<T> CreateBBParam<T>(string varName)
{
return new BBParameter<T> { name = varName };
}
Sequencer CreateSequencer(BehaviourTree tree, string name)
{
var seq = (Sequencer)tree.AddNode(typeof(Sequencer));
seq.name = name;
return seq;
}
void ApplyTreeLayout(BehaviourTree tree)
{
if (tree.primeNode == null) return;
const float xStep = 220f;
const float yStep = 150f;
System.Action<Node, float, float> traverse = null;
traverse = (node, x, y) =>
{
if (node == null) return;
node.position = new Vector2(x, y);
if (node is BTComposite composite)
{
int childCount = composite.outConnections.Count;
float startX = x - (childCount - 1) * xStep * 0.5f;
for (int i = 0; i < childCount; i++)
{
var child = composite.outConnections[i].targetNode;
traverse(child, startX + i * xStep, y + yStep);
}
}
};
traverse(tree.primeNode, 400f, 60f);
}
#endregion
#region Animator 生成
void GenerateAnimatorAssets()
{
if (!System.IO.Directory.Exists(ANIM_PATH))
System.IO.Directory.CreateDirectory(ANIM_PATH);
// Idle Vibration
var idleClip = new AnimationClip();
idleClip.name = "Kart_Idle";
idleClip.legacy = false;
idleClip.wrapMode = WrapMode.Loop;
var curveY = new AnimationCurve();
curveY.AddKey(0f, 0f);
curveY.AddKey(0.033f, 0.005f);
curveY.AddKey(0.066f, 0f);
curveY.AddKey(0.1f, -0.005f);
curveY.AddKey(0.133f, 0f);
idleClip.SetCurve("", typeof(Transform), "localPosition.y", curveY);
var idlePath = ANIM_PATH + "/Kart_Idle.anim";
AssetDatabase.CreateAsset(idleClip, idlePath);
// Brake Pitch
var brakeClip = new AnimationClip();
brakeClip.name = "Kart_Brake";
var curvePitch = new AnimationCurve();
curvePitch.AddKey(0f, 0f);
curvePitch.AddKey(0.2f, 3f);
brakeClip.SetCurve("", typeof(Transform), "localRotation.x", curvePitch);
var brakePath = ANIM_PATH + "/Kart_Brake.anim";
AssetDatabase.CreateAsset(brakeClip, brakePath);
// Accelerate Pitch
var accelClip = new AnimationClip();
accelClip.name = "Kart_Accelerate";
var curvePitch2 = new AnimationCurve();
curvePitch2.AddKey(0f, 0f);
curvePitch2.AddKey(0.2f, -3f);
accelClip.SetCurve("", typeof(Transform), "localRotation.x", curvePitch2);
var accelPath = ANIM_PATH + "/Kart_Accelerate.anim";
AssetDatabase.CreateAsset(accelClip, accelPath);
// AnimatorController
// 使用 CreateAnimatorControllerAtPath 可确保正确生成含 StateMachine 的 Layer
var ctrlPath = ANIM_PATH + "/VehicleStateController.controller";
if (System.IO.File.Exists(ctrlPath))
{
AssetDatabase.DeleteAsset(ctrlPath);
}
var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(ctrlPath);
controller.name = "VehicleStateController";
controller.AddParameter("VehicleState", AnimatorControllerParameterType.Int);
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
controller.AddParameter("Steering", AnimatorControllerParameterType.Float);
controller.AddParameter("Grounded", AnimatorControllerParameterType.Bool);
controller.AddParameter("EngineOn", AnimatorControllerParameterType.Bool);
var sm = controller.layers[0].stateMachine;
var stateOff = sm.AddState("Off", new Vector3(300, 100, 0));
var stateIgnition = sm.AddState("Ignition", new Vector3(300, 200, 0));
var stateIdle = sm.AddState("Idle", new Vector3(300, 300, 0));
var stateIdleAccel = sm.AddState("IdleAccelerate", new Vector3(500, 300, 0));
var stateCruise = sm.AddState("Cruise", new Vector3(300, 400, 0));
var stateCruiseBrake = sm.AddState("CruiseBrake", new Vector3(500, 400, 0));
var stateReverse = sm.AddState("Reverse", new Vector3(100, 400, 0));
var stateStuck = sm.AddState("Stuck", new Vector3(300, 500, 0));
stateIdle.motion = idleClip;
stateCruiseBrake.motion = brakeClip;
stateIdleAccel.motion = accelClip;
// EngineOff -> Off
var anyToOff = sm.AddAnyStateTransition(stateOff);
anyToOff.AddCondition(UnityEditor.Animations.AnimatorConditionMode.IfNot, 0, "EngineOn");
anyToOff.duration = 0.1f;
// Off -> Ignition
var offToIgnition = stateOff.AddTransition(stateIgnition);
offToIgnition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, "EngineOn");
offToIgnition.duration = 0.1f;
// Ignition -> Idle
var ignitionToIdle = stateIgnition.AddTransition(stateIdle);
ignitionToIdle.duration = 1.0f;
ignitionToIdle.hasExitTime = true;
ignitionToIdle.exitTime = 1f;
// Idle <-> IdleAccelerate (VehicleState == 3)
var idleToAccel = stateIdle.AddTransition(stateIdleAccel);
idleToAccel.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 3, "VehicleState");
idleToAccel.duration = 0.2f;
var accelToIdle = stateIdleAccel.AddTransition(stateIdle);
accelToIdle.AddCondition(UnityEditor.Animations.AnimatorConditionMode.NotEqual, 3, "VehicleState");
accelToIdle.duration = 0.2f;
// Idle <-> Cruise (VehicleState == 4)
var idleToCruise = stateIdle.AddTransition(stateCruise);
idleToCruise.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 4, "VehicleState");
idleToCruise.duration = 0.3f;
var cruiseToIdle = stateCruise.AddTransition(stateIdle);
cruiseToIdle.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 2, "VehicleState");
cruiseToIdle.duration = 0.3f;
// Cruise <-> CruiseBrake (VehicleState == 5)
var cruiseToBrake = stateCruise.AddTransition(stateCruiseBrake);
cruiseToBrake.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 5, "VehicleState");
cruiseToBrake.duration = 0.1f;
var brakeToCruise = stateCruiseBrake.AddTransition(stateCruise);
brakeToCruise.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 4, "VehicleState");
brakeToCruise.duration = 0.2f;
// Idle <-> Reverse (VehicleState == 6)
var idleToReverse = stateIdle.AddTransition(stateReverse);
idleToReverse.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 6, "VehicleState");
idleToReverse.duration = 0.2f;
var reverseToIdle = stateReverse.AddTransition(stateIdle);
reverseToIdle.AddCondition(UnityEditor.Animations.AnimatorConditionMode.NotEqual, 6, "VehicleState");
reverseToIdle.duration = 0.2f;
// Any -> Stuck (VehicleState == 7)
var anyToStuck = sm.AddAnyStateTransition(stateStuck);
anyToStuck.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 7, "VehicleState");
anyToStuck.duration = 0.1f;
var stuckToIdle = stateStuck.AddTransition(stateIdle);
stuckToIdle.AddCondition(UnityEditor.Animations.AnimatorConditionMode.NotEqual, 7, "VehicleState");
stuckToIdle.duration = 0.3f;
AssetDatabase.SaveAssets();
Log($"AnimatorController 生成完成: {ANIM_PATH}/VehicleStateController.controller");
}
#endregion
#region 场景配置
void SetupKartInScene()
{
var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(SCENE_PATH, UnityEditor.SceneManagement.OpenSceneMode.Single);
GameObject kartGo = null;
var roots = scene.GetRootGameObjects();
foreach (var r in roots)
{
if (r.name.Contains("KartClassic_Player_02"))
{
kartGo = r;
break;
}
var found = r.transform.Find("KartClassic_Player_02");
if (found != null) kartGo = found.gameObject;
}
if (kartGo == null)
{
LogError("未在场景中找到 KartClassic_Player_02");
return;
}
var arcadeKart = kartGo.GetComponent<ArcadeKart>();
if (arcadeKart == null)
{
LogError("KartClassic_Player_02 上未找到 ArcadeKart 组件!");
return;
}
// 添加扩展组件
var gearbox = kartGo.GetComponent<KartManualGearbox>() ?? kartGo.AddComponent<KartManualGearbox>();
var fuel = kartGo.GetComponent<KartFuelSystem>() ?? kartGo.AddComponent<KartFuelSystem>();
var electrical = kartGo.GetComponent<KartElectricalSystem>() ?? kartGo.AddComponent<KartElectricalSystem>();
var physicsEffects = kartGo.GetComponent<KartPhysicsEffects>() ?? kartGo.AddComponent<KartPhysicsEffects>();
var stateMachine = kartGo.GetComponent<KartStateMachine>() ?? kartGo.AddComponent<KartStateMachine>();
arcadeKart.ManualGearbox = gearbox;
arcadeKart.FuelSystem = fuel;
stateMachine.Kart = arcadeKart;
stateMachine.Gearbox = gearbox;
stateMachine.Fuel = fuel;
if (physicsEffects.VisualBody == null)
{
var kartVisual = kartGo.transform.Find("KartVisual");
if (kartVisual != null)
{
var car = kartVisual.Find("car");
physicsEffects.VisualBody = car != null ? car : kartVisual;
}
}
// Animator
var animator = kartGo.GetComponentInChildren<Animator>();
if (animator == null && physicsEffects.VisualBody != null)
{
animator = physicsEffects.VisualBody.gameObject.AddComponent<Animator>();
}
if (animator != null)
{
var ctrl = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(ANIM_PATH + "/VehicleStateController.controller");
if (ctrl != null) animator.runtimeAnimatorController = ctrl;
stateMachine.Animator = animator;
}
// 输入系统
var btInput = kartGo.GetComponent<BTInput>() ?? kartGo.AddComponent<BTInput>();
var kbInput = kartGo.GetComponent<KartKeyboardInput>() ?? kartGo.AddComponent<KartKeyboardInput>();
// 移除旧的 IInput如果有 KeyboardInput 继承 BaseInput 的旧版本)
// 由于我们已经重写了 KartKeyboardInput 不再继承 BaseInput这里不需要移除
// BehaviourTreeOwner
var btOwner = kartGo.GetComponent<BehaviourTreeOwner>();
if (btOwner == null) btOwner = kartGo.AddComponent<BehaviourTreeOwner>();
var advancedBT = AssetDatabase.LoadAssetAtPath<BehaviourTree>(BT_PATH + "/BT_Kart_Advanced.asset");
if (advancedBT != null)
{
btOwner.graph = advancedBT;
btOwner.updateMode = Graph.UpdateMode.Manual;
}
var bb = kartGo.GetComponent<Blackboard>() ?? kartGo.AddComponent<Blackboard>();
// 标记修改
EditorUtility.SetDirty(kartGo);
EditorUtility.SetDirty(arcadeKart);
EditorUtility.SetDirty(gearbox);
EditorUtility.SetDirty(fuel);
EditorUtility.SetDirty(electrical);
EditorUtility.SetDirty(physicsEffects);
EditorUtility.SetDirty(stateMachine);
EditorUtility.SetDirty(btOwner);
EditorUtility.SetDirty(bb);
if (animator != null) EditorUtility.SetDirty(animator);
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene);
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene);
Log("KartClassic_Player_02 配置完成。请检查 Inspector 中的引用是否正确。");
}
void LogError(string msg)
{
m_Log += $"[Error] {msg}\n";
Debug.LogError($"[KartSetup] {msg}");
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,611 @@
using GameplayEditor.Config;
using GameplayEditor.Runtime;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 本地化编辑器窗口
/// 提供多语言文本编辑、导入导出、语言切换预览等功能
/// </summary>
public class LocalizationEditorWindow : EditorWindow
{
private Vector2 _dialogListScrollPos;
private Vector2 _editScrollPos;
// 当前编辑状态
private DialogInfoDatabase _database;
private List<DialogInfoData> _dialogs = new List<DialogInfoData>();
private int _selectedDialogIndex = -1;
private LanguageType _previewLanguage = LanguageType.Chinese;
private LanguageType _editLanguage = LanguageType.Chinese;
// 搜索过滤
private string _searchFilter = "";
// 折叠状态
private bool _showDialogList = true;
private bool _showEditPanel = true;
// 编辑中的文本
private string _editText = "";
// 样式
private GUIStyle _selectedItemStyle;
private GUIStyle _missingTranslationStyle;
private bool _stylesInitialized;
[MenuItem("Window/Activity Editor/Localization Editor")]
public static void ShowWindow()
{
var window = GetWindow<LocalizationEditorWindow>("Localization");
window.minSize = new Vector2(800, 600);
window.Show();
}
private void InitializeStyles()
{
if (_stylesInitialized) return;
_selectedItemStyle = new GUIStyle(EditorStyles.label);
_selectedItemStyle.normal.background = MakeTexture(2, 2, new Color(0.2f, 0.4f, 0.8f, 0.5f));
_missingTranslationStyle = new GUIStyle(EditorStyles.label);
_missingTranslationStyle.normal.textColor = Color.red;
_stylesInitialized = true;
}
private Texture2D MakeTexture(int width, int height, Color color)
{
Color[] pixels = new Color[width * height];
for (int i = 0; i < pixels.Length; i++)
pixels[i] = color;
Texture2D result = new Texture2D(width, height);
result.SetPixels(pixels);
result.Apply();
return result;
}
private void OnEnable()
{
LoadDatabase();
}
private void LoadDatabase()
{
// 查找数据库
var guids = AssetDatabase.FindAssets("t:DialogInfoDatabase");
if (guids.Length > 0)
{
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
_database = AssetDatabase.LoadAssetAtPath<DialogInfoDatabase>(path);
if (_database != null)
{
_dialogs = _database.GetAllDialogs().ToList();
}
}
}
private void OnGUI()
{
InitializeStyles();
DrawToolbar();
if (_database == null)
{
DrawNoDatabaseMessage();
return;
}
EditorGUILayout.BeginHorizontal();
// 左侧:对话列表
DrawDialogListPanel();
// 右侧:编辑区域
DrawEditPanel();
EditorGUILayout.EndHorizontal();
}
#region 工具栏
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
LoadDatabase();
}
if (GUILayout.Button("导入CSV", EditorStyles.toolbarButton, GUILayout.Width(70)))
{
ImportCSV();
}
if (GUILayout.Button("导出CSV", EditorStyles.toolbarButton, GUILayout.Width(70)))
{
ExportCSV();
}
GUILayout.FlexibleSpace();
// 预览语言选择
EditorGUILayout.LabelField("预览语言:", GUILayout.Width(60));
_previewLanguage = (LanguageType)EditorGUILayout.EnumPopup(_previewLanguage, EditorStyles.toolbarPopup, GUILayout.Width(100));
GUILayout.Space(10);
// 缺失翻译检查
if (GUILayout.Button("检查缺失", EditorStyles.toolbarButton, GUILayout.Width(70)))
{
CheckMissingTranslations();
}
EditorGUILayout.EndHorizontal();
}
#endregion
#region 对话列表面板
private void DrawDialogListPanel()
{
EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(300), GUILayout.ExpandHeight(true));
_showDialogList = EditorGUILayout.Foldout(_showDialogList, "对话列表", EditorStyles.foldoutHeader);
if (!_showDialogList)
{
EditorGUILayout.EndVertical();
return;
}
// 搜索和过滤
EditorGUILayout.BeginHorizontal();
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField);
if (GUILayout.Button("×", GUILayout.Width(20)))
{
_searchFilter = "";
}
EditorGUILayout.EndHorizontal();
// 统计信息
int totalCount = _dialogs.Count;
int missingCount = _dialogs.Count(d => IsMissingTranslation(d, _previewLanguage));
EditorGUILayout.LabelField($"总计: {totalCount} | 缺失翻译: {missingCount}", EditorStyles.miniLabel);
EditorGUILayout.Space(5);
// 列表
_dialogListScrollPos = EditorGUILayout.BeginScrollView(_dialogListScrollPos);
for (int i = 0; i < _dialogs.Count; i++)
{
var dialog = _dialogs[i];
// 搜索过滤
if (!string.IsNullOrEmpty(_searchFilter))
{
var searchLower = _searchFilter.ToLower();
if (!dialog.ID.ToString().Contains(searchLower) &&
!dialog.CharacterName.ToLower().Contains(searchLower) &&
!GetDisplayText(dialog).ToLower().Contains(searchLower))
{
continue;
}
}
DrawDialogListItem(dialog, i);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void DrawDialogListItem(DialogInfoData dialog, int index)
{
bool isSelected = _selectedDialogIndex == index;
bool isMissing = IsMissingTranslation(dialog, _previewLanguage);
var style = isSelected ? _selectedItemStyle : EditorStyles.label;
if (isMissing && !isSelected)
{
style = _missingTranslationStyle;
}
EditorGUILayout.BeginHorizontal(style);
// 缺失翻译标记
if (isMissing)
{
GUILayout.Label("⚠", GUILayout.Width(20));
}
else
{
GUILayout.Label("✓", GUILayout.Width(20));
}
// ID
EditorGUILayout.LabelField($"{dialog.ID}", GUILayout.Width(50));
// 角色名
EditorGUILayout.LabelField(dialog.CharacterName, GUILayout.Width(80));
// 预览文本(截断)
string displayText = GetDisplayText(dialog);
if (displayText.Length > 20)
{
displayText = displayText.Substring(0, 20) + "...";
}
EditorGUILayout.LabelField(displayText, EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal();
// 点击选择
var rect = GUILayoutUtility.GetLastRect();
if (Event.current.type == UnityEngine.EventType.MouseDown && rect.Contains(Event.current.mousePosition))
{
_selectedDialogIndex = index;
StartEdit(dialog);
Event.current.Use();
}
}
private string GetDisplayText(DialogInfoData dialog)
{
return _previewLanguage switch
{
LanguageType.Chinese => dialog.Text,
LanguageType.English => string.IsNullOrEmpty(dialog.Text_EN) ? dialog.Text : dialog.Text_EN,
LanguageType.Japanese => string.IsNullOrEmpty(dialog.Text_JP) ? dialog.Text : dialog.Text_JP,
LanguageType.Korean => string.IsNullOrEmpty(dialog.Text_KR) ? dialog.Text : dialog.Text_KR,
LanguageType.French => string.IsNullOrEmpty(dialog.Text_FR) ? dialog.Text : dialog.Text_FR,
LanguageType.German => string.IsNullOrEmpty(dialog.Text_DE) ? dialog.Text : dialog.Text_DE,
LanguageType.Spanish => string.IsNullOrEmpty(dialog.Text_ES) ? dialog.Text : dialog.Text_ES,
_ => dialog.Text
};
}
private bool IsMissingTranslation(DialogInfoData dialog, LanguageType language)
{
if (language == LanguageType.Chinese) return false; // 中文是默认语言
return language switch
{
LanguageType.English => string.IsNullOrEmpty(dialog.Text_EN),
LanguageType.Japanese => string.IsNullOrEmpty(dialog.Text_JP),
LanguageType.Korean => string.IsNullOrEmpty(dialog.Text_KR),
LanguageType.French => string.IsNullOrEmpty(dialog.Text_FR),
LanguageType.German => string.IsNullOrEmpty(dialog.Text_DE),
LanguageType.Spanish => string.IsNullOrEmpty(dialog.Text_ES),
_ => false
};
}
#endregion
#region 编辑面板
private void DrawEditPanel()
{
EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
if (_selectedDialogIndex < 0 || _selectedDialogIndex >= _dialogs.Count)
{
EditorGUILayout.LabelField("请选择一个对话进行编辑", EditorStyles.centeredGreyMiniLabel);
EditorGUILayout.EndVertical();
return;
}
var dialog = _dialogs[_selectedDialogIndex];
// 工具栏
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField($"Dialog ID: {dialog.ID}", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
if (GUILayout.Button("保存", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
SaveEdit(dialog);
}
if (GUILayout.Button("取消", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
CancelEdit();
}
EditorGUILayout.EndHorizontal();
_showEditPanel = EditorGUILayout.Foldout(_showEditPanel, "文本编辑", EditorStyles.foldoutHeader);
if (!_showEditPanel)
{
EditorGUILayout.EndVertical();
return;
}
_editScrollPos = EditorGUILayout.BeginScrollView(_editScrollPos);
// 中文(默认)
EditorGUILayout.LabelField("中文 (默认)", EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextArea(dialog.Text, GUILayout.MinHeight(60));
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space(10);
// 语言选择
_editLanguage = (LanguageType)EditorGUILayout.EnumPopup("编辑语言:", _editLanguage);
EditorGUILayout.Space(5);
// 编辑区域
if (_editLanguage == LanguageType.Chinese)
{
EditorGUILayout.HelpBox("中文是默认语言请在DialogInfo配置中编辑", MessageType.Info);
}
else
{
EditorGUILayout.LabelField($"{_editLanguage} 翻译:", EditorStyles.boldLabel);
_editText = EditorGUILayout.TextArea(_editText, GUILayout.MinHeight(100));
// 字符统计
EditorGUILayout.LabelField($"字符数: {_editText.Length}", EditorStyles.miniLabel);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void StartEdit(DialogInfoData dialog)
{
_editText = GetEditText(dialog, _editLanguage);
}
private string GetEditText(DialogInfoData dialog, LanguageType language)
{
return language switch
{
LanguageType.English => dialog.Text_EN,
LanguageType.Japanese => dialog.Text_JP,
LanguageType.Korean => dialog.Text_KR,
LanguageType.French => dialog.Text_FR,
LanguageType.German => dialog.Text_DE,
LanguageType.Spanish => dialog.Text_ES,
_ => dialog.Text
};
}
private void SaveEdit(DialogInfoData dialog)
{
if (_editLanguage == LanguageType.Chinese) return;
// 更新数据
switch (_editLanguage)
{
case LanguageType.English: dialog.Text_EN = _editText; break;
case LanguageType.Japanese: dialog.Text_JP = _editText; break;
case LanguageType.Korean: dialog.Text_KR = _editText; break;
case LanguageType.French: dialog.Text_FR = _editText; break;
case LanguageType.German: dialog.Text_DE = _editText; break;
case LanguageType.Spanish: dialog.Text_ES = _editText; break;
}
// 标记为已修改
EditorUtility.SetDirty(_database);
Debug.Log($"[LocalizationEditor] 保存对话 {dialog.ID} 的 {_editLanguage} 翻译");
}
private void CancelEdit()
{
if (_selectedDialogIndex >= 0 && _selectedDialogIndex < _dialogs.Count)
{
StartEdit(_dialogs[_selectedDialogIndex]);
}
}
#endregion
#region 导入导出
private void ImportCSV()
{
var path = EditorUtility.OpenFilePanel("导入本地化CSV", "", "csv");
if (string.IsNullOrEmpty(path)) return;
try
{
var csvContent = File.ReadAllText(path, Encoding.UTF8);
var lines = csvContent.Split('\n');
if (lines.Length < 2)
{
EditorUtility.DisplayDialog("错误", "CSV文件格式不正确", "确定");
return;
}
// 解析表头
var headers = lines[0].Split(',');
var languageMap = new Dictionary<int, LanguageType>();
for (int i = 1; i < headers.Length; i++)
{
if (System.Enum.TryParse<LanguageType>(headers[i].Trim(), out var lang))
{
languageMap[i] = lang;
}
}
// 解析数据
int successCount = 0;
for (int i = 1; i < lines.Length; i++)
{
var fields = lines[i].Split(',');
if (fields.Length < 2) continue;
if (!int.TryParse(fields[0].Trim(), out var dialogId)) continue;
var dialog = _dialogs.Find(d => d.ID == dialogId);
if (dialog == null) continue;
for (int j = 1; j < fields.Length && j < headers.Length; j++)
{
if (languageMap.TryGetValue(j, out var lang))
{
SetDialogText(dialog, lang, fields[j].Trim());
}
}
successCount++;
}
EditorUtility.SetDirty(_database);
EditorUtility.DisplayDialog("导入完成", $"成功导入 {successCount} 条翻译", "确定");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导入失败: {ex.Message}", "确定");
}
}
private void SetDialogText(DialogInfoData dialog, LanguageType language, string text)
{
switch (language)
{
case LanguageType.English: dialog.Text_EN = text; break;
case LanguageType.Japanese: dialog.Text_JP = text; break;
case LanguageType.Korean: dialog.Text_KR = text; break;
case LanguageType.French: dialog.Text_FR = text; break;
case LanguageType.German: dialog.Text_DE = text; break;
case LanguageType.Spanish: dialog.Text_ES = text; break;
}
}
private void ExportCSV()
{
var path = EditorUtility.SaveFilePanel("导出本地化CSV", "", "localization", "csv");
if (string.IsNullOrEmpty(path)) return;
try
{
var sb = new StringBuilder();
// 表头
sb.AppendLine("ID,CharacterName,Chinese,English,Japanese,Korean,French,German,Spanish");
// 数据
foreach (var dialog in _dialogs)
{
sb.AppendLine($"{dialog.ID},{EscapeCSV(dialog.CharacterName)},{EscapeCSV(dialog.Text)},{EscapeCSV(dialog.Text_EN)},{EscapeCSV(dialog.Text_JP)},{EscapeCSV(dialog.Text_KR)},{EscapeCSV(dialog.Text_FR)},{EscapeCSV(dialog.Text_DE)},{EscapeCSV(dialog.Text_ES)}");
}
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
EditorUtility.DisplayDialog("导出完成", $"已导出 {_dialogs.Count} 条对话", "确定");
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("错误", $"导出失败: {ex.Message}", "确定");
}
}
private string EscapeCSV(string text)
{
if (string.IsNullOrEmpty(text)) return "";
// 如果包含逗号、引号或换行,需要用引号包裹
if (text.Contains(",") || text.Contains("\"") || text.Contains("\n") || text.Contains("\r"))
{
text = text.Replace("\"", "\"\""); // 双引号转义
return $"\"{text}\"";
}
return text;
}
#endregion
#region 工具方法
private void CheckMissingTranslations()
{
var missing = new List<(int id, LanguageType lang)>();
foreach (var dialog in _dialogs)
{
var languages = System.Enum.GetValues(typeof(LanguageType)) as LanguageType[];
foreach (var lang in languages)
{
if (lang == LanguageType.Chinese) continue;
if (IsMissingTranslation(dialog, lang))
{
missing.Add((dialog.ID, lang));
}
}
}
if (missing.Count == 0)
{
EditorUtility.DisplayDialog("检查结果", "所有对话都有完整翻译!", "确定");
}
else
{
var sb = new StringBuilder();
sb.AppendLine($"发现 {missing.Count} 处缺失翻译:");
foreach (var (id, lang) in missing.Take(20))
{
sb.AppendLine($" Dialog {id}: {lang}");
}
if (missing.Count > 20)
{
sb.AppendLine($" ... 还有 {missing.Count - 20} 处");
}
EditorUtility.DisplayDialog("检查结果", sb.ToString(), "确定");
}
}
private void DrawNoDatabaseMessage()
{
EditorGUILayout.Space(50);
EditorGUILayout.HelpBox("未找到 DialogInfoDatabase\n请先创建对话数据库", MessageType.Warning);
if (GUILayout.Button("创建数据库", GUILayout.Height(40)))
{
CreateDatabase();
}
}
private void CreateDatabase()
{
var database = CreateInstance<DialogInfoDatabase>();
var path = "Assets/Resources/DialogInfoDatabase.asset";
if (!Directory.Exists("Assets/Resources"))
{
Directory.CreateDirectory("Assets/Resources");
}
AssetDatabase.CreateAsset(database, path);
AssetDatabase.SaveAssets();
LoadDatabase();
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,391 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Config;
using GameplayEditor.Excel;
using GameplayEditor.Runtime;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 本地化导出/导入窗口
/// 管理对话多语言的导出、导入和翻译进度
/// </summary>
public class LocalizationExportWindow : EditorWindow
{
private DialogInfoDatabase _database;
private List<LanguageType> _targetLanguages = new List<LanguageType>();
private Vector2 _scrollPos;
private string _exportPath = "Localization";
private string _importPath = "";
// 统计信息
private Dictionary<LanguageType, int> _translationStats = new Dictionary<LanguageType, int>();
private bool _showStats = true;
private bool _showMissing = true;
private List<MissingTranslationInfo> _missingList = new List<MissingTranslationInfo>();
[MenuItem("Window/Activity Editor/Localization Manager")]
public static void ShowWindow()
{
var window = GetWindow<LocalizationExportWindow>("本地化 manager");
window.minSize = new Vector2(500, 600);
window.Show();
}
private void OnEnable()
{
// 默认选择常用语言
_targetLanguages = new List<LanguageType>
{
LanguageType.English,
LanguageType.Japanese
};
RefreshStats();
}
private void OnGUI()
{
EditorGUILayout.LabelField("对话本地化 manager", EditorStyles.boldLabel);
EditorGUILayout.Space();
// 数据库选择
DrawDatabaseSelection();
EditorGUILayout.Space();
if (_database == null)
{
EditorGUILayout.HelpBox("请选择一个DialogInfoDatabase", MessageType.Info);
return;
}
// 统计信息
if (_showStats)
{
DrawStatistics();
EditorGUILayout.Space();
}
// 目标语言选择
DrawLanguageSelection();
EditorGUILayout.Space();
// 导出/导入操作
DrawExportImport();
EditorGUILayout.Space();
// 缺失翻译列表
if (_showMissing)
{
DrawMissingTranslations();
}
}
private void DrawDatabaseSelection()
{
EditorGUILayout.BeginHorizontal();
_database = EditorGUILayout.ObjectField("对话数据库", _database,
typeof(DialogInfoDatabase), false) as DialogInfoDatabase;
if (GUILayout.Button("自动查找", GUILayout.Width(80)))
{
var guids = AssetDatabase.FindAssets("t:DialogInfoDatabase");
if (guids.Length > 0)
{
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
_database = AssetDatabase.LoadAssetAtPath<DialogInfoDatabase>(path);
RefreshStats();
}
}
EditorGUILayout.EndHorizontal();
}
private void DrawStatistics()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("翻译统计", EditorStyles.boldLabel);
// 计算统计
CalculateTranslationStats();
// 显示各语言翻译完成度
foreach (var kvp in _translationStats)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(GetLanguageDisplayName(kvp.Key), GUILayout.Width(100));
float percentage = (float)kvp.Value / GetTotalDialogCount() * 100;
var color = percentage >= 100 ? Color.green : percentage >= 50 ? Color.yellow : Color.red;
GUI.color = color;
EditorGUILayout.LabelField($"{kvp.Value}/{GetTotalDialogCount()} ({percentage:F0}%)", GUILayout.Width(120));
GUI.color = Color.white;
// 进度条
Rect rect = EditorGUILayout.GetControlRect(false, 20);
EditorGUI.ProgressBar(rect, percentage / 100, "");
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
}
private void DrawLanguageSelection()
{
EditorGUILayout.LabelField("目标语言", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
var allLanguages = System.Enum.GetValues(typeof(LanguageType)).Cast<LanguageType>();
foreach (var lang in allLanguages)
{
if (lang == LanguageType.Chinese) continue; // 中文是源语言
bool isSelected = _targetLanguages.Contains(lang);
bool newSelected = EditorGUILayout.Toggle(GetLanguageDisplayName(lang), isSelected);
if (newSelected && !isSelected)
_targetLanguages.Add(lang);
else if (!newSelected && isSelected)
_targetLanguages.Remove(lang);
}
EditorGUILayout.EndVertical();
}
private void DrawExportImport()
{
EditorGUILayout.LabelField("导出/导入", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 导出路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("导出文件夹:", GUILayout.Width(80));
_exportPath = EditorGUILayout.TextField(_exportPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
var path = EditorUtility.OpenFolderPanel("选择导出文件夹", _exportPath, "");
if (!string.IsNullOrEmpty(path))
{
_exportPath = path;
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 按钮
EditorGUILayout.BeginHorizontal();
GUI.enabled = _targetLanguages.Count > 0;
if (GUILayout.Button("导出Excel", GUILayout.Height(30)))
{
ExportToExcel();
}
GUI.enabled = true;
if (GUILayout.Button("导出CSV", GUILayout.Height(30)))
{
ExportToCSV();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 导入
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("导入文件:", GUILayout.Width(80));
_importPath = EditorGUILayout.TextField(_importPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
_importPath = EditorUtility.OpenFilePanel("选择导入文件", "", "xlsx,csv");
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("导入翻译", GUILayout.Height(30)))
{
ImportTranslations();
}
EditorGUILayout.EndVertical();
}
private void DrawMissingTranslations()
{
EditorGUILayout.LabelField($"缺失翻译 ({_missingList.Count})", EditorStyles.boldLabel);
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.Height(200));
foreach (var missing in _missingList.Take(50)) // 只显示前50条
{
EditorGUILayout.BeginHorizontal(GUI.skin.box);
EditorGUILayout.LabelField($"ID:{missing.DialogId}", GUILayout.Width(80));
EditorGUILayout.LabelField(missing.SourceText.Substring(0, Mathf.Min(20, missing.SourceText.Length)), GUILayout.Width(150));
EditorGUILayout.LabelField(GetLanguageDisplayName(missing.TargetLanguage), GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
if (_missingList.Count > 50)
{
EditorGUILayout.LabelField($"... 还有 {_missingList.Count - 50} 条 ...", EditorStyles.centeredGreyMiniLabel);
}
EditorGUILayout.EndScrollView();
}
private void ExportToExcel()
{
if (_database == null || _targetLanguages.Count == 0) return;
var exporter = new DialogLocalizationExporter();
string fileName = $"DialogLocalization_{System.DateTime.Now:yyyyMMdd_HHmmss}.xlsx";
string fullPath = Path.Combine(_exportPath, fileName);
exporter.ExportToExcel(_database, fullPath, _targetLanguages);
EditorUtility.DisplayDialog("导出完成", $"已导出到:\n{fullPath}", "确定");
// 打开文件夹
EditorUtility.RevealInFinder(fullPath);
}
private void ExportToCSV()
{
if (_database == null || _targetLanguages.Count == 0) return;
var exporter = new LocalizationManager();
exporter.BuildFromDialogDatabase(_database);
foreach (var lang in _targetLanguages)
{
string csv = exporter.ExportForTranslation(lang);
string fileName = $"DialogLocalization_{lang}_{System.DateTime.Now:yyyyMMdd}.csv";
string fullPath = Path.Combine(_exportPath, fileName);
File.WriteAllText(fullPath, csv);
}
EditorUtility.DisplayDialog("导出完成", $"已导出 {_targetLanguages.Count} 个语言文件到:\n{_exportPath}", "确定");
}
private void ImportTranslations()
{
if (string.IsNullOrEmpty(_importPath) || !File.Exists(_importPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的导入文件", "确定");
return;
}
var exporter = new DialogLocalizationExporter();
exporter.ImportFromExcel(_importPath, _database);
EditorUtility.DisplayDialog("导入完成", "翻译已导入到数据库", "确定");
RefreshStats();
}
private void RefreshStats()
{
CalculateTranslationStats();
CalculateMissingTranslations();
}
private void CalculateTranslationStats()
{
_translationStats.Clear();
if (_database == null) return;
foreach (var lang in System.Enum.GetValues(typeof(LanguageType)).Cast<LanguageType>())
{
if (lang == LanguageType.Chinese) continue;
int count = 0;
foreach (var config in _database.Configs)
{
foreach (var dialog in config.Dialogs)
{
if (HasTranslation(dialog, lang))
count++;
}
}
_translationStats[lang] = count;
}
}
private void CalculateMissingTranslations()
{
_missingList.Clear();
if (_database == null) return;
var targetLangs = _targetLanguages.Count > 0 ? _targetLanguages :
System.Enum.GetValues(typeof(LanguageType)).Cast<LanguageType>().Where(l => l != LanguageType.Chinese).ToList();
foreach (var config in _database.Configs)
{
foreach (var dialog in config.Dialogs)
{
foreach (var lang in targetLangs)
{
if (!HasTranslation(dialog, lang))
{
_missingList.Add(new MissingTranslationInfo
{
DialogId = dialog.ID,
SheetName = config.SheetName,
SourceText = dialog.Text,
TargetLanguage = lang
});
}
}
}
}
}
private bool HasTranslation(DialogInfoData dialog, LanguageType lang)
{
return lang switch
{
LanguageType.English => !string.IsNullOrEmpty(dialog.Text_EN),
LanguageType.Japanese => !string.IsNullOrEmpty(dialog.Text_JP),
LanguageType.Korean => !string.IsNullOrEmpty(dialog.Text_KR),
LanguageType.French => !string.IsNullOrEmpty(dialog.Text_FR),
LanguageType.German => !string.IsNullOrEmpty(dialog.Text_DE),
LanguageType.Spanish => !string.IsNullOrEmpty(dialog.Text_ES),
_ => false
};
}
private int GetTotalDialogCount()
{
if (_database == null) return 0;
return _database.Configs.Sum(c => c.Dialogs.Count);
}
private string GetLanguageDisplayName(LanguageType lang)
{
return lang switch
{
LanguageType.Chinese => "中文",
LanguageType.English => "English",
LanguageType.Japanese => "日本語",
LanguageType.Korean => "한국어",
LanguageType.French => "Français",
LanguageType.German => "Deutsch",
LanguageType.Spanish => "Español",
_ => lang.ToString()
};
}
}
}

View File

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

View File

@ -0,0 +1,152 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameplayEditor.Config;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 地图点位CSV导入/导出工具
/// </summary>
public static class MapPointCSVImporter
{
/// <summary>
/// 从CSV导入点位
/// </summary>
public static void ImportFromCSV(ActivityMapInfoLut lut, string csvPath)
{
if (lut == null || !File.Exists(csvPath))
{
Debug.LogError("[MapPointCSVImporter] 无效的Lut或CSV路径");
return;
}
Undo.RecordObject(lut, "Import Map Points from CSV");
var lines = File.ReadAllLines(csvPath);
int importedCount = 0;
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue;
var parts = line.Split(',');
if (parts.Length >= 4)
{
var point = new MapPoint
{
PointID = parts[0].Trim(),
Position = new Vector3(
ParseFloat(parts[1]),
ParseFloat(parts[2]),
ParseFloat(parts[3])
)
};
// 检查ID是否已存在
var existing = lut.Points.Find(p => p.PointID == point.PointID);
if (existing != null)
{
existing.Position = point.Position;
}
else
{
lut.Points.Add(point);
}
importedCount++;
}
}
EditorUtility.SetDirty(lut);
Debug.Log($"[MapPointCSVImporter] 导入完成: {importedCount} 个点位");
}
/// <summary>
/// 导出点位到CSV
/// </summary>
public static void ExportToCSV(ActivityMapInfoLut lut, string csvPath)
{
if (lut == null)
{
Debug.LogError("[MapPointCSVImporter] 无效的Lut");
return;
}
var lines = new List<string>();
lines.Add("# PointID,X,Y,Z");
lines.Add($"# MapInfoID: {lut.InfoID}");
lines.Add($"# Export Time: {System.DateTime.Now}");
foreach (var point in lut.Points)
{
lines.Add($"{point.PointID},{point.Position.x:F4},{point.Position.y:F4},{point.Position.z:F4}");
}
File.WriteAllLines(csvPath, lines);
Debug.Log($"[MapPointCSVImporter] 导出完成: {lut.Points.Count} 个点位到 {csvPath}");
}
private static float ParseFloat(string s)
{
float.TryParse(s.Trim(), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var result);
return result;
}
}
/// <summary>
/// 地图点位CSV导入窗口
/// </summary>
public class MapPointCSVImportWindow : EditorWindow
{
private ActivityMapInfoLut _targetLut;
private string _csvPath = "";
private bool _overwriteExisting = false;
[MenuItem("Window/Activity Editor/Map Point CSV Import")]
public static void ShowWindow()
{
GetWindow<MapPointCSVImportWindow>("点位CSV导入");
}
private void OnGUI()
{
EditorGUILayout.LabelField("地图点位CSV导入", EditorStyles.boldLabel);
_targetLut = EditorGUILayout.ObjectField("目标Lut", _targetLut,
typeof(ActivityMapInfoLut), false) as ActivityMapInfoLut;
EditorGUILayout.BeginHorizontal();
_csvPath = EditorGUILayout.TextField("CSV文件", _csvPath);
if (GUILayout.Button("浏览", GUILayout.Width(60)))
{
_csvPath = EditorUtility.OpenFilePanel("选择CSV文件", "", "csv");
}
EditorGUILayout.EndHorizontal();
_overwriteExisting = EditorGUILayout.Toggle("覆盖已存在", _overwriteExisting);
EditorGUILayout.Space();
GUI.enabled = _targetLut != null && !string.IsNullOrEmpty(_csvPath);
if (GUILayout.Button("导入", GUILayout.Height(30)))
{
MapPointCSVImporter.ImportFromCSV(_targetLut, _csvPath);
}
if (GUILayout.Button("导出", GUILayout.Height(30)))
{
var path = EditorUtility.SaveFilePanel("导出CSV", "", $"{_targetLut.name}_Points", "csv");
if (!string.IsNullOrEmpty(path))
{
MapPointCSVImporter.ExportToCSV(_targetLut, path);
}
}
GUI.enabled = true;
}
}
}

View File

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

View File

@ -0,0 +1,670 @@
using System.Collections.Generic;
using System.Linq;
using GameplayEditor.Config;
using UnityEditor;
using UnityEngine;
namespace GameplayEditor.Editor
{
/// <summary>
/// 地图点位编辑器主窗口
/// 用于管理ActivityMapInfoLut配置的点位
/// </summary>
public class MapPointEditorWindow : EditorWindow
{
// 当前编辑的Lut
private ActivityMapInfoLut _currentLut;
// 滚动位置
private Vector2 _scrollPosition;
private Vector2 _listScrollPosition;
// 编辑状态
private int _selectedPointIndex = -1;
private string _searchFilter = "";
private bool _autoFocusSceneView = true;
// 新点位默认设置
// private string _newPointId = ""; // 预留
private Vector3 _newPointPosition = Vector3.zero;
// 批量操作
private bool _showBatchOperations = false;
private float _snapGridSize = 1f;
private bool _enableSnap = false;
[MenuItem("Window/Activity Editor/Map Point Editor")]
public static void ShowWindow()
{
var window = GetWindow<MapPointEditorWindow>("地图点位编辑器");
window.minSize = new Vector2(400, 500);
window.Show();
}
private void OnEnable()
{
// 从SceneEditor同步选中状态
_selectedPointIndex = MapPointSceneEditor.GetSelectedPointIndex();
}
private void OnGUI()
{
EditorGUILayout.LabelField("地图点位编辑器", EditorStyles.boldLabel);
EditorGUILayout.Space(10);
// 选择MapInfoLut
DrawLutSelection();
EditorGUILayout.Space(10);
if (_currentLut == null)
{
EditorGUILayout.HelpBox("请选择一个ActivityMapInfoLut配置开始编辑", MessageType.Info);
return;
}
// 统计信息
DrawStatistics();
EditorGUILayout.Space(10);
// 点位列表和详情(左右布局)
EditorGUILayout.BeginHorizontal();
// 左侧:点位列表
EditorGUILayout.BeginVertical(GUILayout.Width(200));
DrawPointList();
EditorGUILayout.EndVertical();
// 分隔线
GUILayout.Box("", GUILayout.Width(1), GUILayout.ExpandHeight(true));
// 右侧:点位详情
EditorGUILayout.BeginVertical();
DrawPointDetails();
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
// 批量操作
DrawBatchOperations();
EditorGUILayout.Space(10);
// 工具按钮
DrawToolbar();
}
/// <summary>
/// 绘制Lut选择区域
/// </summary>
private void DrawLutSelection()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("MapInfoLut:", GUILayout.Width(80));
var newLut = EditorGUILayout.ObjectField(
_currentLut,
typeof(ActivityMapInfoLut),
false
) as ActivityMapInfoLut;
if (newLut != _currentLut)
{
SetCurrentLut(newLut);
}
// 创建新Lut按钮
if (GUILayout.Button("新建", GUILayout.Width(50)))
{
CreateNewLut();
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 设置当前编辑的Lut
/// </summary>
private void SetCurrentLut(ActivityMapInfoLut lut)
{
_currentLut = lut;
_selectedPointIndex = -1;
// 通知SceneEditor
MapPointSceneEditor.SetEditingLut(lut);
if (lut != null)
{
Debug.Log($"[MapPointEditorWindow] 开始编辑: {lut.name}");
}
}
/// <summary>
/// 创建新的MapInfoLut
/// </summary>
private void CreateNewLut()
{
string path = EditorUtility.SaveFilePanelInProject(
"创建MapInfoLut",
"MapInfoLut",
"asset",
"",
"Assets/Configs"
);
if (!string.IsNullOrEmpty(path))
{
var newLut = ScriptableObject.CreateInstance<ActivityMapInfoLut>();
newLut.InfoID = 1000; // 默认ID
newLut.Points = new List<MapPoint>();
AssetDatabase.CreateAsset(newLut, path);
AssetDatabase.SaveAssets();
SetCurrentLut(newLut);
Debug.Log($"[MapPointEditorWindow] 创建新Lut: {path}");
}
}
/// <summary>
/// 绘制统计信息
/// </summary>
private void DrawStatistics()
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
EditorGUILayout.LabelField($"点位总数: {_currentLut.Points?.Count ?? 0}", GUILayout.Width(100));
EditorGUILayout.LabelField($"InfoID: {_currentLut.InfoID}", GUILayout.Width(100));
// ID编辑
EditorGUI.BeginChangeCheck();
int newId = EditorGUILayout.IntField("ID:", _currentLut.InfoID, GUILayout.Width(100));
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(_currentLut, "Change InfoID");
_currentLut.InfoID = newId;
EditorUtility.SetDirty(_currentLut);
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 绘制点位列表
/// </summary>
private void DrawPointList()
{
EditorGUILayout.LabelField("点位列表", EditorStyles.boldLabel);
// 搜索框
_searchFilter = EditorGUILayout.TextField(
EditorGUIUtility.IconContent("Search Icon"),
_searchFilter
);
EditorGUILayout.Space(5);
// 列表
_listScrollPosition = EditorGUILayout.BeginScrollView(
_listScrollPosition,
GUILayout.ExpandHeight(true)
);
if (_currentLut.Points != null)
{
var filteredPoints = GetFilteredPoints();
for (int i = 0; i < filteredPoints.Count; i++)
{
var point = filteredPoints[i];
int actualIndex = GetActualIndex(point);
EditorGUILayout.BeginHorizontal();
// 选中高亮
GUI.backgroundColor = (actualIndex == _selectedPointIndex)
? Color.yellow
: Color.white;
if (GUILayout.Button($"{point.PointID}", EditorStyles.toolbarButton))
{
SelectPoint(actualIndex);
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
}
EditorGUILayout.EndScrollView();
// 快速操作
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+"))
{
CreateNewPoint();
}
GUI.enabled = (_selectedPointIndex >= 0);
if (GUILayout.Button("-"))
{
DeleteSelectedPoint();
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 获取过滤后的点位列表
/// </summary>
private List<MapPoint> GetFilteredPoints()
{
if (string.IsNullOrEmpty(_searchFilter))
{
return _currentLut.Points;
}
return _currentLut.Points
.Where(p => p.PointID.ToLower().Contains(_searchFilter.ToLower()))
.ToList();
}
/// <summary>
/// 根据点位获取实际索引
/// </summary>
private int GetActualIndex(MapPoint point)
{
return _currentLut.Points.IndexOf(point);
}
/// <summary>
/// 选择点位
/// </summary>
private void SelectPoint(int index)
{
_selectedPointIndex = index;
// 通知SceneEditor
MapPointSceneEditor.SelectPoint(index);
// 聚焦SceneView
if (_autoFocusSceneView && index >= 0 && index < _currentLut.Points.Count)
{
var point = _currentLut.Points[index];
FocusSceneViewOnPoint(point.Position);
}
Repaint();
}
/// <summary>
/// SceneView聚焦到点位
/// </summary>
private void FocusSceneViewOnPoint(Vector3 position)
{
var sceneView = SceneView.lastActiveSceneView;
if (sceneView != null)
{
sceneView.pivot = position;
sceneView.Repaint();
}
}
/// <summary>
/// 绘制点位详情
/// </summary>
private void DrawPointDetails()
{
EditorGUILayout.LabelField("点位详情", EditorStyles.boldLabel);
if (_selectedPointIndex < 0 || _selectedPointIndex >= _currentLut.Points.Count)
{
EditorGUILayout.HelpBox("选择一个点位进行编辑", MessageType.Info);
return;
}
var point = _currentLut.Points[_selectedPointIndex];
EditorGUI.BeginChangeCheck();
// ID编辑
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("点位ID:", GUILayout.Width(60));
string newId = EditorGUILayout.TextField(point.PointID);
if (newId != point.PointID && IsPointIdUnique(newId, _selectedPointIndex))
{
point.PointID = newId;
}
EditorGUILayout.EndHorizontal();
// 位置编辑
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("位置:", EditorStyles.miniBoldLabel);
Vector3 newPosition = EditorGUILayout.Vector3Field("", point.Position);
// 快捷按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("归零"))
{
newPosition = Vector3.zero;
}
if (GUILayout.Button("向上+1"))
{
newPosition.y += 1;
}
if (GUILayout.Button("吸附网格"))
{
newPosition = SnapToGrid(newPosition);
}
EditorGUILayout.EndHorizontal();
// 复制坐标按钮
if (GUILayout.Button("复制坐标到剪贴板"))
{
EditorGUIUtility.systemCopyBuffer = $"{newPosition.x},{newPosition.y},{newPosition.z}";
ShowNotification(new GUIContent("坐标已复制!"), 1f);
}
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(_currentLut, $"Edit Point {point.PointID}");
point.Position = newPosition;
EditorUtility.SetDirty(_currentLut);
// 刷新SceneView
SceneView.RepaintAll();
}
EditorGUILayout.Space(10);
// 删除按钮
if (GUILayout.Button("删除此点位", GUILayout.Height(30)))
{
DeleteSelectedPoint();
}
}
/// <summary>
/// 检查点位ID是否唯一
/// </summary>
private bool IsPointIdUnique(string id, int excludeIndex)
{
for (int i = 0; i < _currentLut.Points.Count; i++)
{
if (i != excludeIndex && _currentLut.Points[i].PointID == id)
{
return false;
}
}
return true;
}
/// <summary>
/// 吸附到网格
/// </summary>
private Vector3 SnapToGrid(Vector3 position)
{
return new Vector3(
Mathf.Round(position.x / _snapGridSize) * _snapGridSize,
Mathf.Round(position.y / _snapGridSize) * _snapGridSize,
Mathf.Round(position.z / _snapGridSize) * _snapGridSize
);
}
/// <summary>
/// 绘制批量操作
/// </summary>
private void DrawBatchOperations()
{
_showBatchOperations = EditorGUILayout.Foldout(_showBatchOperations, "批量操作");
if (!_showBatchOperations)
return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// 网格吸附设置
EditorGUILayout.BeginHorizontal();
_enableSnap = EditorGUILayout.Toggle("启用网格吸附", _enableSnap);
_snapGridSize = EditorGUILayout.FloatField("网格大小", _snapGridSize);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// 批量操作按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全部归零Y"))
{
BatchSetY(0);
}
if (GUILayout.Button("全部吸附网格"))
{
BatchSnapToGrid();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// ID重命名
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("重新编号"))
{
if (EditorUtility.DisplayDialog("确认", "将所有点位重新编号为Point_0, Point_1...?", "确定", "取消"))
{
BatchRenamePoints();
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
/// <summary>
/// 批量设置Y坐标
/// </summary>
private void BatchSetY(float y)
{
Undo.RecordObject(_currentLut, "Batch Set Y");
foreach (var point in _currentLut.Points)
{
var pos = point.Position;
pos.y = y;
point.Position = pos;
}
EditorUtility.SetDirty(_currentLut);
SceneView.RepaintAll();
Debug.Log($"[MapPointEditorWindow] 批量设置Y坐标为: {y}");
}
/// <summary>
/// 批量吸附到网格
/// </summary>
private void BatchSnapToGrid()
{
Undo.RecordObject(_currentLut, "Batch Snap To Grid");
foreach (var point in _currentLut.Points)
{
point.Position = SnapToGrid(point.Position);
}
EditorUtility.SetDirty(_currentLut);
SceneView.RepaintAll();
Debug.Log("[MapPointEditorWindow] 批量吸附到网格");
}
/// <summary>
/// 批量重命名点位
/// </summary>
private void BatchRenamePoints()
{
Undo.RecordObject(_currentLut, "Batch Rename Points");
for (int i = 0; i < _currentLut.Points.Count; i++)
{
_currentLut.Points[i].PointID = $"Point_{i}";
}
EditorUtility.SetDirty(_currentLut);
Repaint();
Debug.Log("[MapPointEditorWindow] 批量重命名完成");
}
/// <summary>
/// 绘制工具栏
/// </summary>
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
// 在SceneView中显示/隐藏
if (GUILayout.Toggle(
MapPointSceneEditor.IsEditing(),
"在SceneView中显示",
EditorStyles.toolbarButton
))
{
if (!MapPointSceneEditor.IsEditing())
{
MapPointSceneEditor.SetEditingLut(_currentLut);
}
}
else
{
if (MapPointSceneEditor.IsEditing())
{
MapPointSceneEditor.StopEditing();
}
}
GUILayout.FlexibleSpace();
// 聚焦按钮
if (GUILayout.Button("聚焦选中", EditorStyles.toolbarButton))
{
if (_selectedPointIndex >= 0 && _selectedPointIndex < _currentLut.Points.Count)
{
FocusSceneViewOnPoint(_currentLut.Points[_selectedPointIndex].Position);
}
}
// 导出按钮
if (GUILayout.Button("导出数据", EditorStyles.toolbarButton))
{
ExportPointData();
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 创建新点位
/// </summary>
private void CreateNewPoint()
{
Vector3 position = Vector3.zero;
// 如果有选中点位,在新位置附近创建
if (_selectedPointIndex >= 0 && _selectedPointIndex < _currentLut.Points.Count)
{
position = _currentLut.Points[_selectedPointIndex].Position + Vector3.right * 2;
}
else
{
// 使用SceneView中心
var sceneView = SceneView.lastActiveSceneView;
if (sceneView != null)
{
position = sceneView.pivot;
}
}
// 通过SceneEditor创建
MapPointSceneEditor.CreatePointAt(position);
Repaint();
}
/// <summary>
/// 删除选中的点位
/// </summary>
private void DeleteSelectedPoint()
{
MapPointSceneEditor.DeleteSelectedPoint();
Repaint();
}
/// <summary>
/// 导出点位数据
/// </summary>
private void ExportPointData()
{
string path = EditorUtility.SaveFilePanel(
"导出点位数据",
"",
$"{_currentLut.name}_Points",
"txt"
);
if (string.IsNullOrEmpty(path))
return;
var sb = new System.Text.StringBuilder();
sb.AppendLine($"# {_currentLut.name} 点位数据");
sb.AppendLine($"# InfoID: {_currentLut.InfoID}");
sb.AppendLine($"# 导出时间: {System.DateTime.Now}");
sb.AppendLine();
foreach (var point in _currentLut.Points)
{
sb.AppendLine($"{point.PointID}\t{point.Position.x:F2}\t{point.Position.y:F2}\t{point.Position.z:F2}");
}
System.IO.File.WriteAllText(path, sb.ToString());
Debug.Log($"[MapPointEditorWindow] 导出到: {path}");
ShowNotification(new GUIContent("导出成功!"), 2f);
}
/// <summary>
/// SceneEditor选中点位时的回调
/// </summary>
public static void OnPointSelected(int index)
{
var window = GetWindow<MapPointEditorWindow>();
window._selectedPointIndex = index;
window.Repaint();
}
private void OnDisable()
{
// 停止SceneView编辑
MapPointSceneEditor.StopEditing();
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More