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 abort
value from the FlowControl category of the BehaviorTree Decorator node.bIsObservingBB
is true; the default value ofbIsObservingBB
isfalse
, 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{} |