refactor: 드로그 BT 의사결정 투명화 — 모든 조건을 BT 노드로 표시

- BossCombatPatternRole enum 완전 제거, BossPatternData에 직접 필드 추가
- 14개 패턴별 Check*/Use*Action → CheckPatternReadyCondition + UsePatternByRoleAction으로 통합
- BT 계단식 Branch 체인 구조 도입 (BranchingConditionComposite + FloatingPort)
- 패턴별 고유 전제 조건을 BT Condition으로 분리
  - Punish: IsDownedTargetInRangeCondition (다운 대상 반경)
  - Mobility: IsTargetBeyondDistanceCondition (원거리 대상)
  - Utility: IsTargetBeyondDistanceCondition (원거리 대상)
  - Primary: IsTargetInAttackRangeCondition (사거리 이내)
- Phase 진입 조건을 BT에서 확인 가능하도록 IsMinPhaseSatisfiedCondition 추가
- IsPatternReady()에서 minPhase 체크 분리 → 전용 Condition으로 노출
- Secondary 패턴 개념 제거 (secondaryPattern, 보조 차례, 교대 카운터 로직 전부 삭제)
- CanResolvePatternTargetCondition 삭제 (7개 중 5개가 노이즈)
- RebuildDrogBehaviorAuthoringGraph로 BT 에셋 자동 재구성 메뉴 제공
This commit is contained in:
2026-03-30 15:34:21 +09:00
parent dea7fd39ec
commit c6fc56e9c6
56 changed files with 3287 additions and 3541 deletions

View File

@@ -1,8 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Colosseum.AI;
using Colosseum.AI.BehaviorActions.Conditions;
using Colosseum.Enemy;
using UnityEditor;
using UnityEngine;
@@ -10,6 +15,7 @@ namespace Colosseum.Editor
{
/// <summary>
/// 드로그 Behavior Graph authoring 자산을 현재 BT 우선순위 구조로 재생성합니다.
/// Check 노드는 ConditionalGuardAction + Condition 조합으로 구현됩니다.
/// </summary>
public static class RebuildDrogBehaviorAuthoringGraph
{
@@ -21,8 +27,32 @@ namespace Colosseum.Editor
UnityEngine.Object graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath);
if (graphAsset == null)
{
Debug.LogError($"[DrogBTRebuild] 그래프 자산을 찾을 수 없습니다: {GraphAssetPath}");
return;
// 에셋이 없으면 기존 에셋 경로의 타입을 리플렉션으로 찾아 생성합니다.
// BehaviorAuthoringGraph는 Unity.Behavior.Editor 어셈블리에 있습니다.
Type authoringGraphType = null;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
authoringGraphType = assembly.GetType("Unity.Behavior.Authoring.BehaviorAuthoringGraph");
if (authoringGraphType != null)
break;
}
if (authoringGraphType == null)
{
Debug.LogError("[DrogBTRebuild] BehaviorAuthoringGraph 타입을 모든 어셈블리에서 찾지 못했습니다.");
return;
}
graphAsset = ScriptableObject.CreateInstance(authoringGraphType);
if (graphAsset == null)
{
Debug.LogError("[DrogBTRebuild] BehaviorAuthoringGraph 인스턴스를 생성할 수 없습니다.");
return;
}
AssetDatabase.CreateAsset(graphAsset, GraphAssetPath);
AssetDatabase.SaveAssets();
Debug.Log("[DrogBTRebuild] 새 그래프 자산을 생성했습니다.");
}
try
@@ -31,8 +61,10 @@ namespace Colosseum.Editor
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 createNodePortsMethod = authoringGraphType.BaseType?.GetMethod("CreateNodePortsForNode", BindingFlags.Instance | BindingFlags.NonPublic | 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);
@@ -45,16 +77,100 @@ namespace Colosseum.Editor
return;
}
SerializedObject serializedObject = new SerializedObject(graphAsset);
SerializedProperty nodesProperty = serializedObject.FindProperty("m_Nodes");
if (nodesProperty == null)
// ConditionalGuard 리플렉션 타입 (internal)
Type conditionalGuardType = runtimeAssembly.GetType("Unity.Behavior.ConditionalGuardAction");
Type conditionUtilityType = authoringAssembly.GetType("Unity.Behavior.ConditionUtility");
Type conditionModelType = authoringAssembly.GetType("Unity.Behavior.ConditionModel");
Type graphNodeModelType = authoringAssembly.GetType("Unity.Behavior.BehaviorGraphNodeModel");
Type conditionInfoType = authoringAssembly.GetType("Unity.Behavior.ConditionInfo");
if (conditionalGuardType == null) { Debug.LogError("[DrogBTRebuild] ConditionalGuardAction 타입을 찾지 못했습니다."); return; }
if (conditionUtilityType == null) { Debug.LogError("[DrogBTRebuild] ConditionUtility 타입을 찾지 못했습니다."); return; }
if (conditionModelType == null) { Debug.LogError("[DrogBTRebuild] ConditionModel 타입을 찾지 못했습니다."); return; }
if (graphNodeModelType == null) { Debug.LogError("[DrogBTRebuild] BehaviorGraphNodeModel 타입을 찾지 못했습니다."); return; }
if (conditionInfoType == null)
{
Debug.LogError("[DrogBTRebuild] m_Nodes 프로퍼티를 찾지 못했습니다.");
Debug.LogError("[DrogBTRebuild] ConditionInfo 타입을 찾지 못했습니다.");
return;
}
nodesProperty.ClearArray();
serializedObject.ApplyModifiedPropertiesWithoutUndo();
Type branchCompositeType = runtimeAssembly.GetType("Unity.Behavior.BranchingConditionComposite");
if (branchCompositeType == null)
{
Debug.LogError("[DrogBTRebuild] BranchingConditionComposite 타입을 찾지 못했습니다.");
return;
}
// SetField(string, VariableModel, Type) — 제네릭 버전과 구분하기 위해 파라미터 수로 필터링
MethodInfo setFieldMethod = conditionModelType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.FirstOrDefault(m => m.Name == "SetField" && !m.IsGenericMethod && m.GetParameters().Length == 3);
// SetField<T>(string, T) — BehaviorGraphNodeModel 기반 클래스에서 조회 (ConditionModel과 Action 노드 모두 사용)
MethodInfo setFieldValueMethod = graphNodeModelType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
if (setFieldMethod == null)
{
Debug.LogError("[DrogBTRebuild] ConditionModel.SetField 메서드를 찾지 못했습니다.");
return;
}
if (setFieldValueMethod == null)
{
Debug.LogError("[DrogBTRebuild] SetField<T> 제네릭 메서드를 찾지 못했습니다.");
return;
}
// 기존 에셋의 서브에셋(BehaviorGraph 등)에서 깨진 managed references 클리어
Type behaviorGraphType = typeof(Unity.Behavior.BehaviorGraph);
UnityEngine.Object[] subAssets = AssetDatabase.LoadAllAssetsAtPath(GraphAssetPath);
foreach (var subAsset in subAssets)
{
if (subAsset != null && subAsset.GetType() == behaviorGraphType)
{
UnityEditor.SerializationUtility.ClearAllManagedReferencesWithMissingTypes(subAsset);
EditorUtility.SetDirty(subAsset);
}
}
// AuthoringGraph 자체에서도 깨진 references 클리어
UnityEditor.SerializationUtility.ClearAllManagedReferencesWithMissingTypes(graphAsset);
// 노드 클리어 — 전체 타입 계층에서 필드 찾기
FieldInfo nodesField = FindFieldInHierarchy(authoringGraphType, "m_RootNodes");
if (nodesField == null)
{
Debug.LogError("[DrogBTRebuild] m_RootNodes 필드를 타입 계층 전체에서 찾지 못했습니다.");
return;
}
nodesField.SetValue(graphAsset, Activator.CreateInstance(nodesField.FieldType));
FieldInfo nodesListField = FindFieldInHierarchy(authoringGraphType, "m_Nodes");
if (nodesListField != null)
nodesListField.SetValue(graphAsset, Activator.CreateInstance(nodesListField.FieldType));
FieldInfo nodeModelsInfoField = FindFieldInHierarchy(authoringGraphType, "m_NodeModelsInfo");
if (nodeModelsInfoField != null)
nodeModelsInfoField.SetValue(graphAsset, Activator.CreateInstance(nodeModelsInfoField.FieldType));
FieldInfo runtimeGraphField = FindFieldInHierarchy(authoringGraphType, "m_RuntimeGraph");
if (runtimeGraphField != null)
runtimeGraphField.SetValue(graphAsset, null);
// 클리어 후 에셋을 저장하고 다시 로드하여 잔류 참조가 메모리에 남지 않게 합니다.
EditorUtility.SetDirty(graphAsset);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 에셋을 다시 로드 (직렬화된 상태에서 로드하여 클리어 상태 확보)
graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath);
if (graphAsset == null)
{
Debug.LogError("[DrogBTRebuild] 에셋 재로드 실패.");
return;
}
authoringGraphType = graphAsset.GetType();
object targetVariable = FindBlackboardVariableModel("Target");
if (targetVariable == null)
@@ -63,103 +179,205 @@ namespace Colosseum.Editor
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 startNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.Start", true), new Vector2(420f, -800f));
object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(420f, -620f));
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(-780f, -40f));
object utilitySequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(-380f, -40f));
object leapSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(20f, -40f));
object slamSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(420f, -40f));
object mainSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(820f, -40f));
object slamFallbackSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1220f, -40f));
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(1620f, -40f));
// ── 프리팹에서 패턴 에셋 로드 ──
const string prefabPath = "Assets/_Game/Prefabs/Bosses/Prefab_Boss_Drog.prefab";
GameObject prefab = AssetDatabase.LoadMainAssetAtPath(prefabPath) as GameObject;
BossCombatBehaviorContext context = prefab?.GetComponent<BossCombatBehaviorContext>();
if (context == null)
{
Debug.LogError("[DrogBTRebuild] 드로그 프리팹에서 BossCombatBehaviorContext를 찾지 못했습니다.");
return;
}
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));
// protected 필드에서 BossPatternData 에셋 읽기 (리플렉션)
BossPatternData punishPattern = ReadProtectedField<BossPatternData>(context, "punishPattern");
BossPatternData signaturePattern = ReadProtectedField<BossPatternData>(context, "signaturePattern");
BossPatternData mobilityPattern = ReadProtectedField<BossPatternData>(context, "mobilityPattern");
BossPatternData comboPattern = ReadProtectedField<BossPatternData>(context, "comboPattern");
BossPatternData primaryPattern = ReadProtectedField<BossPatternData>(context, "primaryPattern");
BossPatternData utilityPattern = ReadProtectedField<BossPatternData>(context, "utilityPattern");
float punishSearchRadius = ReadProtectedFieldValue<float>(context, "punishSearchRadius", 6f);
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));
// 필수 패턴 검증 (combo는 선택 — 할당되지 않은 경우 해당 Branch만 생략)
if (punishPattern == null || signaturePattern == null || mobilityPattern == null ||
primaryPattern == null || utilityPattern == null)
{
Debug.LogError("[DrogBTRebuild] 프리팹에서 필수 패턴 에셋을 읽지 못했습니다.");
return;
}
object utilitySelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectAlternateTargetByDistanceAction), new Vector2(-500f, 240f));
object utilityReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckUtilityPatternReadyAction), new Vector2(-380f, 240f));
object utilityUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseUtilityPatternAction), new Vector2(-260f, 240f));
if (comboPattern == null)
Debug.LogWarning("[DrogBTRebuild] comboPattern이 할당되지 않았습니다. 해당 Branch를 생략합니다.");
object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(-100f, 240f));
object leapReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckMobilityPatternReadyAction), new Vector2(20f, 240f));
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseMobilityPatternAction), new Vector2(140f, 240f));
// ── 계단식 우선순위 체인 ──
// 설계안 우선순위: 다운 추가타 > 도약 > 집행 개시 > 기본 루프 > 조합 > 유틸리티
// 각 Branch: CheckPatternReady → true → UsePatternByRole
// false → 다음 우선순위 Branch 시도
// 마지막까지 모든 조건이 false이면 Chase (fallback)
//
// 연결 흐름: Branch.True → FloatingPort(True).InputPort → FloatingPort(True).OutputPort → Action.InputPort
// CreateNodePortsForNode를 호출하여 FloatingPortNodeModel을 자동 생성해야 합니다.
//
// 레이아웃 패턴 (사용자 조정 기준):
// Branch: (-800, y)
// True Floating: (-597, y + 110)
// False Floating: (-1011, y + 114)
// Action: (-598, y + 199)
object slamRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(240f, 240f));
object slamHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(360f, 240f));
object slamRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(480f, 240f));
object slamTurnNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternTurnAction), new Vector2(600f, 240f));
object slamReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(720f, 240f));
object slamUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(840f, 240f));
const float branchX = -800f;
const float truePortOffsetX = 203f;
const float truePortOffsetY = 110f;
const float falsePortOffsetX = -211f;
const float falsePortOffsetY = 114f;
const float actionOffsetX = 202f;
const float actionOffsetY = 199f;
const float startY = -800f;
const float stepY = 220f;
object mainRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(760f, 240f));
object mainHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(880f, 240f));
object mainRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(1000f, 240f));
object mainReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckPrimaryPatternReadyAction), new Vector2(1120f, 240f));
object mainUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePrimaryPatternAction), new Vector2(1240f, 240f));
// #1 Punish — 다운 추가타 (전제 조건: 다운된 대상이 반경 이내에 있어야 함)
object downBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY));
AttachPatternReadyCondition(downBranch, punishPattern, authoringAssembly);
AttachConditionWithValue(downBranch, typeof(IsDownedTargetInRangeCondition), "searchRadius", punishSearchRadius, authoringAssembly);
AttachPhaseConditionIfNeeded(downBranch, punishPattern, authoringAssembly);
SetBranchRequiresAll(downBranch, true);
object downUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + actionOffsetY));
SetNodeFieldValue(downUseNode, "Pattern", punishPattern, setFieldValueMethod);
LinkTarget(downUseNode, targetVariable);
object fallbackRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(1160f, 240f));
object fallbackHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1280f, 240f));
object fallbackRangeNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckTargetInAttackRangeAction), new Vector2(1400f, 240f));
object fallbackReadyNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(CheckSecondaryPatternReadyAction), new Vector2(1520f, 240f));
object fallbackUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSecondaryPatternAction), new Vector2(1640f, 240f));
// #2 Mobility — 도약 (전제 조건: 지나치게 먼 대상이 존재해야 함)
float mobilityTriggerDistance = ReadProtectedFieldValue<float>(context, "mobilityTriggerDistance", 8f);
object leapBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY));
AttachPatternReadyCondition(leapBranch, mobilityPattern, authoringAssembly);
AttachConditionWithValue(leapBranch, typeof(IsTargetBeyondDistanceCondition), "minDistance", mobilityTriggerDistance, authoringAssembly);
AttachPhaseConditionIfNeeded(leapBranch, mobilityPattern, authoringAssembly);
SetBranchRequiresAll(leapBranch, true);
object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY + actionOffsetY));
SetNodeFieldValue(leapUseNode, "Pattern", mobilityPattern, setFieldValueMethod);
LinkTarget(leapUseNode, targetVariable);
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(1560f, 240f));
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(1680f, 240f));
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(1800f, 240f));
// #3 Signature — 집행 개시
object signatureBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 2));
AttachPatternReadyCondition(signatureBranch, signaturePattern, authoringAssembly);
AttachPhaseConditionIfNeeded(signatureBranch, signaturePattern, authoringAssembly);
object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 2 + actionOffsetY));
SetNodeFieldValue(signatureUseNode, "Pattern", signaturePattern, setFieldValueMethod);
LinkTarget(signatureUseNode, targetVariable);
// #4 Combo — 콤보 패턴 (드문 조합, 선택적)
object comboBranch = null;
object comboUseNode = null;
if (comboPattern != null)
{
comboBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 3));
AttachPatternReadyCondition(comboBranch, comboPattern, authoringAssembly);
AttachPhaseConditionIfNeeded(comboBranch, comboPattern, authoringAssembly);
comboUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 3 + actionOffsetY));
SetNodeFieldValue(comboUseNode, "Pattern", comboPattern, setFieldValueMethod);
LinkTarget(comboUseNode, targetVariable);
}
// #5 Primary — 사거리 + 기본 패턴 준비 (모두 충족)
object primaryBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 4));
object primaryRangeCondModel = AttachCondition(primaryBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly);
if (primaryRangeCondModel != null) setFieldMethod.Invoke(primaryRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) });
AttachPatternReadyCondition(primaryBranch, primaryPattern, authoringAssembly);
AttachPhaseConditionIfNeeded(primaryBranch, primaryPattern, authoringAssembly);
SetBranchRequiresAll(primaryBranch, true);
object primaryUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 4 + actionOffsetY));
SetNodeFieldValue(primaryUseNode, "Pattern", primaryPattern, setFieldValueMethod);
LinkTarget(primaryUseNode, targetVariable);
// #6 Utility — 유틸리티 (전제 조건: 원거리 대상이 존재해야 함)
float utilityTriggerDistance = ReadProtectedFieldValue<float>(context, "utilityTriggerDistance", 5f);
object utilityBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 5));
AttachPatternReadyCondition(utilityBranch, utilityPattern, authoringAssembly);
AttachConditionWithValue(utilityBranch, typeof(IsTargetBeyondDistanceCondition), "minDistance", utilityTriggerDistance, authoringAssembly);
AttachPhaseConditionIfNeeded(utilityBranch, utilityPattern, authoringAssembly);
SetBranchRequiresAll(utilityBranch, true);
object utilityUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(branchX + actionOffsetX, startY + stepY * 5 + actionOffsetY));
SetNodeFieldValue(utilityUseNode, "Pattern", utilityPattern, setFieldValueMethod);
LinkTarget(utilityUseNode, targetVariable);
// #7 Chase — fallback (Branch 아님, Sequence 사용)
object chaseSequence = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(branchX, startY + stepY * 6));
object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 6 + 80f));
object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(branchX + 320f, startY + stepY * 6 + 80f));
object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(branchX + 480f, startY + stepY * 6 + 80f));
// ── FloatingPortNodeModel 생성 + 위치 보정 ──
// Branch 노드의 NamedPort(True/False)에 대해 FloatingPortNodeModel을 생성합니다.
// CreateNodePortsForNode는 기본 위치(Branch + 200px Y)를 사용하므로, 생성 후 사용자 조정 기준 위치로 이동합니다.
var allBranches = new List<object> { downBranch, leapBranch, signatureBranch };
if (comboBranch != null) allBranches.Add(comboBranch);
allBranches.AddRange(new[] { primaryBranch, utilityBranch });
foreach (object branch in allBranches)
{
createNodePortsMethod?.Invoke(graphAsset, new object[] { branch });
}
// FloatingPortNodeModel 위치를 사용자 조정 기준으로 보정
foreach (object branch in allBranches)
{
// Branch의 현재 위치 읽기 (Position은 public 필드)
FieldInfo posField = branch.GetType().GetField("Position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (posField == null) continue;
Vector2 branchPos = (Vector2)posField.GetValue(branch);
// FloatingPortNodeModel에서 PortName이 "True"/"False"인 것을 찾아 위치 수정
SetFloatingPortPosition(graphAsset, branch, "True", branchPos.x + truePortOffsetX, branchPos.y + truePortOffsetY);
SetFloatingPortPosition(graphAsset, branch, "False", branchPos.x + falsePortOffsetX, branchPos.y + falsePortOffsetY);
}
// ── 연결 ──
// Start → Repeater → 첫 번째 Branch
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode));
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(selectorNode));
ConnectChildren(graphAsset, connectEdgeMethod, selectorNode, signatureSequence, downSequence, utilitySequence, leapSequence, slamSequence, mainSequence, slamFallbackSequence, chaseSequence);
Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(downBranch));
ConnectChildren(graphAsset, connectEdgeMethod, signatureSequence, signatureRefreshNode, signatureHasTargetNode, signatureReadyNode, signatureUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, downSequence, downSelectNode, downReadyNode, downUseNode);
ConnectChildren(graphAsset, connectEdgeMethod, utilitySequence, utilitySelectNode, utilityReadyNode, utilityUseNode);
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);
// 각 Branch의 True FloatingPort → Action
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "True", downUseNode);
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "True", leapUseNode);
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "True", signatureUseNode);
if (comboBranch != null)
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "True", comboUseNode);
ConnectBranch(graphAsset, connectEdgeMethod, primaryBranch, "True", primaryUseNode);
ConnectBranch(graphAsset, connectEdgeMethod, utilityBranch, "True", utilityUseNode);
// 각 Branch의 False FloatingPort → 다음 우선순위 (계단식 체인)
// combo 유무에 따라 연결 경로가 달라짐
object afterSignature = comboBranch ?? primaryBranch;
ConnectBranch(graphAsset, connectEdgeMethod, downBranch, "False", leapBranch);
ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "False", signatureBranch);
ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "False", afterSignature);
if (comboBranch != null)
ConnectBranch(graphAsset, connectEdgeMethod, comboBranch, "False", primaryBranch);
ConnectBranch(graphAsset, connectEdgeMethod, primaryBranch, "False", utilityBranch);
ConnectBranch(graphAsset, connectEdgeMethod, utilityBranch, "False", chaseSequence);
// Chase Sequence 자식 연결
ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode);
LinkTarget(signatureRefreshNode, targetVariable);
LinkTarget(signatureHasTargetNode, targetVariable);
LinkTarget(signatureUseNode, targetVariable);
LinkTarget(downSelectNode, targetVariable);
LinkTarget(downUseNode, targetVariable);
LinkTarget(utilitySelectNode, targetVariable);
LinkTarget(utilityUseNode, 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);
// Chase 노드 블랙보드 변수 연결
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 });
AssetDatabase.SaveAssets();
// BuildRuntimeGraph는 에셋이 직렬화된 후 AssetDatabase.ImportAsset으로 재임포트하여
// OnValidate/AssetPostprocessor에서 자동 빌드되게 합니다.
string assetPath = AssetDatabase.GetAssetPath(graphAsset);
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
saveAssetMethod.Invoke(graphAsset, null);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[DrogBTRebuild] 드로그 Behavior Graph authoring 자산 재구성이 완료되었습니다.");
}
@@ -169,6 +387,209 @@ namespace Colosseum.Editor
}
}
/// <summary>
/// ConditionalGuardAction 노드를 생성하고 지정된 Condition을 부착합니다.
/// </summary>
private static object CreateConditionalGuard(
UnityEngine.Object graphAsset,
MethodInfo createNodeMethod,
MethodInfo getNodeInfoMethod,
Type conditionalGuardType,
Type conditionType,
Vector2 position,
Assembly authoringAssembly)
{
object guardNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, conditionalGuardType, position);
AttachCondition(guardNode, conditionType, authoringAssembly);
return guardNode;
}
/// <summary>
/// ConditionalGuardAction 노드를 생성하고, 블랙보드 변수 참조가 있는 Condition을 부착합니다.
/// </summary>
private static object CreateConditionalGuardWithField(
UnityEngine.Object graphAsset,
MethodInfo createNodeMethod,
MethodInfo getNodeInfoMethod,
Type conditionalGuardType,
MethodInfo setFieldMethod,
Type conditionType,
object targetVariable,
Vector2 position,
Assembly authoringAssembly)
{
object guardNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, conditionalGuardType, position);
object conditionModel = AttachCondition(guardNode, conditionType, authoringAssembly);
// Condition의 Target 필드를 블랙보드 변수에 연결
// SetField는 GetOrCreateField를 호출하여 m_FieldValues에 FieldModel을 생성합니다.
if (conditionModel != null && targetVariable != null)
{
try
{
setFieldMethod.Invoke(conditionModel, new object[] { "Target", targetVariable, typeof(GameObject) });
}
catch (Exception ex)
{
Debug.LogError($"[DrogBTRebuild] SetField 'Target' 실패 for {conditionType.Name}: {ex.GetType().Name}: {ex.Message}");
}
}
return guardNode;
}
/// <summary>
/// 노드에 Condition을 부착합니다.
/// ConditionUtility.GetInfoForConditionType를 사용하여 NodeRegistry와 완벽히 동일한
/// ConditionInfo를 획득합니다. 이렇게 하면 TypeID가 레지스트리와 일치하여
/// EnsureFieldValuesAreUpToDate가 정상 동작하고 UpdateConditionModels가
/// ConditionModel을 삭제하지 않습니다.
/// </summary>
private static object AttachCondition(object guardNode, Type conditionType, Assembly authoringAssembly)
{
try
{
// ConditionUtility.GetInfoForConditionType을 사용하여 ConditionInfo 획득
// 이 메서드는 ConditionAttribute에서 GUID를 읽고, Variables를 리플렉션으로 수집합니다.
Type conditionUtilityType = authoringAssembly.GetType("Unity.Behavior.ConditionUtility");
MethodInfo getInfoForTypeMethod = conditionUtilityType?.GetMethod("GetInfoForConditionType", BindingFlags.Static | BindingFlags.NonPublic);
if (getInfoForTypeMethod == null)
{
Debug.LogError("[DrogBTRebuild] ConditionUtility.GetInfoForConditionType 메서드를 찾지 못했습니다.");
return null;
}
object conditionInfo = getInfoForTypeMethod.Invoke(null, new object[] { conditionType });
if (conditionInfo == null)
{
Debug.LogError($"[DrogBTRebuild] GetInfoForConditionType이 null을 반환: {conditionType.Name}");
return null;
}
Type conditionModelType = authoringAssembly.GetType("Unity.Behavior.ConditionModel");
Type graphNodeModelType = authoringAssembly.GetType("Unity.Behavior.BehaviorGraphNodeModel");
if (conditionModelType == null || graphNodeModelType == null)
{
Debug.LogError("[DrogBTRebuild] ConditionModel/BehaviorGraphNodeModel 타입을 찾지 못했습니다.");
return null;
}
// ConditionModel 생성자 가져오기 (internal)
ConstructorInfo conditionModelCtor = conditionModelType.GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[] { graphNodeModelType, typeof(Unity.Behavior.Condition), conditionInfo.GetType() },
null);
if (conditionModelCtor == null)
{
Debug.LogWarning("[DrogBTRebuild] ConditionModel 생성자를 찾지 못했습니다.");
return null;
}
object conditionModel = conditionModelCtor.Invoke(new object[] { guardNode, null, conditionInfo });
// ConditionModels 리스트에 추가
PropertyInfo conditionModelsProp = guardNode.GetType().GetProperty("ConditionModels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (conditionModelsProp != null)
{
IList conditionModels = conditionModelsProp.GetValue(guardNode) as IList;
conditionModels?.Add(conditionModel);
}
else
{
Debug.LogWarning("[DrogBTRebuild] ConditionModels 속성을 찾지 못했습니다.");
}
return conditionModel;
}
catch (Exception ex)
{
Debug.LogError($"[DrogBTRebuild] AttachCondition 실패 ({conditionType.Name}): {ex.GetType().Name}: {ex.Message}");
return null;
}
}
/// <summary>
/// ConditionModel의 필드를 블랙보드 변수에 연결합니다.
/// </summary>
private static void LinkConditionFieldToVariable(object conditionModel, string fieldName, Type fieldType, object variableModel)
{
MethodInfo setFieldMethod = conditionModel.GetType().GetMethod("SetField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
setFieldMethod?.Invoke(conditionModel, new object[] { fieldName, variableModel, fieldType });
}
/// <summary>
/// Condition을 부착하고, 지정된 enum 필드 값을 설정합니다.
/// CheckPatternReadyCondition처럼 필드 값으로 역할을 구분하는 Condition에 사용합니다.
/// </summary>
private static void AttachConditionWithValue(object guardNode, Type conditionType, string fieldName, object fieldValue, Assembly authoringAssembly)
{
object conditionModel = AttachCondition(guardNode, conditionType, authoringAssembly);
if (conditionModel == null)
{
Debug.LogWarning($"[DrogBTRebuild] AttachConditionWithValue: Condition 생성 실패 ({conditionType.Name})");
return;
}
try
{
// ConditionModel의 실제 타입에서 SetField<T>를 조회
MethodInfo genericSetField = conditionModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
if (genericSetField != null)
{
MethodInfo closedMethod = genericSetField.MakeGenericMethod(fieldValue.GetType());
closedMethod.Invoke(conditionModel, new object[] { fieldName, fieldValue });
}
else
{
Debug.LogWarning($"[DrogBTRebuild] SetField<T>를 찾지 못해 '{fieldName}' 필드를 설정하지 못했습니다.");
}
}
catch (Exception ex)
{
Debug.LogError($"[DrogBTRebuild] AttachConditionWithValue 실패 ({conditionType.Name}.{fieldName}): {ex.GetType().Name}: {ex.Message}");
}
}
/// <summary>
/// 노드 모델의 지정된 enum 필드 값을 설정합니다.
/// UsePatternByRoleAction처럼 필드 값으로 역할을 구분하는 Action에 사용합니다.
/// </summary>
private static void SetNodeFieldValue(object nodeModel, string fieldName, object fieldValue, MethodInfo setFieldValueMethod)
{
if (setFieldValueMethod == null)
{
Debug.LogWarning("[DrogBTRebuild] SetNodeFieldValue: setFieldValueMethod이 null입니다.");
return;
}
try
{
// 실제 노드 모델 타입에서 SetField<T>를 직접 조회하여 타입 불일치 방지
MethodInfo genericMethod = nodeModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
if (genericMethod == null)
{
Debug.LogWarning($"[DrogBTRebuild] SetNodeFieldValue: SetField<T>를 {nodeModel.GetType().Name}에서 찾지 못했습니다.");
return;
}
MethodInfo closedMethod = genericMethod.MakeGenericMethod(fieldValue.GetType());
closedMethod.Invoke(nodeModel, new object[] { fieldName, fieldValue });
}
catch (Exception ex)
{
Debug.LogError($"[DrogBTRebuild] SetNodeFieldValue 실패 ({nodeModel.GetType().Name}.{fieldName}): {ex.GetType().Name}: {ex.Message}");
}
}
private static object CreateNode(UnityEngine.Object graphAsset, MethodInfo createNodeMethod, MethodInfo getNodeInfoMethod, Type runtimeType, Vector2 position)
{
if (runtimeType == null)
@@ -318,5 +739,263 @@ namespace Colosseum.Editor
repeatField?.SetValue(startNode, repeat);
allowField?.SetValue(startNode, allowMultipleRepeatsPerTick);
}
/// <summary>
/// GetField()가 null을 반환하는 문제를 회피하기 위해 전체 타입 계층을 순회하며 필드를 검색합니다.
/// </summary>
private static FieldInfo FindFieldInHierarchy(Type type, string fieldName)
{
Type current = type;
while (current != null)
{
foreach (FieldInfo fi in current.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (fi.Name == fieldName)
return fi;
}
current = current.BaseType;
}
return null;
}
/// <summary>
/// Branch 노드의 지정된 이름의 출력 포트(PortModel)를 반환합니다.
/// BranchingConditionComposite는 NamedChildren(True, False)을 가지므로
/// 기본 출력 포트 대신 이름 기반 포트를 사용해야 합니다.
/// </summary>
private static object GetNamedOutputPort(object node, string portName)
{
MethodInfo method = node.GetType().GetMethod("FindPortModelByName", BindingFlags.Instance | BindingFlags.Public);
if (method == null)
throw new InvalidOperationException("[DrogBTRebuild] FindPortModelByName 메서드를 찾지 못했습니다.");
object port = method.Invoke(node, new object[] { portName });
if (port == null)
throw new InvalidOperationException($"[DrogBTRebuild] '{portName}' 포트를 찾지 못했습니다.");
return port;
}
/// <summary>
/// Branch의 NamedPort(True/False)를 FloatingPortNodeModel을 경유하여 대상 노드에 연결합니다.
/// 올바른 연결 흐름: Branch.NamedPort → FloatingPort.InputPort → FloatingPort.OutputPort → Target.InputPort
/// </summary>
private static void ConnectBranch(UnityEngine.Object graphAsset, MethodInfo connectEdgeMethod, object branchNode, string portName, object targetNode)
{
// Branch의 NamedPort 찾기
object branchPort = GetNamedOutputPort(branchNode, portName);
// FloatingPortNodeModel 찾기 — Branch의 포트에 연결된 FloatingPortNodeModel을 검색
// FloatingPortNodeModel은 GraphAsset.Nodes에 별도 노드로 저장됩니다.
FieldInfo nodesField = FindFieldInHierarchy(graphAsset.GetType(), "m_Nodes");
if (nodesField == null)
{
// 폴백: 직접 연결 (FloatingPort가 없는 경우)
Connect(graphAsset, connectEdgeMethod, branchPort, GetDefaultInputPort(targetNode));
return;
}
IEnumerable nodes = nodesField.GetValue(graphAsset) as IEnumerable;
if (nodes == null)
{
Connect(graphAsset, connectEdgeMethod, branchPort, GetDefaultInputPort(targetNode));
return;
}
object floatingPortOutput = null;
foreach (object node in nodes)
{
if (node == null) continue;
Type nodeType = node.GetType();
if (!nodeType.Name.Contains("FloatingPortNodeModel")) continue;
// PortName 확인
FieldInfo portNameField = nodeType.GetField("PortName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
string currentPortName = portNameField?.GetValue(node) as string;
if (currentPortName != portName) continue;
// ParentNodeID 확인 — 이 Branch의 자식인지
FieldInfo parentNodeIdField = nodeType.GetField("ParentNodeID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (parentNodeIdField == null) continue;
object parentNodeIdValue = parentNodeIdField.GetValue(node);
// Branch의 ID와 비교
FieldInfo branchIdField = branchNode.GetType().GetField("ID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (branchIdField == null) continue;
object branchIdValue = branchIdField.GetValue(branchNode);
if (!parentNodeIdValue.Equals(branchIdValue)) continue;
// FloatingPort의 OutputPort 찾기
PropertyInfo portModelsProp = nodeType.GetProperty("PortModels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
FieldInfo portModelsField = nodeType.GetField("PortModels", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
IEnumerable portModels = portModelsProp?.GetValue(node) as IEnumerable ?? portModelsField?.GetValue(node) as IEnumerable;
if (portModels == null) continue;
foreach (object port in portModels)
{
if (port == null) continue;
FieldInfo portNameF = port.GetType().GetField("m_Name", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
string pName = portNameF?.GetValue(port) as string;
if (pName == "OutputPort")
{
floatingPortOutput = port;
break;
}
}
if (floatingPortOutput != null) break;
}
if (floatingPortOutput != null)
{
Connect(graphAsset, connectEdgeMethod, floatingPortOutput, GetDefaultInputPort(targetNode));
}
else
{
// 폴백: 직접 연결
Connect(graphAsset, connectEdgeMethod, branchPort, GetDefaultInputPort(targetNode));
Debug.LogWarning($"[DrogBTRebuild] FloatingPortNodeModel을 찾지 못해 '{portName}' 포트를 직접 연결합니다.");
}
}
/// <summary>
/// Branch 노드의 RequiresAllConditionsTrue 플래그를 설정합니다.
/// DefaultNodeTransformer가 model.RequiresAllConditionsTrue → runtime.RequiresAllConditions로 복사합니다.
/// </summary>
private static void SetBranchRequiresAll(object branchNode, bool requiresAll)
{
PropertyInfo prop = branchNode.GetType().GetProperty("RequiresAllConditionsTrue", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
prop?.SetValue(branchNode, requiresAll);
}
/// <summary>
/// Branch에 속한 FloatingPortNodeModel의 위치를 설정합니다.
/// Branch의 ID와 PortName으로 FloatingPortNodeModel을 찾아 Position을 변경합니다.
/// </summary>
private static void SetFloatingPortPosition(UnityEngine.Object graphAsset, object branchNode, string portName, float x, float y)
{
// m_Nodes 또는 Nodes에서 FloatingPortNodeModel을 검색
IEnumerable nodes = null;
FieldInfo nodesField = FindFieldInHierarchy(graphAsset.GetType(), "m_Nodes");
if (nodesField != null)
nodes = nodesField.GetValue(graphAsset) as IEnumerable;
if (nodes == null)
{
PropertyInfo nodesProp = graphAsset.GetType().GetProperty("Nodes", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (nodesProp != null)
nodes = nodesProp.GetValue(graphAsset) as IEnumerable;
}
if (nodes == null)
{
Debug.LogWarning("[DrogBTRebuild] SetFloatingPortPosition: Nodes 컬렉션을 찾지 못했습니다.");
return;
}
// Branch의 ID 가져오기
FieldInfo branchIdField = branchNode.GetType().GetField("ID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (branchIdField == null)
{
Debug.LogWarning("[DrogBTRebuild] SetFloatingPortPosition: ID 필드를 찾지 못했습니다.");
return;
}
object branchIdValue = branchIdField.GetValue(branchNode);
foreach (object node in nodes)
{
if (node == null) continue;
Type nodeType = node.GetType();
if (!nodeType.Name.Contains("FloatingPortNodeModel")) continue;
FieldInfo portNameField = nodeType.GetField("PortName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
string currentPortName = portNameField?.GetValue(node) as string;
if (currentPortName != portName) continue;
FieldInfo parentNodeIdField = nodeType.GetField("ParentNodeID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (parentNodeIdField == null) continue;
object parentNodeIdValue = parentNodeIdField.GetValue(node);
bool match = parentNodeIdValue != null && parentNodeIdValue.Equals(branchIdValue);
if (!match) continue;
// Position 설정 (Position은 public 필드)
FieldInfo posField = nodeType.GetField("Position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
posField?.SetValue(node, new Vector2(x, y));
return;
}
}
/// <summary>
/// 컴포넌트의 protected 필드 값을 읽습니다 (참조 타입용).
/// 프리팹에서 BossPatternData 에셋을 로드할 때 사용합니다.
/// </summary>
private static T ReadProtectedField<T>(object obj, string fieldName) where T : class
{
Type type = obj.GetType();
while (type != null)
{
FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (field != null)
return field.GetValue(obj) as T;
type = type.BaseType;
}
Debug.LogError($"[DrogBTRebuild] '{fieldName}' 필드를 {obj.GetType().Name}에서 찾지 못했습니다.");
return null;
}
/// <summary>
/// 컴포넌트의 protected 필드 값을 읽습니다 (값 타입용).
/// </summary>
private static T ReadProtectedFieldValue<T>(object obj, string fieldName, T defaultValue) where T : struct
{
Type type = obj.GetType();
while (type != null)
{
FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (field != null)
return (T)field.GetValue(obj);
type = type.BaseType;
}
return defaultValue;
}
/// <summary>
/// Branch에 CheckPatternReadyCondition을 부착하고 BossPatternData 에셋을 설정합니다.
/// 노드에 패턴명이 표시됩니다 (story의 [Pattern] 치환).
/// </summary>
private static void AttachPatternReadyCondition(object branchNode, BossPatternData pattern, Assembly authoringAssembly)
{
object condModel = AttachCondition(branchNode, typeof(CheckPatternReadyCondition), authoringAssembly);
if (condModel == null)
{
Debug.LogError($"[DrogBTRebuild] CheckPatternReadyCondition 부착 실패: {pattern?.PatternName}");
return;
}
// ConditionModel의 실제 타입에서 SetField<T>를 조회하여 BossPatternData 참조 설정
MethodInfo genericSetField = condModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.FirstOrDefault(m => m.Name == "SetField" && m.IsGenericMethod && m.GetParameters().Length == 2);
if (genericSetField != null)
{
MethodInfo closedMethod = genericSetField.MakeGenericMethod(typeof(BossPatternData));
closedMethod.Invoke(condModel, new object[] { "Pattern", pattern });
}
else
{
Debug.LogError("[DrogBTRebuild] CheckPatternReadyCondition에서 SetField<T>를 찾지 못했습니다.");
}
}
/// <summary>
/// 패턴의 MinPhase가 1보다 큰 경우, Branch에 IsMinPhaseSatisfiedCondition을 부착합니다.
/// Phase 진입 조건을 BT에서 시각적으로 확인할 수 있습니다.
/// </summary>
private static void AttachPhaseConditionIfNeeded(object branchNode, BossPatternData pattern, Assembly authoringAssembly)
{
if (pattern == null || pattern.MinPhase <= 1)
return;
AttachConditionWithValue(branchNode, typeof(IsMinPhaseSatisfiedCondition), "MinPhase", pattern.MinPhase, authoringAssembly);
}
}
}