Master Unreal Engine: Understanding Hard and Soft References

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
When we talk about hard and soft references in Unreal, what we're talking about is when assets get loaded and how they get loaded. If objects are a hard reference, they are loaded immediately when the asset is loaded. So for example, if I right click and open the reference viewer on this character, what we'll have is a clear tree view of every asset that is referenced by this asset. These lines in white, they are hard references. They are assets that will be referenced the moment the character is loaded. So if you have anything that has a hard reference to that character, it will be loaded if that object is loaded. For example, in this case, if I load either of these two maps, it will load this game mode, which has a hard reference to this character, which will then cause these characters assets to get loaded. For example, the initial abilities here will load the gameplay ability melee attack, which in turn loads the montage for the attack. Now, in a small game like this and a small project like this, these assets are relatively small. That may not always be the case. As this game expands, we may end up introducing larger textures, larger montages, which will then increase the load time. So the initial asset being loaded in this case is the map, which then loads the game mode. These two things will always load, which will then cause this character to always load. So whenever we boot the game up into those maps, it will load those characters, which will load all of these assets. You want to keep on top of this from early in game development, because what will end up happening is these references will become out of control as things grow. You may have montages, which then reference other montages, which somehow may reference a texture or some kind. That would become very difficult to unpick later on down the line. And your load times may just slowly increase over time, and it will be hard to notice it. What I recommend from early on is keeping an eye on what's referencing what and being careful how you reference things. So for example, this ability here, do I need this montage to be loaded already? Or could I perhaps asynchronously load it when the ability is activated? So let's go into this ability here. We know that we need this montage to play, but we don't want to load it the second the character is also loaded. So to do this, we're going to go into the Blueprint itself, right click, Edit, going to load up the Blueprint. And you see here how we have montage to play. This is a hard reference right now. So what we can do is if we right click, promote to variable, and you see how it says animation montage, what we want to do now is change this from animation montage. If you drop over here, it has an object reference or a class reference. Both of these are hard references. So you want a soft object reference. And notice now how this has changed from a dark blue color to a light blue color. So if you drag directly off of this and place it in here, it will make this converter node. This converter node will only actually work if the asset has already been loaded into memory by something else. If you was to do this right now, that would actually return a null object. Even though we've got a variable set here, and this variable is set to the barbarian montage that we want, unless something has loaded that montage, this variable will not be valid. It will be null. So what we'll have to do here is on activate ability, we want to take this soft reference and asset load async. So there's two options here. You could have a synchronous load, which is blocking. So whenever the ability activates, it will cease execution on the game thread. The game will hang briefly whilst the asset loads. And then when it finishes loading, the game will complete like nothing has happened. For me, whenever I see a hang in a game, that's normally a bad thing. So I tend to avoid using them unless I'm in a very specific place where people won't notice a hang. In this case, we're going to use an asynchronous load. We'll take the asset soft reference in, make sure the asset is loaded in memory, and then on completion, it will fire this completed pin here. So once this completed pin is fired, the object is loaded in memory and it's safe to use. It will actually output the object here as just the general object reference, but you might want to cast it to the montage to play. In this case, because we know that this object was the one we loaded, I'm just going to do that conversion pin that I mentioned earlier, which will convert this soft reference to a hard reference. The only difference is here, we know that it's loaded because we went through this async load asset. So that's one way of making this reference here from hard to soft. So now if I refresh the graph, you see how the line is now red. A red line is a soft reference line. And it means that it's loaded when we tell it to load. So in this case, we've told it to load the second the ability is activated. If I go into the game, just play briefly, the character still does the attack because he now loads the montage. It only does that. That node is almost a no op. When it has the asset already loaded, it will check to see if it's loaded in memory first. And if it is, it will just continue execution via the completed node. It's nice and straightforward like that. Anyway, so that's one way of going about it. That means that the second that the character's loaded, the initial abilities are granted to the player. Those abilities then in turn don't have loads of little sub references that need to be pulled in as part of this. But that's an example of how to do it in Blueprint. If you wanted to do that same thing in code, it's going to be slightly different. So let's just show you that. So in order to load a soft object pointer or a soft class pointer in C++, what you want to do is grab the UAssetManager, grab the streamable manager from that, and then call the requestAsyncLoad function. You then pass in the soft object pointer or soft class pointer that you want, and then call .ToSoftObjectPath. There is an override for this which also takes in arrays. So if you wanted to load multiple assets at once, you could do it that way too. And then you tell it the callback that it needs to file when the asset has finished loading. So if there's an array of assets, this will call when all of the assets have finished loading. If it's a single asset like this, it will just be when that one asset has finished loading. And that will call soft object ref loaded. So let's go into here. It's going to just check, hey, that soft object reference that we told you about, is it now valid? And is valid is just going to check to see if it's been loaded in memory. And then we're just going to output a log saying it's loaded. Just because we don't want to do anything with it now, that's just an example. You notice here that I assigned a variable of handle. So this requestAsyncLoad is going to return a streamable handle. And that handle keeps those objects are loaded in memory until you tell it to sit. It will keep them loaded in memory forever. So what you want to do is make sure that you grab that handle when you load an asset, just stash it off somewhere. And then when it makes sense, cancel that handle. So it releases the object from memory. This can kind of cause some problems down the line if you accidentally hold on to references that shouldn't be held onto and an asset lives forever. For example, if you have a game instance and you load some assets into that that you don't need loaded, they will stay in memory forever. It's kind of a best practice I use to make sure I'm just, if I'm requesting something to load, I want to make sure I stash the handle to it. And then I make sure that I unload it. I've seen some places in the Unreal code base that don't do this. And I'm not sure why they don't do it because it feels wrong that they don't do it. But in this case, I'm just going to make sure that we, I show you how to do it. And then you can make your own choice if you want to do it in the future. Anyway, so yeah, this returns a F-stringable handle. It's just a shared pointer to that handle. And then on begin destroy, all I'm going to do is check to see if the handle is active still, which means it hasn't been canceled and it is pointing to valid objects in memory. And if that handle is active, then I'm just going to cancel the handle. That should allow the memory to get G-seed. If you get like really deep into a project and you have everything hard referenced, you could end up having lots of problems trying to unpick that and turn them into soft references and do this kind of asynchronous loading down the line. So it's best to go into a project knowing that this is a thing and doing this early when possible. Just as a side note, there is another function that you can call, which instead of being asynchronous is just synchronous. It will halt the game thread until this asset is loaded. I do not recommend using it. But if you are doing that transition from hard references to soft references and you have a lot of code that expects that asset to exist immediately, as a placeholder, you might want to use soft object pointers, but then use the synchronous function to immediately load them whilst you go and refactor the rest of the code to make it deal with the asynchronous nature of that function. So for example, you would just call, instead of async load, you just request sync load. This doesn't take in a delegate at all. And that will load the asset there and then, and it will block everything from running until it's finished loading. Like I said, I don't recommend it, but it might help you in places where you're trying to migrate code to use soft object pointers. Another thing to look out for is whenever you referenced another class in Blueprint, you're actually creating a hard reference to it. So looking at this reference viewer now, we can see that this melee attack doesn't reference the character directly. The character references it directly, but it doesn't reference the character back. So if I was to, for example, when iterating over this array of actors, instead of using the ability system component interface, if I was to cast this directly to the character, so BP character, and then get the ability system, if I was to do that, what I've now done is introduce a hard reference. So if I go save and compile, get a reference viewer, you now see that the ability itself references the character, which means the character is also loaded. In this instance, because the character is the thing loading the ability, it's not such an issue because we already know the character's loaded. But if we were referencing, let's say another character, what would happen is this ability, whenever it's loaded for character one, would end up loading character two, which could be like double the amount of memory being used. So be very careful when using casts to a specific character. So one of the better ways to do it is try and create interfaces for those classes to work from. In this case, we had the ability system component being gathered from the ability system blueprint library. The way that this works, it's just casting to the interface on the character, which we've got here. So it has iAbilitySystem interface. This is just a straightforward interface which just returns a component. You can easily create interfaces in both C++ and native. You can implement them by going to your class settings, just dropping down interfaces. And if you've defined one in native, you can see it here. If you define it in blueprint, you'll also be able to see it here. But if you communicate between classes using interfaces, you avoid creating hard references between them, which means the assets don't get loaded in memory. Your load time should be faster because you're not loading large assets just to do a simple cast to something. I've seen things like blueprint libraries, which have a single cast node to a specific character. Now, anytime that blueprint library is used, it then causes that character to get loaded, which is really bad. You don't want that to happen in a game that you're working on because you'll end up having your assets loaded in memory, which make no sense to be loaded in memory. It's basically it with hard and soft references. It's quite simple in blueprint. It's quite simple in C++. It's just a matter of getting it in your head and like making sure that you're keeping an eye on it throughout the entire life cycle of your project. In the descriptions, I'll leave some additional links for stuff that I find useful. If you have any questions or comments, leave them below. Also, just as a massive side note, because I'm going off track here, thank you for everyone who's been following and subscribing from this. We gained like 400 subscribers just from the last video alone. So thank you so much for that. That makes me really glad that I'm making these because if people can find some sort of value out of this, it makes it worth my time. So thank you. Anyway, I will see you soon. The next video, hopefully the next video is going to be about a new dialogue system that I've been building because I decided to ask on Twitter what to build and Twitter decided that the next thing is going to be a whole Fallout style dialogue system, which I may or may not regret building because this is quite complicated. Thank you so much for watching and yeah, just keep awesome. See ya.
Info
Channel: Dan Goodayle
Views: 4,729
Rating: undefined out of 5
Keywords: unreal engine, ue4, game development, unreal slow load times
Id: ZhboW6Tw-LY
Channel Id: undefined
Length: 13min 30sec (810 seconds)
Published: Mon Oct 23 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.