Looking at C++ after years of Haskell

I am an experienced Haskell programmer, although I only ever worked on one Haskell/PureScript project that was commercial. This year I had the weird idea to go into game development and I dived into the unreal engine which is all C++ for obvious historic reasons. Other game development engines (Unity and Godot come to mind) realized how C++ isn’t the best option and Unity chose C# quite sensibly, while Godot more boldly provides its own scripting language, which is really, really a good idea and makes working in Godot fun.

I didn’t chose unreal for it’s reliance on C++ but in spite of it.

Now I was wondering: Why is this Unreal/C++ bad, exactly? Is there a problem with object oriented programming, too? Are there advantages at all? Given my goal of finishing a game, how important is the programming language, really?

This goes to show what really lies at the core of my high productivity in Haskell. And I guess monads are part of it.

And these would be my answers/insights/claims after half a year of going back to C++:

  1. Somewhat unsurprisingly, C++ and the unreal engine itself are old workhorses. Regarding programming paradigms they aren’t desirable tools and that hurts everywhere. The whole package that is the unreal engine still seems to be my best option. I.e. the programming language doesn’t matter as much as the whole toolbox.

  2. In C++ good practices (often from newer versions) exist next to bad practices and the there is a lot of bad-practice-legacy-code, also game devs often are hobby programmer’s who don’t explore best practices as much as professional C++ devs might.

  3. The C++ community often sabotages itself with misplaced discussion of optimal performance. Example:

setVisible(true);

supposedly ought to be replaced by

if(!bVisible)
{
    setVisible(true);
}

to “avoid a potentiallly redudant function call”.

  1. This sabotage becomes even more hurtfull when elegant abstraction (which are increasingly powerful in newer C++ versions) are dismissed for supposed suboptimal performance.

  2. In C++, the programmer has full control over pass-by-value or pass-by-reference, and in practice that’s not a good thing, here’s why: Supposedly pass-by-reference performs better because, indeed, imagine a sufficiently large object and it’s true. Imagine small objects, pass-by-value regularly produced better code because copying might be worth it to avoid subsequent pointer indirections. So in the end, I don’t know exactly when to use what.

  3. C++ can’t be fixed. This should be clear from the above. If I was to create a new C++ project from scratch, I could use C++ without object-orientation and I would have great tools, even some functional stuff. Given that I use C++ because of powerful old tooling, C++ really implies all the problems above, always.

  4. The unreal engine has some design flaws that have nothing to do with C++. This is especially funny/annoying. When you get started in unreal, you have to make a distinction: Your code might currently be executed in the editor (e.g. a game designer drags an object into the scene which causes a call of your constructor) OR outside the editor in the final game. So far so good. Unreal offers a pragma #IF WITH_EDITOR to explicitly fork your code at compile-time and at runtime, there are objects like one called GameInstance which are null in the editor and properly initialized in the game. This means the notorious check for uninitalized objects in C++, if(!GameInstance) { return; } isn’t avoided but embraced. Even funnier: if you don’t deal with it correctly, not only your game crashes but the whole editor.

  5. Object-orientation is often nothing more than modularity. So object-orientation in unreal/C++ forces you to divide your code into modules, which is a good thing. The resulting ambiguities not so much. E.g. why is it GetWorld()->GetActorByClass(MyActorClass) and not one of MyActorClass::GetActors(GetWorld), GetActorByClass(MyActorClass, GetWorld())?

  6. Object-orientation gets into the way of higher-level abstractions by unreal. This can be understood best by an example. In unreal there are actors to represent anything in the 3D-world, e.g. a bullet from a gun. Then there are components that add features to actors, e.g. the component for ballistic movement. In C++, this is done by inheriting from AActor to get an actor, then adding the component as property to your class MyBulletActor : public AActor and then registering the component via AActor::SetupAttachment(ballisticMovementComponent). So far so good. But how odd: what if I don’t register the component? Usually an undesirable state, but sometimes you want to share component data with other actors like this. E.g. the WindActor influences the ballistic movement of your sniper bullet. The owning actor is identified only by the call to SetupAttachment. In Haskell, without objects, there are no class properties and to set up an actor-component-relationship, you would obviously call some function, probably in the context of world building. Sharing your component data with other actors would be a different concern entirely (pass it around as argument or put it into a reader) and thus easily separated.

  7. The boilerplate, no-one likes it and it’s everywhere, sometimes small, sometimes big, sometimes it implies entire files (public/myobject.h and private/myobject.cpp!) and in case of the header files it even affects compile-time and just putting all your code in .h-files isn’t advisable.

Sort of a conclusion:

I thought about how it would be nice to somehow write Haskell for my game and in Godot there exist Haskell bindings for the versions 3.x. I realized, however, that even if it would be possible, another language on top of unreal wouldn’t help a lot. Bad design includes language-specific bad practices but it protrudes interfaces and also the stuff behind those.

And despite all of this, I don’t complain. Unreal engine has a very generous pricing model for indie-devs and I basically get all the latest graphic features for free to write a game that, in principle, competes with AAA titles.

9 Likes

I would say a very small part of it:

As Rust shows, having constant-by-default bindings and a robust type-checking regime already stomps out many a bug, and higher-order functions are also beneficial. But as much as some despise it, Haskell’s non-strict semantics just makes programming that much easier - for example:

any p = or . map p

instead of:

any p []     = False
any p (y:ys) = y || any p ys

Otherwise, we would all be happily using Standard ML.

2 Likes

passing functions as arguments works in C++, but it still very much feels like we have handed a lighter to some cavemen who feel more comfortable with their flint stones. Same with the lambdas from C++ 11.

1 Like

As far as I recall from participating and communicating with many gamedev communities, the very performance you talk about is the point of using Unreal engine. I have seen that most game programmers praise Unreal’s C++ for performance gain: C++ gives more control on how to allocate and use memory.
Essentially, 3D game programming requires lots of computing power on varying consumer ends. They warrant juicing out every bit of performance possible, reducing time spent and memory usage wherever feasible. AND this applies to indie devs as well - after all, those I talked with were indie game developers. If indie devs were so considerate on it, you could only imagine how AAA gamedevs would think of. So I would say, performance is the crux of programming in gamedev.

I agree with the gamedevs that Unreal’s reliance on C++ is a good one. Indeed, Rust might have been better. Never something like haskell, though.

This isn’t true. Historically, yes, but nowadays game development makes use of “visual programming” which is bytecode script. This is true for Godot, Unity and Unreal. You can see how unreal is even pushing their visual scripting language and highlighting the advantages over C++ despite the weaker performance.

Performance-critical code has to be written in C++ because the visual scripting bytecode is about 10 times slower. But if you follow Unreal’s guidelines, you implement your whole game in their visual scripting language and only bother to translate selected parts in the end–if at all.

Unreal’s visual scripting is a great domain-specific language and has none of the features that make C++ supposedly the language of choice for game development. If there would be a text version of the visual scripting (rather than the weird drag and drop graphical mode) I would use it all the time. The visual scripting is also way more declarative then imperative.

So, there is no strict reason (anymore) why you couldn’t use Haskell for game development. More importantly, a tight control on memory and hardware aren’t a strict requirement anymore, which gives leeway for all kinds of higher level abstractions.

2 Likes

A bit unrelated to Haskell and C++ but recently the Unreal Rust project was recently released (which is a Rust integration of Unreal Engine). Maybe it could hit a sweet spot for you. I see Rust sitting in a middle between C++ and Haskell.

6 Likes

It was about 5 years ago I last interacted with gamedevs, so yes it might be slightly outdated. Still at that time, many gamedevs told me that a proper 3D game cannot be made entirely in visual scripting. They said at least hot loops should be written in C++ for performance reasons. Perhaps you can for toy projects, but anything outside of that requires that degree of control. GC is another big hurdle, as well.

They did love rust, so I bet Unreal Rust would be the star of the show by now.

2 Likes

Not weird at all. Gamedev exercises a diverse set of skills and getting them is fun and insightful.

Aside from some proprietary platforms and weaker/specialized hardware this option was always present. Haskell have seen a lots of hobby gamedev for quite some time.

3 Likes

not unrelated at all! Indeed, rust on top of unreal is very interesting.

But I am torn:

I don’t believe you can get rid of the bad that is in unreal engine, by stacking bindings to another language on top. What game development would need, in my humble opinion: a bold abstraction like functional reactive programming ist for user interfaces. Unreal’s visual scripting is an abstraction, but by no means revolutionary (to me, someone who can’t be bothered with drag-and-drop-style graphical programming).

Yet, just skimming the paragraph of the “unreal rust” homepage, there seem to be some low hanging fruits to be picked by just putting rust on top of unreal. I will definitely watch them. I am afraid, working directly within C++ with all the quirks of both, C++ and unreal engine, will be more productive for a while … but this really depends on the progress of projects like unreal rust. (Personally, I could hardly think of a more tedious challenge: to build a nice interface for a not really well-behaved underlying toolbox - are they ever going to keep up with the C+±engine? Won’t the unreal devs introduce more weirdness with every update?)

Well OK. If you say: “game devs rely on C++” in the sense of “performance-critical code needs to be written in something like C/C++”, yes you’re right and probably will be for a while.

I don’t like do over-emphasize this. Do I have to spend 90% of my development time in C++ to make a game? I don’t think so, and my particular skills do not have to be pointers and C-style for-loops, I just need to be good enough and – if the tools were there – I could go back to Haskell to actually programm my game.

It’s just that how unreal engine works, I imagine it difficult to successfully provide a nice abstraction in a functional language on top. I am not even sure what the best abstraction for the programming of computer games is. Game devs are quite used to write code in the “loop”: the function that gets executed based on your framerate at least 60 times per second.

In a proper abstraction you probably wouldn’t need to write this code explicitly. Just like you don’t register callbacks in reflex functional reactive programming.

just to give you a somewhat profane example of what it means to work with unreal engine:

I just changed this line:

const auto VecStartDirection = Spline->GetTangentAtSplinePoint(Indices[i], ESplineCoordinateSpace::World);

to this:

const auto VecStartDirection = Spline->GetTangentAtDistanceAlongSpline(Indices[i] * splineMeshLength, ESplineCoordinateSpace::World);

… and my result is visually clearly broken. Turns out my tangents are messed up. Googling brings me to the unreal user forum where someone had the same problem in 2018 and someone confirms the problem persists in 2021: This latter function returns tangents multiplied by some undocumented factor. I will now research what that factor is and correct for that.

This has nothing to do with C++, Haskell or rust. You can assume, though, that the origin of this behavior lies in several ambiguities that haven’t been resolved consistently at the time of the implementation of these functions. And this can have to do with bad design choices and missing abstractions and so on.

And in order to address problems at this level, unreal would need to provide proper documentation on their code, which they don’t.

After a career of doing other “stuff” I’ve also been playing around with game development, not weird, it’s a great little tester of your skills. I played around with something in Purescript, then built more or less the same thing in predominantly OO Typescript. I think I didn’t miss monads so much as plain old function currying. I find currying surprisingly useful in game development, quite often you want to pass a partially completed pure calculation around. I am aware there are libraries/ways to do this in the Js ecosystem.

Point 8 in your original post is not mentioned enough. Code organisation is enforced by OO by design. Some of those Haskell hobby projects in /r/haskellgamedev are not very readable to anyone who didn’t author the code.

C++ lost me around the time they introduced move semantics. It is just too much of a loaded weapon now. I am now eyeing off C# unity, although the comments above about Rust piqued my interest. Rust takes terribly long to build though.

2 Likes

Yeah, it is sad but I think you need to use C++ everywhere in this case. It is unrealistic to have 2 layers like that when you are dealing with scripting. You would often need to profile your game, locate the hotspots, decide where to optimize and rewrite those portions. This task could be unfeasible if you are going to use 2. For instance, if you are using haskell you have to at least write the glue code. Also the FFI overhead stack up. You are going to be calling these code in something like hot loops, and the overhead would end up being quite huge. I don’t think this way is going to be realistic.

It is pity that Unreal lacks proper abstraction, though. My guess is that the framework is created with the idea that performance is always critical, so that individual gamedevs should be micro-managing the loops. It did not have to be this way.

1 Like

IDK on that. I’ve seen plenty of C++ spaghetti, with so much implicits flying around that I’m exhausted after a few minutes trying to untangle the data flow in there.

Before I forget again:

…and while you’re waiting for that next “bold abstraction”, how about an “old[er] abstraction”: one which has some resemblance to DCTP FRP?

In his thesis Functional Real-Time Programming: The Language Ruth And Its Semantics, a simple game is uesd by Dave Harrison to illustrate the various facilities of Ruth, an early real-time functional language whose semantics assume normal-order evaluation by default: a non-strict approach (see page 53). Of particular interest:

  • (page 48)
    A channel is an infinite stream of timestamped data values, or messages, each message denoting an event in the system. […]

    type Channel a = [Event a]
    data Event a   = At Time a
    
  • (page 61)
    In the semantics given in Chapter 5 every Ruth program is supplied with a tree of time values (or clock) as suggested in this paper and each Ruth process is given a different sub-tree of the clock […]

    type Program = Clock -> ...
    type Process a = Clock -> a
    
    type Clock = Tree Time
    left  :: Clock -> Clock
    right :: Clock -> Clock
    

    A clock tree is composed of a node holding a non-negative integer denoting the current time and two sub-trees containing the times of future events. As the tree is (lazily) evaluated each of the nodes is instantiated with the value of system time at the time at which the node is instantiated, thus giving programs reference to the current time. […]

    type Time   = Integer  -- must be zero or larger 
    data Tree a = Node { contents :: a,
                         left     :: Tree a,
                         right    :: Tree a }
    
    currentTime :: Clock -> Time
    currentTime = contents
    

But if that’s still too much like FRP, @augustss’s keynote presentation about Verse should be appearing soon - apparently it now has “several unusual features”

1 Like

pretty cooll stuff! I’ll have to look into that. Even if I stay with C++, some good design choices can be “backported” sometimes.

void UStateLib::WithPlayerUIUnsafe(const UObject* Object, const std::function<FPlayerUI(FPlayerUI)>& Func)
{
	const auto PS = Object->GetWorld()->GetFirstPlayerController()->GetPlayerState<AMyPlayerState>();
	if(!PS)
	{
		UE_LOG(LogPlayerController, Error, TEXT("%s: PlayerState null"), *Object->GetFullName())
		return;
	}
	PS->PlayerUI = Func(PS->PlayerUI);
}

Higher order functions in C++. Could be worse.

The “multi-paradigm programming language”: such a delight to work with…


…and yes, I have [briefly] tried using C | | : it was on a small Haskell-centric system; the intention being to use it as the proverbial “better C”, having read about similar usage for Linux:

It also ended in similar fashion:

1 Like