It documents the process of me smashing my head against the keyboard to build a game called LETSGO
It’s gotten long enough to break into several sections:
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 fromTWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr>
tovoid*'
- 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 fromTWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr>
tovoid*'
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 fromTWeakObjectPtr<UQuartzClockHandle,FWeakObjectPtr>
tovoid*'
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:
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:
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: