Home

2024

Worklog

LETSGO Game

Pointers Are Hard

Pointers Are Hard

Tags
C++Engineering
Owner
J
Justin Nearing
🎶
This is part of a ongoing series called Building A Music EngineBuilding A Music Engine

It documents the process of me smashing my head against the keyboard to build a game called LETSGOLETSGO

It’s gotten long enough to break into several sections:

😇
Note from the future: This article comes as a live debugging session trying to debug impenetrable Unreal C++ errors. One of those poorly documented Unreal things that you just have to learn through osmosis and PAIN This document is me smashing my head against the keyboard for 2 weeks trying to figure it out. If you don’t want to see the process in which a man falls completely apart only to randomly stumble on the solution, you can skip to the next part here: 🥁Building the Drum Machine

It’s day 7 of trying to make a working get/set method in Unreal C++.

These are the kind of thoughts you find yourself having:

To have the ALetsGoGameMode have working get/set method accessors for the UQuartzClockHandle I attempted to set the reference of MainClock as a TWeakObjectPtr. This satisifies Rider- no squiggly lines in the Editor. Compile/Build fails with:

------ Building 11 action(s) started ------
  [1/11] Compile [x64] SharedPCH.Engine.Cpp20.InclOrderUnreal5_0.cpp
  [2/11] Compile [x64] LETSGO.init.gen.cpp
  [3/11] Compile [x64] LetsGoGameMode.cpp
  [4/11] Compile [x64] ULetsGoMusicEngine.cpp
  [5/11] Compile [x64] LETSGO.cpp
  [6/11] Compile [x64] LetsGoGameMode.gen.cpp
  [7/11] Compile [x64] ULetsGoMusicEngine.gen.cpp
  [8/11] Compile [x64] MainClock.gen.cpp
  [9/11] Compile [x64] LetsGoGameState.gen.cpp
  [10/11] Compile [x64] LetsGoGameState.cpp
  Projects\LETSGO\Source\LETSGO\GameModes\LetsGoGameState.cpp(14): error C2440: 'delete': cannot convert from 'TWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr>' to 'void*'
  Projects\LETSGO\Source\LETSGO\GameModes\LetsGoGameState.cpp(14): note: No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
  [11/11] Compile [x64] MainClock.cpp
  Total time in Parallel executor: 74.35 seconds
  Total execution time: 84.68 seconds
Build failed.

Translating Garbage Words

The error:

Cannot convert from TWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr> to void*'
  • WeakPointers are smart pointers that are more permissive about the existence of things. That’s my guess anyways. It’s… something.
  • Void sounds like nothing. A * is a pointer. A pointer to nothing.

So… I can’t convert something… into nothing.

It does have this helpful note:

No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called

I assume I am the user- le author of this code.

An operator is like = I guess.

Current Theory

I think it’s telling me that I have not defined a way for Get() to convert the pointer of my clock into nothing.

This could be translated as I have not defined a way to destroy the reference to this object.

I believe this is related to the C++ focus on memory allocation. I’ve watched a bunch of YouTube videos on C++ pointers and the key take-aways have been:

  • Pointers not that bad, actually
  • Memory allocation is kind of a big deal
  • C++ can be described as a language where memory allocation is treated as a first class citizen
  • I find this difficult because the languages I have used thus far make reasonable assumptions about memory allocation.

This is the reason I avoided C++ in the first place. Learning memory management AND the principles of software design at the same time was like: No, pay me to write you some janky Node code right now.

What I’m trying to accomplish in the first place

I need a metronome for LETSGO. A master main metronome to set the time of all instruments to.

Unreal has a subsystem called Quartz that provides a Clock containing a metronome. That gives BPM, time signatures, etc. Exactly what I need for a music game.

I have an Actor representing a Drum that needs access to the Actor representing MainClock

One does not simply get access to another actors members. The problem is if you start spawning a shit-ton of actors, and everyone’s accessing everyone else’s stuff, you run out of memory or something and things get slow.

Slow game bad.

So I believe that Unreal’s pattern for Actors accessing shared state is through its GameMode and GameState

So I’ve created a custom GameMode ALetsGoGameMode

And a GameState ALetsGoGameState

I have defined GameMode with methods Get() and Set() to access members of GameState.

The intent is for GameState to just have a bunch of publicly accessible members needed at runtime. Any Actor can Set/Get objects as needed.

The intent is for GameState to rarely initialize objects, just be the place where objects are shared.

I am trying to share this this clock/metronome UQuartzClockHandle

UCLASS()
class LETSGO_API ALetsGoGameState : public AGameState
{
	GENERATED_BODY()

public:
	ALetsGoGameState();
	virtual ~ALetsGoGameState() override;

	// UPROPERTY(BlueprintType) Is this needed? No idea. 
	TWeakObjectPtr<UQuartzClockHandle> MainClock;
};

In GameState we have an object called MainClock.

Except its not an object. It is a pointer to an address in memory for where the object is stored.

That’s fine, if any Actor needs to Get() this thing, it can just dereference in its own context and continue on its merry way.

Except its not a pointer. It is a TWeakObjectPtr - an Unreal object that represents a pointer that is more chill about the pointers existence.

Except of course, that the error is it freaking about the pointers existence:

Cannot convert from TWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr> to void*'

Cool. Cool cool cool.

Humans deserve to be conquered

Ok so, here’s the thing. ALetsGoGameMode looks a lot like the word ALetsGoGameState

This whole time I thought it was complaining about the GameMode’s Get() function:

UQuartzClockHandle* ALetsGoGameMode::GetMainClock()
{
	return GetGameState<ALetsGoGameState>()->MainClock.Get();
}

Which is on line 14 of ALetsGoGameMode cpp file.

The actual error references line 14 of ALetsGoGameState

ALetsGoGameState::~ALetsGoGameState()
{
	if (MainClock != nullptr)
	{
		delete MainClock; // Deallocate memory for MainClock
		MainClock = nullptr;
	}
}

JIPPITY WOULD NEVER MAKE THIS MISTAKE

Damn dirty apes.

So this error suddenly makes some amount of sense now that I’m looking AT THE ACTUAL CORRECT FILE

I don’t remember writing this code. This was copypasta from probably Jippity just trying to get the thing working end to end.

It didn’t start as TWeakObjectPtr<UQuartzClockHandle> MainClock; either.

It would be insane to start with that.

No, it started with the reasonable:

UCLASS()
class LETSGO_API ALetsGoGameState : public AGameState
{
	GENERATED_BODY()

public:
	ALetsGoGameState();
	virtual ~ALetsGoGameState() override;

	// You need a MainClock, here's a damned clock
	UQuartzClockHandle MainClock;
};

But then like, Rider complained in Set() in MainClock = newClock

A squiggly line on the =

I am unfamiliar with languages telling me = is wrong.

Usually the = is right regardless of how wrong my code is.

So then I hop in the Discord and Laura, who is a superstar on the #cpp channel hinted that I can’t pass around UObjects by value or something, asking if I’m missing a ref somewhere or something.

Unfortunately I lost the thread, would link if I still had it.

I reaaally truly think I missing a ref somewhere. Or something.

So I try the only true way of writing C++ : randomly placing * and & characters in front of things to see if Rider stops complaining.

Rider did not stop complaining.

I watch the YouTube videos on pointers not being complicated actually.

Rider did not stop complaining.

I stumble on a TWeakObjectPtr, which sounds like it would make Rider chill out about pointers and let my = assign value somehow.

Now I get a conversion error trying to delete a TWeakObjectPtr. If you look at the wrong file, this error is incomprehensible.

If you look at this code:

ALetsGoGameState::~ALetsGoGameState()
{
	if (MainClock != nullptr)
	{
		delete MainClock; // Deallocate memory for MainClock
		MainClock = nullptr;
	}
}

It certainly looks like code that would break with an error like

Cannot convert from TWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr> to void*'

Delete This Nephew

Immediately and without thinking the first thing I do is attempt compile with the following:

ALetsGoGameState::~ALetsGoGameState()
{
	// if (MainClock != nullptr)
	// {
	// 	delete MainClock; // Deallocate memory for MainClock
	// 	MainClock = nullptr;
	// }
}

Effective Debugging Technique:

Comment out your code until things start working again

Didn’t instantly fix, but did give another error, which is good, because:

New errors are a sign of progress.

It’s kind of profound that this is an honest to God fundamental truth of programming. Probably because it extends to all facets of life: New kinds of failure is literally progress.

Anyways, the original error specifies the line delete MainClock;

It would be reasonable to assume that one does not just delete a TWeakObjectPtr. They put magic in there managing a pointers lifecycle- the cost of said magic is that you can’t just treat it like you would other pointers.

At least, that’s a random assumption I’m pulling out of thin air. (Also a more-effective-than-youd-assume tool for effective programming)

Yo Dawg I heard you like Garbage Words

The new error:

LetsGoGameMode.gen.cpp.obj : error LNK2019: unresolved external symbol "__declspec(dllimport) class UClass * __cdecl Z_Construct_UClass_UQuartzClockHandle_NoRegister(void)" (__imp_?Z_Construct_UClass_UQuartzClockHandle_NoRegister@@YAPEAVUClass@@XZ) referenced in function "void __cdecl `dynamic initializer for 'public: static struct UECodeGen_Private::FObjectPropertyParams const Z_Construct_UFunction_ALetsGoGameMode_GetMainClock_Statics::NewProp_ReturnValue''(void)" (??__E?NewProp_ReturnValue@Z_Construct_UFunction_ALetsGoGameMode_GetMainClock_Statics@@2UFObjectPropertyParams@UECodeGen_Private@@B@@YAXXZ)
LetsGoGameState.gen.cpp.obj : error LNK2001: unresolved external symbol "__declspec(dllimport) class UClass * __cdecl Z_Construct_UClass_UQuartzClockHandle_NoRegister(void)" (__imp_?Z_Construct_UClass_UQuartzClockHandle_NoRegister@@YAPEAVUClass@@XZ)

Ok, not making the wrong file mistake again: This error appears to occur on both State and Mode.

Specifically, the Unreal Header generation object(?)

Different error numbers, both prepended with code LNK

Google unreal header error code yields nothing.

unresolved external symbol yeilds result containing keyword LNK2019 to page:

So the Unreal Header tool, which does the magic to convert UCLASS into Unreal’s reflection system to actually use your class in Unreal can not find something called:

"__declspec(dllimport) class UClass * __cdecl Z_Construct_UClass_UQuartzClockHandle_NoRegister(void)”

Garbage words.

class UClass * generating a pointer to a UClass object

__cdecl Z_Construct_UClass_ ID + Uclass name prepend

UQuartzClockHandle Hey thats the thing I’m trying to get to work!

_NoRegister(void) Hey this looks like the thing the original error complained about.

So an unregistered clock cannot be found in GameMode and GameState. That would make sense that you would not be able to find something that has not been registered.

This is happening in the Destructor for ALetsGoGameState which would be responsible for the voiding/nulling/deregistering/destroying a GameState.

Two things I want to try: Lets remove the destructor (dtor) completely and see if there’s different errors, then try calling a ~TWeakObjectPtr() dtor in the ALetsGoGameState dtor with the thought being it needs to be dtor turtles all the way down.

Aaaaand commenting all dtors has no change to the error. This would make sense as I have defined an empty dtor. Commenting it out will simply give it the default dtor which would be, probably, an empty dtor.

Day 8: Feeling Actually Unintelligent

Ok this is the property defined in GameState:

UQuartzClockHandle MainClock;

The naive approach is:

UQuartzClockHandle ALetsGoGameMode::GetMainClock()
{
	ALetsGoGameState* GameState = GetGameState<ALetsGoGameState>();
	UQuartzClockHandle Handle = GameState->MainClock;
	return Handle;
}

This isn’t possible:

No operator= matches arguments of type UQuartzClockHandle and UQuartzClockHandle*

Asking about this in the Discord #cpp channel points to the Stickied comment pointing you to random blogs for:

and

So, Unreal is actually C# on top of C++. Neat I guess.

The section on UObjects points to something called Lyra which is a sample Unreal game you can download.

Finding a link to it on GitHub gives the following:

header:

ULyraAbilitySystemComponent* GetLyraAbilitySystemComponent() const { return AbilitySystemComponent; }
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;

// The ability system component sub-object used by player characters.
	UPROPERTY(VisibleAnywhere, Category = "Lyra|PlayerState")
	TObjectPtr<ULyraAbilitySystemComponent> AbilitySystemComponent;

cpp

UAbilitySystemComponent* ALyraPlayerState::GetAbilitySystemComponent() const
{
	return GetLyraAbilitySystemComponent();
}

This seems to mirror what I’m trying to accomplish with UQuartzClockHandle .

Fine. Lets try TObjectPtr instead of TWeakObjectPtr.

So replicating that in my code:

// LetsGoGameState.h
public:
UPROPERTY(VisibleAnywhere, Category = "LetsGo|State")  
	TObjectPtr<UQuartzClockHandle> MainClock;

// LetsGoGameMode.h
UFUNCTION(BlueprintPure, Category="LetsGo|Clock")
	UQuartzClockHandle* GetMainClock() const;
	
	UFUNCTION(BlueprintCallable, Category="LetsGo|Clock")
	virtual void SetMainClock(UQuartzClockHandle* Clock);

//LetsGoGameMode.cpp
UQuartzClockHandle* ALetsGoGameMode::GetMainClock() const
{
	return GetGameState<ALetsGoGameState>()->MainClock;;
}

void ALetsGoGameMode::SetMainClock(UQuartzClockHandle* Clock)
{
	ALetsGoGameState* GameState = GetGameState<ALetsGoGameState>();
	GameState->MainClock = Clock;
}

So I get a new error on this, so progress I guess.

LetsGoGameMode.cpp(21): error C4458: declaration of 'GameState' hides class member GameModeBase.h(129): note: see declaration of 'AGameModeBase::GameState'
//GameModeBase.h(129)
/** GameState is used to replicate game state relevant properties to all clients. */
	UPROPERTY(Transient)
	TObjectPtr<AGameStateBase> GameState;

Ok so lets do the same all-in-one-line as the Get:

void ALetsGoGameMode::SetMainClock(UQuartzClockHandle* Clock)
{
	GetGameState<ALetsGoGameState>()->MainClock = Clock;
}

Aaaaand we’re back:

error LNK2019: unresolved external symbol "__declspec(dllimport) class UClass * __cdecl Z_Construct_UClass_UQuartzClockHandle_NoRegister(void)

COOOOOOL.

Ok back to Lyra:

void SetPawnData(const ULyraPawnData* InPawnData);

void ALyraPlayerState::SetPawnData(const ULyraPawnData* InPawnData)
{
	check(InPawnData);

	if (GetLocalRole() != ROLE_Authority)
	{
		return;
	}

	if (PawnData)
	{
		UE_LOG(LogLyra, Error, TEXT("Trying to set PawnData [%s] on player state [%s] that already has valid PawnData [%s]."), *GetNameSafe(InPawnData), *GetNameSafe(this), *GetNameSafe(PawnData));
		return;
	}

	MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, PawnData, this);
	PawnData = InPawnData;

	for (const ULyraAbilitySet* AbilitySet : PawnData->AbilitySets)
	{
		if (AbilitySet)
		{
			AbilitySet->GiveToAbilitySystem(AbilitySystemComponent, nullptr);
		}
	}

	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, NAME_LyraAbilityReady);
	
	ForceNetUpdate();
}

There’s nothing in here indicating that I’m doing it wrong.

BUT

I did see something in that Slacker Unreal article posted above:

Quote:

💡
During compilation an unresolved external linker error might pop up, something that looks like the following:

error LNK2019: unresolved external symbol "__declspec(dllimport) public: static class UClass * __cdecl UUserWidget::StaticClass(void)" (__imp_?StaticClass@UUserWidget@@SAPEAVUClass@@XZ) referenced in function "class UUserWidget * __cdecl NewObject<class UUserWidget>(class UObject *)" (??$NewObject@VUUserWidget@@@@YAPEAVUUserWidget@@PEAVUObject@@@Z)

This error indicates that you are trying to access something that the linker is unable to find the symbols for.

This can be caused by a few different reasons, described below:

Missing dependency

The most common cause of this within unreal is missing a module dependency.

In the case of the example error above, we can see that is complaining about something related to the UUserWidget class. So, we google for the documentation page of this class, where we land on this:

image

As we can see, the module is mentioned on this page. To include this module in our project, we need to add it to our depencies. To do this open your <projectname>.Build.cs file (normally found in the project’s Source/<projectname> folder) and add the module as a string to the public or private dependencies, in case of this example we add the "UMG" module at the end:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore"

UQuartzClockHandle is part of AudioMixer.

Adding AudioMixer to the LETSGO.Build.cs file makes it compile.

So it works compiles now.

I’m not happy about this. There’s no joy in getting this to work.

This is a resigned, defeated victory. Phyyric in every sense of the word:

A Pyrrhic victory is a victory that inflicts such a devastating toll on the victor that it is tantamount to defeat.

Creating a Get/Set method for a single property in Unreal has inflicted such a devastating toll on me that it is tantamount to defeat.

PAIN

But at least now I can get on with it:

🥁Building the Drum Machine