add post on quick fixes
This commit is contained in:
parent
5d63140cf3
commit
1fefff0d83
|
@ -8,232 +8,12 @@
|
||||||
- both the fun and the good, and that which ruins my mood
|
- both the fun and the good, and that which ruins my mood
|
||||||
|
|
||||||
% id = "programming/unreal-engine/blueprint"
|
% id = "programming/unreal-engine/blueprint"
|
||||||
+ ### Blueprint
|
content.link = "programming/unreal-engine/blueprint"
|
||||||
|
+ ### thoughts on Blueprint
|
||||||
|
|
||||||
% id = "01H8Y0CKD1H9MFQ74ERYNQHF35"
|
% id = "01HP1FESY5WVJG4X80AZ4ZBX5D"
|
||||||
+ NOTE TO SELF: this section could really use some screenshots
|
- ### random but cool things
|
||||||
|
|
||||||
% id = "01H8Y0CKD1QWDY8304APN69VG6"
|
% content.link = "programming/unreal-engine/fixes"
|
||||||
- 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 = "01HP1FESY5ZS6YTZXA8QTT5V1Z"
|
||||||
|
+ data validation quick fixes
|
||||||
% 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.
|
|
||||||
|
|
229
content/programming/unreal-engine/blueprint.tree
Normal file
229
content/programming/unreal-engine/blueprint.tree
Normal file
|
@ -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.
|
120
content/programming/unreal-engine/fixes.tree
Normal file
120
content/programming/unreal-engine/fixes.tree
Normal file
|
@ -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:
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Loading…
Reference in a new issue