What the actual heck is going on behind the curtains of an Actor Blueprint Editor?

For last week, I’ve been working on an Actor Blueprint which I’ll use do design some action and bake it into an asset. Thus I use virtual functions like OnConstruction, PostEditChangeProperty and etc…
While I was using them I was having weird bugs and crashes about them. For example, I was inserting an element to a TArray which index is out of bounds at OnConstruction script. Normally, insert function should resize the array if index is greater than arraynum. So when OnConstruction is called after a Property change (like after I change a value in details pane) it works as intended. But if I press the compile button, that insert will give me an access violation error at the memmove function.

While trying to debug what is going on I realized OnConstruction script and other kind of scripts do not get called for once. At least from what I understand when I open an Actor Blueprint Editor, there are like 3-4 instances of the Actor which has different flags and properties.

So these are the calls for OnConstruction function when you first open an Actor Blueprint Editor from content browser. I may have misremembering the actual function calls amount but you will get the point.
2 OnConstruction calls made. One call cames from Thumbnail (somehow) and spawns a preview actor. Other one comes from some kind of Blueprint base class and spawns a preview actor. Both of those have Package Names as Engine/Transient and their Package folder name is none.

These are the calls for OnConstruction function when you change a property from details panel. I may have misremembering the actual function calls amount but you will get the point.
5 OnConstruction calls are made. The all of the package flags and properties are same for all of them. Their difference occurs at their object flag. 2(3?) of them has only RF_Transactional flag. 3 (2?) of them has RF_Transactional and RF_Transient. Also 1 one of them only gets called when the Viewport tab is open on Editor. I believe that is the actor object that responsible to get displayed in the viewport (I have no idea why they needed to separately create it).

These are the calls for OnConstruction function when you when you press compile button. I may have misremembering the actual function calls amount but you will get the point.
First 2 OnConstruction function calls occurs. Both of them have only RF_Transactional flags. Later “Compact Hash” log gets displayed on screen. Which makes me think that those 2 OnConstruction calls are the objects that are created to get held in Transaction buffer before compilation.
Later 5 OnConstruction calls occurs again. That 5 calls are exactly same with 5 calls that are occured when you change a property from details panel. But 2 of that 5 calls are called at the previous objects that I think that they may be created for transaction buffer. I am certain about that because I hold a counter integer for all calls.

Other than that I think the reason of 2 number repating is one call is for CDO and one call is for the instance we see in editor.

Also another weird thing is when I call GetPackage() and reach some package properties on all of that OnConstruction calls, all them have Package Name as “Engine/Transient” and IsPackageEmpty() returns True for all of them.
But when I handle that Package checks from PostEditChangeProperty, one more package appears with the PackageName as the path of the asset. Also that package is not empty and the “this” object that responsible that PostEditChangeProperty call has flags as RF_Public, RF_MarkAsNative, RF_ClassDefaultObject, RF_WasLoaded and RF_LoadCompleted.

So what the heck? What is actually going on behind these calls? How does this makes sense? What would be the safest way to do any checks before OnConstruction or other virtual functions? Thank you for your time and interest!

First, the engine will create multiple instances of any class you define. The first instance is likely the Class Default Object (CDO) and after that, there are instances created by the editor – as you found out, for example, to render the object to the thumbnail in the content browser. This is how the editor works. When you change the object and apply/compile the changes, the thumbnail needs to be updated, so a new instance will be created and rendered, and so on.

You should expect your actor class to be instantiated many times, especially in the editor, but even at runtime, you will always get the CDO instance in addition to any other instances your game manually creates. If this is not what you want, then perhaps your object should be something with a defined lifetime instead, like a Subsystem?

Second, virtual functions should all be “safe” by the time the blueprint system is up and running. If you get crashes, it’s perhaps possible that you’re trying to call methods on a null object? Blueprints generally should turn this into a non-crashing event, but the construction script is a little special because it runs so early in the cycle, that perhaps there’s some problem there. An actual reproduction step, perhaps with a screen shot of the crash, would help explain what’s going on.

For example, here’s a repeatable set of steps:

  1. Create Blueprint Class
  2. Subclass Actor
  3. Add a Array of float Variable
  4. Implement the Construction Script
  5. In the OnConstruction method, call Set() on the Array variable

image

This, of course, works fine, so there’s something else going on in your code.

Maybe you’re using C++? If so, what happens when you put a breakpoint in your overridden function in the debugger, and step through all the arguments?
Are you making sure to call Super in your method before doing your own things?

1 Like

Thank you so much. Now I understand a bit better about how packages work. For the Insert crash thing, I was using C++ for the insert crash. I tried to recreate the problem quickly from my home with a new project and it seems like it causes a crash too. Here are the steps that I followed. I am using 4.27

  1. A new game project created using c++
  2. Created a new class called TestActor that is derived from AActor.
  3. Created 2 members marked as UPROPERTY(EditAnywhere). One of them is FTransform TestTransform and other one is uint8 TestIndex.
  4. Created another member marked as UPROPERTY(VisibleAnywhere) that is a TArray TestArray.
  5. I declare the virtual void OnConstruction(const FTransform& Transform) override; in my header. Here is what my header looks like :
UCLASS(Blueprintable)
class ATestActor : public AActor
{
	GENERATED_BODY()

	ATestActor();
protected:
	virtual void OnConstruction(const FTransform& Transform) override;

public:
	UPROPERTY(EditAnywhere)
	FTransform TestTransform;
	UPROPERTY(EditAnywhere)
	uint8 TestIndex;
	UPROPERTY(VisibleAnywhere)
	TArray<FTransform> TestArray;
};
  1. In the .cpp unit I initialize default properties in c++ construtction like that :
ATestActor::ATestActor()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	TestIndex = 0;
	TestArray = TArray<FTransform>();
}
  1. In the .cpp unit I log the package name and insert the TestTransform to TestArray at the index of TestIndex. This is what is the definition of OnConstruction looks like:
void ATestActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

	UE_LOG(LogTemp, Warning, TEXT("Package Name %s"), *(GetPackage()->GetName()));
	TestArray.Insert(TestTransform, TestIndex);
}
  1. After compiling the project while editor is closed, I open the project and create a new bleuprint called TestBlueprint from my TestActor class.
  2. I add a cube as a component to actor blueprint to make it create a thumbnail for the TestBlueprint. I also set the show floor enabled. (Thumbnail is important i believe, later I explained below).
  3. Then I close the blueprint editor and reopen it.
  4. I increase the TestIndex to 1. When I increase it due to the value change I guess OnConstruction is triggered. These are the logs that I get when I make a single change.
LogTemp: Warning: Package Name /Engine/Transient
LogTemp: Warning: Package Name /Engine/Transient
LogTemp: Warning: Package Name /Engine/Transient
LogTemp: Warning: Package Name /Engine/Transient
LogTemp: Warning: Package Name /Engine/Transient
  1. Then I hit the compile button of Actor Blueprint Editor and I got a crash. This is the screenshot of what riders shows when it is in debug mode.

I put a break point to TestArray.Insert(TestTransform, TestIndex). I realized inside of InsertUninitializedImpl fuction it does a check to call ResizeGrow function. here is the code for it :

		const SizeType OldNum = ArrayNum;
		if ((ArrayNum += Count) > ArrayMax)
		{
			ResizeGrow(OldNum);
		}

At the one of the packages when it calls this function, (ArrayNum += Count) > ArrayMax condition turns out to be false. Thus it does not call the ResizeGrow(OldNum) function. Later it gives crash at the memmove function which is defined at vcruntime_string.h.

About the Thumbnail thing, if I follow the exact steps except adding a cube to blueprint as a component, it does not gives a crash for the first time. When I increase the TestIndex and press compile it works fine. But later when I close the blueprint editor and try to reopen it, it does not open anymore. So could it be that unreal tries to generate a thumbnail when I try to open the blueprint editor for the second time and that thumbnail instance causes the crash? I am really confused about this problem…

I am really grateful to you for your time and interest. Thank you so much!

Okay I tried to do some more debug about what and where one of those functions are called and I got more confused. Here is the code that I wrote to print these logs:

I have added these macros to header for simplicity:

#define LOG_BOOL(X) UE_LOG(LogTemp, Warning, TEXT("%s : %s"), *(FString(#X)), X ? *(FString("true")) : *(FString("false")))
#define LOG_STRING(X) UE_LOG(LogTemp, Warning, TEXT("%s : %s"), *(FString(#X)), *(X))
#define LOG_INT(X) UE_LOG(LogTemp, Warning, TEXT("%s : %i"), *(FString(#X)), X )

And my .cpp looks like this now :


void ATestActor::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);
	FString Seperator = FString("===========================PACKAGE SEPERATOR================================");
	FString OnConstruction = FString("----------OnConstruction----------");
	LOG_STRING(Seperator);
	LOG_STRING(OnConstruction);
	LOG_STRING(GetFullName());
	LOG_INT(GetFlags());
}

void ATestActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);
	FString PostEditChangeProperty = FString("----------PostEditChangeProperty----------");
	LOG_STRING(PostEditChangeProperty);
	LOG_STRING(GetFullName());
	LOG_INT(GetFlags());
}

Log screen that I get when I press compile button from Actor Blueprint Editor :

Log screen that I get when I change a property from details panel :

PostEditChangeProperty occure to run for a different object named Default_NewBlueprint1_C when I chane a property but OnConstruction does not get called for that object. Is this the CDO for blueprint? If this is the CDO, what was the others :smiley: ? Why OnConstruction doesn’t get called for it? Why PostEditChangeProperty for it didn’t get called for that Default_NewBleuprint1_C object?

Also Flags corresponds to that :

  • 2621497 : RF_Public, RF_MarkAsNative, RF_ClassDefaultObject, RF_ArchetypeObject, RF_WasLoaded and RF_LoadCompleted
  • 72 : RF_Transient, RF_Transactional
  • 64 : RF_Transient
  • 8 : RF_Transactional

I think objects are created for undo/redo purposes, as well as for serialization when saving/loading.

Btw, OnConstructed() is a very low-level function AFAICT, and trying to do things in it that interact with the blueprint serialization system may be causing this problem, but I don’t know off-hand WHY that would be so.

But, the Unreal editor will create/destroy objects and serialize/unserialize all the time. If you move an object in the editor, it will re-setup construction for each step you’re moving it with the mouse.

If there’s some state that you believe “must” be had in the object, such as a “return to home position,” perhaps a better place to set that up is in somwhere where all that setup work (and networking work!) has completed, such as BeginPlay()? E g, in BeginPlay(), check whether the list of transforms is empty, and if so, insert your current transform.
This is very much a “suggested work-around” and not a “root cause” though!

To make it easier to debug code in C++, I find that adding #pragma optimize("", off) after the includes in each of my .cpp files will help stepping and looking at variable values. Just remember to take 'em all out before shipping :slight_smile:

1 Like

Thank you again for your answer! I dig the source all day and that is what I found out for now.

We mainly have three objects. Let’s say they are located at the addresses of A, B and C.

  • Object at address A : The object that is owned by the world of Actor Blueprint Editor.
  • Object at the address B : The object that is owned by the world of Thumbnail generation world.
  • Object at the address C : The object that is owned by the world of Actor Blueprint Editor but only purpose of it is to get displayed at the viewport.

To indicate instances of them lets give each of them a number. Initially we will have A0, B0 and C0 addresses.

When we press compile on Blueprint Editor this is what is seems to be happening.

  1. CDO will be reconstructed. This reconstruction is done for an CDO object with suffix SKEL of the object we desire. Unreal holds a specific UBlueprint* for blueprint object and calls the specific type “SkeletonBlueprint”. I don’t know what is it’s purpose but I created a new question topic on forum about that.
  2. CDO will be reconstructed for our desired object. This is the CDO that we all know.
  3. The objects at A0 and B0 will reinstanced to a new address. Let’s name them A1 and B1. To let them keep the name they own, Unreal will rename the objects that are in the A0 and B0 with the REINST__ prefix.
  4. After that Unreal will destroy all the references for REINST__ objects, so GC will collect them and cause to fire Destroyed() on them.
  5. The objects at A1 and B1 will be constructed and the OnConstruction() method for objects at A1 and B1 will be fired. This is the two OnConstruction log that we see before “CompctHash” log.
  6. Unreal will call OnConstruction for the objects at A1 and B1 for one more time again. I don’t know why.
  7. Later, object at the location B1 (Thumbnail world) will be destroyed. Maybe Unreal creates a world to take a snapshot for the thumbnail when we pressed the compile button and after the thumbnail generation is done, it destroys it?
  8. This is weird now. Unreal will call OnConstruction method for the object at A1 once more again. The more weird thing comes after this. It will create another object in the thumbnail world and call OnConstruction over it. I will call it’s address as B1_1 since it is in the thumbnail world again and also this object will be REINST___ at the next compilation. This is the root cause of the TArray.Insert problem. If I print the TArray.Num() before and after the Insert method in OnConstruction script, every other object will hold the value that is previously edited except this. For example, if the first compile will log 1-2 as TArray.Num(), next compile will log 2-3. And it will continue like that increasing at each compilation untill you close the Blueprint Editor. This is also a weird behaviour for an insert method (which should only resize the array when it needed) in my opinion but I can understand that… The surprising thing is whatever you do, the object located at the B1_1 (B2_2 for next compilation…) will always log 0-1. When it comes to this with the insert method, it crashes.
  9. That’s all…We are finally done. Only thing left is Unreal calls OnConstruction for the object at C0 which is responsible for the viewport display. This object is never REINST__ thus the address of it never changes. But weird thing about it is it has the same exact name with the object that located at A1. I don’t know how Unreal allows two object that has different addresses to share same name…

I still don’t get a lot of things but for now I am sure that OnConstruction is not a place to do any Array operations :smiley: I hope this topic will help someone to find out what is actually going on…

Also one more thing… When I edit a Uproperty from c++, the change will not reflect to details panel. For example, even if my case works the TestArray at the display panel would not recieve that change even though array’s itself has actually changed. It’s only the visual in the details panel that does not change. Do you know anything about it? Should I call a specific function after any change on editor-time?

Thank you again, you are awesome!

Interesting.

Perhaps this gets called after deserialization? It seems like the paths PostEditChangeProperty and PostEditMove both call it. The documentation just says

Called when an instance of this class is placed (in editor) or spawned.

It’s called from RerunConstructionScripts() which seems to mean it’s more of “re-initialization” than something like a C++ constructor. (And, being virtual, that makes sense – the full object must have been constructed before a virtual dispatch will make sense.)
I think the most confusing thing here is probably the name – it would probably be better named something like OnReuseOfObject() or OnReinitialization() or something …

I know I’ve looked up how to refresh the editor sometime in the past, but I forget how I did it. Maybe by hooking into the undo system? Committing a command for an object might provoke it to re-scan the state? Also a property that has been overridden in the blueprint editor will still be overridden even if you change the C++ code, until you revert the override by clicking the little curly arrow next to the property.

Yeah that deserialisation idea looks possible for me too. Thinking the naming as OnReinitialization makes more sense for this case to me too… For the details editing problem, I will take a look at the commands thing tomorrow. I really appreciate your help and time! You are awesome, have a nice day!