Files
Colosseum/Assets/_Game/Scripts/Editor/RebuildDrogBehaviorAuthoringGraph.cs
dal4segno 0889bb0f25 feat: 드로그 집행 개시 패턴 및 낙인 디버프 추가
- 드로그 시그니처 패턴 역할과 집행 개시 패턴 데이터를 추가하고 BT 브랜치에 연결
- 시그니처 차단 성공과 실패 흐름을 BossCombatBehaviorContext에 구현하고 authoring 그래프를 재구성
- 집행자의 낙인 이상상태를 추가하고 받는 피해 배율 증가가 플레이어 대미지 계산에 반영되도록 정리
- 집행 실패 시 광역 피해, 넉백, 다운, 낙인 부여 설정을 드로그 프리팹에 연결
- 성공 경로 검증 중 확인된 보스 Hit 트리거 오류를 방어 로직으로 수정
- Unity 플레이 검증으로 집행 개시 실패와 성공 분기를 모두 확인하고 설계값은 원복
2026-03-23 18:14:18 +09:00

315 lines
21 KiB
C#

using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Colosseum.Editor
{
/// <summary>
/// 드로그 Behavior Graph authoring 자산을 현재 BT 우선순위 구조로 재생성합니다.
/// </summary>
public static class RebuildDrogBehaviorAuthoringGraph
{
private const string GraphAssetPath = "Assets/_Game/AI/BT_Drog.asset";
[MenuItem("Tools/Colosseum/Rebuild Drog Behavior Authoring Graph")]
private static void Rebuild()
{
UnityEngine.Object graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath);
if (graphAsset == null)
{
Debug.LogError($"[DrogBTRebuild] 그래프 자산을 찾을 수 없습니다: {GraphAssetPath}");
return;
}
try
{
Type authoringGraphType = graphAsset.GetType();
Assembly authoringAssembly = authoringGraphType.Assembly;
Assembly runtimeAssembly = typeof(Unity.Behavior.BehaviorGraph).Assembly;
MethodInfo createNodeMethod = authoringGraphType.BaseType?.GetMethod("CreateNode", BindingFlags.Instance | BindingFlags.Public);
MethodInfo connectEdgeMethod = authoringGraphType.BaseType?.GetMethod("ConnectEdge", BindingFlags.Instance | BindingFlags.Public);
MethodInfo buildRuntimeGraphMethod = authoringGraphType.GetMethod("BuildRuntimeGraph", BindingFlags.Instance | BindingFlags.Public);
MethodInfo saveAssetMethod = authoringGraphType.BaseType?.GetMethod("SaveAsset", BindingFlags.Instance | BindingFlags.Public);
MethodInfo setAssetDirtyMethod = authoringGraphType.BaseType?.GetMethod("SetAssetDirty", BindingFlags.Instance | BindingFlags.Public);
MethodInfo getNodeInfoMethod = authoringAssembly.GetType("Unity.Behavior.NodeRegistry", true)
?.GetMethod("GetInfo", BindingFlags.Static | BindingFlags.NonPublic);
if (createNodeMethod == null || connectEdgeMethod == null || buildRuntimeGraphMethod == null || saveAssetMethod == null || setAssetDirtyMethod == null || getNodeInfoMethod == null)
{
Debug.LogError("[DrogBTRebuild] Behavior Authoring 리플렉션 메서드를 찾지 못했습니다.");
return;
}
SerializedObject serializedObject = new SerializedObject(graphAsset);
SerializedProperty nodesProperty = serializedObject.FindProperty("m_Nodes");
if (nodesProperty == null)
{
Debug.LogError("[DrogBTRebuild] m_Nodes 프로퍼티를 찾지 못했습니다.");
return;
}
nodesProperty.ClearArray();
serializedObject.ApplyModifiedPropertiesWithoutUndo();
object targetVariable = FindBlackboardVariableModel("Target");
if (targetVariable == null)
{
Debug.LogError("[DrogBTRebuild] Target 블랙보드 변수를 찾지 못했습니다.");
return;
}
object startNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.Start", true), new Vector2(420f, -620f));
object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(420f, -470f));
object selectorNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SelectorComposite", true), new Vector2(420f, -280f));
object signatureSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-1020f, -40f));
object downSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-620f, -40f));
object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-220f, -40f));
object slamSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(180f, -40f));
object mainSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(580f, -40f));
object slamFallbackSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(980f, -40f));
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1380f, -40f));
object signatureRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(-1140f, 240f));
object signatureHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(-1020f, 240f));
object signatureReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSignaturePatternReadyAction), new Vector2(-900f, 240f));
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSignaturePatternAction), new Vector2(-780f, 240f));
object downSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(-740f, 240f));
object downReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPunishPatternReadyAction), new Vector2(-620f, 240f));
object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePunishPatternAction), new Vector2(-500f, 240f));
object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(-340f, 240f));
object leapReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckMobilityPatternReadyAction), new Vector2(-220f, 240f));
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseMobilityPatternAction), new Vector2(-100f, 240f));
object slamRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(0f, 240f));
object slamHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(120f, 240f));
object slamRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(240f, 240f));
object slamTurnNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternTurnAction), new Vector2(360f, 240f));
object slamReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(480f, 240f));
object slamUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(600f, 240f));
object mainRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(520f, 240f));
object mainHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(640f, 240f));
object mainRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(760f, 240f));
object mainReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPrimaryPatternReadyAction), new Vector2(880f, 240f));
object mainUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePrimaryPatternAction), new Vector2(1000f, 240f));
object fallbackRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(920f, 240f));
object fallbackHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1040f, 240f));
object fallbackRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(1160f, 240f));
object fallbackReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(1280f, 240f));
object fallbackUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(1400f, 240f));
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(1320f, 240f));
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1440f, 240f));
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(1560f, 240f));
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(selectorNode));
ConnectChildren(graphAsset, connectEdgeMethod, selectorNode, signatureSequence, downSequence, leapSequence, slamSequence, mainSequence, slamFallbackSequence, chaseSequence);
ConnectChildren(graphAsset, connectEdgeMethod, signatureSequence, signatureRefreshNode, signatureHasTargetNode, signatureReadyNode, signatureUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downReadyNode, downUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapReadyNode, leapUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, slamSequence, slamRefreshNode, slamHasTargetNode, slamRangeNode, slamTurnNode, slamReadyNode, slamUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, mainSequence, mainRefreshNode, mainHasTargetNode, mainRangeNode, mainReadyNode, mainUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, slamFallbackSequence, fallbackRefreshNode, fallbackHasTargetNode, fallbackRangeNode, fallbackReadyNode, fallbackUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
LinkTarget(signatureRefreshNode, targetVariable);
LinkTarget(signatureHasTargetNode, targetVariable);
LinkTarget(signatureUseNode, targetVariable);
LinkTarget(downSelectNode, targetVariable);
LinkTarget(downUseNode, targetVariable);
LinkTarget(leapSelectNode, targetVariable);
LinkTarget(leapUseNode, targetVariable);
LinkTarget(slamRefreshNode, targetVariable);
LinkTarget(slamHasTargetNode, targetVariable);
LinkTarget(slamRangeNode, targetVariable);
LinkTarget(slamUseNode, targetVariable);
LinkTarget(mainRefreshNode, targetVariable);
LinkTarget(mainHasTargetNode, targetVariable);
LinkTarget(mainRangeNode, targetVariable);
LinkTarget(mainUseNode, targetVariable);
LinkTarget(fallbackRefreshNode, targetVariable);
LinkTarget(fallbackHasTargetNode, targetVariable);
LinkTarget(fallbackRangeNode, targetVariable);
LinkTarget(fallbackUseNode, targetVariable);
LinkTarget(chaseRefreshNode, targetVariable);
LinkTarget(chaseHasTargetNode, targetVariable);
LinkTarget(chaseUseNode, targetVariable);
SetStartRepeatFlags(startNode, repeat: true, allowMultipleRepeatsPerTick: false);
setAssetDirtyMethod.Invoke(graphAsset, new object[] { true });
buildRuntimeGraphMethod.Invoke(graphAsset, new object[] { true });
saveAssetMethod.Invoke(graphAsset, null);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[DrogBTRebuild] 드로그 Behavior Graph authoring 자산 재구성이 완료되었습니다.");
}
catch (Exception exception)
{
Debug.LogException(exception);
}
}
private static object CreateNode(UnityEngine.Object graphAsset, MethodInfo createNodeMethod, MethodInfo getNodeInfoMethod, Type runtimeType, Vector2 position)
{
if (runtimeType == null)
throw new InvalidOperationException("[DrogBTRebuild] 런타임 타입이 null입니다.");
object nodeInfo = getNodeInfoMethod.Invoke(null, new object[] { runtimeType });
if (nodeInfo == null)
throw new InvalidOperationException($"[DrogBTRebuild] NodeInfo를 찾지 못했습니다: {runtimeType.FullName}");
Type nodeInfoType = nodeInfo.GetType();
FieldInfo modelTypeField = nodeInfoType.GetField("ModelType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
object serializableType = modelTypeField?.GetValue(nodeInfo);
PropertyInfo serializableTypeValueProperty = serializableType?.GetType().GetProperty("Type", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Type modelType = serializableTypeValueProperty?.GetValue(serializableType) as Type;
if (modelType == null)
throw new InvalidOperationException($"[DrogBTRebuild] ModelType을 찾지 못했습니다: {runtimeType.FullName}");
return createNodeMethod.Invoke(graphAsset, new object[] { modelType, position, null, new object[] { nodeInfo } });
}
private static void ConnectChildren(UnityEngine.Object graphAsset, MethodInfo connectEdgeMethod, object parentNode, params object[] children)
{
object outputPort = GetDefaultOutputPort(parentNode);
for (int i = 0; i < children.Length; i++)
{
Connect(graphAsset, connectEdgeMethod, outputPort, GetDefaultInputPort(children[i]));
}
}
private static void Connect(UnityEngine.Object graphAsset, MethodInfo connectEdgeMethod, object outputPort, object inputPort)
{
if (outputPort == null || inputPort == null)
throw new InvalidOperationException("[DrogBTRebuild] 포트 연결 대상이 null입니다.");
connectEdgeMethod.Invoke(graphAsset, new[] { outputPort, inputPort });
}
private static object GetDefaultInputPort(object node)
{
return GetDefaultPort(node, "TryDefaultInputPortModel");
}
private static object GetDefaultOutputPort(object node)
{
return GetDefaultPort(node, "TryDefaultOutputPortModel");
}
private static object GetDefaultPort(object node, string methodName)
{
MethodInfo method = node.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public);
object[] parameters = { null };
bool success = method != null && (bool)method.Invoke(node, parameters);
return success ? parameters[0] : null;
}
private static object FindBlackboardVariableModel(string variableName)
{
UnityEngine.Object blackboardAsset = AssetDatabase.LoadAllAssetsAtPath(GraphAssetPath)
.FirstOrDefault(asset => asset != null && asset.GetType().Name.Contains("BehaviorBlackboardAuthoringAsset", StringComparison.Ordinal));
if (blackboardAsset == null)
return null;
PropertyInfo variablesProperty = blackboardAsset.GetType().GetProperty("Variables", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo variablesField = blackboardAsset.GetType().GetField("m_Variables", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
IEnumerable variables = variablesProperty?.GetValue(blackboardAsset) as IEnumerable ?? variablesField?.GetValue(blackboardAsset) as IEnumerable;
if (variables == null)
return null;
foreach (object variable in variables)
{
if (variable == null)
continue;
PropertyInfo nameProperty = variable.GetType().GetProperty("Name", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo nameField = variable.GetType().GetField("Name", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
string name = nameProperty?.GetValue(variable) as string ?? nameField?.GetValue(variable) as string;
if (name == variableName)
return variable;
}
return null;
}
private static void LinkTarget(object node, object targetVariable)
{
LinkFieldToVariable(node, "Target", typeof(GameObject), targetVariable);
}
private static void LinkFieldToVariable(object node, string fieldName, Type fieldType, object variableModel)
{
MethodInfo getVariableLinkMethod = node.GetType().GetMethod("GetVariableLink", BindingFlags.Instance | BindingFlags.Public);
object variableLink = getVariableLinkMethod?.Invoke(node, new object[] { fieldName, fieldType });
if (variableLink != null)
{
PropertyInfo blackboardVariableProperty = variableLink.GetType().GetProperty("BlackboardVariable", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
blackboardVariableProperty?.SetValue(variableLink, variableModel);
}
object fieldModel = FindFieldModel(node, fieldName);
if (fieldModel == null)
return;
PropertyInfo linkedVariableProperty = fieldModel.GetType().GetProperty("LinkedVariable", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo linkedVariableField = fieldModel.GetType().GetField("LinkedVariable", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (linkedVariableProperty != null && linkedVariableProperty.CanWrite)
{
linkedVariableProperty.SetValue(fieldModel, variableModel);
}
else
{
linkedVariableField?.SetValue(fieldModel, variableModel);
}
}
private static object FindFieldModel(object node, string fieldName)
{
FieldInfo fieldValuesField = node.GetType().GetField("m_FieldValues", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
PropertyInfo fieldValuesProperty = node.GetType().GetProperty("FieldValues", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
IEnumerable fieldValues = fieldValuesField?.GetValue(node) as IEnumerable ?? fieldValuesProperty?.GetValue(node) as IEnumerable;
if (fieldValues == null)
return null;
foreach (object fieldModel in fieldValues)
{
if (fieldModel == null)
continue;
PropertyInfo fieldNameProperty = fieldModel.GetType().GetProperty("FieldName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo fieldNameField = fieldModel.GetType().GetField("FieldName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
string currentFieldName = fieldNameProperty?.GetValue(fieldModel) as string ?? fieldNameField?.GetValue(fieldModel) as string;
if (currentFieldName == fieldName)
return fieldModel;
}
return null;
}
private static void SetStartRepeatFlags(object startNode, bool repeat, bool allowMultipleRepeatsPerTick)
{
Type startNodeType = startNode.GetType();
FieldInfo repeatField = startNodeType.GetField("Repeat", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo allowField = startNodeType.GetField("AllowMultipleRepeatsPerTick", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
repeatField?.SetValue(startNode, repeat);
allowField?.SetValue(startNode, allowMultipleRepeatsPerTick);
}
}
}