Hi there again!
First things first: If my assumptions are wrong this might not be a bug.
The problem:
To my understanding the transaction sytstem should be used as follows:
- Start a transaction (they can be nested)
- Call modify() on objects that are about to be modified (So they get saved in the transaction)
- Set newly created objects to pending kill and call modify on them, then reset the kill flag
- Actually modify the objects
- Close the transaction
However sometimes the editor modifies relevant UObjects outside of the outermost transaction.
This is bad because there is no way to tell then if an object got modified by a tool and what part got modified which gives people who write editor extensions like myself a hard time.
An example:
The OnBasicShapeCreated Lambda in ComponentTypeRegistry.cpp modifies the material of a component outside of a transaction:
const auto OnBasicShapeCreated = [](UActorComponent* Component)
{
UStaticMeshComponent* SMC = Cast<UStaticMeshComponent>(Component);
if (SMC)
{
const FString MaterialName = TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial");
UMaterial* MaterialAsset = FindOrLoadObject<UMaterial>(MaterialName);
SMC->SetMaterial(0, MaterialAsset);
// If the component object is an archetype (template), propagate the material setting to any instances, as instances
// of the archetype will end up being created BEFORE we are able to set the override material on the template object.
if (SMC->HasAnyFlags(RF_ArchetypeObject))
{
TArray<UObject*> ArchetypeInstances;
SMC->GetArchetypeInstances(ArchetypeInstances);
for (UObject* ArchetypeInstance : ArchetypeInstances)
{
CastChecked<UStaticMeshComponent>(ArchetypeInstance)->SetMaterial(0, MaterialAsset);
}
}
}
};
This specific issue can be easily resolved by adding a FScopedTransaction before this bit in SComponentClassCombo.cpp:
UActorComponent* NewActorComponent = OnComponentClassSelected.Execute(ComponentClass, InItem->GetComponentCreateAction(), InItem->GetAssetOverride());
if(NewActorComponent)
{
InItem->GetOnComponentCreated().ExecuteIfBound(NewActorComponent);
}
However since all of these issues require a specific fix they can be hard to locate.
How to find issues of this kind:
I found an easy way to check whether an object gets modified outside of a transaction:
- Create delegates in the UTransactor class that fire whenever a transaction starts and ends.
- Keep track of all the UObjects that get modified during this transaction
- At the end of the outermost transaction serialize all UProperties of these UObjects into a StringArray
- On Tick compare the StringArray to the current property values of the UObjects by serializing them again
- If they mismatch the object got modified outside of the transaction
Fixing these issues would help a great deal in making the UE4 codebase more consistent and more accessible for everyone!
Thanks a lot!