Recently, I encountered a very strange error during the project packaging:
1 | // package log |

I debugged the UE4 code and analyzed the cause and solution process.
First, let’s look at the source code location of the error:
1 | // Runtime\AIModule\Private\BehaviorTree\Decorators\BTDecorator_BlueprintBase.cpp [Line: 67] |
The conditions for entering ensure(ObservedKeyNames.Num() > 0); are:
GetFlowAbortMode() != EBTFlowAbortMode::None;GetFlowAbortMode()retrieves theObserve abortvalue from the FlowControl category of the BehaviorTree Decorator node.bIsObservingBBis true; the default value ofbIsObservingBBisfalse, which is later obtained throughUBTDecorator_BlueprintBase::PostInitProperties()->UBTDecorator_BlueprintBase::InitializeProperties();.
1 | // Runtime\AIModule\Private\BehaviorTree\Decorators\BTDecorator_BlueprintBase.cpp |
This function is used to determine whether the current Decorator has FBlackboardSelectors properties by calling BlueprintNodeHelpers::HasAnyBlackboardSelectors.
1 | // Runtime\AIModule\Private\BehaviorTree\BlueprintNodeHelpers.cpp |
In other words: If the Decorator contains an FBlackboardKeySelector property, bIsObservingBB will be true.
Thus, processing within the condition is straightforward:
1 | // Runtime\AIModule\Private\BehaviorTree\Decorators\BTDecorator_BlueprintBase.cpp [Line: 67] |
It calls BlueprintNodeHelpers::CollectBlackboardSelectors to obtain the list of all FBlackboardKeySelector names from the current Decorator object.
1 | // Runtime\AIModule\Private\BehaviorTree\BlueprintNodeHelpers.cpp |
Since above bIsObservingBB is true, it is expected that a non-empty list can be obtained here, meaning ObservedKeyNames.Num() is certainly >0. If not, it will trigger an assertion. I initially guessed that this issue arose due to inconsistencies between property acquisitions in UBTDecorator_BlueprintBase::InitializeProperties and UBTDecorator_BlueprintBase::PostLoad, as I found that bIsObservingBB was only set in UBTDecorator_BlueprintBase::InitializeProperties.
Thus, I created a function library to mimic BlueprintNodeHelpers::CollectBlackboardSelectors, exposing it to Blueprints for detection, which checks whether the passed Decorator has a BlackboardKey:
1 | // AIModuleFlib.h |
1 | // AIModuleFlib.cpp |
I tested our Decorator in Blueprint (taking BTD_Check_AttackTargetDistance.uasset as an example, which is also the problematic Decorator that fails to package):
However, in this test, I found that our Decorator did not contain a BlackboardKey, which was quite strange, as I found that the variable bIsObservingBB was only set in UBTDecorator_BlueprintBase::InitializeProperties, so it must have been set somewhere else.
Knowing this problem, I created a blank project, added a selector or sequence into an empty BehaviorTree, attached the BTD_Check_AttackTargetDistance.uasset Decorator, and designated a Pawn to use this behavior tree.
Then, I started debugging with breakpoints from engine startup (you can use this method for VS debugging: UE4 and VR Development Tech Notes #VS debugging standalone UE projects).
Ultimately, I found that this Decorator had a problem during serialization:
The call stack is as follows:
Data obtained:
It can be seen that the serialized property bIsObservingBB was set to true.
I suspect that this Decorator‘s uasset resource in Blueprint is problematic. We completely recreated the Decorator from BehaviorTree and copied the existing logic, and after replacing it in the behavior tree, the packaging was successful.
However, I am still unclear about the reason for the inconsistent serialized properties (I suspect that there was an issue with the serialized properties in the uasset), so the solution to this problem is simply to create a new Decorator and copy its implementation.
Those interested in analyzing this problem can download this Decorator (BTD_Check_AttackTargetDistance.uasset) for debugging.
Moreover, UE’s Blueprints are also resources; while convenient, similar serialization issues can be quite frustrating.
Additionally, UE’s serialization is designed to implement both serialization and deserialization through the same interface:
1 | // Serializer. |
It follows the identical logic, while the difference lies in the type of FArchive being either Loading or Writing, which can be determined through the FArchive‘s IsLoading interface:
1 | bool FArchive::IsLoading() const{} |