- 드로그 BT를 페이즈 전환, 부활 트리거, 가중치 근접 패턴 중심으로 재구성 - 땅 울리기 및 콤보-기본기1_3 패턴/스킬/이펙트를 추가하고 기존 평타 파생 자산을 정리 - 드로그 행동 검증용 PlayMode/Editor 테스트와 관련 런타임 상태 추적을 추가
1401 lines
81 KiB
C#
1401 lines
81 KiB
C#
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.Enemy;
|
|
using Colosseum.Skills;
|
|
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Colosseum.Editor
|
|
{
|
|
/// <summary>
|
|
/// 드로그 Behavior Graph authoring 자산을 현재 BT 우선순위 구조로 재생성합니다.
|
|
/// Check 노드는 ConditionalGuardAction + Condition 조합으로 구현됩니다.
|
|
/// </summary>
|
|
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 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 float DefaultComboPatternWeight = 26f;
|
|
private const float DefaultPressurePatternWeight = 24f;
|
|
private const float DefaultPrimaryPatternWeight = 22f;
|
|
private const float DefaultSecondaryPatternWeight = 16f;
|
|
private const float DefaultTertiaryPatternWeight = 12f;
|
|
|
|
[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<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 = EnsureBlackboardVariable<GameObject>("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<BossPatternData>(DefaultPunishPatternPath, "밟기 패턴");
|
|
BossPatternData signaturePattern = LoadRequiredAsset<BossPatternData>(DefaultSignaturePatternPath, "집행 패턴");
|
|
BossPatternData groundShakePattern = LoadRequiredAsset<BossPatternData>(DefaultGroundShakePatternPath, "땅 울리기 패턴");
|
|
BossPatternData mobilityPattern = LoadRequiredAsset<BossPatternData>(DefaultMobilityPatternPath, "도약 패턴");
|
|
BossPatternData primaryPattern = LoadRequiredAsset<BossPatternData>(DefaultPrimaryPatternPath, "콤보-기본기1 패턴");
|
|
BossPatternData secondaryPattern = LoadRequiredAsset<BossPatternData>(DefaultSecondaryPatternPath, "콤보-기본기2 패턴");
|
|
BossPatternData tertiaryPattern = LoadRequiredAsset<BossPatternData>(DefaultTertiaryPatternPath, "콤보-기본기3 패턴");
|
|
BossPatternData comboPattern = LoadRequiredAsset<BossPatternData>(DefaultComboPatternPath, "콤보-강타 패턴");
|
|
BossPatternData pressurePattern = LoadRequiredAsset<BossPatternData>(DefaultPressurePatternPath, "콤보-발구르기 패턴");
|
|
BossPatternData utilityPattern = LoadRequiredAsset<BossPatternData>(DefaultUtilityPatternPath, "투척 패턴");
|
|
SkillData phase3TransitionSkill = LoadRequiredAsset<SkillData>(DefaultPhase3TransitionSkillPath, "포효 스킬");
|
|
|
|
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 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));
|
|
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,
|
|
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);
|
|
ConnectChildren(graphAsset, connectEdgeMethod, punishSequence, punishSelectNode, punishUseNode);
|
|
|
|
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);
|
|
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 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));
|
|
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,
|
|
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);
|
|
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);
|
|
ConnectChildren(graphAsset, connectEdgeMethod, leapSequence, leapSelectNode, leapValidateNode, leapUseNode);
|
|
|
|
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);
|
|
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 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, 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);
|
|
ConnectChildren(graphAsset, connectEdgeMethod, meleeSequence, meleeValidateNode, meleeUseNode);
|
|
|
|
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<object>
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
/// 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}");
|
|
}
|
|
}
|
|
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
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);
|
|
}
|
|
|
|
/// <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>
|
|
/// BT 재빌드에 필요한 자산을 지정 경로에서 직접 로드합니다.
|
|
/// </summary>
|
|
private static T LoadRequiredAsset<T>(string assetPath, string assetLabel) where T : UnityEngine.Object
|
|
{
|
|
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
|
|
if (asset == null)
|
|
Debug.LogError($"[DrogBTRebuild] {assetLabel} 로드 실패: {assetPath}");
|
|
|
|
return asset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 선택 자산을 로드합니다. 없으면 null을 반환합니다.
|
|
/// </summary>
|
|
private static T LoadOptionalAsset<T>(string assetPath) where T : UnityEngine.Object
|
|
{
|
|
return AssetDatabase.LoadAssetAtPath<T>(assetPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// BT 블랙보드에 필요한 변수가 없으면 기본값으로 생성하고, 있으면 기존 값을 유지합니다.
|
|
/// </summary>
|
|
private static object EnsureBlackboardVariable<T>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 그래프에서 더 이상 쓰지 않는 블랙보드 변수를 제거합니다.
|
|
/// </summary>
|
|
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<string> variableNameSet = new HashSet<string>(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);
|
|
}
|
|
|
|
/// <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 부착 실패");
|
|
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<T>를 찾지 못했습니다.");
|
|
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<T>를 찾지 못했습니다.");
|
|
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 });
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
|
|
}
|
|
}
|