using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using Colosseum.AI; using Colosseum.AI.BehaviorActions.Actions; using Colosseum.AI.BehaviorActions.Conditions; using Colosseum.Abnormalities; using Colosseum.Enemy; using Colosseum.Skills; using UnityEditor; using UnityEngine; namespace Colosseum.Editor { /// /// 드로그 Behavior Graph authoring 자산을 현재 BT 우선순위 구조로 재생성합니다. /// Check 노드는 ConditionalGuardAction + Condition 조합으로 구현됩니다. /// public static class RebuildDrogBehaviorAuthoringGraph { private const string GraphAssetPath = "Assets/_Game/AI/BT_Drog.asset"; private const string DefaultPunishPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_밟기.asset"; private const string DefaultSignaturePatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_집행.asset"; private const string DefaultGroundShakePatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_땅 울리기.asset"; private const string DefaultMobilityPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_도약.asset"; private const string DefaultPrimaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기1.asset"; private const string DefaultSecondaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기2.asset"; private const string DefaultTertiaryPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기3.asset"; private const string DefaultComboPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-강타.asset"; private const string DefaultPressurePatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-발구르기.asset"; private const string DefaultUtilityPatternPath = "Assets/_Game/Data/Patterns/Data_Pattern_Drog_투척.asset"; private const string DefaultPhase3TransitionSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Drog_포효.asset"; private const string DefaultSignatureFailureAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_집행자의낙인.asset"; private const float DefaultDownedTargetSearchRadius = 6f; private const float DefaultLeapTargetMinDistance = 8f; private const float DefaultThrowTargetMinDistance = 5f; private const float DefaultPrimaryBranchAttackRange = 3f; private const float DefaultTargetSearchRange = 20f; private const float DefaultThrowAvailabilityDelay = 4f; private const float DefaultPhaseTransitionLockDuration = 1.25f; private const float DefaultSignatureRepeatInterval = 90f; private const float DefaultGroundShakeInterval = 12f; private const float DefaultPhase2EnterHealthPercent = 75f; private const float DefaultPhase3EnterHealthPercent = 40f; private const int DefaultBasicLoopRequirementBeforeBigPattern = 2; private const float DefaultComboPatternWeight = 26f; private const float DefaultPressurePatternWeight = 24f; private const float DefaultPrimaryPatternWeight = 22f; private const float DefaultSecondaryPatternWeight = 16f; private const float DefaultTertiaryPatternWeight = 12f; private const float DefaultSignatureFailureDamage = 40f; private const float DefaultSignatureFailureKnockbackRadius = 8f; private const float DefaultSignatureFailureDownRadius = 3f; private const float DefaultSignatureFailureKnockbackSpeed = 12f; private const float DefaultSignatureFailureKnockbackDuration = 0.35f; private const float DefaultSignatureFailureDownDuration = 2f; [MenuItem("Tools/Colosseum/Rebuild Drog Behavior Authoring Graph")] private static void Rebuild() { UnityEngine.Object graphAsset = AssetDatabase.LoadMainAssetAtPath(GraphAssetPath); if (graphAsset == null) { // 에셋이 없으면 기존 에셋 경로의 타입을 리플렉션으로 찾아 생성합니다. // 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 { 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 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); 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; } // 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] ConditionInfo 타입을 찾지 못했습니다."); return; } 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(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 제네릭 메서드를 찾지 못했습니다."); 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 = EnsureBlackboardVariable("Target", null); if (targetVariable == null) { Debug.LogError("[DrogBTRebuild] Target 블랙보드 변수를 찾지 못했습니다."); return; } // 구조 노드 object startNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.Start", true), new Vector2(-1320f, -920f)); object repeatNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.RepeaterModifier", true), new Vector2(-1320f, -720f)); // ── 드로그 패턴과 판단 수치는 노드 로컬 값으로 두고, Target만 블랙보드로 공유합니다. ── RemoveBlackboardVariables( "PunishPattern", "SignaturePattern", "MobilityPattern", "ComboPattern", "PrimaryPattern", "UtilityPattern", "PunishSearchRadius", "MobilityTriggerDistance", "UtilityTriggerDistance", "PrimaryAttackRange", "SelectedMeleePattern", "Phase2HealthPercent", "Phase3HealthPercent", "SightRange", "AttackRange", "MoveSpeed"); BossPatternData punishPattern = LoadRequiredAsset(DefaultPunishPatternPath, "밟기 패턴"); BossPatternData signaturePattern = LoadRequiredAsset(DefaultSignaturePatternPath, "집행 패턴"); BossPatternData groundShakePattern = LoadRequiredAsset(DefaultGroundShakePatternPath, "땅 울리기 패턴"); BossPatternData mobilityPattern = LoadRequiredAsset(DefaultMobilityPatternPath, "도약 패턴"); BossPatternData primaryPattern = LoadRequiredAsset(DefaultPrimaryPatternPath, "콤보-기본기1 패턴"); BossPatternData secondaryPattern = LoadRequiredAsset(DefaultSecondaryPatternPath, "콤보-기본기2 패턴"); BossPatternData tertiaryPattern = LoadRequiredAsset(DefaultTertiaryPatternPath, "콤보-기본기3 패턴"); BossPatternData comboPattern = LoadRequiredAsset(DefaultComboPatternPath, "콤보-강타 패턴"); BossPatternData pressurePattern = LoadRequiredAsset(DefaultPressurePatternPath, "콤보-발구르기 패턴"); BossPatternData utilityPattern = LoadRequiredAsset(DefaultUtilityPatternPath, "투척 패턴"); SkillData phase3TransitionSkill = LoadRequiredAsset(DefaultPhase3TransitionSkillPath, "포효 스킬"); AbnormalityData signatureFailureAbnormality = LoadRequiredAsset(DefaultSignatureFailureAbnormalityPath, "집행 실패 디버프"); if (punishPattern == null || signaturePattern == null || groundShakePattern == null || mobilityPattern == null || primaryPattern == null || secondaryPattern == null || tertiaryPattern == null || comboPattern == null || pressurePattern == null || utilityPattern == null || phase3TransitionSkill == null) { Debug.LogError("[DrogBTRebuild] 드로그 BT 재구성에 필요한 패턴/스킬 에셋을 읽지 못했습니다."); return; } const float branchX = -900f; const float rootRefreshX = branchX - 500f; const float sequenceX = branchX + 360f; const float actionX1 = sequenceX + 360f; const float actionX2 = actionX1 + 320f; const float actionX3 = actionX2 + 320f; const float actionX4 = actionX3 + 320f; const float actionX5 = actionX4 + 320f; const float actionX6 = actionX5 + 320f; const float truePortOffsetX = 203f; const float truePortOffsetY = 120f; const float falsePortOffsetX = -211f; const float falsePortOffsetY = 124f; const float startY = -1320f; const float stepY = 360f; object phase2TransitionBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY)); AttachConditionWithValue(phase2TransitionBranch, typeof(IsCurrentPhaseCondition), "Phase", 1, authoringAssembly); AttachConditionWithValue(phase2TransitionBranch, typeof(IsHealthBelowCondition), "HealthPercent", DefaultPhase2EnterHealthPercent, authoringAssembly); SetBranchRequiresAll(phase2TransitionBranch, true); object phase2TransitionSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY)); object phase2TransitionWaitNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(WaitAction), new Vector2(actionX1, startY)); SetNodeFieldValue(phase2TransitionWaitNode, "Duration", DefaultPhaseTransitionLockDuration, setFieldValueMethod); object phase2SetPhaseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX2, startY)); SetNodeFieldValue(phase2SetPhaseNode, "TargetPhase", 2, setFieldValueMethod); SetNodeFieldValue(phase2SetPhaseNode, "ResetTimer", true, setFieldValueMethod); ConnectChildren(graphAsset, connectEdgeMethod, phase2TransitionSequence, phase2TransitionWaitNode, phase2SetPhaseNode); object phase3TransitionBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY)); AttachConditionWithValue(phase3TransitionBranch, typeof(IsCurrentPhaseCondition), "Phase", 2, authoringAssembly); AttachConditionWithValue(phase3TransitionBranch, typeof(IsHealthBelowCondition), "HealthPercent", DefaultPhase3EnterHealthPercent, authoringAssembly); SetBranchRequiresAll(phase3TransitionBranch, true); object phase3TransitionSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY)); object phase3RoarNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseSkillAction), new Vector2(actionX1, startY + stepY)); SetNodeFieldValue(phase3RoarNode, "스킬", phase3TransitionSkill, setFieldValueMethod); object phase3TransitionWaitNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(WaitAction), new Vector2(actionX2, startY + stepY)); SetNodeFieldValue(phase3TransitionWaitNode, "Duration", DefaultPhaseTransitionLockDuration, setFieldValueMethod); object phase3SetPhaseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX3, startY + stepY)); SetNodeFieldValue(phase3SetPhaseNode, "TargetPhase", 3, setFieldValueMethod); SetNodeFieldValue(phase3SetPhaseNode, "ResetTimer", true, setFieldValueMethod); object phase3RefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(actionX4, startY + stepY)); SetNodeFieldValue(phase3RefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod); object phase3ValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX5, startY + stepY)); object phase3SignatureResetLoopNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ResetBasicLoopCountAction), new Vector2(actionX6 - 200f, startY + stepY)); object phase3UseSignatureNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX6, startY + stepY)); SetNodeFieldValue(phase3UseSignatureNode, "Pattern", signaturePattern, setFieldValueMethod); SetNodeFieldValue(phase3UseSignatureNode, "ContinueOnResolvedFailure", true, setFieldValueMethod); object phase3SignatureResultBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(actionX6 + 420f, startY + stepY)); AttachConditionWithValue(phase3SignatureResultBranch, typeof(IsPatternExecutionResultCondition), "Result", BossPatternExecutionResult.Failed, authoringAssembly); SetBranchRequiresAll(phase3SignatureResultBranch, true); object phase3SignatureStaggerNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(BossStaggerAction), new Vector2(actionX6 + 820f, startY + stepY - 120f)); object phase3SignatureFailureNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SignatureFailureEffectsAction), new Vector2(actionX6 + 820f, startY + stepY + 120f)); ConfigureSignatureFailureNode(phase3SignatureFailureNode, signatureFailureAbnormality, setFieldValueMethod); object phase3SignatureTimerResetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX6 + 1220f, startY + stepY)); SetNodeFieldValue(phase3SignatureTimerResetNode, "TargetPhase", 3, setFieldValueMethod); SetNodeFieldValue(phase3SignatureTimerResetNode, "ResetTimer", true, setFieldValueMethod); ConnectChildren( graphAsset, connectEdgeMethod, phase3TransitionSequence, phase3RoarNode, phase3TransitionWaitNode, phase3SetPhaseNode, phase3RefreshNode, phase3ValidateNode, phase3SignatureResetLoopNode, phase3UseSignatureNode, phase3SignatureResultBranch, phase3SignatureTimerResetNode); object rootRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(rootRefreshX, startY + stepY * 2f - 120f)); SetNodeFieldValue(rootRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod); object punishBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 2f)); AttachPatternReadyCondition(punishBranch, punishPattern, authoringAssembly); AttachConditionWithValue(punishBranch, typeof(IsDownedTargetInRangeCondition), "SearchRadius", DefaultDownedTargetSearchRadius, authoringAssembly); SetBranchRequiresAll(punishBranch, true); object punishSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY * 2f)); object punishSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectNearestDownedTargetAction), new Vector2(actionX1, startY + stepY * 2f)); SetNodeFieldValue(punishSelectNode, "SearchRadius", DefaultDownedTargetSearchRadius, setFieldValueMethod); object punishUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX2, startY + stepY * 2f)); SetNodeFieldValue(punishUseNode, "Pattern", punishPattern, setFieldValueMethod); object punishResetLoopNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ResetBasicLoopCountAction), new Vector2(actionX3, startY + stepY * 2f)); ConnectChildren(graphAsset, connectEdgeMethod, punishSequence, punishSelectNode, punishUseNode, punishResetLoopNode); object signatureBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 3f)); AttachConditionWithValue(signatureBranch, typeof(IsCurrentPhaseCondition), "Phase", 3, authoringAssembly); AttachConditionWithValue(signatureBranch, typeof(IsPhaseElapsedTimeAboveCondition), "Seconds", DefaultSignatureRepeatInterval, authoringAssembly); AttachPatternReadyCondition(signatureBranch, signaturePattern, authoringAssembly); AttachConditionWithValue(signatureBranch, typeof(IsBasicLoopCountAtLeastCondition), "Count", DefaultBasicLoopRequirementBeforeBigPattern, authoringAssembly); SetBranchRequiresAll(signatureBranch, true); object signatureSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY * 3f)); object signatureRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(actionX1, startY + stepY * 3f)); SetNodeFieldValue(signatureRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod); object signatureValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX2, startY + stepY * 3f)); object signatureUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX3, startY + stepY * 3f)); SetNodeFieldValue(signatureUseNode, "Pattern", signaturePattern, setFieldValueMethod); SetNodeFieldValue(signatureUseNode, "ContinueOnResolvedFailure", true, setFieldValueMethod); object signatureResetLoopNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ResetBasicLoopCountAction), new Vector2(actionX3 + 220f, startY + stepY * 3f)); object signatureResultBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(actionX4, startY + stepY * 3f)); AttachConditionWithValue(signatureResultBranch, typeof(IsPatternExecutionResultCondition), "Result", BossPatternExecutionResult.Failed, authoringAssembly); SetBranchRequiresAll(signatureResultBranch, true); object signatureStaggerNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(BossStaggerAction), new Vector2(actionX4 + 420f, startY + stepY * 3f - 120f)); object signatureFailureNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SignatureFailureEffectsAction), new Vector2(actionX4 + 420f, startY + stepY * 3f + 120f)); ConfigureSignatureFailureNode(signatureFailureNode, signatureFailureAbnormality, setFieldValueMethod); object signatureTimerResetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX5, startY + stepY * 3f)); SetNodeFieldValue(signatureTimerResetNode, "TargetPhase", 3, setFieldValueMethod); SetNodeFieldValue(signatureTimerResetNode, "ResetTimer", true, setFieldValueMethod); ConnectChildren( graphAsset, connectEdgeMethod, signatureSequence, signatureRefreshNode, signatureValidateNode, signatureUseNode, signatureResetLoopNode, signatureResultBranch, signatureTimerResetNode); object throwBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 4f)); AttachPatternReadyCondition(throwBranch, utilityPattern, authoringAssembly); AttachConditionWithValue(throwBranch, typeof(IsRecentReviveTriggerCondition), "MaxAge", DefaultThrowAvailabilityDelay, authoringAssembly); SetBranchRequiresAll(throwBranch, true); object throwSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY * 4f)); object throwSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectRecentReviveTargetAction), new Vector2(actionX1, startY + stepY * 4f)); SetNodeFieldValue(throwSelectNode, "MaxAge", DefaultThrowAvailabilityDelay, setFieldValueMethod); SetNodeFieldValue(throwSelectNode, "PreferCaster", true, setFieldValueMethod); SetNodeFieldValue(throwSelectNode, "FallbackToRevivedTarget", true, setFieldValueMethod); object throwUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX2, startY + stepY * 4f)); SetNodeFieldValue(throwUseNode, "Pattern", utilityPattern, setFieldValueMethod); ConnectChildren(graphAsset, connectEdgeMethod, throwSequence, throwSelectNode, throwUseNode); object leapBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 5f)); AttachPatternReadyCondition(leapBranch, mobilityPattern, authoringAssembly); AttachConditionWithValue(leapBranch, typeof(IsTargetBeyondDistanceCondition), "MinDistance", DefaultLeapTargetMinDistance, authoringAssembly); AttachConditionWithValue(leapBranch, typeof(IsBasicLoopCountAtLeastCondition), "Count", DefaultBasicLoopRequirementBeforeBigPattern, authoringAssembly); SetBranchRequiresAll(leapBranch, true); object leapSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY * 5f)); object leapSelectNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SelectTargetByDistanceAction), new Vector2(actionX1, startY + stepY * 5f)); SetNodeFieldValue(leapSelectNode, "MinRange", DefaultLeapTargetMinDistance, setFieldValueMethod); SetNodeFieldValue(leapSelectNode, "MaxRange", DefaultTargetSearchRange, setFieldValueMethod); SetNodeFieldValue(leapSelectNode, "SelectionMode", DistanceTargetSelectionMode.Farthest, setFieldValueMethod); object leapValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX2, startY + stepY * 5f)); object leapUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX3, startY + stepY * 5f)); SetNodeFieldValue(leapUseNode, "Pattern", mobilityPattern, setFieldValueMethod); object leapResetLoopNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ResetBasicLoopCountAction), new Vector2(actionX4, startY + stepY * 5f)); ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapValidateNode, leapUseNode, leapResetLoopNode); object groundShakeBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 6f)); AttachConditionWithValue(groundShakeBranch, typeof(IsCurrentPhaseCondition), "Phase", 2, authoringAssembly); AttachConditionWithValue(groundShakeBranch, typeof(IsPhaseElapsedTimeAboveCondition), "Seconds", DefaultGroundShakeInterval, authoringAssembly); AttachPatternReadyCondition(groundShakeBranch, groundShakePattern, authoringAssembly); AttachConditionWithValue(groundShakeBranch, typeof(IsBasicLoopCountAtLeastCondition), "Count", DefaultBasicLoopRequirementBeforeBigPattern, authoringAssembly); SetBranchRequiresAll(groundShakeBranch, true); object groundShakeSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY * 6f)); object groundShakeRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(actionX1, startY + stepY * 6f)); SetNodeFieldValue(groundShakeRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod); object groundShakeValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX2, startY + stepY * 6f)); object groundShakeUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UsePatternByRoleAction), new Vector2(actionX3, startY + stepY * 6f)); SetNodeFieldValue(groundShakeUseNode, "Pattern", groundShakePattern, setFieldValueMethod); object groundShakeResetLoopNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ResetBasicLoopCountAction), new Vector2(actionX4 - 180f, startY + stepY * 6f)); object groundShakeTimerResetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(SetBossPhaseAction), new Vector2(actionX4, startY + stepY * 6f)); SetNodeFieldValue(groundShakeTimerResetNode, "TargetPhase", 2, setFieldValueMethod); SetNodeFieldValue(groundShakeTimerResetNode, "ResetTimer", true, setFieldValueMethod); ConnectChildren(graphAsset, connectEdgeMethod, groundShakeSequence, groundShakeRefreshNode, groundShakeValidateNode, groundShakeUseNode, groundShakeResetLoopNode, groundShakeTimerResetNode); object meleeBranch = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, branchCompositeType, new Vector2(branchX, startY + stepY * 7f)); object meleeRangeCondModel = AttachCondition(meleeBranch, typeof(IsTargetInAttackRangeCondition), authoringAssembly); if (meleeRangeCondModel != null) setFieldMethod.Invoke(meleeRangeCondModel, new object[] { "Target", targetVariable, typeof(GameObject) }); if (meleeRangeCondModel != null) SetConditionFieldValue(meleeRangeCondModel, "AttackRange", DefaultPrimaryBranchAttackRange); object meleeReadyCondModel = AttachCondition(meleeBranch, typeof(HasAnyReadyPatternCondition), authoringAssembly); SetWeightedPatternFields(meleeReadyCondModel, setFieldMethod, comboPattern, DefaultComboPatternWeight, pressurePattern, DefaultPressurePatternWeight, primaryPattern, DefaultPrimaryPatternWeight, secondaryPattern, DefaultSecondaryPatternWeight, tertiaryPattern, DefaultTertiaryPatternWeight); SetBranchRequiresAll(meleeBranch, true); object meleeSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(sequenceX, startY + stepY * 7f)); object meleeValidateNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(actionX1, startY + stepY * 7f)); object meleeUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(UseWeightedReadyPatternAction), new Vector2(actionX2, startY + stepY * 7f)); SetWeightedPatternFields(meleeUseNode, setFieldValueMethod, comboPattern, DefaultComboPatternWeight, pressurePattern, DefaultPressurePatternWeight, primaryPattern, DefaultPrimaryPatternWeight, secondaryPattern, DefaultSecondaryPatternWeight, tertiaryPattern, DefaultTertiaryPatternWeight); object meleeIncrementLoopNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(IncrementBasicLoopCountAction), new Vector2(actionX3, startY + stepY * 7f)); SetNodeFieldValue(meleeIncrementLoopNode, "Count", 1, setFieldValueMethod); ConnectChildren(graphAsset, connectEdgeMethod, meleeSequence, meleeValidateNode, meleeUseNode, meleeIncrementLoopNode); object chaseSequence = CreateNode( graphAsset, createNodeMethod, getNodeInfoMethod, runtimeAssembly.GetType("Unity.Behavior.SequenceComposite", true), new Vector2(branchX, startY + stepY * 11.5f)); object chaseRefreshNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(RefreshPrimaryTargetAction), new Vector2(branchX + 160f, startY + stepY * 11.5f + 80f)); SetNodeFieldValue(chaseRefreshNode, "SearchRange", DefaultTargetSearchRange, setFieldValueMethod); object chaseHasTargetNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ValidateTargetAction), new Vector2(branchX + 480f, startY + stepY * 11.5f + 80f)); object chaseUseNode = CreateNode(graphAsset, createNodeMethod, getNodeInfoMethod, typeof(ChaseTargetAction), new Vector2(branchX + 800f, startY + stepY * 11.5f + 80f)); SetNodeFieldValue(chaseUseNode, "StopDistance", DefaultPrimaryBranchAttackRange, setFieldValueMethod); LinkTarget(rootRefreshNode, targetVariable); LinkTarget(phase3RefreshNode, targetVariable); LinkTarget(phase3ValidateNode, targetVariable); LinkTarget(phase3UseSignatureNode, targetVariable); LinkTarget(punishSelectNode, targetVariable); LinkTarget(punishUseNode, targetVariable); LinkTarget(signatureRefreshNode, targetVariable); LinkTarget(signatureValidateNode, targetVariable); LinkTarget(signatureUseNode, targetVariable); LinkTarget(throwSelectNode, targetVariable); LinkTarget(throwUseNode, targetVariable); LinkTarget(leapSelectNode, targetVariable); LinkTarget(leapValidateNode, targetVariable); LinkTarget(leapUseNode, targetVariable); LinkTarget(groundShakeRefreshNode, targetVariable); LinkTarget(groundShakeValidateNode, targetVariable); LinkTarget(groundShakeUseNode, targetVariable); LinkTarget(meleeValidateNode, targetVariable); LinkTarget(meleeUseNode, targetVariable); LinkTarget(chaseRefreshNode, targetVariable); LinkTarget(chaseHasTargetNode, targetVariable); LinkTarget(chaseUseNode, targetVariable); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(rootRefreshNode), GetDefaultInputPort(punishBranch)); var allBranches = new List { phase2TransitionBranch, phase3TransitionBranch, punishBranch, signatureBranch, throwBranch, leapBranch, groundShakeBranch, meleeBranch, phase3SignatureResultBranch, signatureResultBranch, }; foreach (object branch in allBranches) { createNodePortsMethod?.Invoke(graphAsset, new object[] { branch }); } foreach (object branch in allBranches) { FieldInfo posField = branch.GetType().GetField("Position", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (posField == null) continue; Vector2 branchPos = (Vector2)posField.GetValue(branch); SetFloatingPortPosition(graphAsset, branch, "True", branchPos.x + truePortOffsetX, branchPos.y + truePortOffsetY); SetFloatingPortPosition(graphAsset, branch, "False", branchPos.x + falsePortOffsetX, branchPos.y + falsePortOffsetY); } Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(startNode), GetDefaultInputPort(repeatNode)); Connect(graphAsset, connectEdgeMethod, GetDefaultOutputPort(repeatNode), GetDefaultInputPort(phase2TransitionBranch)); ConnectBranch(graphAsset, connectEdgeMethod, phase2TransitionBranch, "True", phase2TransitionSequence); ConnectBranch(graphAsset, connectEdgeMethod, phase2TransitionBranch, "False", phase3TransitionBranch); ConnectBranch(graphAsset, connectEdgeMethod, phase3TransitionBranch, "True", phase3TransitionSequence); ConnectBranch(graphAsset, connectEdgeMethod, phase3TransitionBranch, "False", rootRefreshNode); ConnectBranch(graphAsset, connectEdgeMethod, phase3SignatureResultBranch, "True", phase3SignatureStaggerNode); ConnectBranch(graphAsset, connectEdgeMethod, phase3SignatureResultBranch, "False", phase3SignatureFailureNode); ConnectBranch(graphAsset, connectEdgeMethod, signatureResultBranch, "True", signatureStaggerNode); ConnectBranch(graphAsset, connectEdgeMethod, signatureResultBranch, "False", signatureFailureNode); ConnectBranch(graphAsset, connectEdgeMethod, punishBranch, "True", punishSequence); ConnectBranch(graphAsset, connectEdgeMethod, punishBranch, "False", signatureBranch); ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "True", signatureSequence); ConnectBranch(graphAsset, connectEdgeMethod, signatureBranch, "False", throwBranch); ConnectBranch(graphAsset, connectEdgeMethod, throwBranch, "True", throwSequence); ConnectBranch(graphAsset, connectEdgeMethod, throwBranch, "False", leapBranch); ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "True", leapSequence); ConnectBranch(graphAsset, connectEdgeMethod, leapBranch, "False", groundShakeBranch); ConnectBranch(graphAsset, connectEdgeMethod, groundShakeBranch, "True", groundShakeSequence); ConnectBranch(graphAsset, connectEdgeMethod, groundShakeBranch, "False", meleeBranch); ConnectBranch(graphAsset, connectEdgeMethod, meleeBranch, "True", meleeSequence); ConnectBranch(graphAsset, connectEdgeMethod, meleeBranch, "False", chaseSequence); ConnectChildren(graphAsset, connectEdgeMethod, chaseSequence, chaseRefreshNode, chaseHasTargetNode, chaseUseNode); // 저장 SetStartRepeatFlags(startNode, repeat: true, allowMultipleRepeatsPerTick: false); setAssetDirtyMethod.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(); Debug.Log("[DrogBTRebuild] 드로그 Behavior Graph authoring 자산 재구성이 완료되었습니다."); } catch (Exception exception) { Debug.LogException(exception); } } /// /// ConditionalGuardAction 노드를 생성하고 지정된 Condition을 부착합니다. /// 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; } /// /// ConditionalGuardAction 노드를 생성하고, 블랙보드 변수 참조가 있는 Condition을 부착합니다. /// 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; } /// /// 노드에 Condition을 부착합니다. /// ConditionUtility.GetInfoForConditionType를 사용하여 NodeRegistry와 완벽히 동일한 /// ConditionInfo를 획득합니다. 이렇게 하면 TypeID가 레지스트리와 일치하여 /// EnsureFieldValuesAreUpToDate가 정상 동작하고 UpdateConditionModels가 /// ConditionModel을 삭제하지 않습니다. /// 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; } } /// /// Condition을 부착하고, 지정된 enum 필드 값을 설정합니다. /// CheckPatternReadyCondition처럼 필드 값으로 역할을 구분하는 Condition에 사용합니다. /// 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를 조회 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를 찾지 못해 '{fieldName}' 필드를 설정하지 못했습니다."); } } catch (Exception ex) { Debug.LogError($"[DrogBTRebuild] AttachConditionWithValue 실패 ({conditionType.Name}.{fieldName}): {ex.GetType().Name}: {ex.Message}"); } } private static void SetConditionFieldValue(object conditionModel, string fieldName, object fieldValue) { if (conditionModel == null || fieldValue == null) return; try { MethodInfo genericSetField = conditionModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) .FirstOrDefault(method => method.Name == "SetField" && method.IsGenericMethod && method.GetParameters().Length == 2); if (genericSetField == null) { Debug.LogWarning($"[DrogBTRebuild] SetConditionFieldValue: '{fieldName}' 필드 설정 메서드를 찾지 못했습니다."); return; } MethodInfo closedMethod = genericSetField.MakeGenericMethod(fieldValue.GetType()); closedMethod.Invoke(conditionModel, new object[] { fieldName, fieldValue }); } catch (Exception ex) { Debug.LogError($"[DrogBTRebuild] SetConditionFieldValue 실패 ({fieldName}): {ex.GetType().Name}: {ex.Message}"); } } /// /// 노드 모델의 지정된 enum 필드 값을 설정합니다. /// UsePatternByRoleAction처럼 필드 값으로 역할을 구분하는 Action에 사용합니다. /// private static void SetNodeFieldValue(object nodeModel, string fieldName, object fieldValue, MethodInfo setFieldValueMethod) { if (setFieldValueMethod == null) { Debug.LogWarning("[DrogBTRebuild] SetNodeFieldValue: setFieldValueMethod이 null입니다."); return; } try { // 실제 노드 모델 타입에서 SetField를 직접 조회하여 타입 불일치 방지 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를 {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 void ConfigureSignatureFailureNode(object nodeModel, AbnormalityData failureAbnormality, MethodInfo setFieldValueMethod) { SetNodeFieldValue(nodeModel, "FailureDamage", DefaultSignatureFailureDamage, setFieldValueMethod); SetNodeFieldValue(nodeModel, "FailureAbnormality", failureAbnormality, setFieldValueMethod); SetNodeFieldValue(nodeModel, "KnockbackRadius", DefaultSignatureFailureKnockbackRadius, setFieldValueMethod); SetNodeFieldValue(nodeModel, "DownRadius", DefaultSignatureFailureDownRadius, setFieldValueMethod); SetNodeFieldValue(nodeModel, "KnockbackSpeed", DefaultSignatureFailureKnockbackSpeed, setFieldValueMethod); SetNodeFieldValue(nodeModel, "KnockbackDuration", DefaultSignatureFailureKnockbackDuration, setFieldValueMethod); SetNodeFieldValue(nodeModel, "DownDuration", DefaultSignatureFailureDownDuration, setFieldValueMethod); } 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 IList GetBlackboardVariables(UnityEngine.Object blackboardAsset) { 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); return variablesProperty?.GetValue(blackboardAsset) as IList ?? variablesField?.GetValue(blackboardAsset) as IList; } private static string GetVariableName(object variableModel) { return GetFieldOrPropertyValue(variableModel, "Name") as string; } private static Type ResolveTypedVariableModelGenericType(IList existingVariables) { for (int i = 0; i < existingVariables.Count; i++) { object variable = existingVariables[i]; if (variable == null) continue; Type variableType = variable.GetType(); if (variableType.IsGenericType) return variableType.GetGenericTypeDefinition(); } return null; } private static object CreateSerializableGuid(Type variableModelType) { FieldInfo idField = variableModelType.GetField("ID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Type guidType = idField?.FieldType; ConstructorInfo guidConstructor = guidType?.GetConstructor(new[] { typeof(ulong), typeof(ulong) }); if (guidConstructor == null) return null; byte[] guidBytes = Guid.NewGuid().ToByteArray(); ulong value0 = BitConverter.ToUInt64(guidBytes, 0); ulong value1 = BitConverter.ToUInt64(guidBytes, 8); return guidConstructor.Invoke(new object[] { value0, value1 }); } private static object GetFieldOrPropertyValue(object target, string memberName) { if (target == null) return null; PropertyInfo property = target.GetType().GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) return property.GetValue(target); FieldInfo field = target.GetType().GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return field?.GetValue(target); } private static void SetFieldOrPropertyValue(object target, string memberName, object value) { if (target == null) return; PropertyInfo property = target.GetType().GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.CanWrite) { property.SetValue(target, value); return; } FieldInfo field = target.GetType().GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); field?.SetValue(target, value); } 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); } /// /// GetField()가 null을 반환하는 문제를 회피하기 위해 전체 타입 계층을 순회하며 필드를 검색합니다. /// 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; } /// /// Branch 노드의 지정된 이름의 출력 포트(PortModel)를 반환합니다. /// BranchingConditionComposite는 NamedChildren(True, False)을 가지므로 /// 기본 출력 포트 대신 이름 기반 포트를 사용해야 합니다. /// 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; } /// /// Branch의 NamedPort(True/False)를 FloatingPortNodeModel을 경유하여 대상 노드에 연결합니다. /// 올바른 연결 흐름: Branch.NamedPort → FloatingPort.InputPort → FloatingPort.OutputPort → Target.InputPort /// 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}' 포트를 직접 연결합니다."); } } /// /// Branch 노드의 RequiresAllConditionsTrue 플래그를 설정합니다. /// DefaultNodeTransformer가 model.RequiresAllConditionsTrue → runtime.RequiresAllConditions로 복사합니다. /// private static void SetBranchRequiresAll(object branchNode, bool requiresAll) { PropertyInfo prop = branchNode.GetType().GetProperty("RequiresAllConditionsTrue", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); prop?.SetValue(branchNode, requiresAll); } /// /// Branch에 속한 FloatingPortNodeModel의 위치를 설정합니다. /// Branch의 ID와 PortName으로 FloatingPortNodeModel을 찾아 Position을 변경합니다. /// 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; } } /// /// BT 재빌드에 필요한 자산을 지정 경로에서 직접 로드합니다. /// private static T LoadRequiredAsset(string assetPath, string assetLabel) where T : UnityEngine.Object { T asset = AssetDatabase.LoadAssetAtPath(assetPath); if (asset == null) Debug.LogError($"[DrogBTRebuild] {assetLabel} 로드 실패: {assetPath}"); return asset; } /// /// 선택 자산을 로드합니다. 없으면 null을 반환합니다. /// private static T LoadOptionalAsset(string assetPath) where T : UnityEngine.Object { return AssetDatabase.LoadAssetAtPath(assetPath); } /// /// BT 블랙보드에 필요한 변수가 없으면 기본값으로 생성하고, 있으면 기존 값을 유지합니다. /// private static object EnsureBlackboardVariable(string variableName, T defaultValue) { UnityEngine.Object blackboardAsset = AssetDatabase.LoadAllAssetsAtPath(GraphAssetPath) .FirstOrDefault(asset => asset != null && asset.GetType().Name.Contains("BehaviorBlackboardAuthoringAsset", StringComparison.Ordinal)); if (blackboardAsset == null) { Debug.LogError($"[DrogBTRebuild] 블랙보드 에셋을 찾지 못했습니다: {variableName}"); return null; } IList variables = GetBlackboardVariables(blackboardAsset); if (variables == null) { Debug.LogError($"[DrogBTRebuild] 블랙보드 변수 목록을 읽지 못했습니다: {variableName}"); return null; } for (int i = 0; i < variables.Count; i++) { object variable = variables[i]; if (variable == null || GetVariableName(variable) != variableName) continue; return variable; } Type typedVariableModelGeneric = ResolveTypedVariableModelGenericType(variables); if (typedVariableModelGeneric == null) { Debug.LogError($"[DrogBTRebuild] TypedVariableModel<> 타입을 찾지 못했습니다: {variableName}"); return null; } Type typedVariableModelType = typedVariableModelGeneric.MakeGenericType(typeof(T)); object variableModel = Activator.CreateInstance(typedVariableModelType); if (variableModel == null) { Debug.LogError($"[DrogBTRebuild] 블랙보드 변수 생성 실패: {variableName}"); return null; } SetFieldOrPropertyValue(variableModel, "ID", CreateSerializableGuid(typedVariableModelType)); SetFieldOrPropertyValue(variableModel, "Name", variableName); SetFieldOrPropertyValue(variableModel, "IsExposed", true); SetFieldOrPropertyValue(variableModel, "IsShared", false); SetFieldOrPropertyValue(variableModel, "m_Value", defaultValue); SetFieldOrPropertyValue(variableModel, "ObjectValue", defaultValue); variables.Add(variableModel); EditorUtility.SetDirty(blackboardAsset); return variableModel; } /// /// 현재 그래프에서 더 이상 쓰지 않는 블랙보드 변수를 제거합니다. /// private static void RemoveBlackboardVariables(params string[] variableNames) { if (variableNames == null || variableNames.Length == 0) return; UnityEngine.Object blackboardAsset = AssetDatabase.LoadAllAssetsAtPath(GraphAssetPath) .FirstOrDefault(asset => asset != null && asset.GetType().Name.Contains("BehaviorBlackboardAuthoringAsset", StringComparison.Ordinal)); if (blackboardAsset == null) return; IList variables = GetBlackboardVariables(blackboardAsset); if (variables == null) return; HashSet variableNameSet = new HashSet(variableNames, StringComparer.Ordinal); for (int i = variables.Count - 1; i >= 0; i--) { object variable = variables[i]; if (variable == null || !variableNameSet.Contains(GetVariableName(variable))) continue; variables.RemoveAt(i); } EditorUtility.SetDirty(blackboardAsset); } /// /// Branch에 CheckPatternReadyCondition을 부착하고 BossPatternData 에셋을 설정합니다. /// 노드에 패턴명이 표시됩니다 (story의 [Pattern] 치환). /// private static void AttachPatternReadyCondition(object branchNode, BossPatternData pattern, Assembly authoringAssembly) { object condModel = AttachCondition(branchNode, typeof(CheckPatternReadyCondition), authoringAssembly); if (condModel == null) { Debug.LogError("[DrogBTRebuild] CheckPatternReadyCondition 부착 실패"); return; } MethodInfo genericSetField = condModel.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) .FirstOrDefault(method => method.Name == "SetField" && method.IsGenericMethod && method.GetParameters().Length == 2); if (genericSetField == null) { Debug.LogError("[DrogBTRebuild] CheckPatternReadyCondition에서 SetField를 찾지 못했습니다."); return; } MethodInfo closedMethod = genericSetField.MakeGenericMethod(typeof(BossPatternData)); closedMethod.Invoke(condModel, new object[] { "Pattern", pattern }); } private static void SetWeightedPatternFields( object nodeOrCondition, MethodInfo setFieldMethod, BossPatternData pattern1, float weight1, BossPatternData pattern2, float weight2, BossPatternData pattern3, float weight3, BossPatternData pattern4, float weight4, BossPatternData pattern5, float weight5) { if (nodeOrCondition == null) return; MethodInfo genericSetFieldMethod = setFieldMethod; if (genericSetFieldMethod == null || !genericSetFieldMethod.IsGenericMethod) { genericSetFieldMethod = nodeOrCondition.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) .FirstOrDefault(method => method.Name == "SetField" && method.IsGenericMethod && method.GetParameters().Length == 2); } if (genericSetFieldMethod == null) { Debug.LogError("[DrogBTRebuild] 가중치 패턴 필드 설정용 SetField를 찾지 못했습니다."); return; } SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern1", pattern1); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight1", weight1); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern2", pattern2); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight2", weight2); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern3", pattern3); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight3", weight3); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern4", pattern4); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight4", weight4); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Pattern5", pattern5); SetFieldValue(nodeOrCondition, genericSetFieldMethod, "Weight5", weight5); } private static void SetFieldValue(object target, MethodInfo genericSetFieldMethod, string fieldName, object value) { if (target == null || genericSetFieldMethod == null || value == null) return; MethodInfo closedMethod = genericSetFieldMethod.MakeGenericMethod(value.GetType()); closedMethod.Invoke(target, new[] { fieldName, value }); } /// /// 패턴의 MinPhase가 1보다 큰 경우, Branch에 IsMinPhaseSatisfiedCondition을 부착합니다. /// Phase 진입 조건을 BT에서 시각적으로 확인할 수 있습니다. /// 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); } } }