From 1fefff0d832f79a8c593f91982e92a3424f29e31 Mon Sep 17 00:00:00 2001 From: liquidev Date: Wed, 7 Feb 2024 11:35:32 +0100 Subject: [PATCH] add post on quick fixes --- content/programming/unreal-engine.tree | 234 +----------------- .../programming/unreal-engine/blueprint.tree | 229 +++++++++++++++++ content/programming/unreal-engine/fixes.tree | 120 +++++++++ ...Y1NV1F1RR1-data-validation-quick-fixes.png | Bin 0 -> 10968 bytes 4 files changed, 356 insertions(+), 227 deletions(-) create mode 100644 content/programming/unreal-engine/blueprint.tree create mode 100644 content/programming/unreal-engine/fixes.tree create mode 100644 static/pic/01HP1G5WC29GP4KQY1NV1F1RR1-data-validation-quick-fixes.png diff --git a/content/programming/unreal-engine.tree b/content/programming/unreal-engine.tree index a2d0f72..9340adb 100644 --- a/content/programming/unreal-engine.tree +++ b/content/programming/unreal-engine.tree @@ -8,232 +8,12 @@ - both the fun and the good, and that which ruins my mood % id = "programming/unreal-engine/blueprint" -+ ### Blueprint + content.link = "programming/unreal-engine/blueprint" ++ ### thoughts on Blueprint - % id = "01H8Y0CKD1H9MFQ74ERYNQHF35" - + NOTE TO SELF: this section could really use some screenshots +% id = "01HP1FESY5WVJG4X80AZ4ZBX5D" +- ### random but cool things - % id = "01H8Y0CKD1QWDY8304APN69VG6" - - I don't have UE installed on my home computer yet, so you'll have to take my word for a lot of these things - - % id = "01H8Y0CKD1VENHWERX7CTGCE6M" - - anyways back to your regularly scheduled bullet points, *\*ahem\** - - % id = "01H8Y0CKD1QHMTGFCK2RC3NPDV" - + Blueprint, _my arch nemesis!_ - - % id = "01H8Y427AYC9SDK6SFSSEHH376" - - _WHAT! you don't remember me? how rude!_ - - % id = "01H8Y0CKD1NJPAH1SBNN5T9KQT" - - actually I really like Blueprint - - % id = "01H8Y0CKD1YPXAR8KH3JPCD3HW" - + lots of visual languages generally feel really unpleasant to write (looking at you Scratch), but somehow Blueprint feels like exactly the opposite - - % id = "01H8Y427AYWPYJHGGS63B7C8SN" - - (it's possible Scratch got better since I last used it like 5 years ago but I remember it being pretty cumbersome to use. - I had no reason to revisit it since so I simply don't know if it's any better now, but if it is, [let's chat][branch:hello]!) - - % id = "01H8Y0CKD1G06CG62XWZ02JREC" - + writing Blueprints is actually pretty darn nice and there's a lot to learn from the editor UX and basic refactoring tools - - % id = "01H8Y0CKD1G86TY8AF6Z2SQ85P" - + when you drag your mouse out of a pin, and a window will pop up giving you suggestions on what could be connected there - - % id = "01H8Y0CKD1P9SNK3TV967M705C" - + then you select from the list and it creates the node, autoconnecting stuff as necessary - - % id = "01H8Y0CKD1PQS2J2WB22ASZY17" - - one of the nicer autoconnections it can do is when you want to call a function on an actor component - - % id = "01H8Y0CKD16PFNWJ6EAARHA7BD" - - instance functions require you to pass in a `self` to call the function on - - % id = "01H8Y0CKD1FVPCA5NBY5KK1F5X" - - so what it does is it suggests functions to call on all the _concrete components - you already have in your actor_, and when you select something like `Destroy Component (DefaultSceneRoot)`, - it'll not only create a function call node, but also hook up `DefaultSceneRoot` as its `self` parameter - - % id = "01H8Y0CKD1KM42PKVSGB4QASG3" - + when you try to connect two pins of incompatible types, it'll automatically insert a conversion node for you - - % id = "01H8Y0CKD1NES5RBGVTPRH3F0Q" - - Blueprint is strongly typed so you can't just pass in a `Float` where a `String` is expected, but luckily this makes the system way less painful - - % id = "01H8Y0CKD10GQBNNP0RBJEWKW4" - - I do have a gripe with this though and it's where it places the node. it tries very hard to center it but unfortunately never succeeds. - - % id = "01H987QXFE445XNN5YDS2FYR5F" - + perhaps a bug I could fix one day? - - % id = "01H987QXFEYSE260S996BE84DV" - - from [Oskar Kogut](https://github.com/kretoskar): if you change a `BlueprintCallable` function to `const` (or otherwise make it pure,) the editor will automatically correct the `Exec` pin flow for you - - % id = "01H8Y0CKD1C5TJXXD40B99WQ3C" - + the design of Blueprint the Language is pretty dated - it is still an imperative language and has a concept similar to statements (`Exec` pins), which breaks the entire idea of pure data - flow and introduces control flow into the mix - - % id = "01H8Y0CKD1J2P2HZ507YBSNVKK" - - this split is called _pure_ and _impure_ nodes, where impure nodes are those that perform control flow (ie. those with `Exec` pins) - - % id = "01H8Y0CKD1P5JQCGHEY405KKPH" - + this results in weird edge cases like needing two separate node types to handle branching (one that can have side effects - `Branch`, and another that can't - `Select`) - - % id = "01H8Y0CKD1EGWTQYHT2WYQSZY5" - - `Branch` is used to shuttle control flow between two `Exec` lines, based on a `Bool`. you get a `True` branch and a `False` branch - - % id = "01H8Y0CKD1WFEWNYPZWGCH3N7X" - + and `Select` is used to choose a value based on another value, kind of like a ternary in C++ - - % id = "01H8Y0CKD156C0ZAXK7JS9W81D" - - however it is quite annoying because you can only switch on enums (and other stuff that has a finite set of values,) whereas with `Branch` you can use any `Bool` condition you want - - % id = "01H8Y0CKD1YP7Q3HVJJZNJAJKG" - + this would be fine if not for the existence of Gameplay Tags, which precisely _do not_ have a finite set of values - - % id = "01H8Y0CKD1P7D9QMPVQYWSDFT5" - - there is a `Switch on Gameplay Tag` node, but that is for control flow and not data flow! - - % id = "01H8Y0CKD126918N282MNBXD2C" - - this isn't an unsolvable problem but it illustrates the pure vs impure issue pretty well - you'd have to duplicate the implementation of `Switch on Gameplay Tag` to have - a version without an `Exec` pin - - % id = "01H8Y0CKD1N37WTCY66CM7R198" - + I'm seeing a bit of a semblance to the classic [function coloring problem][def:article/function_coloring] - - % id = "01H8Y0CKD19JMY87YY50M3RCQF" - + except where it usually applies to the `async` concept found in most modern programming languages, here it applies to the concept of control flow vs data flow - - % id = "01H8Y0CKD1Q8RXXK3KE4F4XAFF" - - and speaking of `async`, [Blueprint handles the classic `async` problem very gracefully][branch:01H8Y0CKD106HXQAJK87XV0H93]! - - % id = "01H8Y0CKD106HXQAJK87XV0H93" - + despite its flaws, one of Blueprint's strengths is asynchronous and latent tasks - - % id = "01H8Y427B03JNPTWW025ES176K" - - since control flow is based on those `Exec` pins, you can easily map your classic concept of callbacks to simply firing off the appropriate `Exec` pin - - % id = "01H8Y427B05AER1R4XK1GZ6A0M" - - for example the node for playing animations (`Play AnimMontage`) has a few pins coming out of it that signal playback events like `On Ended`, `On Blend Out`, `On Interrupted` - - % id = "01H8Y427B01SE52ZYA410QKWDQ" - - and you wouldn't even know these are implemented as delegates in C++landia. it just feels like a first-class feature - - % id = "01H8Y427B0V2Y76JJ3Z229PD15" - - the only gripe I have is that you can only have latent nodes in the main event graph (you cannot have them within functions) - - % id = "01H8Y427B0SPJ3NJCFS7S4YGR9" - - which is annoying but not terrible, since most of that latent, high level gameplay logic happens inside the main event graph anyways - - % id = "01H8Y96DGCWWTAP5X8ZYPCJZQC" - + the editor UI, despite [being helpful as it is][branch:01H8Y0CKD1G06CG62XWZ02JREC], is far from perfect - - % id = "01H8Y96DGCRP0W6YWJ3PYJ2GES" - - it's actually quite janky and there tend to be brief flashes of content falling into place - - % id = "01H8Y96DGDVZ4DZ4AZE7EDYNFP" - - this is true of other parts of the Unreal editor UI but I'll focus on Blueprint here - - % id = "01H8Y96DGDDRA8DD6PRGDCF8GP" - + node UIs are laid out lazily, by default using an algorithm that does not produce precise results for pin locations - - % id = "01H8Y96DGDT0Y23G86HEKNWF4H" - - which means that when you first load a Blueprint, some of the wires will be bent in weird ways, and will fall into place as you zoom out and explore the graph more - - % id = "01H8Y96DGDCEF6FEQWGX2BQ6AH" - - the effect looks quite janky and really upsets my inner perfectionist - - % id = "01H8Y96DGDJG41P3XTFE8F4Y1M" - + as you zoom out, the nodes start looking just a little bit different due to font sizes being snapped to integers - - % id = "01H8Y96DGDRCBKWBXC7AXZV43E" - - at very low zoom levels, nodes stop rendering their text altogether, which makes their layout shift even more. - - % id = "01H8Y96DGDGRBZXWH31G08FRYM" - - the effect of this is that nodes themselves take up less space, and it appears as though there is more space between them than there actually is - - % id = "01H8Y96DGD1QSAJ0QWD2ZYXKEW" - - which can make it frustrating to lay nodes out on a large scale, because you'll only find out your nodes are overlapping when you zoom in real close - - % id = "01H8Y96DGDSM03EJJVN9FS6SSB" - - there's a `Straighten Connection` feature, which you can use to make your graphs look more aesthetically pleasing, but it only straightens the nodes such that they look good on the current zoom level - - % id = "01H8Y96DGDNTKMGSJZDJN2GSJB" - - so what'll happen with it sometimes is you'll straighten a connection, then zoom in, and the connection won't be quite straight because of the aforementioned layout shift - - % id = "01H8Y0CKD1103FHF332M2Q4MG7" - + maintaining Blueprints in a large project could be a lot better - - % id = "01H8Y0CKD1PPEPX8EEFRAM2VE1" - + with regards to your graphs becoming really large - - % id = "01H8Y0CKD1DFSJ4BDPPFMGK66M" - - everybody kinda sorta just formats nodes however they see fit, and there is no unified autoformatter - - % id = "01H8Y0CKD1VRVNMZ827RF7XBCA" - - there's a reason people say Go's tooling is frickin' amazing, you know. it keeps codebases consistent! - - % id = "01H8Y0CKD1980VGHFBYGM8PN3E" - + this results in what we Hat in Time modders used to call _Kismetti_ in the Unreal Engine 3 days but nowadays we'd call _Blueprintti_ - - % id = "01H8Y0CKD179SBZ58F5JMDC5F3" - + which is a portmanteau of Blueprint and spaghetti. - - % id = "01H8Y427B0PJN75GA33S9CZJYH" - - I can't believe I remembered the spelling of that word. *portmanteau*. - - % id = "01H8YT7R15B3Z3T486DB53TR51" - - there are plugins on the marketplace that solve this, but I refuse to believe Epic Games doesn't have this problem themselves - - % id = "01H8YT7R15VTF7VPX5QK7K233J" - - I'd guess it's just not very high up their priorities - - % id = "01HA4HJKRVQ9GVMNY6NSY0T3T6" - - refactoring your nodes is a hell of a pain. Blueprint is kind of write-only - - % id = "01HA4HJKRVBSRB5T1GM0TBE1C7" - - say you're calling `ApplyGameplayEffectToTarget` a bunch of times and you want to replace those occurrences with a custom function that wraps `ApplyGameplayEffectToTarget` - with some extra semantic information, to make it more discoverable, searchable, and easy to use - - % id = "01HA4HJKRVGD7CW6T3MHRKNJS7" - - well then good luck :hueh: - - % id = "01HA4HJKRVBN3RR32Q9WHMKVFY" - - in a text-based language you could use a dumb search and replace to accomplish this task, but there is no such thing in Blueprint. :cry: - - % id = "01H8Y0CKD1QF5YBTKF3JEZCN5W" - + with regards to assets (how long this darn stuff takes to load) - - % id = "01H8Y0CKD1B1MZH39Z7EKB6KDT" - - the biggest offender here being that hard references are the default - - % id = "01H8Y427B17K7219TPH8VRFZ96" - - thus you can add a Mesh Component to your Blueprint and it'll load *the entire mesh with all the textures and skeleton and everything* when you wanna tweak a variable or some logic in the event graph - - % id = "01H8Y427B1QNZHJ5Z8W0QNFYPE" - - and not asynchronously in the background, you will have to _wait_ for it to finish loading. which is pretty annoying - - % id = "01H8Y0CKD16HEQKQX1NJG9GG42" - + the runtime performance isn't the best for a few reasons - - % id = "01H8Y427B1ZKJA16V17NG80QHS" - - the VM isn't implemented in the most optimal way - - % id = "01H8Y427B1JJD3EK24DCG76GR0" - - I've analyzed this in my [`dispatchers` repository][def:dispatchers/repo] if you wanna have a read - - % id = "01H8Y427B1T148Y0T8E48DKJ3N" - - and that hard reference thing can make gameplay stutter when you're loading in new assets, but that's a more widespread issue than just with Blueprints - - % id = "01H8Y427B15BD0HJWM2Y3EZBSZ" - - but in reality most of the logic you're implementing in Blueprints (high-level gameplay stuff) shouldn't be that performance sensitive - - % id = "01H8Y427B1G7ZYE3S8RRHG5WRQ" - - and it's not hard to extract the performance sensitive parts to C++ because Blueprint offers tools for refactoring your code - - % id = "01H8YT7R15MFW3RRQAF8CM4EP6" - - but all that doesn't prevent me from liking it! - - % id = "01H8YT7R15ZJWBY0TN6F6HN95Y" - - since it's way more pleasant to write game logic in than C++, given that you don't need to wait a minute for your code to recompile. + % content.link = "programming/unreal-engine/fixes" + id = "01HP1FESY5ZS6YTZXA8QTT5V1Z" + + data validation quick fixes diff --git a/content/programming/unreal-engine/blueprint.tree b/content/programming/unreal-engine/blueprint.tree new file mode 100644 index 0000000..1386ae0 --- /dev/null +++ b/content/programming/unreal-engine/blueprint.tree @@ -0,0 +1,229 @@ +%% title = "thoughts on Blueprint" + +% id = "01H8Y0CKD1H9MFQ74ERYNQHF35" ++ NOTE TO SELF: this section could really use some screenshots + + % id = "01H8Y0CKD1QWDY8304APN69VG6" + - I don't have UE installed on my home computer yet, so you'll have to take my word for a lot of these things + + % id = "01H8Y0CKD1VENHWERX7CTGCE6M" + - anyways back to your regularly scheduled bullet points, *\*ahem\** + +% id = "01H8Y0CKD1QHMTGFCK2RC3NPDV" ++ Blueprint, _my arch nemesis!_ + + % id = "01H8Y427AYC9SDK6SFSSEHH376" + - _WHAT! you don't remember me? how rude!_ + +% id = "01H8Y0CKD1NJPAH1SBNN5T9KQT" +- actually I really like Blueprint + + % id = "01H8Y0CKD1YPXAR8KH3JPCD3HW" + + lots of visual languages generally feel really unpleasant to write (looking at you Scratch), but somehow Blueprint feels like exactly the opposite + + % id = "01H8Y427AYWPYJHGGS63B7C8SN" + - (it's possible Scratch got better since I last used it like 5 years ago but I remember it being pretty cumbersome to use. + I had no reason to revisit it since so I simply don't know if it's any better now, but if it is, [let's chat][branch:hello]!) + +% id = "01H8Y0CKD1G06CG62XWZ02JREC" ++ writing Blueprints is actually pretty darn nice and there's a lot to learn from the editor UX and basic refactoring tools + + % id = "01H8Y0CKD1G86TY8AF6Z2SQ85P" + + when you drag your mouse out of a pin, and a window will pop up giving you suggestions on what could be connected there + + % id = "01H8Y0CKD1P9SNK3TV967M705C" + + then you select from the list and it creates the node, autoconnecting stuff as necessary + + % id = "01H8Y0CKD1PQS2J2WB22ASZY17" + - one of the nicer autoconnections it can do is when you want to call a function on an actor component + + % id = "01H8Y0CKD16PFNWJ6EAARHA7BD" + - instance functions require you to pass in a `self` to call the function on + + % id = "01H8Y0CKD1FVPCA5NBY5KK1F5X" + - so what it does is it suggests functions to call on all the _concrete components + you already have in your actor_, and when you select something like `Destroy Component (DefaultSceneRoot)`, + it'll not only create a function call node, but also hook up `DefaultSceneRoot` as its `self` parameter + + % id = "01H8Y0CKD1KM42PKVSGB4QASG3" + + when you try to connect two pins of incompatible types, it'll automatically insert a conversion node for you + + % id = "01H8Y0CKD1NES5RBGVTPRH3F0Q" + - Blueprint is strongly typed so you can't just pass in a `Float` where a `String` is expected, but luckily this makes the system way less painful + + % id = "01H8Y0CKD10GQBNNP0RBJEWKW4" + - I do have a gripe with this though and it's where it places the node. it tries very hard to center it but unfortunately never succeeds. + + % id = "01H987QXFE445XNN5YDS2FYR5F" + + perhaps a bug I could fix one day? + + % id = "01H987QXFEYSE260S996BE84DV" + - from [Oskar Kogut](https://github.com/kretoskar): if you change a `BlueprintCallable` function to `const` (or otherwise make it pure,) the editor will automatically correct the `Exec` pin flow for you + +% id = "01H8Y0CKD1C5TJXXD40B99WQ3C" ++ the design of Blueprint the Language is pretty dated - it is still an imperative language and has a concept similar to statements (`Exec` pins), which breaks the entire idea of pure data +flow and introduces control flow into the mix + + % id = "01H8Y0CKD1J2P2HZ507YBSNVKK" + - this split is called _pure_ and _impure_ nodes, where impure nodes are those that perform control flow (ie. those with `Exec` pins) + + % id = "01H8Y0CKD1P5JQCGHEY405KKPH" + + this results in weird edge cases like needing two separate node types to handle branching (one that can have side effects - `Branch`, and another that can't - `Select`) + + % id = "01H8Y0CKD1EGWTQYHT2WYQSZY5" + - `Branch` is used to shuttle control flow between two `Exec` lines, based on a `Bool`. you get a `True` branch and a `False` branch + + % id = "01H8Y0CKD1WFEWNYPZWGCH3N7X" + + and `Select` is used to choose a value based on another value, kind of like a ternary in C++ + + % id = "01H8Y0CKD156C0ZAXK7JS9W81D" + - however it is quite annoying because you can only switch on enums (and other stuff that has a finite set of values,) whereas with `Branch` you can use any `Bool` condition you want + + % id = "01H8Y0CKD1YP7Q3HVJJZNJAJKG" + + this would be fine if not for the existence of Gameplay Tags, which precisely _do not_ have a finite set of values + + % id = "01H8Y0CKD1P7D9QMPVQYWSDFT5" + - there is a `Switch on Gameplay Tag` node, but that is for control flow and not data flow! + + % id = "01H8Y0CKD126918N282MNBXD2C" + - this isn't an unsolvable problem but it illustrates the pure vs impure issue pretty well - you'd have to duplicate the implementation of `Switch on Gameplay Tag` to have + a version without an `Exec` pin + + % id = "01H8Y0CKD1N37WTCY66CM7R198" + + I'm seeing a bit of a semblance to the classic [function coloring problem][def:article/function_coloring] + + % id = "01H8Y0CKD19JMY87YY50M3RCQF" + + except where it usually applies to the `async` concept found in most modern programming languages, here it applies to the concept of control flow vs data flow + + % id = "01H8Y0CKD1Q8RXXK3KE4F4XAFF" + - and speaking of `async`, [Blueprint handles the classic `async` problem very gracefully][branch:01H8Y0CKD106HXQAJK87XV0H93]! + + % id = "01H8Y0CKD106HXQAJK87XV0H93" + + despite its flaws, one of Blueprint's strengths is asynchronous and latent tasks + + % id = "01H8Y427B03JNPTWW025ES176K" + - since control flow is based on those `Exec` pins, you can easily map your classic concept of callbacks to simply firing off the appropriate `Exec` pin + + % id = "01H8Y427B05AER1R4XK1GZ6A0M" + - for example the node for playing animations (`Play AnimMontage`) has a few pins coming out of it that signal playback events like `On Ended`, `On Blend Out`, `On Interrupted` + + % id = "01H8Y427B01SE52ZYA410QKWDQ" + - and you wouldn't even know these are implemented as delegates in C++landia. it just feels like a first-class feature + + % id = "01H8Y427B0V2Y76JJ3Z229PD15" + - the only gripe I have is that you can only have latent nodes in the main event graph (you cannot have them within functions) + + % id = "01H8Y427B0SPJ3NJCFS7S4YGR9" + - which is annoying but not terrible, since most of that latent, high level gameplay logic happens inside the main event graph anyways + +% id = "01H8Y96DGCWWTAP5X8ZYPCJZQC" ++ the editor UI, despite [being helpful as it is][branch:01H8Y0CKD1G06CG62XWZ02JREC], is far from perfect + + % id = "01H8Y96DGCRP0W6YWJ3PYJ2GES" + - it's actually quite janky and there tend to be brief flashes of content falling into place + + % id = "01H8Y96DGDVZ4DZ4AZE7EDYNFP" + - this is true of other parts of the Unreal editor UI but I'll focus on Blueprint here + + % id = "01H8Y96DGDDRA8DD6PRGDCF8GP" + + node UIs are laid out lazily, by default using an algorithm that does not produce precise results for pin locations + + % id = "01H8Y96DGDT0Y23G86HEKNWF4H" + - which means that when you first load a Blueprint, some of the wires will be bent in weird ways, and will fall into place as you zoom out and explore the graph more + + % id = "01H8Y96DGDCEF6FEQWGX2BQ6AH" + - the effect looks quite janky and really upsets my inner perfectionist + + % id = "01H8Y96DGDJG41P3XTFE8F4Y1M" + + as you zoom out, the nodes start looking just a little bit different due to font sizes being snapped to integers + + % id = "01H8Y96DGDRCBKWBXC7AXZV43E" + - at very low zoom levels, nodes stop rendering their text altogether, which makes their layout shift even more. + + % id = "01H8Y96DGDGRBZXWH31G08FRYM" + - the effect of this is that nodes themselves take up less space, and it appears as though there is more space between them than there actually is + + % id = "01H8Y96DGD1QSAJ0QWD2ZYXKEW" + - which can make it frustrating to lay nodes out on a large scale, because you'll only find out your nodes are overlapping when you zoom in real close + + % id = "01H8Y96DGDSM03EJJVN9FS6SSB" + - there's a `Straighten Connection` feature, which you can use to make your graphs look more aesthetically pleasing, but it only straightens the nodes such that they look good on the current zoom level + + % id = "01H8Y96DGDNTKMGSJZDJN2GSJB" + - so what'll happen with it sometimes is you'll straighten a connection, then zoom in, and the connection won't be quite straight because of the aforementioned layout shift + +% id = "01H8Y0CKD1103FHF332M2Q4MG7" ++ maintaining Blueprints in a large project could be a lot better + + % id = "01H8Y0CKD1PPEPX8EEFRAM2VE1" + + with regards to your graphs becoming really large + + % id = "01H8Y0CKD1DFSJ4BDPPFMGK66M" + - everybody kinda sorta just formats nodes however they see fit, and there is no unified autoformatter + + % id = "01H8Y0CKD1VRVNMZ827RF7XBCA" + - there's a reason people say Go's tooling is frickin' amazing, you know. it keeps codebases consistent! + + % id = "01H8Y0CKD1980VGHFBYGM8PN3E" + + this results in what we Hat in Time modders used to call _Kismetti_ in the Unreal Engine 3 days but nowadays we'd call _Blueprintti_ + + % id = "01H8Y0CKD179SBZ58F5JMDC5F3" + + which is a portmanteau of Blueprint and spaghetti. + + % id = "01H8Y427B0PJN75GA33S9CZJYH" + - I can't believe I remembered the spelling of that word. *portmanteau*. + + % id = "01H8YT7R15B3Z3T486DB53TR51" + - there are plugins on the marketplace that solve this, but I refuse to believe Epic Games doesn't have this problem themselves + + % id = "01H8YT7R15VTF7VPX5QK7K233J" + - I'd guess it's just not very high up their priorities + + % id = "01HA4HJKRVQ9GVMNY6NSY0T3T6" + - refactoring your nodes is a hell of a pain. Blueprint is kind of write-only + + % id = "01HA4HJKRVBSRB5T1GM0TBE1C7" + - say you're calling `ApplyGameplayEffectToTarget` a bunch of times and you want to replace those occurrences with a custom function that wraps `ApplyGameplayEffectToTarget` + with some extra semantic information, to make it more discoverable, searchable, and easy to use + + % id = "01HA4HJKRVGD7CW6T3MHRKNJS7" + - well then good luck :hueh: + + % id = "01HA4HJKRVBN3RR32Q9WHMKVFY" + - in a text-based language you could use a dumb search and replace to accomplish this task, but there is no such thing in Blueprint. :cry: + + % id = "01H8Y0CKD1QF5YBTKF3JEZCN5W" + + with regards to assets (how long this darn stuff takes to load) + + % id = "01H8Y0CKD1B1MZH39Z7EKB6KDT" + - the biggest offender here being that hard references are the default + + % id = "01H8Y427B17K7219TPH8VRFZ96" + - thus you can add a Mesh Component to your Blueprint and it'll load *the entire mesh with all the textures and skeleton and everything* when you wanna tweak a variable or some logic in the event graph + + % id = "01H8Y427B1QNZHJ5Z8W0QNFYPE" + - and not asynchronously in the background, you will have to _wait_ for it to finish loading. which is pretty annoying + +% id = "01H8Y0CKD16HEQKQX1NJG9GG42" ++ the runtime performance isn't the best for a few reasons + + % id = "01H8Y427B1ZKJA16V17NG80QHS" + - the VM isn't implemented in the most optimal way + + % id = "01H8Y427B1JJD3EK24DCG76GR0" + - I've analyzed this in my [`dispatchers` repository][def:dispatchers/repo] if you wanna have a read + + % id = "01H8Y427B1T148Y0T8E48DKJ3N" + - and that hard reference thing can make gameplay stutter when you're loading in new assets, but that's a more widespread issue than just with Blueprints + + % id = "01H8Y427B15BD0HJWM2Y3EZBSZ" + - but in reality most of the logic you're implementing in Blueprints (high-level gameplay stuff) shouldn't be that performance sensitive + + % id = "01H8Y427B1G7ZYE3S8RRHG5WRQ" + - and it's not hard to extract the performance sensitive parts to C++ because Blueprint offers tools for refactoring your code + +% id = "01H8YT7R15MFW3RRQAF8CM4EP6" +- but all that doesn't prevent me from liking it! + + % id = "01H8YT7R15ZJWBY0TN6F6HN95Y" + - since it's way more pleasant to write game logic in than C++, given that you don't need to wait a minute for your code to recompile. diff --git a/content/programming/unreal-engine/fixes.tree b/content/programming/unreal-engine/fixes.tree new file mode 100644 index 0000000..99dbb87 --- /dev/null +++ b/content/programming/unreal-engine/fixes.tree @@ -0,0 +1,120 @@ +%% title = "data validation quick fixes in Unreal Engine 5.4" + +% id = "01HP1FESY3H9K1QVSM1XMNC8NS" +- a few days ago I got a really cool change into Unreal, which allows you to add quick fixes to any data validation warning/error you emit: + +![a screenshot with a validation error in it; beneath the error there's a hint that it may be fixed automatically, coupled with a link you can click on to fix the issue][pic:01HP1G5WC29GP4KQY1NV1F1RR1] + +% id = "01HP1FESY3K11EJJ9NSFBDCTWS" ++ you can get this change in... + + % id = "01HP1FESY3CP8AJ00AQ9B6FQB2" + - [Git commit `0a6de35a5beb8534e19579cf1058460b4eb2bc79`](https://github.com/EpicGames/UnrealEngine/commit/0a6de35a5beb8534e19579cf1058460b4eb2bc79) + + % id = "01HP1FESY3RW98YQZFS04Q7E7C" + - Perforce changelist 31126352 + +% id = "01HP1FESY3XJRMHKB173XWF130" ++ the API revolves around the `UE::DataValidation::IFixer` type. _fixers_ are pieces of code that can fix your issues given their application conditions are met + + % id = "01HP1FESY38JWXNSJKPZNHP33Q" + + the interface you have to implement is: + ```cpp + struct IFixer + { + virtual EFixApplicability GetApplicability(int32 FixIndex) const = 0; + virtual FFixResult ApplyFix(int32 FixIndex) = 0; + } + ``` + + % id = "01HP1FESY3PZH00WDD85CNF1BX" + - for the curious cats among us: `FixIndex` is an arbitrary number that can be passed to `FFixToken`, to handle multiple different fixes with one fixer + + % id = "01HP1FESY3328D7ECZZX3EZCNE" + + first, the editor calls `GetApplicability` to know whether the fix can be applied + + % id = "01HP1FESY32EFSRX75ZWJ0K7PW" + - `EFixApplicability::CanBeApplied` should be returned if the fix can be applied at the moment + + % id = "01HP1FESY3RN3ECQHJ8R3HS4GD" + - `EFixApplicability::Applied` should be returned if the fix has been applied and can no longer be applied anymore + + % id = "01HP1FESY327FZMRZ64844XC9A" + - `EFixApplicability::DidNotApply` should be returned if the fix was not applied and can no longer be applied anymore + + % id = "01HP1FESY3N19T00CNY2T182VG" + + then, if `GetApplicability()` returns `EFixApplicability::CanBeApplied`, `ApplyFix` is called to actually run the fix + + % id = "01HP1FESY3NGQRKA57QTTVC19W" + - this function is safe to assume the caller has ensured that the fix `CanBeApplied` and therefore need not do any extra validity checks + + % id = "01HP1FESY3AAEHF4HSMAJ3B21H" + - other than that it's free to do whatever it wants + + % id = "01HP1FESY3CBMBJA3CQPEMCFND" + - after a fix is applied, the fixer returns an `FFixResult` which is used to display a notification to the user informing them of the changes + + % id = "01HP1FESY3GB4N76JDV8RVG4VD" + - fortunately you don't have to define a new struct for each fix, and instead can use `UE::DataValidation::TLambdaFixer` together with the `UE::DataValidation::MakeFix` function for more convenience + +% id = "01HP1FESY3EJ7TK4SPTD0TBBN9" ++ once you have a fixer, you can display it in your (`FTokenizedMessage`-based) validation messages using `FFixToken` + + % id = "01HP1FESY30HDA6JNR323GXD3D" + - minimal example: + ```cpp + auto Message = FTokenizedMessage::Create( + EMessageVerbosity::Error, + LOCTEXT( + "FoundNoneEntries", + "Found None entries in ability array." + "Please remove them, otherwise they may cause crashes during runtime." + ) + ); + Message->AddToken( + UE::DataValidation::MakeFix( + [this] + { + Abilities.SetNum(Algo::RemoveIf( + Abilities, + [](const UGameplayAbility* Ability) { return Ability == nullptr; } + )); + return FFixResult::Success(LOCTEXT("NoneEntriesRemoved", "None entries have been removed")); + } + ) + ->CreateToken(LOCTEXT("RemoveNoneEntries", "Remove the None entries")) + ); + Context.AddMessage(MoveTemp(Message)); + ``` + +% id = "01HP1FESY3V73R3JZEQHQ36V58" +- the user can then apply a fix by clicking on it in the Message Log message that contains it + +% id = "01HP1FESY3JM8WPZMRNCZC8GBA" ++ fixers can be freely stacked and composed - there are a few such layers available out of the box in the engine + + % id = "01HP1FESY3VK9X1TB81VR2VA83" + - the set is quite limited at the moment, but you're free to create your own or contribute them to mainline Unreal :ralsei_love: + + % id = "01HP1FESY3TC6DMBWQ3CEMGRW1" + - `UE::DataValidation::FSingleUseFixer` makes it so that your fix's applicability becomes `EFixApplicability::Applied` after the user applies the fix + + % id = "01HP1FESY32ZA4K57ATCKPM800" + - `UE::DataValidation::FObjectSetDependentFixer` makes it so that your fix becomes `DidNotApply` after the specified objects are deleted from memory + + % id = "01HP1FESY3W381HMBCEGCZ3HK7" + - `UE::DataValidation::FAutoSavingFixer` tells the user to save any assets modified by the fix after it's applied + + % id = "01HP1FESY3SENV59V4462YSZJY" + - `UE::DataValidation::FValidatingFixer` runs data validation on any assets modified by the fix after it's applied + + % id = "01HP1FESY3AZN3AVF7Z02JHXZA" + - `UE::DataValidation::FMutuallyExclusiveFixSet` is actually not a fixer, but a fixer _builder_ - + you give it a set of fixers, and it will make it so that when one is applied, it becomes `EFixApplicability::Applied`, and the rest becomes `EFixApplicability::DidNotApply` - + thus creating a set of mutually-exclusive fixes + + % id = "01HP1FESY3CC1NZZMQQP56TTD4" + - you can refer to the `DataValidationFixers.h` header file for more documentation + +% id = "01HP1FESY3WH21KDDYAHWZG294" +- all of this is going to be available in Unreal 5.4 :sparkles: diff --git a/static/pic/01HP1G5WC29GP4KQY1NV1F1RR1-data-validation-quick-fixes.png b/static/pic/01HP1G5WC29GP4KQY1NV1F1RR1-data-validation-quick-fixes.png new file mode 100644 index 0000000000000000000000000000000000000000..f7aa28506f1f6f5cddb64c09dd0356c02e773815 GIT binary patch literal 10968 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%0AG>B z6!$>B2iT0>s^+bK*_n3CXLTUKObW3KJf|?r^GK4h)45i!p@Y>yGXs4?e~_Iw^*xiA z(?IPfnA-rYrgJ|xrr3}+#QIXIUF~$$BV>xf@lYfXo4&hi;r828;{L1`>BFfaD&Vs$ zFGGO1K(!(A@oXu8%o#imHOL~D4eso}*bW;*%ivnnpK~6O$o}@h;$$YTG5(>L1NRoP zE*PULi{y^5wTlf^Yh60A^VmzHusgSd15yrvl>rWB9e(KidQx(BN}F)%aYE?}Ii1T*-1r_UZ9Ok}|VRq+LKHRe1P zOZZH`_nQPR&h$m6OfcLS)m4_eZBs`NaiSt=ENL2^xNJSZKq#PrscY1Po^trpMD^G+ z5~eXhX!}{-D#K z^VK42^JzF57iwHlbb!$;H>Xh#)F5ja3Vn*B69vqRh4Yqp z{Lj_-p=>pK%scpKI6~}w0QwiiX6pYN*vSc{LQSW+HwE3lDU;ood>_z8{-gH`yv`=Y z+ho(QZp2?H#WD%U_h+50PV_4tf*nyzy6R7N0-eA~?O-!0$+R~Zy6%xjg>~?92Cbw9 zn=~;gYhQr(fleEfxBE6A&H@?G)vdf=%=UHDWBn2C;Z^YO*mmxYN=;@7xr5&uT${Pb9)FqE?} z11~gAm|e}DO3tqQxV`Q2<@Jk3Y<&-0Yhp?WHu#=(#xUo*CrZgX z0en+cI-|finmqrCP-?LlAu`E#wjf*B5Wj%%9Toab+Dj|=QzFco^ZG-UMQZp)q~H4jt^HS4(3XnrCxpPXK) z1V8BkKa)(Puk^X+Y_Sa=mZw~z{Z_HPeXo$(Jf*`1fsU}8*c>Yu+^1~!vDC+bp#&&} z_)t=mw45@`9A|+f<|mGP-p^YwJ1^!+G>e!edCZGgUPKe5Z?a=lV!_e*>c4l)s`6ym zNLw1m;fFgj#Uy9&M>L8$t%SDB1h5kW)7xTL^3tX<8O{UIamD8+l4ejlN!G(A&HL5+#b@b*p0U`0f+}4> z)Ly3qTK7_g6hY~W1$bf?b<7h1ecn3$B<@{2Vcdx*{nNWRMagM@ey!n^xl*77!@bCV7QdE zVOm`_R{&<>YMLpGgo$K!Z~o#lX>79gSr$w=RrqCH;JhTqaX)(|Qv0E#Jx7l7`miVh zoIE9PR(V_5$KYINei&hJ#hm1q)GJ%T=F5-qPLBAHSi}|}UninB3^INBA)~5n#K%iu zR{!7ps>ab>A7LF+)gzLecz8E@DBDQ`zt+azWzA{T%E8J(!iv}3UmnrSdW>N1Ms!HU zY)0Fbx8F3KEl%~JB*V*jw;9e>SqmkyzB&-<;){QW={bySa;Z7=@$JFfNgsC~UgPM2 zW&}sXS?JG46?tDaZFgk(BM!$kROCAt`N@B5%jx6|k^bznhrp)cNY~U%P_5&^a~cP% z>y!HSgboew2BU@NSzG{)#vg`8ERrrh>x_(0^{;5#cu{qnI(bPZ^y3Vcnj4ctu%CLP za`Cs{1NRhp%OAWsG5!6dw0xS~uI0i+b?W+pBWOZfb*eHNkaBMzA%**(bJpE#r-4)k zx}GO1i~~s`G2JyaqK<+ShxdSZ(rRjC$$v%=gIk(WIbc9;tiRmz^32P?bCcC#aI=#& zNtYQxt3jQAza!*%pi8?wg66wz{Vz7jI#XB{*+yR^*$|l`n3kRP!Husj6-X`bum`GO z#}|`54ti)Go}JdXFudF<4r+Ry9t068fD)Ycf}bGh>w9aechg8a4UZZg3m>K)9Z#;E z-iL`n2~Y@r+iatr_LicGuAF4ch#1L>Ld%F}o!LD+dQF;$R-E20_Ly_(`zy8)u*@$CWQD{6@BKBl!0`HNXF9E^uw(C|P%Y)IW8+9#U> zzoRnkOQX@%evBWoFB50@u^tj@;R6NgQ?fjNh|$Nc?YAsn_=_~mQCjCkTxjKdvTB`~ zvVk=}`#>sv_PV$LWTQ^k8`wBXoI z$^GVQ^Snprhrb4V%F8#5C8nYyV!JJZ>ppvlfRC-CZ`fH9MRE7xe(}+KzN6iFgCEj{ zW-?l`E5D{%hN(C)tr>qNQ0J85mi_h4J>?_1I*Nn1y1R|#xWXaC<-81l_~FX#9xzZV z1Z;9BZRszx(s*DZd1((t$V!~-{Nmi*s8dW|V+YL6M0_&|SFLpuZGHTbFqh3y*+d}; z$EBe2qcN3Lmr*0@C1M(ZXId)n%HQ_>F3@NUa@D+3LUxU-l-iZHKP!yX-#@dOYiTUy z#3+^SN9dI{^ll{pE?|5u<7h2WVA*Bt`O2w8l?nE>HDKXa9h8^>ifFBe%0Q_&J*3n9 zh1cyuAaNRZfmBiHNLD43Vi_5i>Ky|ZH_Pu0^{{x&&>KFCNZqy+={46C>C~51KcYsG zB>t|cKyh1pNF97-snWtqkA1~I2J_bU&r8;p!YBz(^}W967ij%!lXH2Ruh2P`V3(89xxFDd7Q0G^E*x2tD{0Z161(pqJ-zHS|&YNBM*0-UL(KbZkLFRe zDpCm)jIQ|Lnsk)k4nF$f2asvtrW|?qVdn^Wq$b44X`Op0Mk4gwnrs#r%N2?v_z#*p zS9~^&XcH@68ZgD1hXV8~@K|U$6wnRl>}MM);SXCn#(x6I3w{;S|L%tqQ^9(|DSCo> z$j1M>8J^6kQ)lzgW46(`zBm@WxPD*)fPhdg?0ncsx1Kaja}{AH^LHFuE_GtG(OMM5ZV!|MlGc-$#id zlx1SscW?)D<>Q|%E_t3!WWzPN2r7_KF?y5^-!){Dfc}?8h4J@WoZRMh^_29X*GwA2@d6}ST$q%qKBvrJuI}= z)n73K(p63bc$~5GfFt@<8fhb$lh&6N<`r}0aI3n;rGJSSFvM-jXC&C`rgZoN5B`=`PL?OsS5y&MRMTetcb9QeVPzK<0mHb0qsg!GfKj~laEvL4(& z1D4YVeczPtoc{IVPS8XFBaZ1-SX?i5b@(XmQS}ZqY~)|zzKZozdEJ=g6H68!Uo0B! z;hh{9V~cs0Rk$YRMt~WZXEu@{K~^ZsK2u|*QN}cAI$&3s$N-iZ=r6q=3Ux`u8BFAk zw@3A$yh(^WZ_uT0%V2M(G=R>an=GDuew$obI;0L5V*Ly&%~D9G%b@#T*3o9utg`v&{IW`RmJ0Dm)FHHWrobq&wcB z!5!W2&8?-7Jxd8xLZ@$P-^zQGI#{SiX8y@~%x5`D^Ep42iaPS!*I$_SRc7@KVtD}c zt8Hoyr2Tp$DeVAWMZsV7HGoy6Y76BXpa?^@nc0+M$xrM{L-*5-`Y zPPV>;c*Vqh{@t=Pu}(Imi1Z@QleE?I*V#N};nHiby#L)7LQ$4(qoeS?e$I1H*Rh8p z+k4@_p_v`x$IMB9x24^FukGTN0Y#vl`a z06$SqS0cGR;D)gWk4!T&(#M;p?acVxZ^|sG|Jo(c>All3+mtZr>YQgD)$^tqvSmR9 z%xlUd<{ep)6~6)GLxBv=dD4Hg4E-GjDt&J!%hJ!zcN><8D#g)mxDnA6kh4c`FmeGd zw_-WvUSR`PR3oUo!};Ef&aaTbAiI) zt35o=2Oa(NUuDJd@(l)(m1qf!eO&^a`a3#mmj?$QS?SCQBEG0z;h5LDpzEgoZUYS1 z#ya+R6`UtA!qI-BzW$i)K1OCZP39yuJ_t|1*W>z=`g=*h(BZVidfNJ1>v5oC=Pm=R z%7qXlF2L|XO62I(1x-BWK-eIzhD4%ByJU#^LJ}1hu@c)8*KKG#FB{1?Mc2$=xjpXN zW*iNqRIQCxc3!3ATUOIbF9j;i>J}q5EA=YXSuf$%7%0Z@*4s$Dj zNS2JT!uwz-@iV_55!99Y4f>s!XoCl!)SFr)6##dPu5}` zd2s*IvHoW$26_WpRVgOm-yv& zbt5vE?d8X>ao_a;hhLUbHAU3fd^n`uvyld<^C%OMTcc``C!iV_FVYw?iIHIY-I}R9 zUT!cS$sqA&4n<>-3A>D2OmiNVKc8l9Xlh`K7=^>{g#^bAKNy{01PVj0fO~b&F>sfD zYyePY-QXlV3NWmZuNbc3E$)?2$jO&*3p*0iQjR1d-^0}TuNmx}CV{r68?*g#C zkO-GXQmK4v<7>E`)c5*)$m<6b9>kW?!hiNFrsq{l0iD{=|2Vj_^?j=z4+ZU`w+{rR zONqI7b&?G??Jz{h^;m9{f*KOuO^gB60;Y@QTN5Q>I(Y1xrGm0*!@A+-W@b_Ed@Vr9 zq1x^DfKno6>k6P&=eX*9o6D<;XtCdeeP4{3OGQ=uO~o5PcexlX?2HB=3BqeCwzhKz zf(QAfUdq$UeKARoc2WXl_Lq9h{&)?W_?$o&-T)4G)Sc1Mpmv8-iC_r!=*Cz2>>3OtOyVvPX73i@wN`ngEi1@<>HZ!IXE}H1oV3%LqrHmu{ zD^t9!d>Nb(4UBqjvqk8DWnu}jOQh#18$7DU=^MM`!y-PCwZKcE!b`d%(2tx5y`=-c6so(~W$S(@%A8LUlCKqcRp zj%(6azXim9y5AIE#l&Zw$@3&z7x&|d*Xe!*2nS~%lkXm&eN$odMOpjw3P?y%n4zKT z0JRQ|^bYzQTH4+~aTM!&bR)61<4~GHHBUK{JJUb*JuxE>=B_N2-h*`{kKUUf78bF7 zx7VjbEgpx@vPd`10U`A<7@z7R;2tYxIJ#+;>1qejcjW+^trH<64WO`Ok$G=Hp;ji6 z2graUk~cU>R~P}oOV3lZr@a?C0uEqs*!OJNJY5^QyUGZGfsCYs`$6+wyAmEj^)^#g z!#}pO-n2J?%@0`>kaF7kUBicpyrskbbuaur-K*i$dXCk8Ly|~U0Y;Wh&}#DZ>=g2F*RUKhvu= zVPJj&afSgd6wx@r4Jd_QF%=*zf-@W*B*o8WxANS+(zFyFM>JsEkW3QD)VunqXp=09 zM32>k*WvyhP2aL*@uw#LWh7A%u2v?Xz==a9P#{Z{G_bP~tEb9raD|SOxY`Gq+kzxs zl>5ccVn0#*Dmsg>*jka}kZe`F{3u(x%{CHIn~X)8NQG48qLT1DB4K4#xCNfcJgR<_eZOh--X(v%`WMa&?G9XW)=+~*|l5(8S_bF+5O zO+17OV0t9G4Bxy-x-^aq+|$?8%&cE2o?W8wG>n}6i5DJGkh^$);s~KjNSs;1r;4Wc znro~B9{QWIi+9<&a?7AqELJ>Duw5t-{S&$NS3o~!bx?q%%obBDGCtt!03>bT{TbLe zbEleIN}Qj)ZM-@ddk`<9a$hDZoV(FSz8X@%_h{Xcg(jE5Te z$;0Vl*iFX%^}1v)q`%SS8!Pn^{JcE*GbqUf*22P=Naml>cB0hChN z0hKfvgY!|x0jG*2;$Q8J>EDhQk~Z-Y3`i@Xd#utL0gk!*UQf;gX=Kw0^S`a9IXe!i zXd!{ir|&J!{k1PS!<{ZP46WrCh}kMo63}ceK4}jL^na=sJJk#+!RB2cRc!+?bc0YszmK>z>% literal 0 HcmV?d00001