I have changed a bunch of my coding standards over teh year. The most important thing is usually to follow whatever your company does. That being said, these are some things that I have grown to adopt.
All of these are suggestions, things to try out. I'm describing things that work for me, not telling you how you "must" do things.
Includes
Include What You Use
Unreal Engine moved to Include What You Use (shortened to "IWYU") at least a while ago. At first I wasn't really sure what it meant in concrete terms.
For me I understand it to mean only include headers where you absolutely have to.
- Don't
#include "CoreMinimal.h"
- Forward declare whereever possible
Forward Declare
Forward declaration is well discussed online but the TL;DR version is:
- You can forward declare something when C++ doesn't need to know the size of it in memory
Example.h
enum class EDogState : uint8;
class UDog;
struct FDogInfo;
// Later in file
void RegisterDog(UDog* Dog);
void SetupDog(const FDogInfo& DogInfo);
void SetDogState(EDogState DogState);
Sort Includes
Sort your #include
statements alphabetically. It makes it way less likely that merge conflicts will happen, and way easier to fix them if they do.
Forward declare, don't inline declare
I used to get lazy and put class
directly in front of where I was using the class in the header file. This gets really noisy really fast. Rider will automatically add the forward declare for you in the right place so there's not much excuse not to do it.
Inline forward declaration
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
UPROPERTY()
TObjectPtr<class UWoofComponent> WoofComponent;
};
class UWoofComponent;
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
UPROPERTY()
TObjectPtr<UWoofComponent> WoofComponent;
};
Use TObjectPtr<T>
This was released with Unreal Engine 5 if I remember correctly.
TObjectPtr is the new hotness
// Old raw pointer
UPROPERTY()
ADog* Dog;
// New TObjectPtr<T>
UPROPERTY()
TObjectPtr<ADog> Dog;
Code Style
Inline assignment
Like all of the suggesions here this is purely subjective but I've gotten used to it and kind of like it.
How I used to write code
UFlower* Flower = Garden->FindFlower(FlowerType);
if (Flower)
{
// Do stuff with flower
}
Preferred
if (UFlower* Flower = Garden->FindFlower(FlowerType))
{
// Do stuff with flower
}
Don't Export Everything
In a lot of my earlier code I used code generators or copied tutorials online and ended up adding the API macros at the start of my header MYPROJECT_API
Instead, only export the symbols (functions) that other modules need.
The table below illustrates different macros in increasing order of how much gets exported.
1. | Nothing | Nothing is exported, neither the class nor its functions will be usable in other modules |
2. | UCLASS(MinimalAPI) | It is possible to use Cast<T> on instances of this class in other modules. |
3. | MYPROJECT_API on specific functions | Only functions marked with the macro will be callable in other modules. |
4. | MYPROJECT_API on class | All functions are exported. |
1. Export Nothing
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
void Woof();
void RollOver();
};
2. MinimalAPI
UCLASS(MinimalAPI)
class ADog : public AActor
{
GENERATED_BODY()
void Woof();
void RollOver();
};
3. Export Specific Functions
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
MYPROJECT_API void Woof();
void RollOver();
};
4. Export Everything
UCLASS()
class MYPROJECT_API ADog : public AActor
{
GENERATED_BODY()
void Woof();
void RollOver();
};
Only use dynamic delegates where needed
I used to make all my deligates DYNAMIC
Now I think about whether it needs to be exposed to Blueprints or not.
Avoiding Crashes
IsValidIndex
before accessing TArray<T>
Even if you think your math is valid, I would seriously recommend wrapping all your calls to TArray<T>
elements with a call to IsValidIndex
. There's nothing worse than a game breaking crash on a build that took hours and hours to create.
Accessing an array element without checking
const int32 Index = ResultOfCalculation();
// If your math is wrong, crash and burn
SomeArray[Index].RiskyBusiness = 1;
Using IsValidIndex before accessing
const int32 Index = ResultOfCalculation();
if (SomeArray.IsValidIndex(Index))
{
SomeArray[Index].DoSomething;
}