Hello! Recently I have been reading a lot about the performance problems related to blueprint casting (i.e. in BPs using nodes ‘cast to’, ‘get all actors of class’, ‘get actor of class’ etc. results in creating hard references to target classes which can also reference to something. Therefore, creating more dependencies on various classes and assets. And that means that when the first BP-class maintaining ALL these hard references to different classes and assets will be loaded to memory, those hardreferenced assets will do as well. As a result, memory keeps it until BP-class containing these refs would be unloaded).
However, I have heard from several sources claimed that casting in C++ is OK somehow?? I could not find information about it or any details that could help to explain WHY it is so.
Please, can somebody explain it, give more details or examples, or share a ref to some article which could make it more clear?
im no expert on C++ but i imagine its because it only has to reference the .h file which is designed to be lightweight.
that said the problem with casting is less hard references and more dependencies/lack of modularity.
so for instance its good to cast to your AnimBP because it will only have one and it’ll always be loaded
but its not good to cast to an interaction because that interaction may not happen or could be many things.
As you stated, whenever you hard reference a blueprint class, it is loaded along with all of its hard references, including the classes that it might be referencing in any ‘cast to’ nodes. This can be typically seen in player character and controller classes, where it starts off a chain of hard references to a point where these classes drag most of the game with them. As a result you have increased memory load as all of this needs to be loaded in memory and longer load times since all of it needs to be loaded, in this case during the level load.
The reason why casting to C++ classes is recommended is twofold:
C++ classes don’t contain asset references (they can but may the lord have mercy on your soul if you do). You can declare an asset reference in C++ but it will be empty for the class itself and you fill it in a child blueprint. So if you were to cast to the blueprint it would load it but casting to the parent C++ class doesn’t since it has nothing to load.
C++ classes can declare virtual functions that are implemented/overridden in the child blueprints. So you can execute any desired functionality of the child blueprint by calling functions on the parent C++ class without having to load anything as per point 1.
This ties to the overall recommended approach to structuring code in UE where you create a base C++ class, declare the API of the object in it along with any core system functionality and then create child blueprints off of it that reference assets and implement any specialized functionality that might need to be changed often.
As @Auran131 stated, there are also other reasons for why you want to avoid casting, such as managing your dependencies. Alternative way of solving the casting issue is to use interfaces which cannot reference assets given their nature. So their use instead of the base C++ class is sort of similar in this scenario. Lastly, these issues can be negated by the proper use of soft pointers. You might still cast to a child blueprint but if it is referencing other classes/assets as soft references, they are not loaded automatically, but on demand. So it’s not as big of an issues in this case, but you need to manage this loading logic yourself.
Soo, I can cast to C++ class itself having a ref to an object of BP (child of C++ class) and access all its properties and methods avoiding triggering the loading of referenced assets (because references are created in C++, but defined in BP)? Is this correct?
keep in mind you still have to spawn the child class which creates a hard reference anyway, it just means you can save the ref as the base class and pass that around, unless of course you Load from a soft reference but this creates other complications.
I am not sure if I fully understand your example, the additional reference confuses me a little. But in general you can have UBaseClass in C++ that has a TSubclassOf property and a DoSomething() BlueprintImplementableEvent function and you can create a child blueprint in which you assign the actual class reference and implement the event. Then you can access the referenced class and invoke the function through the base class instead of having to cast to the concrete blueprint type.
As @Auran131 rightly stated, this sort of does not matter once you deal with actual instantiated objects since those have to be loaded already. To really minimize the amount of hard references, you have to use soft references and lazy load things as they are needed. What we are discussing is sort of specific to cast nodes and how they are included in the reference chain.
Let’s say you had an interactable actors with two implementations, A and B. One spawns 10 different actors and the other plays a bunch of different sounds and particle effects (imagine anything that references external assets, building up the reference chain). If you player controller was implemented in such a way that it casts the actor to A and then B to determine which actor it is in order to invoke their specific functionality, it would hard reference both of them by default and as a result all of the assets they themselves reference. If you instead cast the actor to their parent C++ class with a generic Interact() function that is implemented in the A and B blueprints, this wouldn’t be the case. If the level only had A interactables, their class would be loaded but not the B class. Obviously, if the level contains both, then both will be loaded but that’s not an issue since you are actually using them.
Basically, the whole idea is to not have a bunch of classes loaded in memory for no reason outside of having them referenced in cast nodes that will not succeed because there are no objects of the given type in existence. However, since this is rarely the case this might not be that big of a deal and instead you might need to deal with more serious cases through the use of soft references. But it is a good practice to avoid this by default where you can as this issue can easily hide below the radar and quickly scale to a few gigs of memory out of nowhere.
Sorry, if I am being dumb, just to be clear, in this case we still create cast, but to the C++ UBaseClass which result in hard-referencing the class from TSubclassOf property. However, in this case hard-referencing is reasonable (because object was spawned and its class was loaded to memory anyway)
I understand now that without assist from soft references and interfacecs managing memory effectively can be hard (because hard references to all used assets will be created anyway and to decrease the amount of unnecessary loads to memory we can use all the aforesaid methods + appropriate classes hierarchies and structure).
The uppermentioned information means that following this strategy we do not exclude\reduce hard-references, but decrease the amount of unnecessary loads to memory because BPs inheriting from some C++ class can also have certain hard-refs to assets unlike C++ class (where it is not recomended, i.e. bad practice)
PS: really appreciate your help and spent time in case you won’t answer this
Yes, assuming the cast succeeds it shouldn’t load anything extra because the object instance already exists, hence its class has to be already loaded.
I am not sure if I completely understand you here. You reduce the number of referenced assets in the asset dependency chain of the blueprint class that contains the cast node if you use the base C++ class instead of the child blueprint. Yeah, the child blueprint still has its own hard references but it’s not about them but the fact that this class isn’t used in the cast node, hence it does not propagate further.
Maybe I am not using proper examples. If you have UParentClass, BP_ChildClass and BP_AnotherClass, where BP_AnotherClass contains the cast node:
If the node references BP_ChildClass - the dependency chain of BP_AnotherClass will include BP_ChildClass and any of its hard references, this will propagate further recursively, possibly leading to massive chains
If the node references UParentClass - the dependency chain of BP_AnotherClass will include UParentClass, but since it itself doesn’t reference any external assets, the chain does not propagate further
It might be best to visualize both approaches by applying them and looking at the reference viewer of the class that contains the cast node. I am not actually sure if it shows C++ classes (I think it does not) but the point is that the dependency on the child blueprint class should disappear once you replace the cast node (assuming there is nothing else referencing it) and if it is not there, its own dependencies are gone as well and everything is awesome. You can also check the size map of the asset, which should get reduced accordingly.
Glad to be of help. Hopefully I am right but at least that is my understanding of these concepts.