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