Hello! Devlin here again, with more rehashed content. This time we’re resuming from where I left of in my gear post, covering more things I did in an attempt to improve a particular client’s project.
Now, for all of you who are still rambling on about how my car is going to stop going down the road from a lack of a critical resource – I don’t recommend this sort of setup. Its sort of limited by the fact that its entirely implemented in BP, and it doesn’t even reach its full potential as a BP-only system. If you copy either this post or my previous (the gear one) – at least go run some quick sanity checks. Treat these two posts as if a member of your team had done them, and you’re a bit skeptical of what they did. These posts were basically always meant to provoke some thought into one possible how and why of this sort of abstraction – rather than being meant as a tutorial of some kind.
Now, onto the post. Our client wants to have stats with different stages of modification. Now, we could get away with having a different set of code to handle things way more procedurally- but we’ve started down a path here and we’re going to continue the theme. Regardless of how you actually want to handle the Implementation refers to either making features work or integrating assets. The execution of ideas. Implementation is connecting the pipes and running the wires.... of some kind of complex stats, you’re going to want to have things be component based.
This allows one set of code that has to be maintained, which tends to be a good thing. If you break it, you’ll break everything at once. If you improve it, you’ll improve everything at once. There are other people who will have explained why a component (or something like it) is a good choice better than I care to in this post. On the right, you can see how the stats were handled prior to my changes. This graph is within the Warrior blueprint.
So, why is the right-hand side bad? Every class which is going to have this set of stats will have the exact same set of variables. There’s a named variable for each stage or modifier to a stat. You end up with, say, BaseFireRes, FireResBonus, FireResClassNatural, and so on. You won’t be able to really avoid the memory cost of the variables, but you’ll be able to interact with them in a more maintainable fashion.
The left-hand side is better. The class is still handling its own special-case logic – though this could be a virtual function that is inherited from a super; perhaps an interface – only now we’re using a component to store the value as an element in a modifier stack. Then, for the purposes of the class itself, we’re evaluating the stat with its flat buff (which by the way is 0; more on this later) and storing that in a member var. You’d probably want to do that last part differently as well, but I was slowly moving things to a scalable system rather than just building something from the ground up.
Well now, how’s that any better? Simple – as its a stat with a modifier stack, you can get at the modifiers individually, or apply them in any order. In this particular example, it can easily feel more cumbersome, but it actually is relatively painless to use.
Each stat is actually an array of “modifiers”. This implementation is lacking because each modifier is only a single float. It doesn’t really track temporary states well because of this. In any case, the array of stats is the same size as the enum for the stat types – and so is the modifier array in each stat. This set of nodes gets you a particular modifier for a particular stat.
Here you can see how I init the modifier array for a given stat. Pretty basic setup, really.
And this would be how to init an array of stats – if BP’s ForEachLoop actually gave you a reference. It didn’t during the time I was making this, and I’m unsure if it does at the time of writing. If it doesn’t, you can just save a duplicate of StandardMacros somewhere in your content folder and remove any other macros and change the ForEachLoop to something like ForEachRefLoop and use a Get (by-ref) . If you don’t want to do that, you need to Set Array Elem which will let you update the element in the array.
Here’s another before/after example. Before, you’d have to have a valid reference to an instance, then you’d do something like this – except probably with yet another variable. With this new system, you’re still able to get the original base value, but you’re also able to get the buff as it currently stands, or the value after applying the buff. Applying the modifier to the stat in this case wipes whatever that modifier was.
Hey, this is cool and all, but how does the evaluation of a stat actually work?
Glad you asked. Do you remember earlier when I showed you Get Stat Value After Modifiers ? Well you just call that. If you only want the base value, just pass that. If you want the flat buff you can pass that as well. Want the flat buff applied 3x? Pass an array that contains that 3 times.
Ideally you could just pass an empty array, but BP requires arrays have an input. In any case, the modifiers will have had to been applied previously to this – if not they either won’t have any effect or they’ll result in you getting 0 as a return. You could define any number of modifiers – or composites of them – that can then be applied to any stat.
The major (and I do mean it) drawback here is that all stats have to have a uniform memory layout. This means that with a large amount of stats and modifiers, you’ll have a huge memory footprint. One would think that a UE4 developer using BP isn’t concerned with memory management at all – but you’d be surprised.
If I were to do this again, it’d be in C++ and I’d spend a lot more time polishing the design before using it to make a big demo. I might do that, and if I do, I’ll come update this post.