As an introduction to the series–we will summarise things that we found useful in one week of game development; think of this as a collection of tips and tricks, a collection of mistakes made (so you don’t have to repeat them), a collection of our development patterns (so you can pick something that’s useful for you). We will publish TWWL every Friday evening UK time, unless we get stinkingly drunk, go mad from game development, or get over-run by zombies; in those situations, TWWL will be published later or never, as appropriate.
Control-drag in Blueprint editor
I cannot believe I’ve been using the Blueprint editor for a year before accidentally discovering ctrl-drag. If you hold down ctrl, you can drag single or multiple connections together from their target to another target.
First, let’s start with the old and familiar, but very slow and very manual way.
And here the ctrl-drag super efficiency.
#magic
Do not make typos in default interface implementations
This one took a day of on-and-off searching, blaming Blueprints, questioning the whole design approach, debating whether it even makes sense to have a default implementation in an interface. Why not?–some things have sensible defaults, even single implementation inheritance languages support default default implementations in interfaces. Why yes?–it is an interface, it only declares a contract, if you need defaults, make your own default-valued types. In any case, the cause was a typo. Consider the interface
class KOPI_API IKopiFoo {
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Kopi)
int GetMeaningOfLife() const;
};
Suppose now you wanted to return the sensible default value 42
; normally, you’d provide a definition (an implementation) in int IKopiFoo::GetMeaningOfLife_Implementation() const
; note the _Implementation
suffix. However, if you do this in an interface, you’ll get the compiler complaining that GetMeaningOfLife_Implementation
is not a member of IKopiFoo
. It turns out that you have to provide the declaration of the GetMeaningOfLife_Implementation()
explicitly in the interface header.
// definition
class KOPI_API IKopiFoo {
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Kopi)
int GetMeaningOfLife() const;
virtual int GetMeaningOfLife_Implementation() const;
};
// declaration
int IKopiInteractable::GetMeaningOfLife_Implementation() const {
return 42;
}
However, you must be super careful when typing. I mis-typed CanHighlight_Implementation
definition in the header; I effectively defined some other virtual method that has no connection with the BlueprintNativeEvent GetMeaningOfLife()
. Naturally, there was no compiler error or warning: it is perfectly OK to define arbitrary methods [in an interface]; in effect I had the code below.
// Header
class KOPI_API IKopiFoo {
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Kopi)
int GetMeaningOfLife() const;
virtual int ShaveMyLegsAndCallMeGrandma_Implementation() const;
};
// implementation
int IKopiInteractable::ShaveMyLegsAndCallMeGrandma_Implementation() const {
return 42;
}
In the same vain, remember that const
and reference qualifiers are all part of the method; an _Implementation
with mismatched const
-ness or reference qualifiers will also be unrelated to the BlueprintNativeEvent
method.
Timelines and synchronisation
If you want to have a functionality that has not-a-rig animation (maybe because rig is not a good fit; for example, it is a simple open-door style animation), but you still want to synchronise events, a Timeline
is a great choice. Our example is interaction with a fridge where we want to open and close door following a curve; when a door is opened, we want the player to grab the ingredient.
Without the Timeline
node, this is rather cumbersome: either too heavy-handed with a rig (and thus no nanites, complicated animation notifies), or too much typing or dragging with UCurveFloat
s, ticks
, and the thing I hate the most–arbitrary Delay
nodes. A very convenient way to solve this is to use a Timeline. Simply drop a Timeline node on your Event Graph, double-click on it to add two tracks [or as many as you like]; for our fridge, it was one Float Track and one Event Track.
It is then possible to edit the curves and the position of the notify event as you desire–here the top track is the Event Track called GiveIngredient
and the bottom track is the Float Track called OpenDoor
.
The two tracks then appear as outputs of the Timeline node: the OpenDoor
is a float value that changes as the float tracks are evaluated and the Update
exec pin fires; the GiveIngredient
exec pin executes at the time that matches the Event Track keys.
Captures asynchronous in blocks
If you are using asynchronous nodes; for example to load data, you should always ensure that the data you are going to be using on completion is available during the completion. This sounds self-evident, but it’s possible to get bitten by even a simple for loop. Consider the following blueprint:
The trouble is that this will always print Loaded asset 4
three times! How come 4
you ask, especially since the Last Index
is set to 3
? Well, consider how a for loop might actually execute on your CPU–let’s not go crazy with assembly, it’s quite enough to stick to C like code.
for (int i = 0; i < 4; i++) { body() }
int i;
for_init:
i = 0;
for_body:
body();
i += 1;
if (i < 4) goto for_body;
after_for:
It should be clear why the Blueprint loop prints Loaded asset 4
; it executes three times, and it increments the for counter four times; the last increment causes the loop to exit, but the damage–as in increment of the counter–is done. Think of Blueprints async completion as capturing everything by reference, when you access the outer scope [in this case the captured Index
], you are going through a reference to this
. Even funkier things happen when you access a dangling reference, which is just as easy to miss. Consider the following code:
UFUNCTION(BlueprintImplementableEvent)
void OnFoo(const TArray<int> &Ints);
void Foo() {
TArray<int> LocalInts = {...};
OnFoo(LocalInts);
}
When you implement the event, but access the Ints
const array reference in a completion of an async call. The array is long gone–it was a local variable whose life ended with the scope of the Foo()
method; if you are very unlucky, you will not even get a segmentation fault.
Stand on the shoulders of giants
As always it’s only be standing on the works of people much more experienced than us in Unreal Engine [and game development] that we are even able to try our luck at building our own game. This week, the most often visited blogs are
All UPROPERTY Specifiers ยท ben????ui (benui.ca) – A font of knowledge for the Unreal macros that end up on your classes, methods, properties
GitHub – botman99/ue4-unreal-automation-tool – Great description of the various parameters for the Unreal Build Tool
What Are SDFs Anyway? – Joyrok – Super useful for building out resolution-independent UI elements for our game
Leave a Reply