Merge branch 'main' of http://1.14.122.170:3000/JinYu/ProtypeProject
commit
905b7b37e2
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedGame"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6da43df589d0e9e4486e200a9058c15f
|
||||
guid: c4e90a3b2f66a4843a646f1c775239d1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: eb5849b7cdf4f494ea240a506245ddb7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: efb2368ee8bed9140a42c2805613ed11
|
||||
guid: 4cfe3e64882de254fba06f94153dab56
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 00ebdf5df6fcdac4690f8271175946b9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c3420f187011c1a43824ca0d4c88d6c6
|
||||
guid: c8a2de62fbe079740b13718f921aefa1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a0200246c4705bf4e8857c7cc838f97b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 02c0e07dc8fdfef46a628060d1fffd95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 284c4f6ffb93e9e4f8faa5287e82fce5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 84f836a2bb387f94ca1ee3d55c088233
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 808c51ff53034314d89581dd7d0e7682
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ad328400186ec474d9505f3bb33d8aaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8b5566f322dad2b40b88aedc4eb2cf28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f2fa8e9510aa0cb42a2f9317a57cd344
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e5567297bb87af94ba65bbd1f22e4e7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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段" },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5a86d9dee8c7946448dc913e62bdb508
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 35a481d5fec3d494dbdeabf3f5f331af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9b0d4ae0d9ea94c47ac1dd82536d7126
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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 // 报错
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 34f0a47fe2e1c4d4795b5bc22032d96c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a1a9b6f3bf5e07344b1a1a522f1c9b5a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NodeCanvas.Framework;
|
||||
using ParadoxNotion;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameplayEditor.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 所有活动节点类型的抽象基类
|
||||
/// 每个子类对应一张配置表,有独立的字段结构和导出逻辑
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public abstract class ActivityNodeBase : Node
|
||||
{
|
||||
/// <summary>表格名称,用于路由到对应 CSV 文件</summary>
|
||||
public abstract string TableName { get; }
|
||||
|
||||
/// <summary>字段名(CSV 第1行)</summary>
|
||||
public abstract string[] FieldNames { get; }
|
||||
|
||||
/// <summary>字段类型(CSV 第2行)</summary>
|
||||
public abstract string[] FieldTypes { get; }
|
||||
|
||||
/// <summary>字段说明(CSV 第3行)</summary>
|
||||
public abstract string[] FieldDocs { get; }
|
||||
|
||||
/// <summary>将节点数据序列化为一行 CSV 数据</summary>
|
||||
public abstract Dictionary<string, string> ToExcelRow();
|
||||
|
||||
/// <summary>从 CSV 一行数据反序列化到节点字段</summary>
|
||||
public abstract void FromExcelRow(Dictionary<string, string> data);
|
||||
|
||||
// 所有子类共用的 Node 抽象属性
|
||||
public override int maxInConnections => -1;
|
||||
public override int maxOutConnections => -1;
|
||||
public override Type outConnectionType => typeof(GameplayConnection);
|
||||
public override bool allowAsPrime => true;
|
||||
public override bool canSelfConnect => false;
|
||||
public override Alignment2x2 commentsAlignment => Alignment2x2.Bottom;
|
||||
public override Alignment2x2 iconAlignment => Alignment2x2.Default;
|
||||
|
||||
protected override Status OnExecute(Component agent, IBlackboard blackboard)
|
||||
=> Status.Success;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7d04cbc7dec380c4a80a1cf00bcab601
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 83b34dc6a64ecc647b92d95abe8b7d51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 310af10b7709f194b9c7b59845f5982b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 61da929c7d350454a82337cddf67f1fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: defb45b1a362c7b4da98852b7555089d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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/秒),默认60,Normal模式下有效")]
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9d87f276c171f1c4189eef2d87d9a8f3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 28f26d9d24e8bdc49855477a878fe0fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 48389d3b46d34fa4485ca27fe1656cf3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using NodeCanvas.Framework;
|
||||
|
||||
namespace GameplayEditor.Core
|
||||
{
|
||||
[System.Serializable]
|
||||
public class GameplayConnection : Connection
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b4288e8c48438d9418ecb6578e78f32b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f9cb3ba74e326c2439c1f4cdf13c0123
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 23ad35a149da760419d8fab64e645ef2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f67d4db33ff18b4bb44b95910a59bb6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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} 个无效配置", "确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 15b8374e7900fe544a96e62aacb61b06
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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} 个资产");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c28abfdbaee9ded42b7bbc46ab9adb3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 45bb1bef2f13f0d429cae1c3d863a19d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b1f54c7164d78f74bad06117f903e727
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1404b7bf209474d4b867c3e3a3bb5940
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 97b64b35068ec2f459400e1b3c296769
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: de709a0880fa3a443803d0445660e27a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ad8e2b8480a1bf4dab8d306b0ecb8bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 093a6851003c15e4fbd8eb40b4fd001b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d214a6c2aac5b7e45af9ec01475397c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 21b66a90256007b4682b22dddc58b59b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e76943d880fd46644a37627d1c932074
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0fa84d6286bf4c04b805ddad73d07819
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 91bf777becdb1f24ababe790e850b606
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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("导出失败", "导出过程中发生错误", "确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 04b1cffb2da01174c8ca8dee845b4453
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 348844af3b5945e49902a0183bc77a27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f96b8b8f8860fa48adb209c15f85b5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 417566fde51625c4b828809c61ea63c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5dec57cdc48f2c341935435b804c9b6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue