Friday, May 29, 2020

Something like a Summer Devlog, Part 3: Some things I learned using UE4 in May 2020

I spent the lion's share of May working on a prototype for a local multiplayer turn-based RPG in Unreal Engine 4.25. I feel like I've learned a lot from it, and this post is my attempt to gather some of the more important lessons in one place. At this point, it's not clear if I will continue this project, whether using UE4 or not, but I'll come to that later.

BindWidget

A lot of my project involves UI-based interactions, and the first couple of times through it, I had awkward splits of logic between C++ and Blueprint. I came across this post by Ben Humphreys in my Web searches, and it shone light on something I am sure I had not seen before: BindWidget. This gives you an easy way to access elements added in blueprint widgets. For example, consider a widget like this:

UCLASS()
class FOO_API UCustomWidget : public UUserWidget {
  GENERATED_BODY()

protected:
  UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
  class UTextBlock* TheTextBlock;
}

Given a widget subclass—call it WBP_Custom—you can set its parent class to UCustomWidget, give it a widget called “TheTextBlock”, and then that widget will be accessible to the C++ implementation.

FClassFinder

A crucial counterpart to this for dynamically-created interfaces is ConstructorHelpers::FClassFinder. Let's say I have another widget, UCustomContainer, that needs to make one UCustomWidget for each connected player. That's the kind of thing I've had to do a lot in my project, for example. In order to instantiate the UCustomWidget, I need to know the actual runtime type, which is WBP_Custom, not just the C++ type UCustomWidget. I have done things like this using EditAnywhere UPROPERTY TSubclassOf<UCustomWidget&rt; fields before, but I found them fragile: the links often break when parts of the system are recompiled, leading to tedious plugging of types in through the editor.

Reading through discussion boards and blog posts, I think a more conventional and effective approach for folks who are doing heavy C++ is to use FClassFinder, as in the following example:

UCLASS()
class FOO_API UCustomContainer : public UUserWidget {
  GENERATED_BODY()

  UCustomContainer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
  {
    ConstructorHelpers::FClassFinder<UUserWidget> CustomWidgetClassFinder(TEXT("/Game/Foo/UI/WBP_Custom"));
    CustomWidgetClass = CustomWidgetClassFinder.Class;
  }

public:
  void Setup(TArray<APlayerData*> Players) 
  {
    for (auto Player : Players)
    {
      UCustomWidget* Widget = CreateWidget<UCustomWidget>(GetOwningPlayer());
      Widget->SetupFor(Player);
      PlayerWidgetBox->AddChildToHorizontalBox(Widget);
    }
  }

protected:
  UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
  UHorizontalBox* PlayerWidgetBox;

private:
  TSubclassOf<UUserWidget> CustomWidgetClass;
}

Take note of the custom method (SetupFor) I introduced to handle initialization. You cannot ExposeOnSpawn variables since there's no way to create custom widgets in a deferred way as you can with actors, you need a different way to set up the widget. This method can contain UI construction logic or other initialization logic too, as long as you call the method before adding the custom widget to a container.

I am sure I came across FClassFinder a long time ago in my UE4 programming, but I never really liked it because of the hardcoding of string paths. Indeed, the hardcoding of strings is something that turned me off from Unity many years ago: it's just not robust in the face of refactoring, and refactoring is crucial to keeping the code healthy. That said, this feature did help me build up my UIs much faster than my old approach, so I think it's a matter of idiolectics: to get things done in UE4 C++, you have to approach it on its own terms.

Gameplay Abilities and Gameplay Tags

Anyone want to guess how many times I tried adding UE4's Gameplay Abilities System to this project? I wrote some thoughts about it a few weeks ago, and I spent days exploring, reading, programming, and learning. In the end, every time, I came to the conclusion that for some reason, it was not right for me. Some of those reasons were wrong-headed, which led to my exploring it again, only to find another reason that it wasn't right for me.

My experimentation with GAS did get me also working with Gameplay Tags. The API around them is great for quick and expressive set operations. In Dan's excellent GASDocumentation project, he points out how gameplay tags—especially in combination with GAS—can replace most uses of enumerated types and booleans. As I tried to get my head around these technologies, I found this to be true. Indeed, for my core system of skills and runes, I represented all the data as gameplay tags and got a quick, efficient implementation.

The problem comes from the lack of expressiveness. It seems to me that gameplay tags are like set-theoretic versions of the String datatype: you can put anything in there, which means it can be used for anything, which means it's not tuned for any one thing. For example, in my code, I had some gameplay tags that represented skills ("Skill.X") and some that represented runes ("Rune.X"). However, this meant that when I passed these around, they would both be simply type FGameplayTag, leading to awkward signatures where the meaning of a variable could only be determined by its name and not its type. That is, you get code like the following:

int32 CountSuccesses(FGameplayTag SkillTag, FGameplayTag RuneTag)

This reminds me of my point above about encoding asset paths as string literals in C++: yes, it can work, but it's not very robust or maintainable. I can see why homogenous types are necessary for a complex system like GAS, but I struggle to understand why some of the conventions in the game engines I have used defy the general rules for good software development. By contrast, although I've been away from Java-based game development for years, I never had that feeling when I was programming in PlayN, where I had wonderful libraries to draw from and Java's rock-solid implementation of enumerated types.

Rider for Unreal Engine

I think Rider for Unreal Engine deserves a quick shout out. I joined the Early Preview and have no regrets. One of my biggest frustrations with writing C++ in UE4 was the unpredictable interactions between Visual Studio and Visual Assist X. I know everybody seems to praise these two tools, but I never got a good flow working where I could consistently get the predictive text (intellisense) to work consistently. Sometimes, I would get Visual Studio filling things in, and sometimes Visual Assist. There was a lot of mousing around to try to get the pop-ups to work, and I hate mousing around. Rider had consistently-available keyboard shortcuts, including a handy message when you gave a command that it wasn't ready for yet, such as requesting a build while it was still initializing. The first release I used had a terrible bug that would require terminating a zombie process, but that was fixed in the next release, and I've been very happy with it.

What's next?

I'm still interested in the gameplay idea that I've been exploring. Briefly, the core system involves cooperative bag-building, but where the items in the bag can have data attached to them: for example, if a player uses a rune from the bag, mark the rune, so that the next time they draw it, they get a bonus. Part of the inspiration was to explore what could be done with cooperative bag-building in a digital space that would be arduous or impossible in a tabletop one. It was fun to tinker with, but at some point it would become a content-driven game. Given how long it's taken be to build a minimal engine (that is still incomplete as I waffle on some of the core mechanisms), I am not convinced that I should not just pull the plug on it. Another option is to see if I can whip up a playable prototype in Godot, whose support for rapid iteration on scenes is a great strength over the relatively plodding process of UE4 level development, but of course, this won't solve the content problem.

Speaking of data, I want to mention another Ben Humphreys blog post, this one from last January about data-driven design in UE4. This is a topic near and dear to my software developer's heart, and in some ways, it was both relieving and frustrating to read his conclusions. Yes, UE4 seems not to have a good, all-purpose tool for doing data-driven development, so it's not just me who feels a bit unsettled with my options; but also, why not? Maybe Epic will give him a MegaGrant to build and integrate his ideal tool.

As I wrap up the month, I turn my eye toward the announcement of the Epic Spring Jam starting on June 4. I make no excuses about the state of my project, but I have been distracted by the hubbub around what universities are going to do in the Fall. I know I will have to spend much more of my own time than usual planning my Fall courses, and I still don't have a timeline of when I will hear whether the university will be altering my times, sections, or modes. Something like the Spring Jam would give me five focused days and a theme to continue my summer professional development. I would land in the middle of June afterward, which seems a convenient time to dive into planing—hopefully with a little less ambiguity around me.

Thanks for reading!

No comments:

Post a Comment