claude-code-arcane 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +131 -41
- package/agents/game/ue-blueprint-specialist.md +125 -0
- package/agents/game/ue-gas-specialist.md +133 -0
- package/agents/game/ue-umg-specialist.md +131 -0
- package/dist/cli.js +642 -478
- package/docs/SKILLS-CATALOG.md +42 -3
- package/docs/USER-GUIDE.md +28 -54
- package/hooks/detect-unreal.sh +90 -0
- package/package.json +2 -1
- package/profiles/agile.yaml +1 -1
- package/profiles/ai.yaml +1 -1
- package/profiles/android-native.yaml +1 -1
- package/profiles/audio.yaml +1 -1
- package/profiles/backend-dotnet.yaml +1 -1
- package/profiles/backend-go.yaml +1 -1
- package/profiles/backend-nestjs.yaml +1 -1
- package/profiles/backend-nextjs.yaml +1 -1
- package/profiles/backend-ts.yaml +1 -1
- package/profiles/business.yaml +1 -1
- package/profiles/clevel.yaml +1 -1
- package/profiles/clickup.yaml +1 -1
- package/profiles/core.yaml +1 -1
- package/profiles/database.yaml +1 -1
- package/profiles/design.yaml +1 -1
- package/profiles/docs.yaml +1 -1
- package/profiles/ecommerce.yaml +1 -1
- package/profiles/finance.yaml +1 -1
- package/profiles/flutter.yaml +1 -1
- package/profiles/freelance.yaml +1 -1
- package/profiles/frontend.yaml +1 -1
- package/profiles/infra.yaml +1 -1
- package/profiles/integrations.yaml +1 -1
- package/profiles/ios-native.yaml +1 -1
- package/profiles/jira.yaml +1 -1
- package/profiles/job-hunt.yaml +1 -1
- package/profiles/marketing.yaml +1 -1
- package/profiles/mobile.yaml +1 -1
- package/profiles/regulatory.yaml +1 -1
- package/profiles/second-brain.yaml +1 -1
- package/profiles/security.yaml +1 -1
- package/profiles/self-improving.yaml +1 -1
- package/profiles/statusline.yaml +1 -1
- package/profiles/testing.yaml +1 -1
- package/profiles/unity-design.yaml +1 -1
- package/profiles/unity-dev.yaml +1 -1
- package/profiles/unreal-dev.yaml +97 -0
- package/profiles/visual-novel.yaml +1 -1
- package/rules/gamedev/unreal-code.md +125 -0
- package/skills/arcane-list/SKILL.md +12 -7
- package/skills/install-mcp/SKILL.md +135 -46
- package/skills/install-mcp/references/unreal-operations.md +55 -0
- package/skills/ue-actor-component-architecture/SKILL.md +509 -0
- package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
- package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
- package/skills/ue-ai-navigation/SKILL.md +526 -0
- package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
- package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
- package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
- package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
- package/skills/ue-animation-system/SKILL.md +521 -0
- package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
- package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
- package/skills/ue-async-threading/SKILL.md +433 -0
- package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
- package/skills/ue-async-threading/references/threading-patterns.md +345 -0
- package/skills/ue-audio-system/SKILL.md +527 -0
- package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
- package/skills/ue-blueprints/SKILL.md +134 -0
- package/skills/ue-blueprints/references/communication.md +82 -0
- package/skills/ue-character-movement/SKILL.md +475 -0
- package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
- package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
- package/skills/ue-cpp-foundations/SKILL.md +533 -0
- package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
- package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
- package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
- package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
- package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
- package/skills/ue-data-assets-tables/SKILL.md +527 -0
- package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
- package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
- package/skills/ue-editor-tools/SKILL.md +523 -0
- package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
- package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
- package/skills/ue-game-features/SKILL.md +477 -0
- package/skills/ue-game-features/references/experience-system.md +222 -0
- package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
- package/skills/ue-gameplay-abilities/SKILL.md +518 -0
- package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
- package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
- package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
- package/skills/ue-gameplay-framework/SKILL.md +466 -0
- package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
- package/skills/ue-input-system/SKILL.md +394 -0
- package/skills/ue-input-system/references/cpp-setup.md +131 -0
- package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
- package/skills/ue-input-system/references/input-action-reference.md +731 -0
- package/skills/ue-mass-entity/SKILL.md +476 -0
- package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
- package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
- package/skills/ue-materials-rendering/SKILL.md +526 -0
- package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
- package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
- package/skills/ue-module-build-system/SKILL.md +487 -0
- package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
- package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
- package/skills/ue-naming-conventions/SKILL.md +142 -0
- package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
- package/skills/ue-naming-conventions/references/content-structure.md +83 -0
- package/skills/ue-networking-replication/SKILL.md +452 -0
- package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
- package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
- package/skills/ue-niagara-effects/SKILL.md +531 -0
- package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
- package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
- package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
- package/skills/ue-packaging/SKILL.md +116 -0
- package/skills/ue-physics-collision/SKILL.md +423 -0
- package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
- package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
- package/skills/ue-procedural-generation/SKILL.md +505 -0
- package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
- package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
- package/skills/ue-project-context/SKILL.md +305 -0
- package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
- package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
- package/skills/ue-serialization-savegames/SKILL.md +524 -0
- package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
- package/skills/ue-state-trees/SKILL.md +489 -0
- package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
- package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
- package/skills/ue-testing-debugging/SKILL.md +520 -0
- package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
- package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
- package/skills/ue-ui-umg-slate/SKILL.md +431 -0
- package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
- package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
- package/skills/ue-world-level-streaming/SKILL.md +518 -0
- package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
- package/templates/gamedev/unreal-project-context.md +107 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Behavior Tree Patterns
|
|
2
|
+
|
|
3
|
+
Common, reusable Behavior Tree structures for UE AI. Each pattern shows the tree topology, required Blackboard keys, and key C++ touchpoints from the AIModule source.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Blackboard Keys (Common Set)
|
|
8
|
+
|
|
9
|
+
Define these in your `UBlackboardData` asset as a baseline. Extend per-character:
|
|
10
|
+
|
|
11
|
+
| Key Name | Type | Description |
|
|
12
|
+
|----------|------|-------------|
|
|
13
|
+
| `TargetActor` | Object (AActor) | Current enemy/target |
|
|
14
|
+
| `LastKnownLocation` | Vector | Last confirmed target location |
|
|
15
|
+
| `PatrolLocation` | Vector | Current patrol waypoint |
|
|
16
|
+
| `HomeLocation` | Vector | Spawn/home position |
|
|
17
|
+
| `InvestigateLocation` | Vector | Sound/sight disturbance location |
|
|
18
|
+
| `CoverLocation` | Vector | EQS-found cover position |
|
|
19
|
+
| `IsAlerted` | Bool | Has AI been alerted |
|
|
20
|
+
| `IsInCombat` | Bool | Actively fighting |
|
|
21
|
+
| `AttackCooldown` | Bool | Tag cooldown sentinel (used with BTDecorator_TagCooldown) |
|
|
22
|
+
| `StateFloat` | Float | Generic float (health %, threat score, etc.) |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Pattern 1: Patrol / Chase / Attack
|
|
27
|
+
|
|
28
|
+
Classic three-phase NPC AI. The root Selector tries Combat first (highest priority), then Alerted search, then idle patrol.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Root [Selector]
|
|
32
|
+
├── [Sequence] "Combat" — BTDecorator_Blackboard(TargetActor, IsSet, AbortBoth)
|
|
33
|
+
│ ├── [Service] UpdateTarget (0.3s interval — re-scores nearest hostile)
|
|
34
|
+
│ ├── BTTask_MoveTo (TargetActor key, AcceptRadius=150)
|
|
35
|
+
│ └── BTTask_RunEQSQuery (AttackPositionQuery → CoverLocation)
|
|
36
|
+
│ └── [Sequence] "Attack Sequence"
|
|
37
|
+
│ ├── BTTask_RotateToFaceBBEntry (TargetActor)
|
|
38
|
+
│ └── MyBTTask_FireWeapon
|
|
39
|
+
│ └── BTDecorator_Cooldown (2.0s, AbortSelf)
|
|
40
|
+
│
|
|
41
|
+
├── [Sequence] "Investigate" — BTDecorator_Blackboard(InvestigateLocation, IsSet, AbortBoth)
|
|
42
|
+
│ ├── BTTask_MoveTo (InvestigateLocation, AcceptRadius=100)
|
|
43
|
+
│ ├── BTTask_Wait (2.0s)
|
|
44
|
+
│ └── MyBTTask_ClearInvestigateLocation
|
|
45
|
+
│
|
|
46
|
+
└── [Sequence] "Patrol"
|
|
47
|
+
├── [Service] PickNextPatrolPoint (5.0s interval, calls EQS or patrol spline)
|
|
48
|
+
├── BTTask_MoveTo (PatrolLocation, AcceptRadius=50)
|
|
49
|
+
└── BTTask_Wait (WaitBlackboardTime key="PatrolWait")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Perception Wiring
|
|
53
|
+
|
|
54
|
+
`OnTargetPerceptionUpdated` on the AIController:
|
|
55
|
+
|
|
56
|
+
```cpp
|
|
57
|
+
void AEnemyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
|
|
58
|
+
{
|
|
59
|
+
UBlackboardComponent* BB = GetBlackboardComponent();
|
|
60
|
+
|
|
61
|
+
if (Stimulus.WasSuccessfullySensed())
|
|
62
|
+
{
|
|
63
|
+
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
|
|
64
|
+
{
|
|
65
|
+
BB->SetValueAsObject(TEXT("TargetActor"), Actor);
|
|
66
|
+
BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
|
|
67
|
+
}
|
|
68
|
+
else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>())
|
|
69
|
+
{
|
|
70
|
+
// Only set investigate location if we don't already have a visual target
|
|
71
|
+
if (!BB->GetValueAsObject(TEXT("TargetActor")))
|
|
72
|
+
{
|
|
73
|
+
BB->SetValueAsVector(TEXT("InvestigateLocation"), Stimulus.StimulusLocation);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
// Lost sight — remember last known location
|
|
80
|
+
BB->SetValueAsVector(TEXT("LastKnownLocation"), Stimulus.StimulusLocation);
|
|
81
|
+
// Do NOT clear TargetActor — let the search phase handle it
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Pattern 2: Combat with Cover and Repositioning
|
|
89
|
+
|
|
90
|
+
AI actively seeks cover, attacks from cover, repositions when suppressed.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Root [Selector]
|
|
94
|
+
├── [Sequence] "Take Cover" — BTDecorator_Blackboard(IsInCombat, IsSet, AbortBoth)
|
|
95
|
+
│ ├── [Service] RunEQSCoverService (1.0s — updates CoverLocation)
|
|
96
|
+
│ ├── BTTask_MoveTo (CoverLocation, AcceptRadius=80)
|
|
97
|
+
│ └── [Selector] "Attack or Wait"
|
|
98
|
+
│ ├── [Sequence] "Peek and Shoot"
|
|
99
|
+
│ │ ├── BTDecorator_Blackboard(TargetActor, IsSet)
|
|
100
|
+
│ │ ├── BTTask_RotateToFaceBBEntry (TargetActor)
|
|
101
|
+
│ │ └── MyBTTask_FireWeapon
|
|
102
|
+
│ │ └── BTDecorator_Cooldown (1.5s)
|
|
103
|
+
│ └── BTTask_Wait (1.0s) ← suppress-wait when no LoS
|
|
104
|
+
│
|
|
105
|
+
└── [Sequence] "Find Enemy"
|
|
106
|
+
├── BTTask_RunEQSQuery (LastKnownLocationSearch)
|
|
107
|
+
├── BTTask_MoveTo (PatrolLocation, AcceptRadius=100)
|
|
108
|
+
└── BTTask_Wait (3.0s)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Cover Query Service (C++)
|
|
112
|
+
|
|
113
|
+
```cpp
|
|
114
|
+
// BTService_FindCover.cpp
|
|
115
|
+
void UBTService_FindCover::TickNode(
|
|
116
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
|
117
|
+
{
|
|
118
|
+
AAIController* Controller = OwnerComp.GetAIOwner();
|
|
119
|
+
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
|
120
|
+
|
|
121
|
+
if (!CoverQuery) { return; }
|
|
122
|
+
|
|
123
|
+
FEnvQueryRequest Request(CoverQuery, Controller);
|
|
124
|
+
Request.Execute(EEnvQueryRunMode::RandomBest25Pct,
|
|
125
|
+
FQueryFinishedSignature::CreateWeakLambda(Controller,
|
|
126
|
+
[BB](TSharedPtr<FEnvQueryResult> Result)
|
|
127
|
+
{
|
|
128
|
+
if (Result.IsValid() && Result->IsSuccessful())
|
|
129
|
+
{
|
|
130
|
+
BB->SetValueAsVector(TEXT("CoverLocation"), Result->GetItemAsLocation(0));
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Pattern 3: Flee (Health Threshold Trigger)
|
|
139
|
+
|
|
140
|
+
AI retreats when health drops below a threshold, uses EQS to find a safe escape point.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
Root [Selector]
|
|
144
|
+
├── [Sequence] "Flee" — MyBTDecorator_HealthBelow(30%, AbortBoth)
|
|
145
|
+
│ ├── BTTask_RunEQSQuery (FleePointQuery → PatrolLocation)
|
|
146
|
+
│ │ ← FleePointQuery: DonutGenerator(self, minR=500, maxR=2000)
|
|
147
|
+
│ │ + Trace test (not visible from TargetActor location)
|
|
148
|
+
│ │ + Pathfinding test (reachable)
|
|
149
|
+
│ ├── BTTask_MoveTo (PatrolLocation, AcceptRadius=100, AllowPartialPath=true)
|
|
150
|
+
│ └── BTTask_Wait (5.0s) ← catch breath before re-evaluating
|
|
151
|
+
│
|
|
152
|
+
└── [Sequence] "Normal Combat" (same as Pattern 1 combat branch)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Health Threshold Decorator (C++)
|
|
156
|
+
|
|
157
|
+
```cpp
|
|
158
|
+
// BTDecorator_HealthBelow.h
|
|
159
|
+
UCLASS()
|
|
160
|
+
class UBTDecorator_HealthBelow : public UBTDecorator
|
|
161
|
+
{
|
|
162
|
+
GENERATED_BODY()
|
|
163
|
+
public:
|
|
164
|
+
UBTDecorator_HealthBelow() { bAllowAbortLowerPri = true; bAllowAbortChildNodes = true; }
|
|
165
|
+
|
|
166
|
+
UPROPERTY(EditAnywhere, Category = "Condition", meta = (ClampMin = 0, ClampMax = 1))
|
|
167
|
+
float HealthThresholdNormalized = 0.3f;
|
|
168
|
+
|
|
169
|
+
protected:
|
|
170
|
+
virtual bool CalculateRawConditionValue(
|
|
171
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override
|
|
172
|
+
{
|
|
173
|
+
APawn* Pawn = OwnerComp.GetAIOwner()->GetPawn();
|
|
174
|
+
IMyHealthInterface* HealthInterface = Cast<IMyHealthInterface>(Pawn);
|
|
175
|
+
if (!HealthInterface) { return false; }
|
|
176
|
+
return HealthInterface->GetNormalizedHealth() < HealthThresholdNormalized;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Pattern 4: Investigate Sound / Disturbance
|
|
184
|
+
|
|
185
|
+
AI pauses current activity, moves to sound source, looks around, then resumes.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Root [Selector]
|
|
189
|
+
├── [Sequence] "Investigate" — BTDecorator_Blackboard(InvestigateLocation, IsSet, AbortBoth)
|
|
190
|
+
│ ├── BTTask_MoveTo (InvestigateLocation, AcceptRadius=100)
|
|
191
|
+
│ ├── MyBTTask_LookAround (rotates pawn 360° over 3s)
|
|
192
|
+
│ │ └── BTDecorator_TimeLimit (5.0s) ← abort if takes too long
|
|
193
|
+
│ └── MyBTTask_ClearBBKey (InvestigateLocation)
|
|
194
|
+
│ ← clears BB so decorator deactivates and lower priority resumes
|
|
195
|
+
│
|
|
196
|
+
├── [Sequence] "Combat" (as before)
|
|
197
|
+
│
|
|
198
|
+
└── [Sequence] "Patrol" (as before)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### LookAround Task (C++)
|
|
202
|
+
|
|
203
|
+
```cpp
|
|
204
|
+
// BTTask_LookAround.cpp
|
|
205
|
+
EBTNodeResult::Type UBTTask_LookAround::ExecuteTask(
|
|
206
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
|
207
|
+
{
|
|
208
|
+
auto* Mem = CastInstanceNodeMemory<FBTLookAroundMemory>(NodeMemory);
|
|
209
|
+
Mem->ElapsedTime = 0.f;
|
|
210
|
+
Mem->StartYaw = OwnerComp.GetAIOwner()->GetPawn()->GetActorRotation().Yaw;
|
|
211
|
+
return EBTNodeResult::InProgress;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
void UBTTask_LookAround::TickTask(
|
|
215
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
|
216
|
+
{
|
|
217
|
+
auto* Mem = CastInstanceNodeMemory<FBTLookAroundMemory>(NodeMemory);
|
|
218
|
+
Mem->ElapsedTime += DeltaSeconds;
|
|
219
|
+
|
|
220
|
+
float NewYaw = Mem->StartYaw + (Mem->ElapsedTime / LookDuration) * 360.f;
|
|
221
|
+
OwnerComp.GetAIOwner()->SetFocalPoint(
|
|
222
|
+
OwnerComp.GetAIOwner()->GetPawn()->GetActorLocation() +
|
|
223
|
+
FRotator(0.f, NewYaw, 0.f).Vector() * 500.f);
|
|
224
|
+
|
|
225
|
+
if (Mem->ElapsedTime >= LookDuration)
|
|
226
|
+
{
|
|
227
|
+
OwnerComp.GetAIOwner()->ClearFocus(EAIFocusPriority::Gameplay);
|
|
228
|
+
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Pattern 5: Patrol Along Spline
|
|
236
|
+
|
|
237
|
+
Looping patrol along a spline, with stop duration at each point.
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
Root [Sequence]
|
|
241
|
+
└── [Sequence + BTDecorator_Loop(NumLoops=0)] "Patrol Loop"
|
|
242
|
+
├── MyBTTask_GetNextSplinePoint (writes PatrolLocation + PatrolWaitTime)
|
|
243
|
+
├── BTTask_MoveTo (PatrolLocation, AcceptRadius=50)
|
|
244
|
+
└── BTTask_WaitBlackboardTime (BlackboardKey=PatrolWaitTime)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### GetNextSplinePoint Task (C++)
|
|
248
|
+
|
|
249
|
+
```cpp
|
|
250
|
+
EBTNodeResult::Type UBTTask_GetNextSplinePoint::ExecuteTask(
|
|
251
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
|
252
|
+
{
|
|
253
|
+
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
|
254
|
+
AAIController* Controller = OwnerComp.GetAIOwner();
|
|
255
|
+
|
|
256
|
+
if (!IsValid(PatrolSplineActor)) { return EBTNodeResult::Failed; }
|
|
257
|
+
|
|
258
|
+
USplineComponent* Spline = PatrolSplineActor->FindComponentByClass<USplineComponent>();
|
|
259
|
+
if (!Spline) { return EBTNodeResult::Failed; }
|
|
260
|
+
|
|
261
|
+
int32 NumPoints = Spline->GetNumberOfSplinePoints();
|
|
262
|
+
int32 CurrentIdx = BB->GetValueAsInt(TEXT("PatrolIndex"));
|
|
263
|
+
int32 NextIdx = (CurrentIdx + 1) % NumPoints;
|
|
264
|
+
|
|
265
|
+
BB->SetValueAsInt(TEXT("PatrolIndex"), NextIdx);
|
|
266
|
+
BB->SetValueAsVector(TEXT("PatrolLocation"),
|
|
267
|
+
Spline->GetLocationAtSplinePoint(NextIdx, ESplineCoordinateSpace::World));
|
|
268
|
+
BB->SetValueAsFloat(TEXT("PatrolWaitTime"), WaitTimeAtPoint);
|
|
269
|
+
|
|
270
|
+
return EBTNodeResult::Succeeded;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Pattern 6: Squad AI with Shared Blackboard
|
|
277
|
+
|
|
278
|
+
Multiple AI agents share awareness via instance-synced blackboard keys.
|
|
279
|
+
|
|
280
|
+
**Setup**: Mark `TargetActor` and `AlertLevel` as "Instance Synced" in `UBlackboardData`.
|
|
281
|
+
|
|
282
|
+
When any squad member's AI controller updates these keys, `UAISystem` propagates the change to all blackboards sharing the same asset (see `UBlackboardComponent::SetValue` template for the sync loop).
|
|
283
|
+
|
|
284
|
+
```cpp
|
|
285
|
+
// Squad leader sets target → all squad members receive it
|
|
286
|
+
void ASquadLeaderAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
|
|
287
|
+
{
|
|
288
|
+
if (Stimulus.WasSuccessfullySensed())
|
|
289
|
+
{
|
|
290
|
+
// Setting a synced key broadcasts to all squad members sharing this BB asset
|
|
291
|
+
GetBlackboardComponent()->SetValueAsObject(TEXT("TargetActor"), Actor);
|
|
292
|
+
GetBlackboardComponent()->SetValueAsFloat(TEXT("AlertLevel"), 1.0f);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The BT on each squad member reacts independently to the shared `TargetActor` key via `BTDecorator_Blackboard`.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Pattern 7: Message-Driven Task (WaitForMessage)
|
|
302
|
+
|
|
303
|
+
Used when a task needs to wait for an external event (animation notify, ability end, timer) rather than polling.
|
|
304
|
+
|
|
305
|
+
```cpp
|
|
306
|
+
EBTNodeResult::Type UBTTask_PlayMontage::ExecuteTask(
|
|
307
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
|
308
|
+
{
|
|
309
|
+
ACharacter* Character = Cast<ACharacter>(OwnerComp.GetAIOwner()->GetPawn());
|
|
310
|
+
if (!Character || !MontageToPlay) { return EBTNodeResult::Failed; }
|
|
311
|
+
|
|
312
|
+
// Register to receive a specific message name
|
|
313
|
+
WaitForMessage(OwnerComp, TEXT("MontageCompleted"));
|
|
314
|
+
WaitForMessage(OwnerComp, TEXT("MontageFailed"));
|
|
315
|
+
|
|
316
|
+
// Start the montage
|
|
317
|
+
Character->GetMesh()->GetAnimInstance()->Montage_Play(MontageToPlay);
|
|
318
|
+
|
|
319
|
+
return EBTNodeResult::InProgress;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
void UBTTask_PlayMontage::OnMessage(
|
|
323
|
+
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
|
324
|
+
FName Message, int32 RequestID, bool bSuccess)
|
|
325
|
+
{
|
|
326
|
+
StopWaitingForMessages(OwnerComp);
|
|
327
|
+
FinishLatentTask(OwnerComp,
|
|
328
|
+
(Message == TEXT("MontageCompleted")) ? EBTNodeResult::Succeeded : EBTNodeResult::Failed);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Sender side (e.g., AnimNotify or ability):
|
|
332
|
+
// UAIBlueprintHelperLibrary::SendAIMessage(Pawn, TEXT("MontageCompleted"), nullptr, true);
|
|
333
|
+
// Or from C++:
|
|
334
|
+
// UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(Controller->GetBrainComponent());
|
|
335
|
+
// if (BTComp) { BTComp->HandleMessage(FAIMessage(TEXT("MontageCompleted"), nullptr, true)); }
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Node Memory Pattern
|
|
341
|
+
|
|
342
|
+
For tasks that store per-instance runtime data (not shared across AI using the same tree asset):
|
|
343
|
+
|
|
344
|
+
```cpp
|
|
345
|
+
struct FBTMyTaskMemory
|
|
346
|
+
{
|
|
347
|
+
FAIRequestID MoveRequestID;
|
|
348
|
+
float ElapsedTime = 0.f;
|
|
349
|
+
TWeakObjectPtr<UMyComponent> CachedComp;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
UCLASS()
|
|
353
|
+
class UBTTask_MyTask : public UBTTaskNode
|
|
354
|
+
{
|
|
355
|
+
// ...
|
|
356
|
+
virtual uint16 GetInstanceMemorySize() const override
|
|
357
|
+
{
|
|
358
|
+
return sizeof(FBTMyTaskMemory);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// In ExecuteTask:
|
|
363
|
+
FBTMyTaskMemory* Mem = CastInstanceNodeMemory<FBTMyTaskMemory>(NodeMemory);
|
|
364
|
+
new(Mem) FBTMyTaskMemory(); // placement-new to initialize
|
|
365
|
+
Mem->ElapsedTime = 0.f;
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Decorator Abort Modes
|
|
371
|
+
|
|
372
|
+
| FlowAbortMode | Behavior |
|
|
373
|
+
|---------------|----------|
|
|
374
|
+
| `None` | Decorator never aborts running tasks |
|
|
375
|
+
| `Self` | Aborts tasks within its own subtree if condition changes |
|
|
376
|
+
| `LowerPriority` | Aborts lower-priority branches when condition becomes true |
|
|
377
|
+
| `Both` | Combination of Self and LowerPriority |
|
|
378
|
+
|
|
379
|
+
Most reactive decorators (Blackboard, CanSeeTarget) should use `Both` so the tree re-evaluates when the condition changes in either direction.
|
|
380
|
+
|
|
381
|
+
Set via:
|
|
382
|
+
```cpp
|
|
383
|
+
FlowAbortMode = EBTFlowAbortMode::Both;
|
|
384
|
+
bAllowAbortLowerPri = true;
|
|
385
|
+
bAllowAbortChildNodes = true;
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Then call `ConditionalFlowAbort` from external event handlers to trigger re-evaluation:
|
|
389
|
+
```cpp
|
|
390
|
+
// In perception callback or timer:
|
|
391
|
+
ConditionalFlowAbort(OwnerComp, EBTDecoratorAbortRequest::ConditionResultChanged);
|
|
392
|
+
```
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Behavior Trees — Editor-Side Workflow
|
|
2
|
+
|
|
3
|
+
> Adapted from the `unreal-behavior-trees` skill in [awesome-gamedev-agent-skills](https://github.com/gamedev-skills/awesome-gamedev-agent-skills) (Apache-2.0). Cross-skill references were remapped to Arcane slash commands.
|
|
4
|
+
|
|
5
|
+
Author NPC decision-making in UE5 with Behavior Trees driven by a Blackboard: structure the
|
|
6
|
+
tree with composites, gate branches with decorators, keep state current with services, and run
|
|
7
|
+
it from an AIController. Targets **UE 5.8**.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- Use when building enemy/NPC AI: creating a `BT_`/`BB_` asset pair, structuring
|
|
12
|
+
Selector/Sequence branches, adding decorators (conditions) and services (periodic updates),
|
|
13
|
+
writing custom `BTTask`/`BTService` nodes, or wiring an AIController to run the tree.
|
|
14
|
+
- Use when the project has Behavior Tree (`BT_`) and Blackboard (`BB_`) assets and an
|
|
15
|
+
`AAIController`.
|
|
16
|
+
|
|
17
|
+
**When *not* to use:** the *concept* of AI (FSM vs BT vs steering, cross-engine) →
|
|
18
|
+
`game-ai`. Pure navigation/pathing math is engine navmesh (BT's `MoveTo` uses it). Simple
|
|
19
|
+
one-off logic may be cheaper as a small state machine than a full tree.
|
|
20
|
+
|
|
21
|
+
## Core workflow
|
|
22
|
+
|
|
23
|
+
1. **Create the pair:** a Blackboard (`BB_`) holds typed keys (the AI's memory: `TargetActor`,
|
|
24
|
+
`LastKnownLocation`, `bIsInvestigating`); a Behavior Tree (`BT_`) references that Blackboard.
|
|
25
|
+
2. **Possess and run.** An `AAIController` possesses the pawn and calls `RunBehaviorTree(BT)`,
|
|
26
|
+
which also initializes the referenced Blackboard.
|
|
27
|
+
3. **Structure with composites.** **Selector** runs children left→right until one *succeeds*
|
|
28
|
+
(priority/fallback: "attack, else chase, else patrol"). **Sequence** runs children until one
|
|
29
|
+
*fails* (do-all: "move to cover → reload → peek"). **Simple Parallel** runs one main task
|
|
30
|
+
alongside a secondary.
|
|
31
|
+
4. **Gate branches with Decorators** that read Blackboard keys (e.g. "Has Target?" guards the
|
|
32
|
+
combat branch). Set **Observer Aborts** so the tree re-evaluates when the key changes.
|
|
33
|
+
5. **Keep the Blackboard current with Services** attached to a branch — they tick periodically
|
|
34
|
+
(e.g. update `TargetActor` via a sight check) only while that branch is active.
|
|
35
|
+
6. **Do work in Tasks**, which return `Succeeded`, `Failed`, or `InProgress` (latent tasks like
|
|
36
|
+
`MoveTo` finish later).
|
|
37
|
+
7. **Verify** with the Behavior Tree debugger during PIE — it highlights the running node and
|
|
38
|
+
shows live Blackboard values, so you see exactly which branch executes.
|
|
39
|
+
|
|
40
|
+
## Patterns
|
|
41
|
+
|
|
42
|
+
### 1. AIController that runs the tree (C++)
|
|
43
|
+
|
|
44
|
+
```cpp
|
|
45
|
+
void AEnemyAIController::OnPossess(APawn* InPawn)
|
|
46
|
+
{
|
|
47
|
+
Super::OnPossess(InPawn);
|
|
48
|
+
if (BehaviorTree) // UPROPERTY(EditAnywhere) TObjectPtr<UBehaviorTree>
|
|
49
|
+
RunBehaviorTree(BehaviorTree); // initializes & uses the Blackboard the BT references
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. A priority tree (node structure)
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
ROOT
|
|
57
|
+
└── Selector (try combat, else investigate, else patrol)
|
|
58
|
+
├── Sequence [Decorator: Blackboard 'TargetActor' Is Set, Observer Aborts: Both]
|
|
59
|
+
│ ├── Task: MoveTo (TargetActor) // latent: returns InProgress then Succeeded
|
|
60
|
+
│ └── Task: Attack
|
|
61
|
+
├── Sequence [Decorator: 'LastKnownLocation' Is Set]
|
|
62
|
+
│ ├── Task: MoveTo (LastKnownLocation)
|
|
63
|
+
│ └── Task: Wait (3s) + clear key
|
|
64
|
+
└── Task: Patrol (BTTask_FindPatrolPoint -> MoveTo)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`Observer Aborts: Both` makes the combat branch interrupt patrol the instant `TargetActor` is
|
|
68
|
+
set, and bail out when it's cleared — this is what makes the AI feel reactive.
|
|
69
|
+
|
|
70
|
+
### 3. Updating the Blackboard from code (e.g. on seeing the player)
|
|
71
|
+
|
|
72
|
+
```cpp
|
|
73
|
+
void AEnemyAIController::SetTarget(AActor* Target)
|
|
74
|
+
{
|
|
75
|
+
if (UBlackboardComponent* BB = GetBlackboardComponent())
|
|
76
|
+
BB->SetValueAsObject(TEXT("TargetActor"), Target); // key name must match the BB asset
|
|
77
|
+
}
|
|
78
|
+
// Clear with BB->ClearValue(TEXT("TargetActor")); to drop back to a lower-priority branch.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Pitfalls
|
|
82
|
+
|
|
83
|
+
- **AI never starts** — the pawn isn't possessed (set the Pawn's *Auto Possess AI* to "Placed
|
|
84
|
+
in World or Spawned" and assign the AIController), or `RunBehaviorTree` was never called.
|
|
85
|
+
- **`MoveTo` instantly fails** — no NavMesh in the level (add a Nav Mesh Bounds Volume), or the
|
|
86
|
+
target is off the navmesh.
|
|
87
|
+
- **Branch doesn't react to changes** — the gating Decorator's **Observer Aborts** is set to
|
|
88
|
+
None; set it to Self/Lower Priority/Both so the tree re-evaluates when the key changes.
|
|
89
|
+
- **A task hangs the tree** — a custom task returned `InProgress` and never calls
|
|
90
|
+
`FinishLatentTask`. Always complete latent tasks.
|
|
91
|
+
- **Blackboard key typos** — `SetValueAsObject("Taget", ...)` silently does nothing; match the
|
|
92
|
+
key name and type exactly, or use a cached `FBlackboardKeySelector`.
|
|
93
|
+
- **Sequence vs Selector confusion** — Sequence = AND (stops on first failure); Selector = OR
|
|
94
|
+
(stops on first success). Swapping them inverts the behaviour.
|
|
95
|
+
|
|
96
|
+
## References
|
|
97
|
+
|
|
98
|
+
- For a **custom C++ `UBTTaskNode`** (instant and latent `ExecuteTask` returning `EBTNodeResult`,
|
|
99
|
+
with a `FBlackboardKeySelector`), read `references/custom-bttask.md`.
|
|
100
|
+
- Primary docs: "Behavior Trees in Unreal Engine"
|
|
101
|
+
(`https://dev.epicgames.com/documentation/en-us/unreal-engine/behavior-trees-in-unreal-engine`).
|
|
102
|
+
|
|
103
|
+
## Related skills
|
|
104
|
+
|
|
105
|
+
- `game-ai` — engine-agnostic AI design (FSM, BT, steering, pathfinding choices).
|
|
106
|
+
- `/ue-cpp-foundations` — the AIController and pawn classes in C++.
|
|
107
|
+
- `fps-shooter` / `tower-defense` — genres that compose enemy AI.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
> Adapted from the `unreal-behavior-trees` skill in [awesome-gamedev-agent-skills](https://github.com/gamedev-skills/awesome-gamedev-agent-skills) (Apache-2.0). Cross-skill references were remapped to Arcane slash commands.
|
|
2
|
+
|
|
3
|
+
# Custom BTTask in C++ (UE 5.8)
|
|
4
|
+
|
|
5
|
+
Depth for `/ue-ai-navigation`: writing a custom Behavior Tree task node in C++. Verified
|
|
6
|
+
against the Behavior Trees documentation and the `UBTTaskNode` API. Requires the `AIModule`
|
|
7
|
+
dependency in `*.Build.cs`.
|
|
8
|
+
|
|
9
|
+
## Instant task (returns immediately)
|
|
10
|
+
|
|
11
|
+
```cpp
|
|
12
|
+
// BTTask_ClearTarget.h
|
|
13
|
+
#pragma once
|
|
14
|
+
#include "CoreMinimal.h"
|
|
15
|
+
#include "BehaviorTree/BTTaskNode.h"
|
|
16
|
+
#include "BTTask_ClearTarget.generated.h"
|
|
17
|
+
|
|
18
|
+
UCLASS()
|
|
19
|
+
class MYGAME_API UBTTask_ClearTarget : public UBTTaskNode
|
|
20
|
+
{
|
|
21
|
+
GENERATED_BODY()
|
|
22
|
+
public:
|
|
23
|
+
UBTTask_ClearTarget();
|
|
24
|
+
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp,
|
|
25
|
+
uint8* NodeMemory) override;
|
|
26
|
+
|
|
27
|
+
// Exposes a Blackboard key picker in the BT editor.
|
|
28
|
+
UPROPERTY(EditAnywhere, Category = "Blackboard")
|
|
29
|
+
FBlackboardKeySelector TargetKey;
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```cpp
|
|
34
|
+
// BTTask_ClearTarget.cpp
|
|
35
|
+
#include "BTTask_ClearTarget.h"
|
|
36
|
+
#include "BehaviorTree/BlackboardComponent.h"
|
|
37
|
+
|
|
38
|
+
UBTTask_ClearTarget::UBTTask_ClearTarget()
|
|
39
|
+
{
|
|
40
|
+
NodeName = "Clear Target";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
EBTNodeResult::Type UBTTask_ClearTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp,
|
|
44
|
+
uint8* NodeMemory)
|
|
45
|
+
{
|
|
46
|
+
if (UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent())
|
|
47
|
+
{
|
|
48
|
+
BB->ClearValue(TargetKey.SelectedKeyName);
|
|
49
|
+
return EBTNodeResult::Succeeded; // done now
|
|
50
|
+
}
|
|
51
|
+
return EBTNodeResult::Failed;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`EBTNodeResult::Type` values: `Succeeded`, `Failed`, `Aborted`, `InProgress`.
|
|
56
|
+
|
|
57
|
+
## Latent task (finishes over time)
|
|
58
|
+
|
|
59
|
+
Return `InProgress`, then call `FinishLatentTask` when the work completes (e.g. a timer, an
|
|
60
|
+
async query, or a delegate callback).
|
|
61
|
+
|
|
62
|
+
```cpp
|
|
63
|
+
EBTNodeResult::Type UBTTask_WaitRandom::ExecuteTask(UBehaviorTreeComponent& OwnerComp,
|
|
64
|
+
uint8* NodeMemory)
|
|
65
|
+
{
|
|
66
|
+
AAIController* AICon = OwnerComp.GetAIOwner();
|
|
67
|
+
if (!AICon) return EBTNodeResult::Failed;
|
|
68
|
+
|
|
69
|
+
const float Delay = FMath::FRandRange(MinTime, MaxTime);
|
|
70
|
+
|
|
71
|
+
// Schedule completion; capture the component so we can finish the right task instance.
|
|
72
|
+
FTimerHandle Handle;
|
|
73
|
+
AICon->GetWorldTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda(
|
|
74
|
+
[&OwnerComp, this]()
|
|
75
|
+
{
|
|
76
|
+
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); // resumes the tree
|
|
77
|
+
}), Delay, /*loop*/ false);
|
|
78
|
+
|
|
79
|
+
return EBTNodeResult::InProgress; // tree waits on this node until FinishLatentTask
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If a latent task can be aborted by the tree (e.g. a higher-priority branch takes over),
|
|
84
|
+
override `AbortTask` to cancel timers/queries and return `EBTNodeResult::Aborted`.
|
|
85
|
+
|
|
86
|
+
## Reading and writing typed Blackboard values
|
|
87
|
+
|
|
88
|
+
```cpp
|
|
89
|
+
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
|
90
|
+
AActor* Target = Cast<AActor>(BB->GetValueAsObject(TargetKey.SelectedKeyName));
|
|
91
|
+
FVector Loc = BB->GetValueAsVector(TEXT("LastKnownLocation"));
|
|
92
|
+
BB->SetValueAsBool(TEXT("bIsInvestigating"), true);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
|
|
97
|
+
- Set `NodeName` in the constructor so the node reads clearly in the BT graph.
|
|
98
|
+
- `OwnerComp.GetAIOwner()` gives the `AAIController`; `GetAIOwner()->GetPawn()` the controlled
|
|
99
|
+
pawn — use these instead of caching world pointers.
|
|
100
|
+
- For nodes that need per-instance memory (counters, handles), override `GetInstanceMemorySize`
|
|
101
|
+
and use the `NodeMemory` block; for simple cases, member UPROPERTYs on a non-instanced node
|
|
102
|
+
are shared across all uses, so prefer instance memory or instanced nodes when state matters.
|
|
103
|
+
- A custom **Service** subclasses `UBTService` and overrides `TickNode`; a custom **Decorator**
|
|
104
|
+
subclasses `UBTDecorator` and overrides `CalculateRawConditionValue`.
|